muse-crew 0.14.2 → 0.14.4

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.
Files changed (46) hide show
  1. package/AGENTS.md +1 -1
  2. package/docs/decisions/AGENTS.md +4 -0
  3. package/docs/decisions/publish-path.md +229 -0
  4. package/docs/publish-verification.md +14 -3
  5. package/docs/release-integrity.md +60 -0
  6. package/docs/reviews/critic-0143-redo.md +94 -0
  7. package/docs/reviews/critic-0143.md +108 -0
  8. package/docs/reviews/critic-0144.md +83 -0
  9. package/lib/AGENTS.md +14 -6
  10. package/lib/advance-publish-base.js +8 -0
  11. package/lib/append-ooda-step.js +12 -3
  12. package/lib/build-readback-request.js +10 -0
  13. package/lib/build-registry.js +8 -3
  14. package/lib/classify-publish-absence.js +11 -0
  15. package/lib/classify-surface.js +12 -2
  16. package/lib/commit-scaffold.js +22 -6
  17. package/lib/compose-evidence-caption.js +15 -4
  18. package/lib/compute-publish-diff.js +9 -0
  19. package/lib/crew-api.js +55 -20
  20. package/lib/crew-release.sh +233 -1
  21. package/lib/gitignore.js +23 -5
  22. package/lib/package.json +1 -0
  23. package/lib/publish-note-vocabulary.js +44 -0
  24. package/lib/publish-npm.sh +39 -10
  25. package/lib/read-ooda-verdict.js +11 -3
  26. package/lib/readback-disk.js +9 -0
  27. package/lib/render-html.js +17 -7
  28. package/lib/repo-orchestration.js +21 -5
  29. package/lib/retry-publish.js +79 -44
  30. package/lib/sample-project.js +22 -6
  31. package/lib/scaffold-crew.js +11 -2
  32. package/lib/see-act.js +17 -8
  33. package/lib/serve-artifact.js +12 -6
  34. package/lib/setup-project-repo.js +26 -7
  35. package/lib/test-detached-integrate.sh +79 -3
  36. package/lib/test-publish-preflight.sh +94 -5
  37. package/lib/update-watch.js +34 -17
  38. package/lib/ux-doctrine.js +31 -6
  39. package/lib/verify-publish.js +41 -6
  40. package/lib/worktree-lifecycle.sh +67 -25
  41. package/lib/write-ooda-verdict.js +12 -3
  42. package/package.json +1 -1
  43. package/workflows/bugfix.js +60 -61
  44. package/workflows/chore.js +60 -61
  45. package/workflows/standard.js +60 -42
  46. package/workflows/upgrade.js +4 -2
package/AGENTS.md CHANGED
@@ -7,7 +7,7 @@ Muse Crew source repository. The repo is the product; the personal instance (`$C
7
7
  - `API.md` — the Crew API contract: every action a task service must implement
8
8
  - `.orchestration/` — crew configuration notes (deploy config lives in the crew's project record — the single source of truth, read via the Crew API)
9
9
  - `identities/` — crew member character files and portraits
10
- - `lib/` — shell scripts for release, merge, worktree, and cleanup
10
+ - `lib/` — shipped library: ESM JavaScript CLIs and import-safe modules (pinned by `lib/package.json`'s `{"type": "module"}`), shell scripts for release, merge, worktree, and cleanup, and Python evidence tools. Every entry is executed for real by the release entry gate (`_validate_lib_entries` in `lib/crew-release.sh`; contract: `docs/release-integrity.md`).
11
11
  - `personas/` — QA perspective costumes for Hazel
12
12
  - `seed/` — init source data: everything `crew-init.js` reads when setting up a new crew instance
13
13
  - `workflows/` — executable Muse workflow scripts (JavaScript)
@@ -53,6 +53,10 @@ 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
+ - `#d1-issuance-time` — D1 issuance-time field (`issued_at`; `entry_kind` cut 2026-09-20)
58
+ - `#detached-head-audit` — Detached-HEAD audit (0.14.2)
59
+ - `#detached-head-audit-redo` — Detached-HEAD audit REDO (0.14.3, supersedes findings 3/5)
56
60
 
57
61
  ## qa-reproduce.md — Decision history: qa reproduce
58
62
 
@@ -1278,3 +1278,232 @@ 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 anchors on the oldest matching "submitted" entry by `ts` and
1324
+ binds its `issued_at` (falling back to its `ts` when the capture was
1325
+ unobserved) — one anchor, one field. The issuance entry still sorts first,
1326
+ and the observation entries stay as additional lines.
1327
+
1328
+ <a id="d1-issuance-time"></a>
1329
+ ### D1 (2026-09-19): issuance-time fields
1330
+
1331
+ The trigger instant is the ISSUANCE instant, not the ledger-write instant.
1332
+ Room #23 J1 (room: the Gate 1 clean-room evidence run; J1: the CLI-only,
1333
+ no-project-in-mind journey): a slow ledger write (entry `ts` minutes after
1334
+ the trigger went
1335
+ out) made a fresh build look stale — the manifest's `built_at` fell between
1336
+ the true issuance and the ledger-write `ts`, and the ts-anchored freshness
1337
+ gate failed a build that was actually new.
1338
+
1339
+ Ledger serialization (`recordPublishLedger`, byte-identical in
1340
+ standard/bugfix/chore) carries one field, null on entries with no trigger
1341
+ (refused / unknown / rejected paths):
1342
+ - `issued_at`: an UPPER bound on the trigger-issuance instant (2026-09-20,
1343
+ critic-0144 F-A1: captured in program order AFTER the trigger-agent call
1344
+ returns — a pre-trigger capture is a lower bound, and a stranger build
1345
+ landing between the capture and the true issuance could false-verify).
1346
+ Workflow scripts have no clock (determinism guard), so the instant is
1347
+ ferried from one external shell `date -u +%Y-%m-%dT%H:%M:%SZ` call through
1348
+ a schema'd agent ferry, validated mechanically against strict
1349
+ `YYYY-MM-DDTHH:MM:SSZ` shape, under an attempt-scoped
1350
+ `publish-issued-at-<taskId>` replay key. A failed capture leaves `issued_at`
1351
+ null — the verifier falls back to the ledger-write `ts`. No new wall-clock
1352
+ inference beyond this ferry: the capture IS the design.
1353
+
1354
+ Both writes carry the same captured value — one capture, two entries.
1355
+ (The retired `entry_kind` field — "issuance"/"receipt" — was cut 2026-09-20:
1356
+ issuance and receipt entries carried the same captured `issued_at`, so the
1357
+ preference filter could never change the bound anchor. The distinct
1358
+ agent-call keyTags "issuance"/"receipt" survive as replay keys.)
1359
+
1360
+ Consumers (one anchor, shared): `lib/verify-publish.js`,
1361
+ `lib/crew-api.js::findTriggerEntry`, the classifier
1362
+ (`lib/classify-publish-absence.js`), and the retry writer
1363
+ (`lib/retry-publish.js`) all take the oldest matching "submitted" by `ts`
1364
+ and bind `issued_at || ts`. Platform nonce: deferred, not filed in this
1365
+ change.
1366
+
1367
+ Edge cases:
1368
+ - Explicit ARTIFACT_EDIT_REFUSED still parks rejected BEFORE this site and
1369
+ never writes "submitted" — conclusive negative evidence is unchanged.
1370
+ - A throwing trigger-agent call lands here too: the catch swallows the throw
1371
+ because the edit may still have gone through, and this entry is then the
1372
+ recovery's trigger timestamp. It still does not claim the call succeeded.
1373
+ - `recordPublishLedger` is best-effort and non-gating; a write failure does
1374
+ not break the publish flow.
1375
+ - Distinct agent-call keys per write (2026-09-19 REVIEW): the issuance write
1376
+ and the later receipt-bearing "submitted" write execute in the same pass
1377
+ with the same task/outcome/rework key base. The runtime replays cached
1378
+ agent() calls by key, so the two writes pass keyTag "issuance" / "receipt"
1379
+ and the key construction disambiguates — a shared key would silently drop
1380
+ the second write and its ledger line. They are the only same-pass,
1381
+ same-key pair: the other same-outcome writes are mutually exclusive by
1382
+ verdict arm (rebuildTrigger is non-null only in the receipt arm), and the
1383
+ null-safe "unknown" write is guarded by `if (!publishUnknownFields)`.
1384
+ - Deferred vocabulary (2026-09-19 critic review): a distinct outcome word
1385
+ like `trigger-issued` for the issuance write would make `findTriggerEntry`
1386
+ an exact match and give the two writes naturally distinct agent-call keys.
1387
+ Not this release — the vocabulary is frozen across consumers; renaming
1388
+ widens the blast radius for no behavioral gain.
1389
+ - Crash between the issuance write and the observation (accepted risk,
1390
+ 2026-09-19 REVIEW): the ledger tail is an issuance-only "submitted" with
1391
+ no "unknown" follow-up — indistinguishable from "observation still
1392
+ running" by the ledger alone, so scan/retry do nothing and the task waits
1393
+ for human attention (fail-closed, safe). A Publish-entry "submitted
1394
+ already exists" guard would change resume semantics; deferred — it needs
1395
+ an explicit call, not smuggling into this release. The hazard it would
1396
+ guard: re-running Publish re-issues the trigger with the same
1397
+ rebuildAttemptKey (keyed by totalReworkCount, unchanged) and writes a
1398
+ second issuance "submitted" for the same attempt key.
1399
+
1400
+ <a id="detached-head-audit"></a>
1401
+ ## Detached-HEAD audit (0.14.2, room #22)
1402
+
1403
+ Mandated before room #22 but not performed; performed 2026-09-19 during the
1404
+ 0.14.3 BUILD. Scope: lib/worktree-lifecycle.sh, lib/publish-npm.sh,
1405
+ workflows/{standard,bugfix,chore}.js, lib/test-detached-integrate.sh,
1406
+ tests/integrate-reconcile.test.js.
1407
+
1408
+ Findings:
1409
+
1410
+ 1. `integration_target()` resolves the branch name, or the literal "HEAD"
1411
+ when detached. Nothing in the lifecycle ever checks out a branch
1412
+ (verified by grep — only comments name the old `checkout main` defect).
1413
+ 2. `cmd_integrate` merges into the current checkout — `git merge` advances a
1414
+ detached HEAD directly. The CREW_STAGED_BASE guard (merge-base against
1415
+ HEAD) preserves the reviewed-SHA and crew-init ancestry; the merge record
1416
+ names `integration_target=HEAD`; `verify-merge` checks the record against
1417
+ the LIVE tip (never the record's word for the tip — fixture C).
1418
+ 3. **[SUPERSEDED by #detached-head-audit-redo — do not rely on this finding.]**
1419
+ `do_push` on a detached HEAD reports PUSH_SKIPPED and returns 0. This is
1420
+ deliberate policy, not a git limitation: no remote ref exists for the
1421
+ detached line, and pushing `HEAD:main` would claim a branch the room does
1422
+ not own. The workflow's STEP 2 verdict treats PUSH_SKIPPED as PASS with
1423
+ the merge commit + record + VERIFIED as the evidence. This is honest, not
1424
+ hollow: the merge landed (MERGED, 2-parent commit, VERIFIED) — only
1425
+ propagation is skipped, loudly. In Gate 1 rooms the sample project has no
1426
+ origin (fresh `git init` from the tarball), so "detached + remote" cannot
1427
+ arise; fixture A step 9 pins the skip as the expected behavior.
1428
+ 4. publish-npm.sh resolves its push destination through the same
1429
+ `integration-target` resolver (2026-09-19 REVIEW removed the old
1430
+ hardcoded `git push origin main`).
1431
+ 5. **[SUPERSEDED by #detached-head-audit-redo — do not rely on this finding.]**
1432
+ publish-npm.sh's detached refusal, corrected in place: the refusal itself
1433
+ stands (there is nowhere to push a detached version-bump commit), but it
1434
+ sat at step 13 — AFTER the registry publish — so a detached invocation
1435
+ left a shipped version with a failed phase and a retry loop. Moved to the
1436
+ preflight (PUSH-TARGET-ANCHOR, before any mutation): a detached checkout
1437
+ now fails fast with PUBLISH_FAILED=env and zero side effects. Verified
1438
+ behaviorally: detached → exit 1, no commit, clean tree; branch →
1439
+ proceeds. The npm tarball publish itself was never refused — only the
1440
+ unpushable git record.
1441
+
1442
+ Applies to: lib/publish-npm.sh (preflight), lib/worktree-lifecycle.sh
1443
+ (unchanged — policy confirmed), workflows (unchanged — main STEP 2 verdict
1444
+ wording confirmed accurate; the R5-PUSH line's "nothing to push" wording was
1445
+ corrected separately, 2026-09-19 REVIEW).
1446
+
1447
+ <a id="detached-head-audit-redo"></a>
1448
+ ## Detached-HEAD audit REDO (0.14.3)
1449
+
1450
+ Supersedes findings 3 and 5 of the 0.14.2 `#detached-head-audit` above —
1451
+ finding 3's "PUSH_SKIPPED-as-policy is honest" stance and finding 5's
1452
+ "detached npm publication refuses" stance. The 0.14.3 critic panel blessed
1453
+ the old refusal/PUSH_SKIPPED→PASS takes; those dispositions were REJECTED —
1454
+ they contradict the mandatory contract (Eric, 2026-09-19): **a detached
1455
+ HEAD with a remote pushes as explicit `HEAD:<destination>`; detached npm
1456
+ publication works, it does not refuse; fail closed only when the
1457
+ destination is genuinely unknowable; never check out a local branch;
1458
+ preserve the reviewed-SHA → staged-base → crew-init ancestry.** A critic's
1459
+ blessing is checked against the contract, not taken as truth.
1460
+
1461
+ The contract defines the destination as the remote's default branch; the
1462
+ mechanism implements it by reading `refs/remotes/origin/HEAD` (which
1463
+ `git push` never resolves on its own, so the mechanism names it
1464
+ explicitly). The 0.14.2 audit's error: it treated git's no-automatic-ref
1465
+ behavior as a property of the contract ("no remote ref exists for the
1466
+ detached line"), and made policy out of a gap the mechanism was allowed to
1467
+ close. Room repos (the clean-room proof environments) live detached
1468
+ (reviewed SHA + crew-init commits); rooms #18–#22 carried that shape, so a
1469
+ refusal is a refusal of the normal room. Residual fib (2026-09-19 critic
1470
+ review): `refs/remotes/origin/HEAD` is a *cached* answer, not a live one —
1471
+ a renamed remote default branch makes it stale. Accepted residual risk in
1472
+ room-owned remotes (the deliberate no-network-fallback stance).
1473
+
1474
+ Behavior:
1475
+
1476
+ - `lib/worktree-lifecycle.sh`: `resolve_push_destination()` — attached
1477
+ resolves to the branch name; detached resolves to the remote's default
1478
+ branch (bare name) via `refs/remotes/origin/HEAD`; fails closed
1479
+ ("genuinely unknowable") when origin/HEAD is unset, and the reason is
1480
+ surfaced (2026-09-19 critic finding F-R1 — the loop must see the
1481
+ remediation hint). `push-destination` is the thin command wrapper.
1482
+ `do_push` runs the NO_REMOTE_PUSH check first (unchanged), then pushes
1483
+ the explicit refspec — `<branch>` attached, `HEAD:<dest>` detached —
1484
+ printing `PUSHED: origin/<dest> (refspec <refspec>)`; the reconcile path
1485
+ uses `$dest`. An unknowable destination fails closed — never a silent
1486
+ skip, never a refusal of a knowable push. PUSH_SKIPPED means exactly one
1487
+ thing: no push attempted (no merge record + no lock held, the R5
1488
+ manual path only).
1489
+ - `lib/publish-npm.sh`: the PUSH-TARGET-ANCHOR resolves `PUSH_REFSPEC` in
1490
+ the preflight, before any mutation; step 13 pushes it (never a bare
1491
+ HEAD). Detached + resolvable origin passes through; no origin at all
1492
+ (attached or detached) fails fast with `PUBLISH_FAILED=env` before any
1493
+ mutation (2026-09-19 critic finding F-A1); detached + origin with no
1494
+ origin/HEAD fails fast the same way, repo untouched.
1495
+ - workflows/{standard,bugfix,chore}.js: STEP-2 + R5-PUSH wording —
1496
+ PUSH_SKIPPED names only the nothing-merged path ("no push attempted (no
1497
+ record + no lock)"), and both PUSHED clauses name the detached form (a
1498
+ detached checkout's line prints e.g. `PUSHED: origin/main (refspec
1499
+ HEAD:main)` — that is the normal expected form).
1500
+ - Tests: fixture H in `lib/test-detached-integrate.sh` and cases 6/7 in
1501
+ `lib/test-publish-preflight.sh` pin the contract behaviorally.
1502
+
1503
+ No force-push anywhere; nothing in the lifecycle checks out a branch.
1504
+
1505
+ Applies to: lib/worktree-lifecycle.sh (resolve_push_destination,
1506
+ push-destination, do_push refspec), lib/publish-npm.sh
1507
+ (PUSH-TARGET-ANCHOR refspec), workflows/{standard,bugfix,chore}.js
1508
+ (STEP-2/R5-PUSH wording), lib/test-detached-integrate.sh (fixture H),
1509
+ lib/test-publish-preflight.sh (cases 6/7).
@@ -171,7 +171,15 @@ LLM never judges. The division:
171
171
  stamp back exactly, logs the terminal verdict, and re-queues to
172
172
  `in_progress`. Unparseable findings, mismatches, supersession, and stamp
173
173
  failures all fail CLOSED with a terminal `publish: verification-failed`
174
- verdict — never a stamp.
174
+ verdict — never a stamp. The terminal vocabulary is a closed registry in
175
+ `lib/publish-note-vocabulary.js` (D7, 2026-09-19): `scan-publish-unknown`
176
+ skips a recognized terminal note as terminal with its meaning named (never
177
+ as `unrecognized-publish-note`), and `verify-publish.js`'s `terminal()`
178
+ asserts its emitted verb is in the registry before writing.
179
+ `tests/publish-note-vocabulary.test.js` closes the enum structurally:
180
+ every `publish: <verb>` literal in lib/ must be declared in the terminal
181
+ registry or the pinned transitional set, so no future terminal verb ships
182
+ unrecognized.
175
183
  - **Envelope:** the tick saves the COMPLETE handoff — the full prose
176
184
  report AND the full JSON result, both verbatim (raw prose, JSON, or
177
185
  both concatenated are all accepted). Observed 2026-09-14: the
@@ -273,8 +281,11 @@ For a task parked with `publish: verification-requested <commit>`:
273
281
  1. **Expected** = the agent_id in the park message's `(build …)` suffix.
274
282
  If the suffix says `agent_id unobserved`, look up the workflow's
275
283
  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).
284
+ for the `submitted` entries with this `<commit>`: the OLDEST is the
285
+ trigger issuance (its `agent_id` is always null — the trigger instant,
286
+ not a build identity); the receipt-bearing `submitted`, if present, is
287
+ a LATER entry carrying the observed `agent_id` — use that one here
288
+ (if it is absent or null, this step is vacuous).
278
289
  2. **Live** = whether the expected agent_id appears anywhere in the
279
290
  read-back result (the live artifact status exposes no durable
280
291
  agent_id — only an in-flight correlation ID that expires with the
@@ -0,0 +1,60 @@
1
+ # Release integrity: the lib entry gate
2
+
3
+ Every JS file shipped in a release's `lib/` must actually execute;
4
+ `sh`/`py` entries are parse-checked (real `.sh` execution would risk side
5
+ effects). This page is the contract; the mechanism is
6
+ `_validate_lib_entries` in `lib/crew-release.sh`, wired into `cmd_deploy`
7
+ right after `_validate_workflows`. Behavioral pins live in
8
+ `tests/entry-gate.test.js`.
9
+
10
+ ## ESM-only lib
11
+
12
+ `lib/` is ESM-only, pinned by the shipped `lib/package.json` containing
13
+ exactly `{"type": "module"}`. The root `package.json` stays CommonJS —
14
+ the test suite is CJS and loads the ESM lib through Node 24's
15
+ `require(esm)` (the six modules the suite imports —
16
+ `commit-scaffold`, `gitignore`, `repo-orchestration`, `sample-project`,
17
+ `update-watch`, `ux-doctrine` — carry no top-level `await`, which is what
18
+ keeps `require(esm)` working).
19
+
20
+ ## Shebang ⇔ CLI contract
21
+
22
+ - `#!/usr/bin/env node` as the first line means the file is a **CLI**.
23
+ Every CLI answers `--help` with a usage line on stdout and exit 0,
24
+ handled **before** required-argument parsing.
25
+ - No shebang means the file is an **import-safe module**: importing it
26
+ has no side effects, and a bare `node <file>` exits 0.
27
+
28
+ ## The gate
29
+
30
+ The mechanism is `_validate_lib_entries` in `lib/crew-release.sh` — its
31
+ code header is the full contract (what runs how, verdict aggregation,
32
+ evidence, exit codes). In short: the gate **executes every JS entry for
33
+ real** (CLIs via `--help` through a `$CREW_HOME/current`-shaped symlink,
34
+ shebang-less modules bare) and **parse-checks `sh`/`py`** — real `.sh`
35
+ execution would risk side effects, so the parse check is an accepted
36
+ residual, not a guarantee.
37
+
38
+ `node --check` is banned from the gate for the true reason: it can check
39
+ a file under a different parse goal than the real loader uses (blocker 21
40
+ was checked as a script but loaded as a module), and V8's preparser skips
41
+ function bodies. Only real execution uses the loader's goal.
42
+
43
+ **Parse goal:** whether Node reads a `.js` file as a module
44
+ (`import`/`export`, no top-level `return`) or as a script — set by the
45
+ nearest `package.json`'s `type` field.
46
+
47
+ Boundaries, stated plainly: `--help` short-circuits before argument
48
+ parsing, so the gate proves an entry *loads*, not that its main path
49
+ *behaves* (the suite covers behavior); `workflows/*.js` are not gated
50
+ here — the suite's loader emulation (`publish-verdict-first.test.js`) is
51
+ their true gate; the import-safe half of the shebang-less contract (no
52
+ side effects on import) is unchecked — an accepted residual with no
53
+ cheap mechanism.
54
+
55
+ Verdicts are aggregated and every entry gets one row in
56
+ `entry-gate.json` (excluded `test-*.sh` scripts get `skipped` rows, never
57
+ silence); a FAIL reason folds the first 10 stderr lines in so the row
58
+ names the actual error. On rejection the JSON is preserved in
59
+ `$CREW_HOME` before the staging dir is removed, and the rejection names
60
+ the failing entries.
@@ -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.