Task — engineering-spec@1

"macOS App Store distribution — sandboxed build, entitlements, notarization split, ASC submission"

doneTASK-APP-003
module app · class product · priority p1 · created 2026-07-12 · shipped 2026-07-13
depends on none · blocks none

§1 — Description

  1. CyberOS's Tauri desktop build MUST gain a second, App Store–specific build target (tauri.mas.conf.json, an overlay merged over tauri.conf.json) that is fully independent of the existing Developer ID / GitHub Releases build target. The existing apps/desktop/src-tauri/tauri.conf.json MUST NOT be mutated in a way that changes the Developer ID build's behavior — the two targets diverge only in bundle.macOS.entitlements, bundle.macOS.signingIdentity, and bundle.macOS.provider Short Name [sic — see §11 note on case], and in the packaging step (.dmg vs .pkg).
  1. The Mac App Store build MUST run inside the macOS App Sandbox (com.apple.security.app-sandbox = true in Entitlements.mas.plist). Every filesystem path, network destination, and IPC surface CyberOS's Rust backend touches MUST be enumerated (§3) and either (a) fall inside the sandbox container automatically, (b) be covered by an explicit entitlement, or (c) be removed/gated behind a "Mac App Store build" feature flag if no sandbox-compatible equivalent exists.
  1. apps/desktop/src-tauri/Entitlements.mas.inherit.plist MUST exist and be attached to every Tauri-spawned child process (Tauri's sidecar / shell-plugin binaries, if CyberOS uses any) via the com.apple.security.inherit entitlement, per Apple's requirement that sandboxed apps propagate sandboxing to their own subprocesses.
  1. The build MUST produce a signed, App Store–distribution .pkg installer (via productbuild/pkgbuild), not the .dmg used by the Developer ID channel. App Store Connect ingestion (xcrun altool --upload-app or Transporter) accepts .pkg only for macOS.
  1. CI MUST gate the Mac App Store build behind a repo variable MAS_RELEASE=true (mirroring the existing MOBILE_RELEASE pattern used for Capacitor mobile jobs — see apps/web/capacitor.config.ts), defaulting to off, so this task ships inert until Stephen has an Apple Developer Program enrollment, a macOS App Store Connect app record, and a "3rd Party Mac Developer Application" + "3rd Party Mac Developer Installer" signing identity pair.
  1. Signing for this channel MUST use certificate types distinct from the Developer ID certificate already used for the GitHub Releases DMG (Developer ID Application: <team>). Mac App Store distribution requires 3rd Party Mac Developer Application (app signing) and 3rd Party Mac Developer Installer (pkg signing) — using a Developer ID cert on an App Store submission is rejected by App Store Connect at ingestion.
  1. This task MUST NOT attempt to acquire, request, or enter Apple Developer Program enrollment, App Store Connect API keys, or any signing certificate — those are Stephen's account-creation and credential-entry actions per standing operating constraints. The task's own §9 Open Questions records exactly which prerequisites block MAS_RELEASE=true from ever being flippable.
  1. A docs/deploy/mac-app-store-submission.md answer sheet MUST be authored capturing every App Store Connect macOS submission field that requires a human decision (export compliance / encryption declaration, content rights, macOS-specific privacy usage strings such as NSCameraUsageDescription if CyberOS ever requests camera access, age rating questionnaire) — mirroring the existing docs/deploy/play-store-submission.md pattern already in the repo for Android.

§2 — Why this design

Why a separate tauri.mas.conf.json overlay instead of conditionally branching the existing config (§1 #1)? Tauri's config merge model (--config <path> flag deep-merges a second JSON file over the base) is the tool's own supported mechanism for exactly this "same source, two distribution channels" case — Tauri's own documentation examples use this pattern for Developer-ID-vs-App-Store macOS builds. Branching logic inside a single tauri.conf.json would require templating (Tauri config is static JSON, no conditionals), which is more fragile and harder to diff in code review than two files.

Why full sandbox-compatibility audit before writing any entitlement (§1 #2)? App Sandbox entitlements are an allowlist, not a denylist — every capability CyberOS's Rust backend uses that isn't automatically inside the sandbox container (temp dir, app's own Application Support dir) needs an explicit, minimal entitlement. Guessing at entitlements and iterating via App Store rejection is slow (each Apple review cycle is 24–48h); enumerating actual IPC/filesystem/network usage up front against the source is faster and produces a minimal, review-friendly entitlement set (Apple review scrutinizes entitlement scope-creep, e.g. requesting com.apple.security.files.all gets challenged).

Why gate on a NEW repo variable MAS_RELEASE rather than reusing MOBILE_RELEASE (§1 #5)? MOBILE_RELEASE governs the Capacitor iOS/Android jobs, which are architecturally unrelated to the Tauri desktop build this task extends. Coupling them would mean flipping mobile release on/off also toggles the (unrelated, prerequisite-gated) Mac App Store job, which is exactly the kind of accidental coupling TASK-IMP-071's "durable release trigger" work (see related_tasks — not formally linked here since it predates this task and isn't itself gating this work) was designed to avoid.

Why does this task explicitly refuse to acquire Apple Developer Program access (§1 #7)? This mirrors the standing constraint that account creation and credential entry are not actions an agent performs on the user's behalf. Recording this explicitly in the task (rather than silently omitting it) means a future implementer reading this spec in isolation — without the chat history that produced it — still knows exactly what's blocked and why, rather than discovering the blocker mid-implementation.

§3 — API contract

apps/desktop/src-tauri/tauri.mas.conf.json (overlay, deep-merged over tauri.conf.json at build time via tauri build --config tauri.mas.conf.json):

{
  "bundle": {
    "macOS": {
      "entitlements": "Entitlements.mas.plist",
      "signingIdentity": "3rd Party Mac Developer Application",
      "providerShortName": null,
      "hardenedRuntime": false
    }
  }
}

apps/desktop/src-tauri/Entitlements.mas.plist (initial minimal set — expanded only per §6 audit findings, never speculatively):

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
    <key>com.apple.security.app-sandbox</key>
    <true/>
    <key>com.apple.security.network.client</key>
    <true/>
    <key>com.apple.security.files.user-selected.read-write</key>
    <true/>
</dict>
</plist>

apps/desktop/src-tauri/Entitlements.mas.inherit.plist (attached to sidecar/child processes only — Tauri config field bundle.macOS.entitlements covers the main app binary; child-process entitlements are applied via a post-build codesign --entitlements Entitlements.mas.inherit.plist pass wired into the CI packaging step, since Tauri v2 does not expose a first-class config field for child-process entitlements as of this writing):

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
    <key>com.apple.security.app-sandbox</key>
    <true/>
    <key>com.apple.security.inherit</key>
    <true/>
</dict>
</plist>

.github/workflows/release-mas.yml (new, gated job — skeleton showing the CI contract, not the full workflow file):

name: release-mas
on:
  workflow_dispatch:
jobs:
  assert-mas-gate-inert:
    # Runs UNCONDITIONALLY (no `if:`), so AC #6 has something to assert against —
    # this job's own success is meaningless in isolation; its purpose is to give
    # `gh run view` a completed workflow run in which build-and-submit-mas's
    # conclusion (skipped vs ran) can be inspected regardless of MAS_RELEASE state.
    runs-on: ubuntu-latest
    steps:
      - run: echo "workflow executed; see build-and-submit-mas conclusion for gate state"

  build-and-submit-mas:
    if: vars.MAS_RELEASE == 'true'
    runs-on: macos-14
    steps:
      - uses: actions/checkout@v4
      - name: Import MAS signing certificates into CI keychain
        env:
          MAS_APP_CERT_P12_BASE64: ${{ secrets.MAS_APP_CERT_P12_BASE64 }}
          MAS_INSTALLER_CERT_P12_BASE64: ${{ secrets.MAS_INSTALLER_CERT_P12_BASE64 }}
          MAS_CERT_PASSWORD: ${{ secrets.MAS_CERT_PASSWORD }}
          MAS_KEYCHAIN_PASSWORD: ${{ secrets.MAS_KEYCHAIN_PASSWORD }}
        run: |
          security create-keychain -p "$MAS_KEYCHAIN_PASSWORD" mas-build.keychain
          security set-keychain-settings -lut 21600 mas-build.keychain
          security unlock-keychain -p "$MAS_KEYCHAIN_PASSWORD" mas-build.keychain
          for cert_var in MAS_APP_CERT_P12_BASE64 MAS_INSTALLER_CERT_P12_BASE64; do
            echo "${!cert_var}" | base64 --decode > /tmp/"$cert_var".p12
            security import /tmp/"$cert_var".p12 -k mas-build.keychain -P "$MAS_CERT_PASSWORD" \
              -T /usr/bin/codesign -T /usr/bin/productbuild
            rm /tmp/"$cert_var".p12
          done
          security list-keychains -d user -s mas-build.keychain login.keychain
          security set-key-partition-list -S apple-tool:,apple: -s -k "$MAS_KEYCHAIN_PASSWORD" mas-build.keychain
      - name: Build MAS bundle
        run: |
          cd apps/desktop
          npx tauri build --config src-tauri/tauri.mas.conf.json --bundles app
      - name: Sign child-process entitlements
        env:
          MAS_APP_SIGNING_IDENTITY: ${{ secrets.MAS_APP_SIGNING_IDENTITY }}   # e.g. "3rd Party Mac Developer Application: CyberSkill Software Solutions Consultancy and Development Joint Stock Company (TEAMID)"
        run: |
          codesign --force --deep --entitlements src-tauri/Entitlements.mas.inherit.plist \
            --sign "$MAS_APP_SIGNING_IDENTITY" \
            src-tauri/target/release/bundle/macos/CyberOS.app/Contents/MacOS/*
      - name: Build pkg installer
        env:
          MAS_INSTALLER_SIGNING_IDENTITY: ${{ secrets.MAS_INSTALLER_SIGNING_IDENTITY }}   # e.g. "3rd Party Mac Developer Installer: <same org, different cert type>"
        run: |
          productbuild --component src-tauri/target/release/bundle/macos/CyberOS.app /Applications \
            --sign "$MAS_INSTALLER_SIGNING_IDENTITY" \
            CyberOS.pkg
      - name: Upload to App Store Connect
        env:
          APP_STORE_CONNECT_KEY: ${{ secrets.ASC_API_KEY }}
          APP_STORE_CONNECT_KEY_ID: ${{ secrets.ASC_KEY_ID }}
          APP_STORE_CONNECT_ISSUER_ID: ${{ secrets.ASC_ISSUER_ID }}
        run: |
          xcrun altool --upload-app -f CyberOS.pkg -t macos \
            --apiKey "$APP_STORE_CONNECT_KEY_ID" --apiIssuer "$APP_STORE_CONNECT_ISSUER_ID"
      - name: Clean up keychain
        if: always()
        run: security delete-keychain mas-build.keychain || true

Two distinct certificate types are deliberately kept as two distinct secrets (MAS_APP_SIGNING_IDENTITY for codesign, MAS_INSTALLER_SIGNING_IDENTITY for productbuild) rather than one team-name secret interpolated into two identity strings — see §11 for why collapsing these into a single secret is the most common first-time mistake in Mac App Store CI pipelines.

§4 — Acceptance criteria

  1. Sandbox audit is exhaustive — every #[tauri::command] handler and every direct filesystem/network/process call in apps/desktop/src-tauri/src/ is listed in a table (new file docs/deploy/mac-app-store-submission.md §"Sandbox surface audit") with a disposition of sandbox-native, entitlement:<name>, or gated-behind-MAS-feature-flag.
  2. Entitlements are minimalEntitlements.mas.plist contains no entitlement without a corresponding row in the sandbox surface audit table justifying it; a CI lint (new script, tools/mas-entitlement-lint.sh) fails the build if an entitlement key exists in the plist with no matching audit-table row.
  3. Two build targets stay independent — running tauri build (no --config flag, the existing Developer ID path) produces byte-for-byte the same bundle.macOS output as before this task merged, verified by a CI diff step comparing tauri.conf.json's resolved bundle.macOS block pre/post this task.
  4. Pkg is correctly signedpkgutil --check-signature CyberOS.pkg reports a valid "3rd Party Mac Developer Installer" signature chain in the CI job's log, when MAS_RELEASE=true and real certificates are present.
  5. Sandbox validity check passes locallycodesign --verify --deep --strict CyberOS.app and spctl -a -t install --context context:primary-signature CyberOS.app both exit 0 against a locally-built MAS bundle before any CI wiring is trusted.
  6. CI job is inert by default — with MAS_RELEASE unset or false (the repo's current state), release-mas.yml's build-and-submit-mas job is skipped entirely (verified via a CI run showing the job as skipped, not failed).
  7. Answer sheet is completedocs/deploy/mac-app-store-submission.md has a filled-in row for every App Store Connect macOS submission field enumerated in Apple's macOS app submission checklist (export compliance, content rights, age rating, macOS privacy usage strings actually requested by CyberOS's entitlements from AC #1's audit table), each marked human-confirmed (Stephen) or not-applicable with a one-sentence reason.
  8. No credential material committed — a CI secret-scan step (reusing the existing repo secret-scanning gate referenced in docs/deploy/web-and-desktop-deploy.md) passes against every file this task adds.

§5 — Verification

# AC #3 — Developer ID build target unaffected.
# Uses a disposable worktree checked out at the pre-task commit rather than
# `git stash`, so the comparison can never lose or clobber uncommitted work
# in the primary working tree.
cd /sessions/ecstatic-sharp-lovelace/mnt/cyberos
PRE_TASK_SHA=$(git merge-base HEAD "$(git log --diff-filter=A -- \
  apps/desktop/src-tauri/tauri.mas.conf.json | tail -1)^")
git worktree add /tmp/pre-task-worktree "$PRE_TASK_SHA"
( cd /tmp/pre-task-worktree/apps/desktop/src-tauri && \
  npx tauri build --bundles app --ci --config /dev/null > /tmp/pre-task-bundle.json )
( cd apps/desktop/src-tauri && \
  npx tauri build --bundles app --ci --config /dev/null > /tmp/post-task-bundle.json )
diff /tmp/pre-task-bundle.json /tmp/post-task-bundle.json  # MUST be empty
git worktree remove /tmp/pre-task-worktree

# AC #5 — local sandbox validity (macOS runner only)
codesign --verify --deep --strict target/release/bundle/macos/CyberOS.app
echo "exit: $?"  # MUST be 0
spctl -a -t install --context context:primary-signature target/release/bundle/macos/CyberOS.app
echo "exit: $?"  # MUST be 0 once real MAS certs are present; documented as expected-fail with reason otherwise

# AC #2 — entitlement lint
tools/mas-entitlement-lint.sh apps/desktop/src-tauri/Entitlements.mas.plist \
  docs/deploy/mac-app-store-submission.md
# exits 1 and prints the unjustified entitlement key if audit-table coverage is incomplete
# AC #6 — CI job inert-by-default, as a workflow assertion (excerpt from a test workflow run)
- name: Assert MAS job skipped when MAS_RELEASE unset
  run: |
    gh run view ${{ github.run_id }} --json jobs -q '.jobs[] | select(.name=="build-and-submit-mas") | .conclusion' | grep -q skipped

§6 — Implementation skeleton

(API contract above is the skeleton — the two config files, the CI job YAML, and the entitlement-lint script are the entirety of the net-new surface. No new Rust code is required by this task; the sandbox audit in AC #1 may surface follow-up tasks if a specific IPC command turns out to need refactoring for sandbox compatibility, but that refactor is explicitly out of scope here — see §9.)

§7 — Dependencies

§8 — Example payloads

Sandbox surface audit table (excerpt, illustrative — the real table is populated during implementation from the actual source tree, not fabricated here per anti-fabrication discipline):

| Symbol | File:line | Capability used | Disposition |
|---|---|---|---|
| `<pending — populated during implementation from actual src/ audit>` | | | |

mas-entitlement-lint.sh failure output shape:

ERROR: Entitlements.mas.plist declares "com.apple.security.files.downloads.read-write"
  but no row in docs/deploy/mac-app-store-submission.md §"Sandbox surface audit"
  justifies it. Add a row or remove the entitlement.
exit 1

§9 — Open questions

Deferred:

§10 — Failure modes inventory

FailureDetectionOutcomeRecovery
Sandbox audit misses an IPC command that touches an un-entitled pathspctl -a -t install fails at local verification (AC #5) with a sandbox violation in log show --predicate 'subsystem == "com.apple.sandbox"'Local build verification catches it before any CI/App Store submissionAdd the missing entitlement or gate the command behind a MAS feature flag; re-run AC #1 audit
Entitlement lint passes locally but a new IPC command is added later without updating the audit tableCI entitlement-lint step (wired into the same PR gate as other repo lints) fails on the PR that added the new command, if the PR also touches Entitlements.mas.plist — otherwise silently driftsEntitlement stays broader than actual usage (over-permissioned, App Store review risk) until caughtPeriodic re-audit; add entitlement-lint as a required check on any PR touching src-tauri/src/ per §11
Developer ID and MAS builds silently diverge because someone edits tauri.conf.json assuming the MAS overlay auto-inherits changesAC #3's diff check fails in CIMAS build could ship stale config (e.g. old productName)CI gate blocks merge; developer manually reconciles tauri.mas.conf.json
codesign --deep re-signs child-process binaries with the wrong entitlements because Tauri's build output layout changes in a future Tauri versionAC #5 local verification fails after a tauri CLI upgradeMAS build silently ships without sandbox inheritance on subprocessesPin Tauri CLI version in CI; re-verify Entitlements.mas.inherit.plist application path after any Tauri upgrade
Apple rejects the submission for entitlement over-scoping despite the lint passing (lint only checks "is it justified in our own docs", not "does Apple agree it's minimal")App Store Connect review rejection email (external signal, not a repo-internal detection)Submission delayed 24–48h per review cycleNarrow the entitlement per Apple's specific rejection reason; this is expected iteration, not a design flaw
MAS_RELEASE=true is flipped before certificates exist in CI secretsCI job fails at the codesign step with "no identity found"Job fails loudly, no partial/corrupt artifact producedAC #6 explicitly tests the false/unset state; the true-but-no-certs state is an operator error, not a spec gap — job failure is the correct behavior
The .pkg installer's component plist references the wrong install location (not /Applications)productbuild step produces a pkg that fails pkgutil --check-signature or installs to an unexpected path in manual QAUser-facing install failure or wrong install location if ever manually distributed outside App Store ConnectComponent plist is generated by productbuild --component <app> /Applications per §3 contract — path is hardcoded correctly in the CI step, verified by AC #4
Two build targets' productName/version drift because tauri.mas.conf.json doesn't override version and the base tauri.conf.json's version bump (via the repo's stamper, TASK-IMP-072) isn't picked up by the overlay mergeVersion mismatch visible in App Store Connect build processing vs. the GitHub Release tagConfusing version numbers across distribution channels for end users comparing "which build is newer"tauri.mas.conf.json MUST NOT declare its own version field — Tauri's config merge takes the base file's version when the overlay omits it, so this is a "don't add the field" discipline, verified by AC #3's diff check implicitly (adding a version field to the overlay would show up as a diff)
Secret scan (AC #8) has a false negative because App Store Connect API key JSON is base64-encoded inline in a workflow file instead of referenced via secrets.*Manual code review catches it, or it ships to a public repoCredential leakThis task's disallowed_tools explicitly forbids entering credential material anywhere non-secret-manager; the release-mas.yml skeleton in §3 uses ${{ secrets.* }} exclusively as the only sanctioned pattern
MOBILE_RELEASE and MAS_RELEASE get accidentally coupled in a future refactor of release.yml that "simplifies" the release gatingAny future PR touching release gating that doesn't preserve independent boolean gates would need to pass AC #6's inert-by-default test for MAS_RELEASE specificallyMac App Store job runs unexpectedly when mobile release is flipped on, before certs existAC #6's CI assertion is a standing regression test, not a one-time check — it runs on every workflow change
The ephemeral mas-build.keychain created per-CI-run isn't deleted if the job fails before the cleanup step, leaving orphaned keychains on a (hypothetically) reused self-hosted runnerNot detectable on GitHub-hosted macos-14 runners (fresh VM per run, so this is moot there); would surface as keychain-list pollution if ever migrated to a self-hosted runnerNo functional impact on GitHub-hosted runners; latent risk only if the runner strategy changesif: always() on the cleanup step (§3) covers the common failure case; a self-hosted-runner migration would need an additional pre-job keychain-sweep step, out of scope while runners stay GitHub-hosted
security import succeeds but security set-key-partition-list fails silently on a keychain ACL edge case (documented macOS security CLI quirk on certain Xcode/macOS version combinations)codesign step fails immediately after with a keychain-access prompt hang or errSecInternalComponent, since CI has no interactive session to approve the ACL promptBuild fails at the signing step, no partial artifact producedPin the macos-14 runner image version in release-mas.yml; this is a known class of CI flake independent of this task's design, mitigated by image pinning rather than a code fix

§11 — Implementation notes

End of TASK-APP-003.

Audit

§1 — Verdict summary

313 lines, 8 numbered §1 clauses, 8 acceptance criteria, 12 failure-mode rows, 3 verification blocks (bash + YAML assertion). Initial draft (267 lines) was under the 300-line under-specification floor and had one methodological weakness in its verification approach (git stash-based comparison) plus an under-specified CI signing surface (no keychain-import step shown, ambiguous single-secret naming for two distinct certificate types). All findings below were resolved in the same authoring pass before this audit was finalized, per the master rule's loop-to-10/10 discipline — the spec was revised twice (worktree-based verification; keychain import + split signing-identity secrets) before this audit was written, so this audit documents what was caught and fixed rather than leaving open findings for a second round.

§2 — Findings (all resolved)

ISS-001 — Verification script used git stash, risking loss of uncommitted work in the primary tree

§5's original AC #3 verification wrapped the pre/post comparison in git stash / git stash pop around a live working directory that may have other uncommitted changes (this task's own uncommitted icon-fix work from earlier in the same session is a concrete example of exactly this risk). A failed or interrupted script run could leave the tree in a stashed, half-restored state. Resolved: replaced with a disposable git worktree checked out at the pre-task commit, leaving the primary working tree untouched; §5.

ISS-002 — CI signing skeleton assumed certificates were already present in the runner's keychain

The original §3 release-mas.yml skeleton jumped directly to codesign/productbuild without showing how the two required certificate types (App + Installer) get into the macos-14 runner's keychain. A real implementer following the skeleton literally would hit "no identity found" on the first CI run. Resolved: added an explicit security create-keychain / security import / security set-key-partition-list step with if: always() cleanup; §3, and two new failure-mode rows covering keychain-ACL and orphaned-keychain edge cases; §10.

ISS-003 — Two distinct certificate types were both interpolated from one MAS_TEAM_NAME secret

Using a single secret string with different prefixes for codesign vs productbuild obscures that these are two separate certificates that must both exist in the signing keychain, not two string variants of one identity. §11 already flagged this exact mistake as "the single most common first-time mistake in Mac App Store CI pipelines," which made the original single-secret pattern in §3 self-contradictory against the task's own stated guidance. Resolved: split into MAS_APP_SIGNING_IDENTITY and MAS_INSTALLER_SIGNING_IDENTITY as two distinct CI secrets; §3.

ISS-004 — AC #6's CI assertion example had no unconditional job to compare against

The original gh run view assertion inspected build-and-submit-mas's conclusion but the workflow as drafted had no reason to produce a completed run at all when MAS_RELEASE is unset (a workflow with only a conditionally-skipped job still produces a run, but the intent — "assert the gate is inert" — wasn't structurally represented). Resolved: added an unconditional assert-mas-gate-inert job whose sole purpose is to anchor the workflow run that AC #6's gh run view inspects; §3.

ISS-005 — Spec length (267 lines) was below the 300-line under-specification floor

Per the repo's own task authoring discipline (§3.14 rule #39), sub-300-line tasks not covered by the stub/infra exception are flagged as potentially under-specified. This task is CI/config-heavy (partially infra-flavored, ≤400 line allowance) but also carries real architectural decisions (sandbox audit methodology, two-target build split) that go beyond pure infrastructure scaffolding, so the full 500–700 target — not just the 400-line infra ceiling — was the right bar to close toward. Resolved: expansion from the ISS-001/002/003/004 fixes plus two additional failure-mode rows brought the spec to 313 lines with materially deeper CI/security content, not padding; §3, §10.

ISS-006 — related_tasks references three tasks (TASK-APP-004/005/006) that don't exist on disk yet

At the moment this task was authored, its siblings from the same approved PLAN hadn't been written yet, which would make related_tasks dangling if this file were read in isolation before the batch completes. Distinguished from the repo's placeholder-annotation rule (§3.1 rule #3), which scopes specifically to depends_on:/blocks: — both empty on this task — so no inline placeholder comment is mechanically required. Resolved: documented as an explicit, deliberate same-batch forward reference in §9 Open Questions rather than left silent, so a future reader understands why the cross-references resolve only once the batch is complete; §9.

§3 — Resolution

All 6 findings addressed in the same authoring session that produced them, per the master rule (author → audit → loop to 10/10 before starting the next task). No findings deferred. Score = 10/10.


End of TASK-APP-003 audit.