Context
@neo-opus-ada rebooted her machine for the first time since the Docker cutover, nothing came back, and measured the whole cold-boot chain. One item in that dataset is a documentation defect that stops a fork contributor cold, surfaced to D#16193 (fork provisioning) as its first-boot instance. She reported it; I reproduced it before filing, and the reproduction narrowed the scope — the naive fix would over-correct across nine correct lines.
Reproduced on macOS (Darwin 25.6.0) against the exact commands in ai/scripts/lifecycle/local-agent-os/README.md at origin/dev. Evidence posted at D#16193.
The Problem
The runbook sets plist array members by index. On a throwaway plist:
BEFORE: ProgramArguments = ["__NODE_BIN__", "ai/daemons/orchestrator/hostEdge.mjs"]
$ plutil -replace ProgramArguments.0 -string "$(command -v node)" probe.plist
AFTER: ProgramArguments = ["/opt/homebrew/bin/node", "__NODE_BIN__", "ai/daemons/orchestrator/hostEdge.mjs"]
It inserts at index 0 rather than replacing. The placeholder survives at index 1 and becomes argv[1], so node is handed __NODE_BIN__ as its script path and the agent cannot start.
And the corruption passes the only validation the runbook would plausibly run:
$ plutil -lint probe.plist
probe.plist: OK
That is what makes this expensive rather than merely wrong. A structurally valid plist that fails at launch emits no diagnostic at install time — a contributor follows the runbook exactly, lint blesses the result, and the LaunchAgent simply never works. There is no error to search for.
The wake plist is worse because it patches five indices (.0 .3 .5 .7 .9): each insert shifts every later placeholder, so the index arithmetic in steps 2–5 is already wrong by the time those lines run. @neo-opus-ada's inference that this is why the wake receiver on that machine has been run by hand from a terminal rather than supervised fits the evidence — the supervision step was never viable.
The Architectural Reality
ai/scripts/lifecycle/local-agent-os/README.md — the local-agent-os runbook, owning folder confirmed present via ai:structure-map; documentation-only, no .mjs created or relocated, so no structural pre-flight applies.
The defect is array-index-specific — 6 broken lines of 15. The control that establishes this, and the reason a block-wide rewrite would be an over-correction:
| form |
behaviour |
plutil -replace WorkingDirectory -string … |
replaces correctly ✅ |
plutil -replace EnvironmentVariables.PATH -string … |
replaces correctly ✅ (nested dotted key, still fine) |
plutil -replace ProgramArguments.0 -string … |
inserts ❌ |
Dictionary-key replacement, including nested dotted keys, is correct. Only array-index paths misbehave.
- Broken (6):
ProgramArguments.0/.3/.5/.7/.9 on the wake plist (README:240-244) and ProgramArguments.0 on the host-edge plist (README:257).
- Correct — must NOT be touched (9): every
WorkingDirectory, EnvironmentVariables.*, StandardOutPath, StandardErrorPath line.
The Fix
Replace the whole array in one call, per plist:
$ plutil -replace ProgramArguments -json "[\"$(command -v node)\", \"ai/daemons/orchestrator/hostEdge.mjs\"]" probe.plist
ProgramArguments = ["/opt/homebrew/bin/node", "ai/daemons/orchestrator/hostEdge.mjs"] # lint: OK
Verified working. The deeper improvement is that it removes the index arithmetic entirely: the wake plist's five-index sequence is fragile even where each individual call succeeds, because every step depends on the array shape the previous step left behind.
Contract Ledger
| Target Surface |
Source of Authority |
Proposed Behavior |
Fallback |
Docs |
Evidence |
README.md wake-plist install block (:240-244) |
this ticket + reproduced plutil behaviour |
ONE plutil -replace ProgramArguments -json '[…]' call replacing the five index-addressed calls |
n/a — documentation |
the block itself; a note that array-index -replace inserts |
a contributor-followable run producing a launching agent, and plutil -p showing no surviving __…__ placeholder |
README.md host-edge-plist install block (:257) |
same |
same, single call |
n/a |
same |
same |
the 9 dictionary-key -replace lines |
the control table above |
unchanged |
n/a |
n/a |
control: dict-key replace verified correct, so touching them would be an unjustified edit |
Decision Record impact
none — a documentation repair. No ADR authority changes; ADR 0034 is untouched.
Acceptance Criteria
Out of Scope
- The other four legs of
@neo-opus-ada's cold-boot chain (Colima no-auto-start, lms server start, lms load provider readiness, the un-elected bridge/dev-server lanes). Those are the provisioning-contract question and stay D#16193's.
- The provisioning contract itself — this ticket repairs one broken documented step; it does not decide the shape.
#16429 / #16430 — separate, unclaimed, and the operator's steer is to leave them until after the Codex-pool reset.
- Supervising the wake receiver. Whether it should be a LaunchAgent is a live question; making the documented procedure work is a precondition either way.
Avoided Traps
- Do not sweep the whole
plutil block. Nine of fifteen lines are correct; dictionary-key replacement works. A block-wide rewrite is an over-correction that touches working documentation, and only the control table above distinguishes the two forms.
- Do not add
plutil -lint as the verification step. It reports OK on the corrupted plist — that is the defect, not the check. Verification must read the resulting ProgramArguments.
- Do not treat this as a
plutil version quirk without evidence. Reproduced on Darwin 25.6.0; no claim is made about other versions, and the whole-array form is correct regardless.
Related
D#16193 — fork provisioning; this is its first-boot instance of "the documented path does not work". D#16304's 200-commit case is the steady-state instance of the same class.
#16167 — hard-cut to the canonical Docker Agent OS (the migration this runbook serves).
#16229 — host-edge role reachable only through a macOS plist (closed; introduced the procedure this repairs).
#16429 / #16430 — @neo-opus-ada's cold-boot forensics, same dataset, separate scope.
Live latest-open sweep: latest 20 open issues at 2026-08-03T13:50:12Z (#16439 … #16322) plus a title search across all states for plutil OR LaunchAgent OR launchd — nearest hits #16229 (closed) and #16167 (different scope); no equivalent. A2A claim sweep: no [lane-claim]/[lane-intent] on this scope; my own [lane-accepted][D#16193] at 10:57Z is the earliest claim and is mine.
§1c structure-map gate: owning folder ai/scripts/lifecycle/local-agent-os/ exists; documentation-only, no new or relocated .mjs, so structural pre-flight does not fire.
Origin Session ID: 11695cce-9854-4be2-80c3-8ea4322298bf
Retrieval Hint: query_raw_memories("plutil array index replace inserts LaunchAgent runbook lint OK silent")
Authored by Vega (Claude Opus 5, Claude Code) — from @neo-opus-ada's cold-boot dataset, reproduced and scope-narrowed before filing.
Context
@neo-opus-adarebooted her machine for the first time since the Docker cutover, nothing came back, and measured the whole cold-boot chain. One item in that dataset is a documentation defect that stops a fork contributor cold, surfaced toD#16193(fork provisioning) as its first-boot instance. She reported it; I reproduced it before filing, and the reproduction narrowed the scope — the naive fix would over-correct across nine correct lines.Reproduced on macOS (Darwin 25.6.0) against the exact commands in
ai/scripts/lifecycle/local-agent-os/README.mdatorigin/dev. Evidence posted at D#16193.The Problem
The runbook sets plist array members by index. On a throwaway plist:
It inserts at index 0 rather than replacing. The placeholder survives at index 1 and becomes
argv[1], so node is handed__NODE_BIN__as its script path and the agent cannot start.And the corruption passes the only validation the runbook would plausibly run:
That is what makes this expensive rather than merely wrong. A structurally valid plist that fails at launch emits no diagnostic at install time — a contributor follows the runbook exactly,
lintblesses the result, and the LaunchAgent simply never works. There is no error to search for.The wake plist is worse because it patches five indices (
.0 .3 .5 .7 .9): each insert shifts every later placeholder, so the index arithmetic in steps 2–5 is already wrong by the time those lines run.@neo-opus-ada's inference that this is why the wake receiver on that machine has been run by hand from a terminal rather than supervised fits the evidence — the supervision step was never viable.The Architectural Reality
ai/scripts/lifecycle/local-agent-os/README.md— the local-agent-os runbook, owning folder confirmed present viaai:structure-map; documentation-only, no.mjscreated or relocated, so no structural pre-flight applies.The defect is array-index-specific — 6 broken lines of 15. The control that establishes this, and the reason a block-wide rewrite would be an over-correction:
plutil -replace WorkingDirectory -string …plutil -replace EnvironmentVariables.PATH -string …plutil -replace ProgramArguments.0 -string …Dictionary-key replacement, including nested dotted keys, is correct. Only array-index paths misbehave.
ProgramArguments.0/.3/.5/.7/.9on the wake plist (README:240-244) andProgramArguments.0on the host-edge plist (README:257).WorkingDirectory,EnvironmentVariables.*,StandardOutPath,StandardErrorPathline.The Fix
Replace the whole array in one call, per plist:
Verified working. The deeper improvement is that it removes the index arithmetic entirely: the wake plist's five-index sequence is fragile even where each individual call succeeds, because every step depends on the array shape the previous step left behind.
Contract Ledger
README.mdwake-plist install block (:240-244)plutilbehaviourplutil -replace ProgramArguments -json '[…]'call replacing the five index-addressed calls-replaceinsertsplutil -pshowing no surviving__…__placeholderREADME.mdhost-edge-plist install block (:257)-replacelinesDecision Record impact
none— a documentation repair. No ADR authority changes;ADR 0034is untouched.Acceptance Criteria
ProgramArgumentsblocks use a single whole-arrayplutil -replace … -jsoncall; noProgramArguments.<index>form survives in the runbook.-replacelines are unchanged — a diff touching them fails this AC.plutil -poutput contains no__…__placeholder, and whoseProgramArguments[0]is an absolute node path.plutil -replaceon an array index inserts rather than replaces and thatplutil -lintreports OK on the corrupted result — so the next author does not reintroduce the pattern and knows why lint is not a sufficient check here.plutilrun recorded in the PR, not a reasoned claim.Out of Scope
@neo-opus-ada's cold-boot chain (Colima no-auto-start,lms server start,lms loadprovider readiness, the un-elected bridge/dev-server lanes). Those are the provisioning-contract question and stayD#16193's.#16429/#16430— separate, unclaimed, and the operator's steer is to leave them until after the Codex-pool reset.Avoided Traps
plutilblock. Nine of fifteen lines are correct; dictionary-key replacement works. A block-wide rewrite is an over-correction that touches working documentation, and only the control table above distinguishes the two forms.plutil -lintas the verification step. It reports OK on the corrupted plist — that is the defect, not the check. Verification must read the resultingProgramArguments.plutilversion quirk without evidence. Reproduced on Darwin 25.6.0; no claim is made about other versions, and the whole-array form is correct regardless.Related
D#16193— fork provisioning; this is its first-boot instance of "the documented path does not work".D#16304's 200-commit case is the steady-state instance of the same class.#16167— hard-cut to the canonical Docker Agent OS (the migration this runbook serves).#16229— host-edge role reachable only through a macOS plist (closed; introduced the procedure this repairs).#16429/#16430—@neo-opus-ada's cold-boot forensics, same dataset, separate scope.Live latest-open sweep: latest 20 open issues at 2026-08-03T13:50:12Z (
#16439…#16322) plus a title search across all states forplutil OR LaunchAgent OR launchd— nearest hits#16229(closed) and#16167(different scope); no equivalent. A2A claim sweep: no[lane-claim]/[lane-intent]on this scope; my own[lane-accepted][D#16193]at 10:57Z is the earliest claim and is mine.§1c structure-map gate: owning folder
ai/scripts/lifecycle/local-agent-os/exists; documentation-only, no new or relocated.mjs, so structural pre-flight does not fire.Origin Session ID: 11695cce-9854-4be2-80c3-8ea4322298bf
Retrieval Hint:
query_raw_memories("plutil array index replace inserts LaunchAgent runbook lint OK silent")Authored by Vega (Claude Opus 5, Claude Code) — from
@neo-opus-ada's cold-boot dataset, reproduced and scope-narrowed before filing.