muse-crew 0.14.1 → 0.14.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/AGENTS.md CHANGED
@@ -18,4 +18,4 @@ Muse Crew source repository. The repo is the product; the personal instance (`$C
18
18
  - Git source is authoritative.
19
19
  - Never expose this repo publicly.
20
20
  - Ship implementation and documentation together.
21
- - After editing `workflows/*.js`, run `bash tests/run.sh` — `node --check` does NOT catch syntax errors inside function bodies (V8 lazy preparsing), so it cannot validate workflow edits alone.
21
+ - After editing `workflows/*.js`, run `bash tests/run.sh` — `node --check` does NOT catch syntax errors inside function bodies (V8 lazy preparsing), so it cannot validate workflow edits alone. Worse (2026-09-19): raw `node --check` on the unstripped file is a FALSE NEGATIVE — the file parses as a module (top-level `export`), which masks breakage the release gate's export-strip + async-wrap transform exposes (module/script goal confusion). An unterminated string passed raw `node --check` and was caught only by `publish-verdict-first.test.js`'s loader emulation. After any workflow edit, verify with the true gate: `{ echo "async function __crew_workflow__(args) {"; sed 's/^export //' workflows/<f>.js; echo "}"; } > /tmp/w.js && node --check /tmp/w.js` — or just run the suite.
@@ -53,6 +53,9 @@ re-verified that every `docs/decisions/*.md#anchor` reference in
53
53
  - `#diff-computation` — Diff computation
54
54
  - `#already-merged-hydra` — Already-merged hydration
55
55
  - `#already-merged-idem2` — Already-merged idempotency
56
+ - `#submitted-on-issuance` — Submitted at trigger issuance
57
+ - `#detached-head-audit` — Detached-HEAD audit (0.14.2)
58
+ - `#detached-head-audit-redo` — Detached-HEAD audit REDO (0.14.3, supersedes findings 3/5)
56
59
 
57
60
  ## qa-reproduce.md — Decision history: qa reproduce
58
61
 
@@ -1125,15 +1125,15 @@ Applies to: standard, bugfix, chore.
1125
1125
  ```
1126
1126
  // Deterministic integrate verification: the agent cannot self-certify a
1127
1127
  // merge. After the Integrate agent claims success, the workflow confirms
1128
- // mechanically that the task branch tip is an ancestor of main via the
1128
+ // mechanically that the task branch tip is an ancestor of the integration target via the
1129
1129
  // lifecycle script's verify-merge command (which resolves the branch
1130
1130
  // through the crew registry — never by reconstructing "task/"+taskId — so
1131
1131
  // the check cannot verify the wrong branch). The VERIFIED marker is matched
1132
1132
  // by regex on the script's own stdout; agent prose is never read. This
1133
1133
  // closes the hole where an agent reported "merged empty" while approved
1134
1134
  // commits were still stranded on the task branch (bug b1b1f919). A genuine
1135
- // empty-diff Integrate (MERGED_EMPTY: no commits ahead of main) verifies
1136
- // vacuously — the tip is then an ancestor of main. Verification failure is
1135
+ // empty-diff Integrate (MERGED_EMPTY: no commits ahead of the integration target) verifies
1136
+ // vacuously — the tip is then an ancestor of the integration target. Verification failure is
1137
1137
  // an operational step failure, not a park: the dispatcher retries Integrate
1138
1138
  // under its consecutive-failure cap, and the retry finds the commits still
1139
1139
  // on the branch and performs the real merge — self-healing.
@@ -1149,14 +1149,14 @@ Applies to: standard, bugfix, chore.
1149
1149
  ```
1150
1150
  // Already-merged idempotency: a `repo_diff: none (already-merged:
1151
1151
  // <sha>)` declaration is verified mechanically — <sha> must resolve
1152
- // and be an ancestor of main in the configured repo. A fabricated or
1152
+ // and be an ancestor of the integration target in the configured repo. A fabricated or
1153
1153
  // mistaken declaration fails the phase here (the dispatcher retries
1154
1154
  // Build under its consecutive-failure cap); a verified declaration is
1155
1155
  // recorded in alreadyMergedSha for Review's no-diff branch. Without
1156
1156
  // this guard, Build correctly doing nothing left Review with no
1157
1157
  // mechanical way to accept an empty diff, and Cass rejected for "no
1158
- // commits ahead of main — the builder likely forgot to commit" while
1159
- // the deliverable sat on main (canary 2026-09-15, task 1d692d91).
1158
+ // commits ahead of the integration target — the builder likely forgot to commit" while
1159
+ // the deliverable sat on the integration target (canary 2026-09-15, task 1d692d91).
1160
1160
  // The sha is hex-only by construction (extractAlreadyMerged), so
1161
1161
  // interpolating it into the shell command cannot inject.
1162
1162
  ```
@@ -1278,3 +1278,192 @@ Applies to: standard, bugfix, chore.
1278
1278
  // fails on the extra hex char). Pure — pinned byte-identical across
1279
1279
  // standard/bugfix/chore.
1280
1280
  ```
1281
+
1282
+ <a id="submitted-on-issuance"></a>
1283
+ ## Submitted-on-issuance (room #22)
1284
+
1285
+ Invariant: every non-refused trigger lands `outcome: "submitted"` at
1286
+ issuance — immediately after the trigger-agent call/catch, before any
1287
+ receipt/audit observation. The entry asserts ONLY that the workflow invoked
1288
+ the rebuild-trigger agent and observed no explicit refusal; it does NOT
1289
+ assert `artifact_edit` was called or a build was accepted.
1290
+
1291
+ Applies to: standard, bugfix, chore.
1292
+
1293
+ History: the workflow contract wrote "submitted" only beside a captured receipt
1294
+ agent_id, so receipt-less successful triggers (build-observed audit evidence
1295
+ from STEP 1b, unknown outcomes) left `lib/verify-publish.js` and the
1296
+ unknown-recovery cron with no trigger instant to bind to — the verifier
1297
+ requires the oldest matching `submitted` entry as the trigger-issuance
1298
+ anchor, and all four room #22 journeys parked at Publish on its absence. The
1299
+ gap was in the three workflow files, not the vocabulary: the recovery writer
1300
+ (`lib/retry-publish.js:349`, pre-existing) already wrote
1301
+ issuance-without-receipt "submitted" lines (`agent_id: null`,
1302
+ `workflow: "unknown-recovery"`); the workflow writers never did.
1303
+
1304
+ Outcome vocabulary (one place — the word "submitted" has three meanings;
1305
+ discriminate on `agent_id` / `workflow`, never on the outcome word alone):
1306
+ - "submitted" (a) issuance: workflow writer, `agent_id: null`. The trigger
1307
+ instant — the verifier's anchor. Asserts invocation only.
1308
+ - "submitted" (b) receipt-bearing: workflow writer, `agent_id` non-null. A
1309
+ build with a new agent_id was observed after the trigger — a further
1310
+ observation, not a second issuance.
1311
+ - "submitted" (c) recovery re-issue: `workflow: "unknown-recovery"`,
1312
+ `agent_id: null`. The retry trigger re-issued after a provably-dropped
1313
+ classification.
1314
+ - "build-observed": audit-dir evidence proved the build completed with no
1315
+ captured receipt (H2 verdict-first path).
1316
+ - "unknown": the trigger went out but no build could be attributed to the
1317
+ attempt — fail-closed park; the unknown-recovery scan may retry it.
1318
+ - "failed": a build ran and failed during the poll window (explicit negative
1319
+ evidence, audit report ok=false) — preserved verbatim, never auto-retried.
1320
+ - "rejected": conclusive negative — artifact_edit explicitly refused; the
1321
+ edit provably did not go through. Terminal park, human attention.
1322
+
1323
+ The verifier binds the OLDEST matching "submitted" as the anchor, so the
1324
+ issuance entry must sort first and the observation entries stay as
1325
+ additional lines.
1326
+
1327
+ Edge cases:
1328
+ - Explicit ARTIFACT_EDIT_REFUSED still parks rejected BEFORE this site and
1329
+ never writes "submitted" — conclusive negative evidence is unchanged.
1330
+ - A throwing trigger-agent call lands here too: the catch swallows the throw
1331
+ because the edit may still have gone through, and this entry is then the
1332
+ recovery's trigger timestamp. It still does not claim the call succeeded.
1333
+ - `recordPublishLedger` is best-effort and non-gating; a write failure does
1334
+ not break the publish flow.
1335
+ - Distinct agent-call keys per write (2026-09-19 REVIEW): the issuance write
1336
+ and the later receipt-bearing "submitted" write execute in the same pass
1337
+ with the same task/outcome/rework key base. The runtime replays cached
1338
+ agent() calls by key, so the two writes pass keyTag "issuance" / "receipt"
1339
+ and the key construction disambiguates — a shared key would silently drop
1340
+ the second write and its ledger line. They are the only same-pass,
1341
+ same-key pair: the other same-outcome writes are mutually exclusive by
1342
+ verdict arm (rebuildTrigger is non-null only in the receipt arm), and the
1343
+ null-safe "unknown" write is guarded by `if (!publishUnknownFields)`.
1344
+ - Deferred vocabulary (2026-09-19 critic review): a distinct outcome word
1345
+ like `trigger-issued` for the issuance write would make `findTriggerEntry`
1346
+ an exact match and give the two writes naturally distinct agent-call keys.
1347
+ Not this release — the vocabulary is frozen across consumers; renaming
1348
+ widens the blast radius for no behavioral gain.
1349
+ - Crash between the issuance write and the observation (accepted risk,
1350
+ 2026-09-19 REVIEW): the ledger tail is an issuance-only "submitted" with
1351
+ no "unknown" follow-up — indistinguishable from "observation still
1352
+ running" by the ledger alone, so scan/retry do nothing and the task waits
1353
+ for human attention (fail-closed, safe). A Publish-entry "submitted
1354
+ already exists" guard would change resume semantics; deferred — it needs
1355
+ an explicit call, not smuggling into this release. The hazard it would
1356
+ guard: re-running Publish re-issues the trigger with the same
1357
+ rebuildAttemptKey (keyed by totalReworkCount, unchanged) and writes a
1358
+ second issuance "submitted" for the same attempt key.
1359
+
1360
+ <a id="detached-head-audit"></a>
1361
+ ## Detached-HEAD audit (0.14.2, room #22)
1362
+
1363
+ Mandated before room #22 but not performed; performed 2026-09-19 during the
1364
+ 0.14.3 BUILD. Scope: lib/worktree-lifecycle.sh, lib/publish-npm.sh,
1365
+ workflows/{standard,bugfix,chore}.js, lib/test-detached-integrate.sh,
1366
+ tests/integrate-reconcile.test.js.
1367
+
1368
+ Findings:
1369
+
1370
+ 1. `integration_target()` resolves the branch name, or the literal "HEAD"
1371
+ when detached. Nothing in the lifecycle ever checks out a branch
1372
+ (verified by grep — only comments name the old `checkout main` defect).
1373
+ 2. `cmd_integrate` merges into the current checkout — `git merge` advances a
1374
+ detached HEAD directly. The CREW_STAGED_BASE guard (merge-base against
1375
+ HEAD) preserves the reviewed-SHA and crew-init ancestry; the merge record
1376
+ names `integration_target=HEAD`; `verify-merge` checks the record against
1377
+ the LIVE tip (never the record's word for the tip — fixture C).
1378
+ 3. **[SUPERSEDED by #detached-head-audit-redo — do not rely on this finding.]**
1379
+ `do_push` on a detached HEAD reports PUSH_SKIPPED and returns 0. This is
1380
+ deliberate policy, not a git limitation: no remote ref exists for the
1381
+ detached line, and pushing `HEAD:main` would claim a branch the room does
1382
+ not own. The workflow's STEP 2 verdict treats PUSH_SKIPPED as PASS with
1383
+ the merge commit + record + VERIFIED as the evidence. This is honest, not
1384
+ hollow: the merge landed (MERGED, 2-parent commit, VERIFIED) — only
1385
+ propagation is skipped, loudly. In Gate 1 rooms the sample project has no
1386
+ origin (fresh `git init` from the tarball), so "detached + remote" cannot
1387
+ arise; fixture A step 9 pins the skip as the expected behavior.
1388
+ 4. publish-npm.sh resolves its push destination through the same
1389
+ `integration-target` resolver (2026-09-19 REVIEW removed the old
1390
+ hardcoded `git push origin main`).
1391
+ 5. **[SUPERSEDED by #detached-head-audit-redo — do not rely on this finding.]**
1392
+ publish-npm.sh's detached refusal, corrected in place: the refusal itself
1393
+ stands (there is nowhere to push a detached version-bump commit), but it
1394
+ sat at step 13 — AFTER the registry publish — so a detached invocation
1395
+ left a shipped version with a failed phase and a retry loop. Moved to the
1396
+ preflight (PUSH-TARGET-ANCHOR, before any mutation): a detached checkout
1397
+ now fails fast with PUBLISH_FAILED=env and zero side effects. Verified
1398
+ behaviorally: detached → exit 1, no commit, clean tree; branch →
1399
+ proceeds. The npm tarball publish itself was never refused — only the
1400
+ unpushable git record.
1401
+
1402
+ Applies to: lib/publish-npm.sh (preflight), lib/worktree-lifecycle.sh
1403
+ (unchanged — policy confirmed), workflows (unchanged — main STEP 2 verdict
1404
+ wording confirmed accurate; the R5-PUSH line's "nothing to push" wording was
1405
+ corrected separately, 2026-09-19 REVIEW).
1406
+
1407
+ <a id="detached-head-audit-redo"></a>
1408
+ ## Detached-HEAD audit REDO (0.14.3)
1409
+
1410
+ Supersedes findings 3 and 5 of the 0.14.2 `#detached-head-audit` above —
1411
+ finding 3's "PUSH_SKIPPED-as-policy is honest" stance and finding 5's
1412
+ "detached npm publication refuses" stance. The 0.14.3 critic panel blessed
1413
+ the old refusal/PUSH_SKIPPED→PASS takes; those dispositions were REJECTED —
1414
+ they contradict the mandatory contract (Eric, 2026-09-19): **a detached
1415
+ HEAD with a remote pushes as explicit `HEAD:<destination>`; detached npm
1416
+ publication works, it does not refuse; fail closed only when the
1417
+ destination is genuinely unknowable; never check out a local branch;
1418
+ preserve the reviewed-SHA → staged-base → crew-init ancestry.** A critic's
1419
+ blessing is checked against the contract, not taken as truth.
1420
+
1421
+ The contract defines the destination as the remote's default branch; the
1422
+ mechanism implements it by reading `refs/remotes/origin/HEAD` (which
1423
+ `git push` never resolves on its own, so the mechanism names it
1424
+ explicitly). The 0.14.2 audit's error: it treated git's no-automatic-ref
1425
+ behavior as a property of the contract ("no remote ref exists for the
1426
+ detached line"), and made policy out of a gap the mechanism was allowed to
1427
+ close. Room repos (the clean-room proof environments) live detached
1428
+ (reviewed SHA + crew-init commits); rooms #18–#22 carried that shape, so a
1429
+ refusal is a refusal of the normal room. Residual fib (2026-09-19 critic
1430
+ review): `refs/remotes/origin/HEAD` is a *cached* answer, not a live one —
1431
+ a renamed remote default branch makes it stale. Accepted residual risk in
1432
+ room-owned remotes (the deliberate no-network-fallback stance).
1433
+
1434
+ Behavior:
1435
+
1436
+ - `lib/worktree-lifecycle.sh`: `resolve_push_destination()` — attached
1437
+ resolves to the branch name; detached resolves to the remote's default
1438
+ branch (bare name) via `refs/remotes/origin/HEAD`; fails closed
1439
+ ("genuinely unknowable") when origin/HEAD is unset, and the reason is
1440
+ surfaced (2026-09-19 critic finding F-R1 — the loop must see the
1441
+ remediation hint). `push-destination` is the thin command wrapper.
1442
+ `do_push` runs the NO_REMOTE_PUSH check first (unchanged), then pushes
1443
+ the explicit refspec — `<branch>` attached, `HEAD:<dest>` detached —
1444
+ printing `PUSHED: origin/<dest> (refspec <refspec>)`; the reconcile path
1445
+ uses `$dest`. An unknowable destination fails closed — never a silent
1446
+ skip, never a refusal of a knowable push. PUSH_SKIPPED means exactly one
1447
+ thing: no push attempted (no merge record + no lock held, the R5
1448
+ manual path only).
1449
+ - `lib/publish-npm.sh`: the PUSH-TARGET-ANCHOR resolves `PUSH_REFSPEC` in
1450
+ the preflight, before any mutation; step 13 pushes it (never a bare
1451
+ HEAD). Detached + resolvable origin passes through; no origin at all
1452
+ (attached or detached) fails fast with `PUBLISH_FAILED=env` before any
1453
+ mutation (2026-09-19 critic finding F-A1); detached + origin with no
1454
+ origin/HEAD fails fast the same way, repo untouched.
1455
+ - workflows/{standard,bugfix,chore}.js: STEP-2 + R5-PUSH wording —
1456
+ PUSH_SKIPPED names only the nothing-merged path ("no push attempted (no
1457
+ record + no lock)"), and both PUSHED clauses name the detached form (a
1458
+ detached checkout's line prints e.g. `PUSHED: origin/main (refspec
1459
+ HEAD:main)` — that is the normal expected form).
1460
+ - Tests: fixture H in `lib/test-detached-integrate.sh` and cases 6/7 in
1461
+ `lib/test-publish-preflight.sh` pin the contract behaviorally.
1462
+
1463
+ No force-push anywhere; nothing in the lifecycle checks out a branch.
1464
+
1465
+ Applies to: lib/worktree-lifecycle.sh (resolve_push_destination,
1466
+ push-destination, do_push refspec), lib/publish-npm.sh
1467
+ (PUSH-TARGET-ANCHOR refspec), workflows/{standard,bugfix,chore}.js
1468
+ (STEP-2/R5-PUSH wording), lib/test-detached-integrate.sh (fixture H),
1469
+ lib/test-publish-preflight.sh (cases 6/7).
@@ -44,7 +44,7 @@ Applies to: standard, bugfix, chore.
44
44
  // that parks or fails must not leak its worktree, branch, or merge lock.
45
45
  // The lifecycle's terminal-cleanup releases the lock unconditionally and
46
46
  // reclaims the worktree+branch ONLY when the task branch is fully merged
47
- // into main (then it is redundant); unmerged work is preserved for the
47
+ // into the integration target (then it is redundant); unmerged work is preserved for the
48
48
  // human by design. Fire-and-forget with one bounded retry — the merge-lock
49
49
  // lease expiry and the orphan sweep are the backstop for a dead transport.
50
50
  ```
package/docs/guide.md CHANGED
@@ -201,7 +201,7 @@ If you don't have a project in mind, the crew can set up a sample: a tiny flashc
201
201
 
202
202
  ### Worktrees
203
203
 
204
- Each task builds in an isolated git worktree at `<repo>/.worktrees/<task_id>`, on a branch named `task/<task_id>`. The lifecycle manager (`lib/worktree-lifecycle.sh`) owns creation and removal — workflows and agents never run raw `git worktree` commands. The crew's own task→worktree registry (`<repo>/.worktrees/.registry/`) is the source of truth for task→branch/path; `prepare` fails closed when `main` is dirty, and `cleanup` is forgiving so a failed run never strands a worktree.
204
+ Each task builds in an isolated git worktree at `<repo>/.worktrees/<task_id>`, on a branch named `task/<task_id>`. The lifecycle manager (`lib/worktree-lifecycle.sh`) owns creation and removal — workflows and agents never run raw `git worktree` commands. The crew's own task→worktree registry (`<repo>/.worktrees/.registry/`) is the source of truth for task→branch/path; `prepare` fails closed when the integration target is dirty, and `cleanup` is forgiving so a failed run never strands a worktree.
205
205
 
206
206
  ## Filing a task
207
207
 
@@ -336,7 +336,7 @@ Configuration (per project, via the existing `getconfig` / `updateconfig` action
336
336
 
337
337
  The crew upgrades itself through the normal dispatch loop. An upgrade task carries a `source:` line in its description naming where the new release comes from:
338
338
 
339
- - `source: repo` (default when absent) — deploys the task project's repo at its current HEAD. This is how you ship repo changes to your running crew: change the code, merge to main, then file an upgrade task pointing at that repo.
339
+ - `source: repo` (default when absent) — deploys the task project's repo at its current HEAD. This is how you ship repo changes to your running crew: change the code, merge into the integration target (the repo's current checkout), then file an upgrade task pointing at that repo.
340
340
  - `source: npm@x.y.z` — installs the published `muse-crew` package at that exact version from the public npm registry. Use this to move the crew onto a known-good release — including one that predates the upgrade workflow itself.
341
341
 
342
342
  Both paths deploy through the stable `crew-release.sh` on persistent disk (never the release under upgrade), then verify mechanically: the live release equals the target, the release registry parses, and `crew-api get-state` works. Handover is automatic — the next polling tick launches the dispatcher from the new release through the `current` symlink. In-flight runs finish on the old release via the per-task lib pin, so an upgrade mid-flight does not break running work.
@@ -379,7 +379,7 @@ Each phase has an assigned identity — a character with a defined personality:
379
379
  | Map | **Mara** | Designer |
380
380
  | Build | **Wren** | Quietest one, trusts the plan |
381
381
  | Review | **Cass** | Fair but exacting — holds the spec as the contract |
382
- | Integrate | **Wren** | Merges the work, pushes `main` to the repo (succeeds vacuously when the task branch is empty — runtime-state deliverable) |
382
+ | Integrate | **Wren** | Merges the work into the integration target and pushes it (succeeds vacuously when the task branch is empty — runtime-state deliverable) |
383
383
  | Publish | **Wren** | Ships the merged code to the publish target (skipped when none). The workflow verifies the side effect mechanically — npm via registry version; artifact via an independent content read-back before the parent stamps provenance (see `docs/publish-verification.md`) — and fails closed if the worker's report and system state disagree |
384
384
  | QA | **Hazel** | Code-blind, persistent, wears persona costumes |
385
385
  | Reproduce | **Hazel** | Reproduces bugs before fixing |
@@ -273,8 +273,11 @@ For a task parked with `publish: verification-requested <commit>`:
273
273
  1. **Expected** = the agent_id in the park message's `(build …)` suffix.
274
274
  If the suffix says `agent_id unobserved`, look up the workflow's
275
275
  durable publish ledger at `$CREW_HOME/.publish-ledger/<slug>.jsonl`
276
- for the `submitted` entry with this `<commit>` and use its
277
- `agent_id` field (it may still be null — then this step is vacuous).
276
+ for the `submitted` entries with this `<commit>`: the OLDEST is the
277
+ trigger issuance (its `agent_id` is always null — the trigger instant,
278
+ not a build identity); the receipt-bearing `submitted`, if present, is
279
+ a LATER entry carrying the observed `agent_id` — use that one here
280
+ (if it is absent or null, this step is vacuous).
278
281
  2. **Live** = whether the expected agent_id appears anywhere in the
279
282
  read-back result (the live artifact status exposes no durable
280
283
  agent_id — only an in-flight correlation ID that expires with the
@@ -0,0 +1,94 @@
1
+ # Critic review — detached-HEAD audit REDO + submitted-on-issuance (0.14.3)
2
+
3
+ Change set: uncommitted, on top of `d619f2a` (545 insertions, 138 deletions, 18 files). Full suite green on this tree (EXIT=0); `test-publish-preflight.sh` 12/12 incl. the two new detached cases.
4
+
5
+ Contract checked against (Eric, 2026-09-19): a detached HEAD with a remote pushes as explicit `HEAD:<destination>`; destination = the remote's default branch via refs/remotes/origin/HEAD — the only in-repo mechanical answer; detached npm publication works, never refuses; fail closed only when the destination is genuinely unknowable; never check out a local branch; never force-push; preserve reviewed-SHA → staged-base → crew-init ancestry. The prior panel's blessing of the old refusal / PUSH_SKIPPED→PASS policy was REJECTED against this contract; every disposition below is checked against the contract, not the old policy.
6
+
7
+ ## Verdicts
8
+
9
+ **Architect: CONCERN** — resolver matches the contract on every detached path traced; one real mechanism gap keeps it from ACCEPT.
10
+
11
+ - F-A1 (moderate) — `lib/publish-npm.sh`: the anchor's no-origin check is nested inside the detached branch only (:101–103). An attached checkout with no origin resolves `PUSH_REFSPEC=<branch>` cleanly in the preflight, then step 13 pushes directly (`git push origin "$PUSH_REFSPEC"`, :311, no `do_push`, no NO_REMOTE_PUSH soft path) and fails `PUBLISH_FAILED=push` *after* the version-bump commit (step 8) and the registry publish (step 11) — the "shipped version with a failed phase and a retry loop" the REDO moved to the preflight for detached, still live for attached. Violates the anchor's own rule: "never after a side effect you cannot roll back."
12
+ - F-A2 (low) — `docs/decisions/publish-path.md#submitted-on-issuance`: "Other ledger writes in the pass are each followed by an immediate park/return, so they cannot collide" is false — the `unknown` write continues to post-deploy. The code is safe via a finer mechanism (verdict arms mutually exclusive; the null-safe `unknown` write guarded by `if (!publishUnknownFields)`); the doc's stated mechanism is wrong and this codebase treats decision docs as load-bearing.
13
+
14
+ **Subtractor: ACCEPT** — with the cuts below; nothing is hollow, cuts are prose and duplicated statements, no behavior.
15
+
16
+ - F-S1 — Flag the superseded findings inline: `#detached-head-audit` findings 3 ("PUSH_SKIPPED-as-policy is honest, not hollow") and 5 ("the refusal itself stands") read as live policy to anyone who stops there; the "Supersedes findings 3 and 5" sentence sits one section down. Findings 1, 2, 4 stay unflagged (still true).
17
+ - F-S2 — Cut process narration: REDO "Behavior" workflows bullet (`the "room-local" / "declined by policy" language is deleted`; `Net deletions (workflow size budget holds)` — keep only `PUSH_SKIPPED names only the nothing-merged path`); delete the submitted-on-issuance final paragraph entirely (narration about call-site placement and size budget); compress the fixture bullet to one sentence.
18
+ - F-S3 — `lib/worktree-lifecycle.sh:371–375`: the inline comment restates the `do_push` header comment nearly verbatim. Cut to one line (`# Attached: branch name; detached: remote default branch via resolve_push_destination().`).
19
+ - F-S4 — `lib/verify-publish.js:317–318`: duplicate of the block comment at :298–301. Delete the two inner lines.
20
+ - F-S5 (deferred, not a cut now) — A distinct outcome word (`trigger-issued`) for the issuance write would delete both the three-meanings doc and the keyTag parameter; rejected for this release — renaming now widens the blast radius for no behavioral gain. One line in the doc records the deferred shape.
21
+ - F-S6 (info) — keyTag is load-bearing and the pair is the only one: checked all `recordPublishLedger` call sites; every other same-outcome write is mutually exclusive by verdict arm or immediately parked. Honest fix under the frozen vocabulary.
22
+ - F-S7 (weak, optional) — the preflight calls the lifecycle twice; the explicit `remote get-url` check is subsumed by push-destination's unknowable branch. Not worth the churn; the more precise no-origin error message is worth one subprocess spawn.
23
+
24
+ **Reliability: CONCERN** — mechanism-level fail-closed holds on all paths; concerns are diagnosability and stale rationale.
25
+
26
+ - F-R1 (medium) — `lib/worktree-lifecycle.sh:377`: `dest=$(resolve_push_destination) || return 1` swallows the resolver's reason into `$dest` and discards it. Callers then misreport: `:542` prints `"ERROR: push failed after merge ... retry will recover the push"` — false for unknowable destinations (retry hits the same unknowable destination; only human `git remote set-head origin -a` recovers). `cmd_push_target` (:899/:914) exits 1 with no reason at all. Fail-closed holds (exit 1, no push), but the autonomous loop loses the only remediation hint and gets a false recovery promise.
27
+ - F-R2 (low) — `lib/worktree-lifecycle.sh:502–506` and `lib/AGENTS.md` (NO_REMOTE_RECONCILE line): `"A detached HEAD is local by design (no remote ref is claimed for the detached line), so there is nothing to reconcile."` — the exact rationale the REDO rejected. The skip itself is fine (do_push reconciles lazily on failure); the rationale contradicts the contract and will mislead readers that treat comments as policy.
28
+ - F-R3 (low) — `lib/worktree-lifecycle.sh:345`: the `do_push` header still claims it prints PUSH_SKIPPED; only `cmd_push_target:902` emits it. Same root as F-F1.
29
+ - F-R4 (low) — `lib/publish-npm.sh:97,104`: `bash "${LIFECYCLE:?LIFECYCLE is required}"` under `set -u` dies during word expansion with a raw bash error before `|| fail` is reached — no `PUBLISH_FAILED=<reason>` line, violating the script's header contract. Fail-closed (no mutation before :108), but the marker contract is the script's own.
30
+ - F-R5 (info) — No network fallback (`git ls-remote --symref`); deliberate. Over-conservative fail-closed matches the standing bias. Fixture fidelity confirmed: `crew-standup-room.sh` clones (clone sets origin/HEAD), so H2–H5 model the representative path; H1/case-7 cover the non-clone shape.
31
+ - F-R6 (info) — Reconcile merge on detached preserves ancestry (ancestor-based guards); incorporating origin/`<dest>`'s tip is required by the contract (fast-forward the remote, no force-push) and announced in the merge message.
32
+ - F-R7 (info) — NO_REMOTE_PUSH preserved for attached and detached in `do_push`/`push-target` (checked before target resolution, return 0). The npm-side divergence (detached+no-origin fails hard `PUBLISH_FAILED=env`) is correct: step 13 unconditionally pushes, while Integrate's merge is the deliverable.
33
+ - F-R8 (info) — The deferred "submitted already exists" guard is an honest deferral: oldest-binds across all three consumers neutralizes misanchoring.
34
+ - F-R9 (info) — DRY_RUN now fails `PUBLISH_FAILED=env` on unknowable destinations instead of printing `PUBLISH_DRY_RUN=1` (case 7 pins this): sanctioned by "fail closed only when genuinely unknowable." The DRY_RUN comment should note the preflight now runs first.
35
+
36
+ **First-Time User: CONCERN** — text is honest; misreading hazards are real and fixable.
37
+
38
+ - F-F1 — STEP-2 line (`workflows/standard.js:1188`, ×3): `"PUSH_SKIPPED — nothing merged (no record, no lock), VERDICT: PASS"` sits under `"- MERGED: the merge landed; the script pushed inline. Read the push line:"` — but PUSH_SKIPPED is unreachable on the inline path (`cmd_integrate` calls `do_push` directly; only `cmd_push_target` emits it). And `"nothing merged"` collides with the adjacent `"merged empty: no repo changes"` (MERGED_EMPTY).
39
+ - F-F2 — R5-PUSH line (:1201): `"PUSHED — report the merged hash, VERDICT: PASS"` never names the now-normal detached form `PUSHED: origin/main (refspec HEAD:main)`, and the line prints no hash.
40
+ - F-F3 — publish-path.md:1405: `"Supersedes findings 3 and 5 of the 0.14.2 #detached-head-audit above."` — an anchor-link landing names the findings but doesn't summarize them; the stranger must read the whole old section to learn what changed.
41
+ - F-F4 — :1415–1417: presents the destination as a mechanical discovery, but the contract quoted above already defines it. The mechanism implements a mandated answer, it doesn't derive it.
42
+ - F-F5 — :1418: `"The 0.14.2 audit's fib:"` — "fib" is insider shorthand opaque to a stranger.
43
+ - F-F6 — PUSH-TARGET-ANCHOR comment doesn't explain why npm is stricter on no-remote than the integrate path (fail-closed vs fail-soft) — the contract says "fail closed only when genuinely unknowable," and a stranger can't tell whether detached+no-origin is "unknowable" or just "absent."
44
+ - F-F7 — `"Room"` is never defined in the decision file; the stranger meets it four times with no gloss.
45
+
46
+ Submitted-on-issuance vocabulary: ACCEPT — the (a)/(b)/(c) list is a mechanical discriminator (discriminate on `agent_id`/`workflow`, never the word alone), and the doc tells the reader which meaning to use when reading a ledger.
47
+
48
+ ## Step-back round (run by the coordinator, not delegated)
49
+
50
+ Question: is there a better way altogether — should detached work ever be pushed to origin by name, or should rooms always publish onto a branch? Is the destination-resolution question itself an arrogance?
51
+
52
+ Answer: the current shape stands. The candidates:
53
+
54
+ - **Branch-publishing** (check out a branch, push it): forbidden by the contract, and it would reintroduce the room #21 root cause — branch-checkout confusion between the integration line and the checkout. Detached is the architecture.
55
+ - **Per-room remote branches** (`room/<id>`): invents a naming convention the crew would have to track, and the remote's integration target would no longer carry the line — verify-merge, "remote ref advances" evidence, and downstream room consumers all assume the line is visible at the remote integration target.
56
+ - **No push at all** (old policy): rejected by the contract; hid merged work.
57
+
58
+ The destination question is not a new arrogance: the attached same-name push already asks "who guarantees origin/<branch> is the intended target?" — it's convention in both cases. The repo's integration target is the current checkout (room #21 J3); the remote's integration target is origin/HEAD; the refspec connects the two, making explicit where `git push` refuses to guess.
59
+
60
+ The residual fib: **refs/remotes/origin/HEAD is a cached answer, not a live one.** If the remote's default branch is renamed after clone, the resolver pushes to a stale name — creating or updating a dead branch on the remote. Accepted residual risk (room-owned remotes; F-R5's deliberate no-network stance), but worth one line in the decision doc so the next reader doesn't treat the cache as truth.
61
+
62
+ ## Dispositions
63
+
64
+ Accepted (apply before PUBLISH):
65
+
66
+ 1. **F-A1** — Hoist the no-origin check out of the detached branch in the PUSH-TARGET-ANCHOR: `fail "env"` pre-mutation whenever origin is absent, attached or detached. Rationale: step 13 pushes unconditionally; the anchor's own "never after a side effect you cannot roll back" rule applies to both shapes. (Compatible with F-R7: integrate keeps fail-soft NO_REMOTE_PUSH; npm stays strict.)
67
+ 2. **F-R1** — Surface the unknowable reason: `dest=$(resolve_push_destination) || { echo "$dest"; return 1; }` (or emit on stderr); fix the `:542` retry-promise message to not promise recovery for the unknowable mode; ensure `cmd_push_target` doesn't exit silently.
68
+ 3. **F-R2** — Reword the stale rationale at `worktree-lifecycle.sh:502–506` and the `lib/AGENTS.md` NO_REMOTE_RECONCILE line in mechanical terms (the skip is fine — do_push reconciles lazily — but "local by design" is the rejected rationale).
69
+ 4. **F-R3/F-F1** — Remove PUSH_SKIPPED from the `do_push` header print list; drop PUSH_SKIPPED from the STEP-2 inline line (it belongs only to R5-PUSH); reword the R5-PUSH gloss to `"PUSH_SKIPPED — no push attempted (no merge record + no lock held)"` so it can't be confused with MERGED_EMPTY's "merged empty."
70
+ 5. **F-F2** — Name the detached form in both PUSHED clauses: `"(a detached checkout prints e.g. PUSHED: origin/main (refspec HEAD:main) — that is the normal expected form)"`.
71
+ 6. **F-F6** — Add to the PUSH-TARGET-ANCHOR comment: `"A version-bump commit with nowhere to record it would ship an npm version with no git provenance, so no-remote fails closed here rather than skipping like the integrate path."`
72
+ 7. **F-F3** — Expand the supersedes sentence: `"Supersedes findings 3 and 5 of the 0.14.2 #detached-head-audit above — finding 3's 'PUSH_SKIPPED-as-policy is honest' stance and finding 5's 'detached npm publication refuses' stance."`
73
+ 8. **F-F4** — Reword: `"The contract defines the destination as the remote's default branch; the mechanism implements it by reading refs/remotes/origin/HEAD (which git push never resolves on its own, so the mechanism names it explicitly)."`
74
+ 9. **F-F5** — `"The 0.14.2 audit's fib:"` → `"The 0.14.2 audit's error:"`.
75
+ 10. **F-F7** — First use: `"Room repos (the clean-room proof environments) live detached ..."`.
76
+ 11. **F-S1** — Prefix findings 3 and 5 of the preserved 0.14.2 section with `**SUPERSEDED by #detached-head-audit-redo — do not rely on this finding.**` (findings 1, 2, 4 stay unflagged; the section stays as alignment archeology).
77
+ 12. **F-S2** — Cut the two process-narration paragraphs; compress the fixture bullet to: `fixture H (test-detached-integrate.sh) and cases 6/7 (test-publish-preflight.sh) pin the contract behaviorally.`
78
+ 13. **F-S3** — Cut the `do_push` inline comment to one line; the header owns the contract statement.
79
+ 14. **F-S4** — Delete the duplicate inner comment lines in `lib/verify-publish.js:317–318`.
80
+ 15. **F-A2** — Correct the doc's keyTag mechanism sentence to the real one: other same-outcome writes are mutually exclusive by verdict arm (rebuildTrigger non-null only in the receipt arm) and the null-safe `unknown` write is guarded by `if (!publishUnknownFields)`; the issuance/receipt pair is the only same-pass, same-key pair.
81
+ 16. **F-R4** — Add an explicit markered LIFECYCLE check ahead of the anchor (e.g. `: "${LIFECYCLE:?}"` early, or route through `fail "env"`) so the script's `PUBLISH_FAILED=<reason>` header contract holds.
82
+ 17. **F-R9** — Note in the DRY_RUN comment that the preflight now runs first (DRY_RUN requires a push-resolvable environment).
83
+
84
+ Rejected / deferred (with reason):
85
+
86
+ - **F-S5** (distinct outcome word for issuance) — deferred, not this release. The vocabulary is frozen across consumers; renaming now widens the blast radius for no behavioral gain. Record the one-line deferred shape in the doc: `trigger-issued` would make `findTriggerEntry` an exact match and give the two writes naturally distinct agent-call keys.
87
+ - **F-S7** (collapse the double lifecycle call) — rejected: the explicit no-origin branch's more precise error message is worth one subprocess spawn; not worth the churn.
88
+ - **F-R5** (network fallback for origin/HEAD) — rejected: the contract sanctions "the only in-repo mechanical answer"; a network call is outside it, and over-conservative fail-closed is the standing bias.
89
+ - **F-R6** (reconcile incorporates foreign line) — accepted as information, no change: inherent to the contract (fast-forward the remote, never force-push), same semantics as the attached path, announced in the merge message.
90
+ - **F-S6** (keyTag) — confirmed load-bearing, no change.
91
+ - **F-R8** (deferred "submitted already exists" guard) — honest deferral stands; oldest-binds neutralizes misanchoring.
92
+ - **Step-back alternative (branch publishing / per-room branches / no push)** — rejected per the contract and room #21's root cause, recorded above.
93
+
94
+ Panel consensus: no REJECT anywhere; no fail-open path found by any critic. After the 17 accepted fixes are applied and the suite re-run green, this change set is publishable.
@@ -0,0 +1,108 @@
1
+ # Critic review — prospective release 0.14.3
2
+
3
+ **Date:** 2026-09-19. **Scope:** uncommitted diff in `~/workspace/muse-crew` (submitted-on-issuance fix for the room #22 Publish-tail defect, detached-HEAD audit follow-up in `lib/publish-npm.sh`, byte-tightening, test updates). **Method:** four parallel critic subagents (Architect, Subtractor, Reliability, Room Operator), read-only; coordinator spot-verified the two most load-bearing claims (`keyTag` dead code, oldest/newest anchor divergence) against the code before dispositioning. Full suite green (EXIT=0, 571 assertions) before review.
4
+
5
+ **Overall verdict: CONCERN.** All four critics returned CONCERN; none returned REJECT. No critic could construct a fail-open path — the core mechanism ("submitted = invoked with no refusal observed; verifier binds oldest; every gate still requires a real new build plus read-back") holds up adversarially. But the mechanism does not do what its own comments and decision doc claim in several places, and the ledger vocabulary now has three meanings under one word with none of them written down in one place. Fix the ACCEPTED items below, then ship. Nothing here requires redesigning the change.
6
+
7
+ ---
8
+
9
+ ## 1. Architect — CONCERN
10
+
11
+ **A1 (strong). `keyTag` is dead code; the two same-pass `submitted` writes share one agent-call key.** `recordPublishLedger(entry, rework, keyTag)` accepts `keyTag` (`workflows/standard.js:435`; bugfix `:423`; chore `:482`) and the comment at `standard.js:431–434` states the key "must be unique per write within a pass, or a platform call cache could silently drop the second write without appending its line" — but the key is built as `attemptKey("publish-ledger-" + taskId + "-" + entry.outcome, rework)` (`standard.js:457`), ignoring `keyTag`. Zero call sites pass a third argument. This change creates the first same-pass duplicate-outcome pair (issuance `submitted` at `standard.js:1563`, receipt-bearing `submitted` at `:1684`); per the repo's own incident record (bug 1b8bb875, `tests/replay-key.test.js:9`) a reused explicit key trips the runtime's duplicate-key constraint, and the throw is swallowed by `recordPublishLedger`'s catch (`standard.js:463–466`) — so the failure mode is a **silently lost receipt-bearing line**, including the only ledger record of the receipt `agent_id`. Coordinator verified: parameter present, unused in key, no 3-arg callers.
12
+
13
+ **A2 (moderate). Verifier binds oldest, scan binds newest — divergent now that two `submitted` entries exist.** `lib/verify-publish.js:318–319` sorts oldest-first and takes `candidates[0]` (the issuance entry). `findTriggerEntry` (`lib/crew-api.js:1729–1740`) iterates from the end — newest-first — while its comment claims it returns the entry "recorded when the trigger went out." Pre-change at most one `submitted` existed per attempt, so the divergence was latent; the decision doc blesses only the oldest rule. Reachable misclassification: receipt captured → poll inconclusive → unknown parked → scan binds the receipt-bearing entry (T1) → classifier computes `manifestAdvanced = built_at > T1`; a build completing in (T0, T1] reads as stale → `applied-not-built` (terminal, no retry) where the T0 anchor would yield `verified`. Coordinator verified the newest-first loop at `crew-api.js:1731`.
14
+
15
+ **A3 (minor). Thrown vs returned trigger are indistinguishable in the durable ledger.** Both paths land the identical entry — same fields, same constant `detail`. The disposition exists only in unstructured `log()` lines. No machine consumer needs the distinction, but a human investigator cannot tell from the ledger whether the trigger call succeeded or threw. The test pins ("trigger issuance" + "receipt/observation pending" present; "receipt captured"/"build went through" absent) allow appending a disposition token.
16
+
17
+ **Checked and clean:** composition with all consumers (`verify-publish.js` §5b reads only `ts` + `manifest_before.content_sha256` — both present on the issuance entry; `scan-publish-unknown`/`findTriggerEntry`/`retry-publish.js` read `trigger.ts`/`manifest_before` — both entries carry both; `classify-publish-absence.js` receives issuance values in receipt-less cases, exactly the intended fix; `applied_report` is write-only with constant value `"missing-report"`, so the name cannot mislead); `rejected`/`submitted` exclusivity (every `rejected` write returns before the issuance site: `standard.js:1295, 1322, 1501, 1512, 1548–1556`; the issuance write is unconditional after the try/catch and cannot throw past the site).
18
+
19
+ ## 2. Subtractor — CONCERN
20
+
21
+ **S1. The issuance comment restates its own code and detail string.** `workflows/standard.js:1561–1562` (identical in bugfix/chore): "every non-refused trigger lands `submitted`" duplicates `outcome: "submitted"` two lines below; "invoked, no refusal" duplicates the `detail` string. Only the contract name/date/room earn their keep. Delete the second sentence (also frees bytes — see implementation constraints).
22
+
23
+ **S2. The R5-PUSH verdict wording is not accurate, and the audit claims it is.** `workflows/standard.js:1202`: `"PUSH_SKIPPED or NO_REMOTE_PUSH — nothing to push, VERDICT: PASS"`. For detached-with-remote there *is* a commit; the policy is "we choose not to push the detached line" (`lib/worktree-lifecycle.sh:334–341`, whose own comment is more honest than the workflow text). "Nothing to push" collapses PUSH_SKIPPED with NO_REMOTE_PUSH and erases the loud skip/pass distinction the audit celebrates. The audit's "STEP 2 verdict wording confirmed accurate" is overbroad — scope it to the main STEP 2 path (`standard.js:1189`, which does distinguish via its parenthetical). Policy itself stands: PASS-with-loud-skip is defensible over refuse/park (in Gate 1 rooms the sample project has no origin; a park would stall the room on an unresolvable human decision for a merge that genuinely landed).
24
+
25
+ **S3. `PUSH_SKIPPED` is an overloaded token; the verdict template assumes one meaning.** `lib/worktree-lifecycle.sh:342` = "detached HEAD — declined by policy"; `:862` = "no merge record and no lock held — nothing was merged." The STEP 2 template (`standard.js:1189`) reads PUSH_SKIPPED unconditionally as "the detached line is room-local (the merge commit + record are the evidence)" — in the `:862` case there is no merge commit and no record. Reachable only via push-target/R5 past MERGED_EMPTY, so blast radius is small, but two meanings behind one token is the kind of ambiguity this codebase refuses.
26
+
27
+ **S4. `docs/publish-verification.md` §4b.1 went stale inside this change.** Line 276 says to use "the `submitted` entry with this `<commit>`" and its `agent_id` field ("it may still be null — then this step is vacuous"). Under submitted-on-issuance the oldest matching `submitted` is the issuance entry, whose `agent_id` is *always* null — a literal reading now makes build-ID correlation permanently vacuous, missing the later receipt-bearing entry that carries the observed agent_id. Fix: say "the receipt-bearing `submitted` entry" and distinguish oldest from latest.
28
+
29
+ **S5 (minor). Step 13's comment half-restates the preflight.** `lib/publish-npm.sh:300–302`: the first sentence ("PUSH_TARGET was resolved in the preflight above") is load-bearing cross-reference; the second restates the PUSH-TARGET-ANCHOR contract. Trim the second (frees bytes). Verified: resolution exists in exactly one place (`:95`), consumed at `:303`; the anchor sits after PREFLIGHT-END before DRY_RUN with only read-only probes ahead — "before ANY mutation" holds. The split is justified (resolution+refusal pre-mutation, push post-verify); merging recreates the original bug or a premature push.
30
+
31
+ **Checked:** no test in `tests/publish-submitted-on-issuance.test.js` is exactly redundant with an existing test (closest overlaps are subsumed-but-not-duplicated); no doc duplication (the detached-HEAD audit section is genuinely new material).
32
+
33
+ ## 3. Reliability — CONCERN
34
+
35
+ **R1. Same as A1** (`keyTag` dead code). Independent corroboration — treat as one finding (F1).
36
+
37
+ **R2. Same as A2** (anchor divergence), plus: the verifier's own comment (`lib/verify-publish.js:302` and `:317`) claims "audit observations are now `build-observed`, not `submitted`" — factually wrong while the receipt path still writes a second `outcome: "submitted"`. Fix or remove the stale comment.
38
+
39
+ **R3 (benign, verified).** The verifier's field selection handles the issuance entry correctly: only `e.ts` and `e.manifest_before` are read (`verify-publish.js:320–321`); null `agent_id` irrelevant; null `manifest_before` fails closed at `:329–330`; 1-second timestamp ties resolve by stable sort → issuance first. No action.
40
+
41
+ **R4 (benign).** Thrown-trigger anchor timestamp: stamped by the ledger-write child *after* the trigger call settled, so "build before anchor" reads as stale → `unverifiable-content`/`ambiguous`/`applied-not-built`/`provably-dropped` — all non-stamping. No false pass. One genuine weakening to name: the anchor moved *earlier* than the old receipt-time anchor, widening the `built_at > trigger` window — bounded by the traveling stronger gates (`content_sha256` must differ from the pre-trigger baseline; read-back must match the diff). The decision doc does not acknowledge this relaxation. No action beyond noting it here.
42
+
43
+ **R5 (benign).** No fail-open found. The newly-enabled recovery path is strictly stronger than the old fallback: previously a receipt-less unknown fell back to `--trigger-ts` = park-note timestamp with no `--manifest-before`, and the classifier's `verified` branch ran on time-only freshness; now the issuance entry supplies a real baseline enabling the strict sha-identity branch. The "no submitted ledger entry" fail-closed (`verify-publish.js:327`) still fires exactly where it should (explicit refusal, pre-trigger parks, issuance-write failure). No action.
44
+
45
+ **R6. Crash between issuance write and observation leaves recovery with nothing to act on (pre-existing hazard, newly visible).** Ledger tail is an issuance-only `submitted` with no `unknown` follow-up — indistinguishable from "observation still running" by the ledger alone. `derivePublishUnknownAttempt` requires an unresolved `unknown` entry, so scan/retry do nothing; the task stays parked for human attention (fail-closed, safe). The asymmetry: the change's purpose is giving recovery a trigger anchor, yet the crash-during-observation window — the most likely crash to need recovery — still has no recovery path, because the anchor is only reachable via the `unknown` entry that was never written. Worse, the actual recovery path (workflow resume re-running Publish from the top) re-issues the trigger with the **same** `rebuildAttemptKey` (keyed by `totalReworkCount`, unchanged) — the pre-existing duplicate-edit hazard from 2026-09-12 — and writes a **second** issuance `submitted` for the same attempt key, which now looks intentional. No "submitted already exists for this attempt" guard at the Publish entry.
46
+
47
+ ## 4. Room Operator — CONCERN
48
+
49
+ **O1/O7. The detail string actively negates but never positively states its claim; the double negative dies under fatigue.** `workflows/standard.js:1570`: `"trigger issuance: rebuild-trigger agent invoked, no refusal — receipt/observation pending; asserts neither artifact_edit nor build acceptance"`. A 2am skim lands on `outcome: "submitted"` + "no refusal" — one saccade from "accepted." "Asserts neither X nor Y" requires holding two negations to recover the actual claim. Proposed replacement (pins-safe: keeps "trigger issuance" + "receipt/observation pending"; avoids "receipt captured"/"build went through"): `"trigger issuance: rebuild-trigger agent invoked (no explicit refusal observed); receipt/observation pending. This entry proves ONLY invocation — it is not evidence that artifact_edit ran or that any build was accepted."`
50
+
51
+ **O2. The decision doc's History is factually incomplete.** `docs/decisions/publish-path.md:1293` claims "the old contract wrote `submitted` only beside a captured receipt agent_id." Wrong: `lib/retry-publish.js:349` (pre-existing, untouched) already writes `outcome: "submitted"`, `agent_id: null`, `workflow: "unknown-recovery"`, detail `"retry trigger re-issued after provably-dropped classification"` — asserting something *stronger* than the new issuance entry. After this change, `submitted` + null `agent_id` has two meanings distinguished only by the `workflow` field, and the doc never says so. The "one broken contract in three workflow files" narrative (`publish-path.md:1298`) is overstated: the recovery writer already knew how to write issuance-without-receipt; the workflow writers were the gap. Coordinator verified `retry-publish.js:349`.
52
+
53
+ **O3. No read-site comment warns about `agent_id: null`; both ledger readers ignore `agent_id` entirely.** `lib/verify-publish.js` has zero references to `agent_id`; `lib/retry-publish.js:185` filters on `outcome`/`commit`/`attempt` only. The three-way ambiguity of `submitted` (issuance-null / receipt-bearing / recovery-reissue-null) is documented at zero read sites.
54
+
55
+ **O4. Same as A2/R2** (oldest/newest binding). Independent corroboration.
56
+
57
+ **O5. The decision section does not give the full outcome vocabulary in one place.** `docs/decisions/publish-path.md:1301–1306` lists issuance `submitted`, receipt-bearing `submitted`, `build-observed`, `unknown`, `failed` — but `rejected` appears only in Edge cases with no definition (from the code it is conclusive negative evidence: no trigger issued, park, human attention — the one outcome where "parked" is *terminal*); the receipt-bearing `submitted` is named but never defined in the section; `unknown` is listed but not defined; and nowhere is the **triple meaning of `submitted`** enumerated: (a) issuance-only, null agent_id, workflow writer; (b) receipt-bearing, non-null agent_id, workflow writer; (c) re-issue after provably-dropped, null agent_id, `unknown-recovery` writer.
58
+
59
+ **O6. The old receipt-bearing `submitted` detail now reads as the strong claim under a weakened word.** `workflows/standard.js:1691` (pre-existing): `"fire-and-forget trigger; build receipt captured by workflow-owned build-state observation (pre/post-trigger diff)"`. An operator scanning two `submitted` lines reads the second as the stronger confirmation of the first — backwards from the intended semantics (the first is the anchor; the second is just another observation). Symmetric honesty wants a scope clause on this line too (careful: `publish-ledger.test.js` pins a whole-block substring here — append, don't rewrite).
60
+
61
+ **O8. The detached-HEAD preflight message diagnoses but does not prescribe.** `lib/publish-npm.sh:98`: `"detached HEAD: publish-npm cannot push a version-bump commit from a detached checkout — no remote ref exists for the detached line"`. As an operator seeing `PUBLISH_FAILED=env`: no statement that nothing was mutated, no next step, no retryability signal. Proposed append: `" — no version-bump commit was created and nothing was published; attach the checkout to a branch (git checkout <branch>) and re-run."` (`test-publish-preflight.sh` pins only markers, not message text — safe.)
62
+
63
+ **Checked and sound:** issuance call-site position genuinely after the trigger try/catch and before observation; explicit-refusal path parks `rejected` before the issuance site (no double-record); the new test's structural coverage (position, fields, refusal, first-submitted ordering, symmetry) is the right shape. Note: the pre-existing `rejected` writers already used `agent_id: null`, so null agent_id was never the novelty — only its pairing with `submitted` in the *workflow* was, which makes O2's doc fix the thing to repair, not the code.
64
+
65
+ ---
66
+
67
+ ## 5. Step-back round (coordinator)
68
+
69
+ **The question the panel didn't ask:** *Should the issuance entry have been a new outcome value (e.g. `trigger-issued`) instead of overloading `submitted` — accepting the cost of updating every consumer filter for an unambiguous vocabulary?*
70
+
71
+ **Honest answer:** The overload was load-bearing, not lazy. Four consumers key anchor-finding on `outcome === "submitted"`: `verify-publish.js`'s oldest-match filter, `crew-api.js` `findTriggerEntry`, `retry-publish.js`'s trigger-entry lookup, and the "no submitted ledger entry for commit" fail-closed that was itself the room #22 park reason. A new outcome value would have required touching all four filters plus their tests, and any missed consumer would silently reintroduce the exact gap this change closes (anchor not found → fail closed → parked room). The overload guarantees every existing anchor-reader finds the anchor on day one. The cost — three meanings of `submitted` — is concentrated in *human* legibility, which is repairable with prose (the vocabulary enumeration in F5/F11 below) without touching the machine contract. The one durable consequence: any future consumer that needs "was a receipt observed" must discriminate on `agent_id`/`workflow` fields, never on the outcome word — that rule belongs in the vocabulary doc. **No design change.** The overload stands; the documentation repair is the disposition.
72
+
73
+ (Considered and rejected as the step-back: moving the anchor off the agent-written ledger into workflow-owned state. The workflow scripts cannot read the wall clock — `tests/determinism.test.js` bans it — so the ledger child's `date -u` stamp is the only clock available. The current placement is forced by that constraint, not chosen.)
74
+
75
+ ---
76
+
77
+ ## 6. Synthesis — explicit dispositions
78
+
79
+ Deduped across critics. **ACCEPTED** = fix before 0.14.3 ships. **DEFERRED** = acknowledged, scheduled later or needs the parent's call. **NOTED** = verified benign, no action.
80
+
81
+ | # | Finding (sources) | Disposition | Reason / implementation note |
82
+ |---|---|---|---|
83
+ | F1 | `keyTag` dead code; same-pass duplicate agent-call key risks silent drop of the receipt-bearing line (A1, R1) | **ACCEPTED** | Wire `keyTag` into the key at `standard.js:457` (and bugfix/chore twins), pass `"issuance"` / `"receipt"` at the two call sites, and extend `tests/publish-submitted-on-issuance.test.js` to pin within-pass key disjointness. Two independent critics + coordinator verification; the comment describes a mechanism the code lacks, and the silent-drop failure mode is exactly what the comment warns about. |
84
+ | F2 | Anchor-selection divergence: doc mandates oldest; `findTriggerEntry` (`lib/crew-api.js:1729`) and `retry-publish.js` newest-pick implement newest (A2, R2, O4) | **ACCEPTED** | Unify on oldest in all three to match `verify-publish.js:318–319` and the decision doc. Four-way corroboration; the divergence is newly *active* (two `submitted` lines per attempt is now the normal receipt path) with a reachable misclassification (`applied-not-built` where `verified`). |
85
+ | F3 | Stale verifier comment (`lib/verify-publish.js:302,317`: "audit observations are now `build-observed`, not `submitted`") (R2) | **ACCEPTED** | Factually wrong while the receipt path writes a second `outcome: "submitted"`. One-line fix. |
86
+ | F4 | Decision-doc history inaccurate re pre-existing recovery `submitted` (`lib/retry-publish.js:349`); "one broken contract" narrative overstated (O2) | **ACCEPTED** | Correct `publish-path.md:1293`; coordinator verified the pre-existing writer. A decision doc that misstates history will mislead the next root-cause session. |
87
+ | F5 | Triple meaning of `submitted` undocumented; outcome vocabulary incomplete in one place; no read-site comments (O3, O5) | **ACCEPTED** | Enumerate the three meanings + full outcome vocabulary (`submitted` ×3, `build-observed`, `unknown`, `failed`, `rejected` — with `rejected` defined as conclusive-negative/terminal-park) in `docs/decisions/publish-path.md`, and add one-line `agent_id`/binding comments at `lib/verify-publish.js` ~308 and `lib/retry-publish.js` ~184. |
88
+ | F6 | Issuance detail double-negative; "no refusal" skimmable as "accepted" (O1, O7) | **ACCEPTED** | Replace with the positive-claim wording proposed in O1 (pins-safe: retains "trigger issuance" + "receipt/observation pending", avoids banned phrases). |
89
+ | F7 | Receipt-bearing `submitted` detail lacks scope clause under the weakened word (O6) | **ACCEPTED** (minor) | Append a short scope clause (e.g. "; observation only, not a second issuance") — append-only, to preserve the `publish-ledger.test.js` whole-block substring pin. |
90
+ | F8 | Thrown vs returned trigger indistinguishable in ledger (A3) | **ACCEPTED** (minor) | Append the disposition ("trigger-returned" / "trigger-threw") to the issuance `detail`; test pins allow appending. Cheap durability for the next investigator. |
91
+ | F9 | `docs/publish-verification.md` §4b.1 stale — oldest `submitted` now always null-`agent_id` (S4) | **ACCEPTED** | Direct the reader to the receipt-bearing `submitted` entry and distinguish oldest from latest. The operational doc drifted in the same change that changed the semantics it documents. |
92
+ | F10 | R5-PUSH "nothing to push" wording inaccurate; audit accuracy claim overbroad (S2) | **ACCEPTED** (wording) | Fix the `standard.js:1202` wording to the honest policy ("detached line is room-local by policy — merge landed, not pushed"), and scope the audit's "confirmed accurate" claim in `publish-path.md` to the main STEP 2 path. Mechanism (PASS-with-loud-skip) stands. |
93
+ | F11 | `PUSH_SKIPPED` overloaded token; verdict template assumes the detached meaning (S3) | **ACCEPTED** (minimal) | Narrow the template or disambiguate the token. Small blast radius (R5 path only); keep the fix byte-cheap and check for pinned strings before editing prompt text. |
94
+ | F12 | Issuance comment restates code + detail; delete second sentence (S1) | **ACCEPTED** | Pure deletion; also funds the byte budget (see constraints). |
95
+ | F13 | Step-13 comment half-restates preflight; trim second sentence (S5) | **ACCEPTED** | Trivial; preserves the load-bearing cross-reference. |
96
+ | F14 | Detached-HEAD preflight message not actionable (O8) | **ACCEPTED** | Append the no-mutation assurance + next step (`git checkout <branch>`, re-run). `test-publish-preflight.sh` pins markers only — safe. |
97
+ | F15 | Crash between issuance write and observation: no recovery path; resume re-issues the same attempt key (R6) | **ACCEPTED** (document) / **DEFERRED** (mechanical guard) | Document as accepted risk in the decision doc now. A Publish-entry "submitted already exists" guard changes resume semantics and needs the parent's explicit call — do not smuggle it into this release. |
98
+ | F16 | Reliability benigns: verifier field selection correct; thrown-trigger timestamp fail-closed; no fail-open; new recovery path strictly stronger (R3, R4, R5) | **NOTED** | No action. Recorded so the next review doesn't re-litigate. |
99
+ | F17 | Architect composition otherwise clean: `applied_report` write-only, `manifest_before` honest, `rejected`/`submitted` exclusivity (A brief 1/2/4) | **NOTED** | No action. |
100
+ | F18 | Subtractor: no test redundancy, no doc duplication, preflight/step-13 split justified (S5, Q4, Q5) | **NOTED** | No action. |
101
+
102
+ **Implementation constraints for the parent:**
103
+ 1. **Byte budget.** `workflows/bugfix.js` is at 245,736 of 245,760 (24 bytes headroom). The accepted wording additions (F6, F7, F8, F10, F11) must be byte-neutral or funded by the accepted deletions (F12 ≈ −100, F13 ≈ −60). Re-run `node tests/workflow-size.test.js` after editing.
104
+ 2. **Test pins constraining workflow wording.** The issuance `detail` must keep "trigger issuance" + "receipt/observation pending" and must not contain "receipt captured"/"build went through" (`tests/publish-submitted-on-issuance.test.js`); the anchor comment `// (2026-09-19, room #22) Submitted-on-issuance` is pinned; "Attribution is by window, not by build identity" is pinned verbatim (`tests/publish-audit-fallback.test.js`).
105
+ 3. **New test pin.** Extend `tests/publish-submitted-on-issuance.test.js` with within-pass agent-call-key disjointness for the two `submitted` writes (F1 follow-up).
106
+ 4. **Decision-doc updates** (F4, F5, F15-doc) belong in the same commit as the code fixes — the doc already drifted once this cycle (F9).
107
+
108
+ **Bottom line:** ship-blocking items are F1–F15 (wording, comments, one real code fix in F1, one selection-logic fix in F2, doc repairs). No redesign, no new mechanism, no fail-open anywhere. After the fixes: re-run loader gate + full suite, then proceed to PUBLISH 0.14.3.