LearnNewsExamplesServices
Frontmatter
id16440
titleplutil array-index replace inserts instead of replacing: both LaunchAgent install procedures produce plists that lint OK and cannot launch
stateClosed
labels
bugdocumentationcontributor-experienceai
assigneesneo-opus-vega
createdAtAug 3, 2026, 3:51 PM
updatedAtAug 3, 2026, 4:40 PM
githubUrlhttps://github.com/neomjs/neo/issues/16440
authorneo-opus-vega
commentsCount0
parentIssuenull
subIssues[]
subIssuesCompleted0
subIssuesTotal0
contentTrust
projected
quarantined0
signals[]
blockedBy[]
blocking[]
closedAtAug 3, 2026, 4:40 PM

plutil array-index replace inserts instead of replacing: both LaunchAgent install procedures produce plists that lint OK and cannot launch

Closed Backlog/active-chunk-12 bugdocumentationcontributor-experienceai
neo-opus-vega
neo-opus-vega commented on Aug 3, 2026, 3:51 PM

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

  • Both ProgramArguments blocks use a single whole-array plutil -replace … -json call; no ProgramArguments.<index> form survives in the runbook.
  • The nine dictionary-key -replace lines are unchanged — a diff touching them fails this AC.
  • Following the runbook verbatim on macOS produces a plist whose plutil -p output contains no __…__ placeholder, and whose ProgramArguments[0] is an absolute node path.
  • The runbook states, inline, that plutil -replace on an array index inserts rather than replaces and that plutil -lint reports OK on the corrupted result — so the next author does not reintroduce the pattern and knows why lint is not a sufficient check here.
  • Verification is a real plutil run recorded in the PR, not a reasoned claim.

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.