muse-crew 0.14.3 → 0.14.5

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 +2 -2
  2. package/docs/decisions/AGENTS.md +1 -0
  3. package/docs/decisions/publish-path.md +96 -3
  4. package/docs/guide.md +2 -2
  5. package/docs/publish-unknown-recovery.md +10 -3
  6. package/docs/publish-verification.md +126 -62
  7. package/docs/release-integrity.md +62 -0
  8. package/docs/reviews/critic-0144.md +83 -0
  9. package/docs/reviews/critic-0145.md +106 -0
  10. package/lib/AGENTS.md +12 -4
  11. package/lib/advance-publish-base.js +8 -0
  12. package/lib/append-ooda-step.js +12 -3
  13. package/lib/build-readback-request.js +10 -0
  14. package/lib/build-registry.js +8 -3
  15. package/lib/check-intent-freshness.js +101 -0
  16. package/lib/classify-publish-absence.js +11 -0
  17. package/lib/classify-surface.js +12 -2
  18. package/lib/commit-scaffold.js +22 -6
  19. package/lib/compose-evidence-caption.js +15 -4
  20. package/lib/compute-publish-diff.js +41 -11
  21. package/lib/crew-api.js +478 -26
  22. package/lib/crew-release.sh +233 -1
  23. package/lib/gitignore.js +23 -5
  24. package/lib/package.json +1 -0
  25. package/lib/publish-note-vocabulary.js +64 -0
  26. package/lib/read-ooda-verdict.js +11 -3
  27. package/lib/readback-disk.js +9 -0
  28. package/lib/render-html.js +17 -7
  29. package/lib/repo-orchestration.js +21 -5
  30. package/lib/retry-publish.js +116 -72
  31. package/lib/sample-project.js +22 -6
  32. package/lib/scaffold-crew.js +11 -2
  33. package/lib/see-act.js +17 -8
  34. package/lib/serve-artifact.js +12 -6
  35. package/lib/setup-project-repo.js +26 -7
  36. package/lib/update-watch.js +34 -17
  37. package/lib/ux-doctrine.js +31 -6
  38. package/lib/verify-publish.js +44 -6
  39. package/lib/write-ooda-verdict.js +12 -3
  40. package/package.json +1 -1
  41. package/seed/cron-body-template.md +50 -16
  42. package/workflows/bugfix.js +110 -643
  43. package/workflows/chore.js +110 -643
  44. package/workflows/crew-dispatch.js +36 -0
  45. package/workflows/standard.js +118 -633
  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)
@@ -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. 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.
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 the suite's loader emulation (then `publish-verdict-first.test.js`, now the `_validate_workflows` behavioral drive in `tests/workflow-size.test.js`). 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.
@@ -54,6 +54,7 @@ re-verified that every `docs/decisions/*.md#anchor` reference in
54
54
  - `#already-merged-hydra` — Already-merged hydration
55
55
  - `#already-merged-idem2` — Already-merged idempotency
56
56
  - `#submitted-on-issuance` — Submitted at trigger issuance
57
+ - `#d1-issuance-time` — D1 issuance-time field (`issued_at`; `entry_kind` cut 2026-09-20)
57
58
  - `#detached-head-audit` — Detached-HEAD audit (0.14.2)
58
59
  - `#detached-head-audit-redo` — Detached-HEAD audit REDO (0.14.3, supersedes findings 3/5)
59
60
 
@@ -1320,9 +1320,49 @@ discriminate on `agent_id` / `workflow`, never on the outcome word alone):
1320
1320
  - "rejected": conclusive negative — artifact_edit explicitly refused; the
1321
1321
  edit provably did not go through. Terminal park, human attention.
1322
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.
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.
1326
1366
 
1327
1367
  Edge cases:
1328
1368
  - Explicit ARTIFACT_EDIT_REFUSED still parks rejected BEFORE this site and
@@ -1467,3 +1507,56 @@ push-destination, do_push refspec), lib/publish-npm.sh
1467
1507
  (PUSH-TARGET-ANCHOR refspec), workflows/{standard,bugfix,chore}.js
1468
1508
  (STEP-2/R5-PUSH wording), lib/test-detached-integrate.sh (fixture H),
1469
1509
  lib/test-publish-preflight.sh (cases 6/7).
1510
+
1511
+ ## One-party worker-owned publish (0.14.5, room #24 → blockers 22/23)
1512
+
1513
+ **Problem.** Room #24's four journeys all died at Publish on one mechanical
1514
+ fact: workflow children cannot call `artifact_edit`. The tool requires a
1515
+ parent-conversation session ID that `agent()` children do not have — every
1516
+ trigger-child attempt failed with `missing session id`, and the runtime's
1517
+ JSON-candidate scan threw on the refusal before the workflow could parse it.
1518
+ A second, independent defect: the retry harness (`lib/retry-publish.js`)
1519
+ imported the unavailable `better-sqlite3` and pointed at `crew.db` instead
1520
+ of `crew-state.db`, so no retry could ever run.
1521
+
1522
+ **Decision.** One party owns issuance: the session-carrying tick worker.
1523
+ The workflow prepares (preflight, provenance base, checksummed diff via
1524
+ `lib/compute-publish-diff.js`, toolcheck, pre-trigger manifest baseline),
1525
+ records ONE issuer-stamped ledger entry (`outcome: "publish-intent"`,
1526
+ `issuer: "workflow"`, carrying `diff_path`/`diff_sha256`), and parks with
1527
+ `publish: publish-requested <commit> <attempt>`. The tick claims the intent
1528
+ (`publish: publish-intent-claimed <expiry>`, 1-hour lease, via
1529
+ `scan-publish-unknown`'s `intent` bucket), verifies the staged diff's sha256
1530
+ (regenerating deterministically from `base..commit` when the file is
1531
+ missing), runs the manifest-freshness check on re-claimed intents (the dead
1532
+ tick may have issued and died before recording — never blindly re-issue),
1533
+ calls `artifact_edit` directly in its own turn, and records the outcome
1534
+ through `record-intent-issuance` (CAS on the claim): `accepted`/`recovered`
1535
+ → issuer-stamped `submitted` (`issuer: "tick-worker"`) + mirrored
1536
+ `publish: verification-requested`; `refused` → `rejected` + terminal
1537
+ `publish: publish-refused`.
1538
+
1539
+ **Retired, not reverted.** The trigger child, build-state observation
1540
+ machinery, refusal parser, phantom workflow-owned `submitted`, and the
1541
+ receipt-less unknown machinery's issuance side are deleted. The
1542
+ unknown-recovery classifier survives for legacy parks; its trigger anchor
1543
+ is now issuer-bound (only `issuer: "tick-worker"` binds). The retry
1544
+ protocol survives ported to `node:sqlite` + `crew-state.db` with the
1545
+ missing `CREW_REPO` for the merge lock supplied, and its issuance step is
1546
+ the tick worker's direct call. Verified re-entry: a `submitted` without a
1547
+ mirrored request mirrors `verification-requested` — never re-issues.
1548
+
1549
+ **What it is not.** Not a ledger-as-request queue (no new queue
1550
+ abstraction — the intent IS the ledger entry plus the park note). Not a
1551
+ revert of room #22's trigger-timing work (that machinery was deleted, and
1552
+ room #24 disproved its premise). The new path is proven in parts; the next
1553
+ clean room proves it end-to-end.
1554
+
1555
+ Applies to: workflows/{standard,bugfix,chore}.js (Publish exits at intent),
1556
+ lib/crew-api.js (scan-publish-unknown intent bucket, record-intent-issuance,
1557
+ issuer-bound findTriggerEntry), lib/retry-publish.js (node:sqlite port,
1558
+ CREW_REPO, direct tick issuance), lib/verify-publish.js (issuer-bound
1559
+ trigger anchor), lib/publish-note-vocabulary.js (publish-requested,
1560
+ publish-intent-claimed, publish-refused), seed/cron-body-template.md
1561
+ (steps 4.4b, 4.4 retry direct-issuance), docs/publish-verification.md,
1562
+ docs/publish-unknown-recovery.md.
package/docs/guide.md CHANGED
@@ -366,7 +366,7 @@ The workflow dispatches the Reproduce strategy mechanically on the classificatio
366
366
 
367
367
  ### Publish content verification
368
368
 
369
- The artifact builder's `applied` report is derived from the diff the workflow carries to it, so comparing the report to the diff is circular — canary run 8 (2026-09-11) stamped provenance on a hollow build and every phase went green. The workflow therefore never stamps provenance itself: after the build lands it parks with `publish: verification-requested <commit> (build <agent_id|agent_id unobserved>)`. "Landed" requires positive evidence (canary 2026-09-15, task `1d692d91`): the build poll must have positively observed our build — a running build with the receipt `agent_id`, or a completed-build record matching it. Absence of a running build is not evidence our build ran; an unobserved "done" is an unknown outcome, parked fail-closed with an append-only `unknown` ledger entry — never parked as verification-requested. The parent protocol owns the independent content confirmation (parent-driven — see `docs/publish-verification.md`); the park message records the observed builder build identifier as `(build <agent_id|agent_id unobserved>)`, and the parent correlates the read-back's live build agent_id against it — a mismatch logs `publish: build-mismatch <commit> …`, stays parked, and is never stamped (parent-driven — see `docs/publish-verification.md` step 4b). The independent read-back step is currently unavailable: `artifact_inspect` was removed by the platform (2026-09-14) and no agent-callable replacement exists (`artifact.inspect` is malfunction diagnosis, not a read-back tool), so the parent cannot confirm content independently and tasks stay parked at verification-requested until a read-back path exists. QA's provenance check then enforces the stamp mechanically, so an unverified publish fails loudly in QA instead of passing silently.
369
+ The artifact builder's `applied` report is derived from the diff the workflow carries to it, so comparing the report to the diff is circular — canary run 8 (2026-09-11) stamped provenance on a hollow build and every phase went green. The workflow therefore never stamps provenance itself, and since 2026-09-20 (blocker 22) it doesn't even issue the edit: workflow children cannot call `artifact_edit` (the tool requires a parent-conversation session id they don't have), so publish is one-party. The workflow prepares — preflight, provenance base, checksummed diff, pre-trigger manifest baseline — writes one issuer-stamped `publish-intent` ledger entry, and parks with `publish: publish-requested <commit> <attempt>`. The session-carrying tick worker (the only caller class that can reach `artifact_edit`) claims the intent, verifies the diff's sha256, issues the edit directly in its own turn, and records the issuer-stamped `submitted` entry plus `publish: verification-requested`. An unobserved outcome is an unknown outcome, parked fail-closed with an append-only `unknown` ledger entry — never stamped. The parent protocol owns the independent content confirmation: `lib/readback-disk.js` reads the platform's on-disk working copy of the artifact source and `lib/verify-publish.js` compares it mechanically against the base..commit diff (see `docs/publish-verification.md`); only a match stamps provenance. QA's provenance check then enforces the stamp mechanically, so an unverified publish fails loudly in QA instead of passing silently.
370
370
 
371
371
  ## Identities
372
372
 
@@ -380,7 +380,7 @@ Each phase has an assigned identity — a character with a defined personality:
380
380
  | Build | **Wren** | Quietest one, trusts the plan |
381
381
  | Review | **Cass** | Fair but exacting — holds the spec as the contract |
382
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
- | 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 |
383
+ | Publish | **Wren** | Ships the merged code to the publish target (skipped when none). The workflow prepares the publish but never issues it — artifact issuance is tick-worker-owned (one-party, blocker 22); npm via registry version; artifact via the tick worker's direct edit plus an independent content read-back before the parent stamps provenance (see `docs/publish-verification.md`) — and fails closed on any unverified outcome |
384
384
  | QA | **Hazel** | Code-blind, persistent, wears persona costumes |
385
385
  | Reproduce | **Hazel** | Reproduces bugs before fixing |
386
386
  | Write | **Tate** | Docs writer, observational voice |
@@ -1,10 +1,16 @@
1
- # Publish-unknown recovery (blocker 15, 2026-09-18)
1
+ # Publish-unknown recovery (blocker 15, 2026-09-18; one-party 2026-09-20)
2
2
 
3
3
  When a standard/bugfix Publish parks with "Publish outcome unknown", the
4
4
  artifact-edit trigger went out fire-and-forget and no receipt came back —
5
5
  async was planned for, receipt-less was not. The unknown-recovery loop
6
6
  closes that gap without re-issuing blindly.
7
7
 
8
+ (2026-09-20, blocker 22: the trigger child is retired. New publishes park
9
+ at intent, not unknown — see the one-party section of
10
+ docs/publish-verification.md. This document's unknown path is the legacy
11
+ recovery for pre-one-party parks, plus the retry protocol, which now issues
12
+ through the session-carrying tick worker directly — never a child.)
13
+
8
14
  ## The note is the state machine
9
15
 
10
16
  Recovery state lives in the task's `note` events, keyed on machine-written
@@ -47,8 +53,9 @@ publish: dropped
47
53
  │ (verified → verification-requested; ambiguous → terminal;
48
54
  │ superseded → terminal; deferred → no-op)
49
55
  └─ provably-dropped ──> publish: retry-intended <commit> <ts>
50
- ── trigger (same child shape as the first attempt)
51
- ├─ ARTIFACT_EDIT_REFUSED ──> publish: retry-refused (terminal)
56
+ ── tick issues the edit DIRECTLY in its own turn (never a child;
57
+ blocker 22 — children cannot reach artifact_edit)
58
+ ├─ explicit refusal ──> publish: retry-refused (terminal)
52
59
  └─ no refusal ──> publish: retry-issued <commit>
53
60
  ──> publish: verification-requested <commit> not-before=<ts+20m>
54
61
  (Step 4.5 skips not-before entries until the window passes)
@@ -17,6 +17,61 @@
17
17
  > the verifier fails CLOSED (parked). Staleness can only park a task,
18
18
  > never stamp provenance.
19
19
 
20
+ ## One-party publish (2026-09-20, blocker 22)
21
+
22
+ The publish path used to be two-party: the workflow spawned a trigger
23
+ child to call `artifact_edit`, then observed the outcome. Room #24 proved
24
+ the child cannot reach `artifact_edit` — the tool requires a
25
+ parent-conversation session ID that `agent()` children do not have, and all
26
+ four journeys parked at Publish on exactly that failure.
27
+
28
+ The path is now one-party: the workflow prepares the publish and parks at
29
+ **intent**; the session-carrying tick worker — the only caller class that
30
+ can reach `artifact_edit` — issues the edit directly in its own turn.
31
+
32
+ - **Workflow-owned (Publish phase, read-only):** preflight, provenance
33
+ base, checksummed diff (`lib/compute-publish-diff.js` → staged at
34
+ `$CREW_HOME/.publish-diffs/<taskId>.diff`), artifact toolcheck,
35
+ pre-trigger manifest baseline. It writes ONE issuer-stamped ledger entry
36
+ (`outcome: "publish-intent"`, `issuer: "workflow"`, carrying `diff_path`
37
+ and `diff_sha256`) and parks with
38
+ `publish: publish-requested <commit> <attempt>`. It performs no edit and
39
+ writes no issuance.
40
+ - **Tick-worker-owned (seed/cron-body-template.md, step 4.4b):** the tick
41
+ claims the intent (`publish: publish-intent-claimed <expiry>`, 1-hour
42
+ lease, via `scan-publish-unknown`'s `intent` bucket), verifies the staged
43
+ diff's sha256 (regenerating deterministically from `base..commit` via
44
+ `lib/compute-publish-diff.js --commit` when the file is missing — a diff
45
+ that cannot be (re)generated byte-identically is recorded terminally via
46
+ `record-intent-unissuable` as `publish: publish-unissuable`, never
47
+ re-claimed in a loop; a mismatched diff never becomes an edit), and on a
48
+ re-claimed intent runs the deterministic manifest-freshness check first
49
+ (`lib/check-intent-freshness.js` compares the on-disk manifest's
50
+ `content_sha256`/`built_at` against the intent's `manifest_before` — the
51
+ dead tick may have issued and died before recording, and a re-claim never
52
+ blindly re-issues; the check's JSON evidence is recorded on a `recovered`
53
+ entry). It calls `artifact_edit` directly in its own turn, then
54
+ records the outcome through `record-intent-issuance` (compare-and-swap on
55
+ the claim): `accepted`/`recovered` writes the issuer-stamped
56
+ `submitted` ledger entry (`issuer: "tick-worker"`; a `recovered` entry's
57
+ `issued_at` is the dead tick's original claim time — the lower bound on
58
+ the unobserved issuance) and mirrors `publish: verification-requested`;
59
+ `refused` writes `rejected` and the terminal `publish: publish-refused`
60
+ note. An inconclusive edit call (tool unavailable, timeout, ambiguous
61
+ result) is never recorded — the tick logs it and lets the claim expire.
62
+ - **Verified re-entry:** if the tick died between issuing and mirroring,
63
+ the next scan mirrors the missing `publish: verification-requested`
64
+ from the issuer-stamped `submitted` entry — it never re-issues. The
65
+ workflow that parked at intent never acts on the park note again: its
66
+ Publish session already ended. When a verification-pending task is later
67
+ re-queued and the dispatcher resumes Publish, the workflow's provenance
68
+ re-entry guard (stamp has this `task_id` and `source_commit === HEAD`)
69
+ returns PASS without any publish action.
70
+
71
+ Everything below about parent verification (read-back, mechanical
72
+ comparison, supersession, stamping) is unchanged: the one-party model
73
+ changes WHO issues the edit, not what certifies it.
74
+
20
75
  Provenance is the artifact's claim that its live content came from a specific
21
76
  repo commit. The workflow never stamps it. This document is the parent-side
22
77
  protocol. Deterministic code detects, claims, and certifies; the tick worker
@@ -46,29 +101,21 @@ The workflow dropped the report entirely on 2026-09-16 (clean-room task
46
101
  `e2a8d9f8`): the trigger's JSON closeout contract traveled over the
47
102
  stochastic text channel and the runtime's JSON-candidate heuristic misfired
48
103
  on its prose ("workflow agent output was not JSON"), parking a task whose
49
- edit may have gone through. The trigger is now awaited and scanned for a
50
- single explicit refusal signal — `ARTIFACT_EDIT_REFUSED: <text>` as the
51
- entire trimmed turn output — and nothing else is consumed from the return.
52
- An exact refusal is conclusive negative evidence (parks `rejected`, skips
53
- observation polling); any other output (including prose quoting the signal)
54
- is inconclusive and follows the existing fail-closed observation path.
55
- `applied_report` is `missing-report` on ledger lines for issued triggers
56
- (pre-trigger parks and unattributed-unknown parks write null — no trigger
57
- was observed, so there is nothing to report). The
58
- parent ignores the (absent) report entirely when deciding whether to stamp.
104
+ edit may have gone through. 2026-09-20 retired the trigger child outright
105
+ (blocker 22: children cannot reach `artifact_edit`): the session-carrying
106
+ tick worker issues the edit directly in its own turn and records the
107
+ outcome itself — there is no trigger call to close out, and no report for
108
+ the parent to ignore. `applied_report` survives on ledger lines only as
109
+ `missing-report` (legacy) or null.
59
110
 
60
111
  The contract is split on purpose:
61
112
 
62
- - **Workflow-owned:** carrying the merged diff to the builder, attributing
63
- the edit itself (fire-and-forget trigger — no builder report — via
64
- pre-trigger toolcheck, pre-trigger build-state baseline, and post-trigger
65
- build-state diff), the build-completion poll, post-deploy cleanup,
66
- recording the Publish session completed, and parking with `publish:
67
- verification-requested <commit>` instead of stamping. The workflow does NOT trigger the read-back inspection — an
68
- async inspection triggered from inside a workflow run delivers its result
69
- to the root agent, never back into the run, so a workflow-side trigger is
70
- an orphan the verifier cannot consume. The parent triggers the one
71
- inspection it can actually receive.
113
+ - **Workflow-owned:** preparing the publish read-only (preflight,
114
+ provenance base, checksummed diff, toolcheck, pre-trigger manifest
115
+ baseline), recording the `publish-intent` ledger entry, and parking with
116
+ `publish: publish-requested <commit> <attempt>` instead of stamping. The
117
+ workflow never issues the edit — issuance belongs to the session-carrying
118
+ tick worker (one-party publish, 2026-09-20).
72
119
  - **Parent-owned (deterministic code, ferried by the tick worker):**
73
120
  scanning for verification-pending parks, atomically claiming them,
74
121
  building the read-back request, triggering the inspection, waiting for the
@@ -171,7 +218,15 @@ LLM never judges. The division:
171
218
  stamp back exactly, logs the terminal verdict, and re-queues to
172
219
  `in_progress`. Unparseable findings, mismatches, supersession, and stamp
173
220
  failures all fail CLOSED with a terminal `publish: verification-failed`
174
- verdict — never a stamp.
221
+ verdict — never a stamp. The terminal vocabulary is a closed registry in
222
+ `lib/publish-note-vocabulary.js` (D7, 2026-09-19): `scan-publish-unknown`
223
+ skips a recognized terminal note as terminal with its meaning named (never
224
+ as `unrecognized-publish-note`), and `verify-publish.js`'s `terminal()`
225
+ asserts its emitted verb is in the registry before writing.
226
+ `tests/publish-note-vocabulary.test.js` closes the enum structurally:
227
+ every `publish: <verb>` literal in lib/ must be declared in the terminal
228
+ registry or the pinned transitional set, so no future terminal verb ships
229
+ unrecognized.
175
230
  - **Envelope:** the tick saves the COMPLETE handoff — the full prose
176
231
  report AND the full JSON result, both verbatim (raw prose, JSON, or
177
232
  both concatenated are all accepted). Observed 2026-09-14: the
@@ -348,47 +403,49 @@ For a task parked with `publish: verification-requested <commit>`:
348
403
  second scan sees the unexpired `publish: verification-claimed` note and
349
404
  skips. The lease expiry bounds the damage if a claimer dies.
350
405
 
351
- ## Unknown-outcome recovery (2026-09-14, Gate 1 Journey 3 attempt 7; fire-and-forget 2026-09-16)
352
-
353
- Attempt 7 parked at Publish with outcome `unknown`: the rebuild trigger's
354
- child failed structured closeout and the in-flight-only build-state poll
355
- could not see the completed build — even though the build HAD run (a fresh
356
- platform audit directory existed). 2026-09-16 (clean-room task `e2a8d9f8`)
357
- showed the failure is worse than a catchable throw: the runtime's
358
- JSON-candidate heuristic rejects the trigger call itself ("workflow agent
359
- output was not JSON") whenever the child returns prose, whether or not the
360
- edit went through. The trigger is therefore fire-and-forget — no schema, no
361
- consumed return value — and the workflow always attributes the edit itself.
362
- Two mechanisms close the gap.
363
-
364
- **1. Pre-trigger toolcheck + baseline.** Before the trigger, a tiny schema'd
365
- child proves the artifact tool namespace is available (one bounded retry on
366
- explicit negative evidence — the only safe retry on the publish path:
367
- without the tools the edit provably did not go through) and captures a
368
- pre-trigger build-state baseline. After the trigger, the workflow diffs the
369
- post-trigger build state against the baseline: a build whose `agent_id` is
370
- new relative to the baseline is this edit's receipt. The baseline build's
371
- `agent_id` is never substituted — a build already in flight at baseline
372
- predates the trigger and is never attributed to this edit.
373
-
374
- **2. Workflow-side durable evidence.** Before the rebuild trigger, the
375
- workflow snapshots the artifact's audit-directory listing
376
- (`~/workspace/ts-spaces/<slug>/audits/` — best-effort, never a gate). When
377
- no in-flight receipt was observed, it re-lists and diffs: a timestamped
378
- directory that appeared during the trigger window is positive evidence the
379
- edit went through and the build completed. The fallback never re-issues the
380
- edit, never stamps provenance, and only routes to the parent's independent
381
- content read-back. No new directory still parks `unknown` fail-closed. The
382
- ledger's `detail` line distinguishes the two confirmations: `… edit
383
- confirmed via durable audit evidence (new audit dir …)` vs `… build receipt
384
- captured by workflow-owned build-state observation (pre/post-trigger diff)`.
385
-
386
- The fallback's known limitation: audit directories are not attributed to
387
- tasks, so two concurrent publishes to the same artifact could cross-read.
388
- The consequence is bounded — the fallback only routes to the parent
389
- read-back, and the parent still certifies the exact commit's content
390
- mechanically (a wrong build's content fails closed as `publish:
391
- content-mismatch` / `publish: build-mismatch`, never stamps).
406
+ ## Unknown-outcome recovery (2026-09-14, Gate 1 Journey 3 attempt 7; one-party 2026-09-20)
407
+
408
+ Attempt 7 parked at Publish with outcome `unknown`: the old two-party
409
+ path's rebuild-trigger child failed structured closeout and the
410
+ in-flight-only build-state poll could not see the completed build — even
411
+ though the build HAD run. The two-party machinery (trigger child,
412
+ build-state observation, receipt attribution) was retired 2026-09-20:
413
+ blocker 22 proved children cannot reach `artifact_edit`, so there is no
414
+ trigger child anymore and no observation gap to close.
415
+
416
+ What remains of unknown-recovery is the legacy classifier for parks that
417
+ predate one-party publish, plus the one-party crash windows:
418
+
419
+ - **Legacy `due` parks** (`publish outcome unknown` parks from the old
420
+ path): `scan-publish-unknown` claims them for the deterministic
421
+ six-way classifier (`lib/classify-publish-absence.js`) exactly as
422
+ before. The only accepted trigger anchor is an issuer-stamped
423
+ `submitted` ledger entry (`issuer: "tick-worker"` — the only issuance
424
+ class that exists now); a `submitted` without issuer never binds.
425
+ - **One-party crash windows:** the tick dies after issuing but before
426
+ `record-intent-issuance` → the next scan mirrors
427
+ `publish: verification-requested` from the issuer-stamped `submitted`
428
+ entry (never re-issues). The tick dies before issuing → the intent
429
+ claim expires and the next scan re-claims with `reclaimed: true`, and
430
+ the manifest-freshness check decides between `recovered` and a fresh
431
+ issuance — a re-claim never blindly re-issues.
432
+
433
+ ### Retired two-party machinery (kept for the record)
434
+
435
+ **1. Pre-trigger toolcheck + baseline.** In the two-party path, before the
436
+ trigger, a tiny schema'd child proved the artifact tool namespace was
437
+ available and captured a pre-trigger build-state baseline; after the
438
+ trigger, the workflow diffed the post-trigger build state against the
439
+ baseline for a receipt. In the one-party path the workflow still performs
440
+ the read-only preflight (toolcheck + manifest baseline — the baseline is
441
+ carried in the `publish-intent` ledger entry's `manifest_before`), but
442
+ there is no trigger child and no receipt attribution: the tick worker
443
+ issues the edit directly and records the outcome itself.
444
+
445
+ **2. Workflow-side durable evidence.** In the two-party path, before the
446
+ rebuild trigger, the workflow snapshotted the artifact's audit-directory
447
+ listing and re-diffed it after the trigger as fallback evidence. Retired
448
+ with the trigger child — the observation gap it closed no longer exists.
392
449
 
393
450
  **2. `resolve-publish-unknown` (Crew API).** For attempts already parked
394
451
  `unknown` before this fix: given a task parked with a latest ledger outcome
@@ -408,6 +465,13 @@ build in the window, unobservable window, or an already-resolved attempt
408
465
  Case-sensitive, exact-prefix matches — match on prefixes, never on English
409
466
  meaning:
410
467
 
468
+ - `publish: publish-requested <commit> <attempt>` — workflow park at
469
+ intent (one-party publish, 2026-09-20): the merged change is staged as a
470
+ checksummed diff; the tick worker owns issuance. Not a verdict.
471
+ - `publish: publish-intent-claimed <ISO-expiry>` — tick-worker scan;
472
+ atomic claim with 1-hour lease. Not a verdict.
473
+ - `publish: publish-refused <commit>` — tick worker; the platform refused
474
+ the directly-issued edit. Terminal: parked for human attention.
411
475
  - `publish: verification-requested <commit>` — workflow park; contained in
412
476
  the stored `Parked: …` message.
413
477
  - `publish: verification-claimed <ISO-expiry>` — parent scan; atomic claim
@@ -0,0 +1,62 @@
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 (`workflow-size.test.js`, which
51
+ drives the release script's `_validate_workflows` behaviorally: export-strip
52
+ + async-function-wrap parse plus the size budget) is their true gate; the
53
+ import-safe half of the shebang-less contract (no side effects on import)
54
+ is unchecked — an accepted residual with no
55
+ cheap mechanism.
56
+
57
+ Verdicts are aggregated and every entry gets one row in
58
+ `entry-gate.json` (excluded `test-*.sh` scripts get `skipped` rows, never
59
+ silence); a FAIL reason folds the first 10 stderr lines in so the row
60
+ names the actual error. On rejection the JSON is preserved in
61
+ `$CREW_HOME` before the staging dir is removed, and the rejection names
62
+ the failing entries.
@@ -0,0 +1,83 @@
1
+ # Critic review — 0.14.4 release candidate (blocker-21 fix set + D1 + D7)
2
+
3
+ Change set: `v0.14.3..40889ca` (11 commits, 50 files, +1496/−277). Full suite green on the final tree ("All test suites passed.", exit 0, zero failures). Read-only review; no repo modifications made.
4
+
5
+ Contracts checked against (Eric's standing rules): (a) wording is not a mechanism — prefer mechanical guards over hardened prose; (b) fix the cause first, then the symptom; (c) take critic blessings skeptically — every disposition below is checked against the rules, not the panel's enthusiasm. Two panel claims were verified empirically by the coordinator before acceptance (the `node --check`/ESM claim, the `entry_kind` redundancy).
6
+
7
+ ## Verdicts
8
+
9
+ **Architect: CONCERN** — the entry gate is a genuine mechanism that closes blocker 21's JS trap (verified: it runs against the staging dir, so the shipped `lib/package.json` `{"type":"module"}` pin is in effect during the gate — that is what closes the module-goal trap). One bias flip and one duplicated vocabulary keep it from ACCEPT.
10
+
11
+ - F-A1 (medium) — D1: `issued_at` is a *lower* bound on issuance, not the issuance instant. `workflows/standard.js:1535-1550` captures `triggerIssuedAt` via a full `await agent("Run: date -u …")` round-trip that completes *before* the trigger agent call is issued, so `issued_at` < true issuance by one agent latency. `lib/verify-publish.js:375` requires `built_at > triggerTs` strictly; a stranger build with `built_at ∈ (issued_at, true_trigger]` and a changed sha passes both → false `publish: verified` + stamp. Pre-D1 the ledger-write-ts anchor erred fail-closed (the J1 incident); D1 fixed the misfire by flipping the bias. Narrow window (one agent-call latency) but the bias is real.
12
+ - F-A2 (medium) — D1: `keyTag` ("issuance"/"receipt", replay keys) and `entry_kind` ("issuance"/"receipt", ledger field) are the same vocabulary twice with no mechanical link. Nothing ties them; the tests pin each independently, so renaming one silently diverges the other. (Resolved by the stronger cut — see F-S1: `entry_kind` goes entirely, `keyTag` stays as the replay key.)
13
+ - F-A3 (low) — Entry gate: "executes every entry for real" overclaims for `.sh`/`.py` — `bash -n` and `py_compile` are parse/compile-only, the same vacuity class as the banned `node --check`. Real `.sh` execution is correctly avoided (side effects), so this is a wording gap — but blocker 21's lesson was exactly parse≠load.
14
+ - F-A4 (low) — Entry gate: the import-safe half of the shebang⇔CLI contract is unchecked. Shebang-less files run as bare `node <file>` with exit-0 as the sole criterion — an import-dirty-but-exit-0 module passes silently.
15
+ - F-A5 (info) — Shebang detection is prefix-only `#!/usr/bin/env node`; `env -S` / `#!/bin/node` variants classify as module → bare run → fail-closed, noisy. Match on a `node` token instead.
16
+ - F-A6 (info) — Conversion fidelity spot-check: clean. `createRequire(import.meta.url)` as `cjsRequire` (sync resolution preserved), `require.main` → realpath-compared `isMainModule` (handles the `/current`-symlink production path), `__dirname` → `fileURLToPath(import.meta.url)`; no `module.exports`/bare-`require(` leftovers, no top-level await.
17
+
18
+ **Subtractor: CONCERN** — nothing broken or fail-open; the change set ships one redundant field, one gate blind spot on the exact production path, and ~110 lines of duplicated CLI boilerplate with a clean shared shape.
19
+
20
+ - F-S1 (medium) — `entry_kind` is redundant — cut the field. All three readers (`lib/verify-publish.js:350`, `lib/crew-api.js:1753`, `lib/retry-publish.js:216`) prefer `entry_kind === "issuance"` but bind only `issued_at` and `manifest_before`, which are byte-identical on both entries from the real writer (same `triggerIssuedAt`, same `preTriggerManifest` — `workflows/standard.js:1584-1585` vs `:1707-1708`). The preference filter can never change the bound anchor; the `"receipt"` value is never filtered for anywhere. Coordinator-verified. Smaller shape: `issued_at` alone; readers take the oldest `submitted` by `ts` and bind `issued_at || ts` (the ts-sort stays — it is an improvement over the old file-order return). `keyTag` stays untouched as the distinct agent-call replay key (the 0.14.3 precedent stands).
21
+ - F-S2 (medium) — `tests/verify-publish.test.js:1144` pins an impossible fixture: "issuance-kind preferred over an older receipt-bearing line" constructs a receipt entry with an *earlier* ts than the issuance entry — the real writer emits issuance before receipt in program order, so this ledger can never be produced. The test exists only to justify the redundant filter. Delete with the filter.
22
+ - F-S3 (medium) — Entry gate blind spot: production invokes lib CLIs through a symlink (`node $CREW_HOME/current/lib/<file>`); the gate runs `node $staging/lib/<file>` on a real dir. Verified empirically in scratch: through a symlinked dir the raw `argv[1]` guard no-ops (exit 0) while the realpath guard passes — the gate cannot distinguish the broken guard from the `ff3fb53` fix. The gate proves entries execute on a different path shape than production, on the exact path the fix was about.
23
+ - F-S4 (low) — ~110 lines of duplicated CLI boilerplate: 8 files carry a byte-identical 7-line `isMainModule` realpath IIFE; 15+ files carry 4–9-line inline `--help` blocks differing only in the usage string. Clean shared shape: `lib/cli.js` exporting `isMainModule(callerUrl)` and `printHelpIfRequested(USAGE)`. Deferred, not this release (below).
24
+ - F-S5 (low) — `docs/release-integrity.md` "The gate" section duplicates the `lib/crew-release.sh:213-231` code header. Compress to a pointer; keep the "ESM-only lib" section (it carries the six require(esm)-safe modules / no-top-level-await constraint the header doesn't state).
25
+ - F-S6 (info) — D7 registry is consumed, not hollow: `matchTerminalPublishNote` is consulted by scan-publish-unknown (the real behavior change — `publish: verification-failed` was mislogged as `unrecognized-publish-note`), and `terminal()` asserts membership fail-loud (exit 2). But the "first instance of the enum-guard family" framing is aspiration — don't build the second registry until a second family needs it.
26
+
27
+ **Reliability: ACCEPT** — every failure mode found either fails closed or degrades to behavior strictly better than 0.14.3 (D1's ts-fallback only triggers where pre-D1 *always* used ts); the gate covers the blocker-21 incident class mechanically; the CJS→ESM conversions are behavior-clean.
28
+
29
+ - F-R1 (medium) — D1 null-fallback: when the `issued_at` ferry throws or returns malformed, the block proceeds with `issued_at: null` and the verifier falls back to ledger-write `ts` — the exact anchor whose skew caused room #23 J1. Honest (log line at `workflows/standard.js:1555` + null in the ledger entry) and, crucially, fail-closed: `ts` is an *upper* bound on issuance, so `built_at > ts` can only false-park a good build, never false-verify. The proposed skew-tolerance is rejected (below) — it would push fail-open on inconclusive evidence.
30
+ - F-R2 (medium) — No plausibility bound on `issued_at`: the ferry validates shape only; a well-formed-but-wrong timestamp is bound uncritically (`lib/verify-publish.js:354`, `Date.parse` only). Blast radius contained — worst case is a false park (the sha256-change gate at `:378` and line-level content checks still must pass; malformed → `NaN` → fail-closed). Deferred (below); the platform nonce is the designed fix.
31
+ - F-R3 (low) — Gate "executes for real" is really "loads for real + `--help` branch": `node <file> --help` short-circuits before arg parsing by contract, so main-path breakage (e.g. a broken `loadPlaywright()` body) passes; `render-html.js`/`see-act.js` never touch playwright-core or Chromium under the gate. The blocker-21 class (load-time `SyntaxError`) *is* covered — the module fully evaluates. Document the `--help` short-circuit boundary.
32
+ - F-R4 (low) — `test-*.sh` shipped but ungated, with no skipped-row evidence: excluded by basename, and `entry-gate.json` has no `skipped` verdict, so "0 rows" is indistinguishable from "all skipped". Emit `verdict: "skipped"` rows.
33
+ - F-R5 (low) — Gate failure destroys its own evidence: `cmd_deploy` does `rm -rf "$staging_dir"` on gate failure, deleting `entry-gate.json`; only the stderr FAIL lines survive. Copy `entry-gate.json` to `$CREW_HOME` before removing staging.
34
+ - F-R6 (info, verified clean) — `cjsRequire(playwright-core)` is lazy inside `loadPlaywright()` and playwright-core is declared in root `package.json`: the `5022a3a` carve-out does not weaken load-time integrity (no bare npm imports at load in any lib file). Issuance-site placement verified byte-identical across standard/bugfix/chore (capture→trigger→refusal `rejected`+park→issuance write→observation→receipt write, one shared `triggerIssuedAt`; `upgrade.js` has no artifact publish block — D1's three-workflow scope is complete). Replay keys attempt-scoped, exactly-once capture per block; no collision shape.
35
+
36
+ **First-Time User: CONCERN** — text is honest; two contract-doc claims a stranger can disprove in minutes.
37
+
38
+ - F-F1 (high) — `docs/release-integrity.md:32` (repeated verbatim in `lib/crew-release.sh:215-217` and `tests/entry-gate.test.js:5-6`): *"node --check is banned from this gate: it is vacuous on ESM and exits 0 even on blatant syntax errors."* Coordinator-verified empirically: with the shipped `lib/package.json` `{"type":"module"}` pin, `node --check` **exits 1** on the test's own regression fixture (ESM import + top-level return); the vacuous pass (exit 0) reproduces only **without** the pin (script goal). Blocker 21 was **goal confusion** — checked as a script, loaded as a module — not "ESM vacuousness." The doc misstates the mechanism of the bug it guards against, and a stranger who tests the claim finds the contract doc wrong. The ban itself stays justified (V8's preparser still skips function bodies — correctly documented in `tests/AGENTS.md`), but for the true reason.
39
+ - F-F2 (high) — `docs/publish-verification.md:174-179`: "no future terminal verb ships unrecognized" is overclaimed. Only **one** of six writer sites is guarded (`lib/verify-publish.js:107-126` `terminal()`); `lib/crew-api.js` (5 sites), `lib/retry-publish.js` (2 sites), and `lib/verify-publish.js:408` emit registry verbs as raw template literals. A future verb (or a typo'd verb) written by any of them ships unrecognized with no alarm. The registry header itself is honest (names only `terminal()`); the doc's guarantee is not.
40
+ - F-F3 (medium) — `_gate_js` captures stderr to `$tmp/err`, greps it only for "SyntaxError", then `rm -rf`s the evidence. A non-syntax failure surfaces as `ENTRY-GATE FAIL foo.js (js-cli): exit=1` with the actual error destroyed; the JSON row carries the same information-free reason. Fold the first ~10 lines of stderr into the reason and the row.
41
+ - F-F4 (medium) — Deploy rejection: `die "release $hash rejected: lib entry gate failed (exit $_le_status)"` names the gate but not the file, contradicting `docs/release-integrity.md:44-47` ("naming the gate **and the file**"); "exit 30" is a magic number. Name the failing entries (or point at the evidence) and decode the 30.
42
+ - F-F5 (medium) — "module goal" is never defined where the mechanism is explained. The true `--check` mechanism hinges on it, but the contract doc never mentions goal at all. One sentence: "Parse goal: whether Node reads a `.js` file as a module (`import`/`export`, no top-level `return`) or as a script — set by the nearest `package.json`'s `type` field."
43
+ - F-F6 (low) — `--help` output inconsistent across the 25 shipped CLIs: `Usage:` vs `usage:`; `node lib/x.js` vs `node x.js` invocation prefix; 7 CLIs print no description line at all. Deferred into F-S4's shared helper (below).
44
+ - F-F7 (low) — `test-*.sh` gate exclusion unexplained in the doc. One clause: "(test scripts are exercised by `tests/run.sh`, not shipped as library entries)."
45
+ - F-F8 (low) — `TERMINAL_NOTE_MEANINGS["publish: verified"]`: `"publish verified and stamped; parked (done)"` — next to five siblings ending "parked for human attention," `(done)` reads as contradicting "parked." Reword: `"publish verified and stamped; task re-queued (terminal for the scan)"`.
46
+ - F-F9 (info) — D1 decision record leads with WHY before mechanism (done right), but "Room #23 J1" uses "room" and "J1" undefined in the docs, and `lib/crew-api.js:1733` cites classification codes "A2/R2/O4" defined nowhere. Gloss once; define or drop the codes.
47
+
48
+ ## Step-back round (run by the coordinator, not delegated)
49
+
50
+ Question: is the whole blocker-21 fix set (entry gate + ESM conversions) the right shape, or is there a simpler architecture that dissolves it?
51
+
52
+ Answer: the shape stands. The candidates:
53
+
54
+ - **One smarter check instead of per-entry execution** (e.g. `node --check` under the correct parse goal): impossible. V8's preparser skips function bodies with no eager-check flag, and goal confusion is exactly what bit — the pin fixes the goal, but only real execution proves the loader's path. The gate's per-entry execution is the minimal complete check.
55
+ - **A manifest of entry kinds instead of shebang-sniffing**: more machinery, not less — a second source of truth that drifts. The shebang is self-describing (the file declares its own kind) and misclassification fails closed. The current convention is the simpler architecture.
56
+ - **Gate without the ESM conversions**: no. The conversion is the cause-fix (production loads lib/ as ESM per the pin; a CJS-shaped file is broken regardless of any gate); the gate guards the regression. Cause first, then the guard — the rule's order, honored.
57
+ - **Gate covering workflows/*.js too**: out of scope by design — workflows have their own true gate (the export-strip + async-wrap transform in root AGENTS.md, pinned by `publish-verdict-first.test.js`'s loader emulation). The contract doc should state this scoping explicitly (accepted below).
58
+ - **D1's ferry dance vs shell-stamped ledger lines**: the dedicated schema'd ferry with strict shape validation is the simpler shape — the instant must be captured before the trigger invocation, so it can't piggyback on the post-trigger ledger call, and folding it into the best-effort baseline call would couple the clock to different failure semantics.
59
+ - **D7's registry vs one asserting writer helper**: routing all six writer sites through one helper touches `lib/crew-api.js`'s five sites for the same guarantee a structural test gives. The repo's established mechanical pattern — a structural test scanning `lib/` for `publish: <verb>` literals asserting registry membership — delivers the "closed" in "closed enum" with less churn (accepted below).
60
+
61
+ The residual fibs after the accepted fixes: (a) sh/py remain parse-checked by necessity — real `.sh` execution is unsafe (side effects) — stated honestly in the doc; (b) the `--help` branch short-circuits main paths — the gate proves load, not behavior; the suite covers behavior; (c) import-safety of shebang-less modules is unchecked — accepted residual, documented. None is a fail-open path.
62
+
63
+ ## Dispositions
64
+
65
+ Accepted (apply before PUBLISH):
66
+
67
+ 1. **F-F1/F-F5** — Reword the `node --check` rationale in all three places (`docs/release-integrity.md:32`, `lib/crew-release.sh:215-217` comment, `tests/entry-gate.test.js:5-6`) to the true mechanism: "`node --check` is banned because it can check the file under a different parse goal than the real loader uses (blocker 21: checked as a script, loaded as a module), and V8's preparser skips function bodies. Real execution is the only check that uses the loader's goal." Add the one-sentence "parse goal" definition to the contract doc. The ban itself stands.
68
+ 2. **F-F2/F-A3-wording** — Make D7's "closed" mechanical: add a structural test scanning `lib/` for `publish: <verb>` literals asserting registry membership (the repo's established pattern — prose becomes a guard), and scope the doc sentence to what the mechanism does: "verify-publish.js's `terminal()` asserts its emitted verb is in the registry before writing; the structural test extends the assertion to every literal in lib/."
69
+ 3. **F-S1/F-S2** — Cut `entry_kind`: remove the field from `recordPublishLedger` serialization (3 workflows), the 6 call sites, the 3 reader preference filters (`lib/verify-publish.js:350`, `lib/crew-api.js:1753`, `lib/retry-publish.js:216`) — readers take the oldest `submitted` by `ts` and bind `issued_at || ts`; delete the impossible-fixture test (`tests/verify-publish.test.js:1144`); cut the doc paragraph. `keyTag` stays as the replay key (0.14.3 precedent untouched; the frozen `submitted` vocabulary untouched).
70
+ 4. **F-A1** — Move the `issued_at` capture to *after* the trigger-agent call returns (upper bound on issuance → fail-closed bias), in all three workflows. The J1 case still verifies (`built_at` 01:58 > post-return capture ~01:56); a stranger build inside the old window now fails closed. Small, mechanical, testable (update the skew fixture's expectation).
71
+ 5. **F-S3** — Close the symlink blind spot: in `_validate_lib_entries`, create `$tmp/liblink -> $dir/lib` and invoke JS entries through the link, mirroring production's `$CREW_HOME/current` path shape; pin it in `tests/entry-gate.test.js` with a symlinked fixture lib dir (fail-closed if the link can't be created). This makes the gate exercise the `ff3fb53` realpath guard it exists to protect.
72
+ 6. **F-A3/F-R3** — Headline the gate honestly: "executes every JS entry; parse-checks sh/py" (doc + code header); document the `--help` short-circuit boundary in `docs/release-integrity.md` (the gate proves load, not main-path behavior; the suite covers behavior). Add the scoping sentence: workflows are covered by the suite's loader emulation, not this gate.
73
+ 7. **F-R4/F-R5/F-F7** — Gate evidence: emit `verdict: "skipped"` rows for excluded `test-*.sh` entries (with the one-clause rationale); copy `entry-gate.json` to `$CREW_HOME` before `cmd_deploy` removes the staging dir on failure.
74
+ 8. **F-F3/F-F4** — Diagnosability: fold the first ~10 lines of entry stderr into the FAIL reason and the JSON row; make the deploy `die` message name the failing entries (or point at the preserved evidence) and decode exit 30.
75
+ 9. **F-A5/F-A4-doc/F-F8/F-F9/F-S5** — Small fixes: shebang match on a `node` token; document the import-safe residual (accepted — no cheap mechanism); reword the `"publish: verified"` meaning; gloss "Room #23 J1" once and define-or-drop "A2/R2/O4"; compress the doc's "The gate" section to a pointer at the code header.
76
+
77
+ Rejected / deferred (with reason):
78
+
79
+ - **F-R1's skew-tolerance half** — rejected. When `issued_at` is null the verifier falls back to ledger-write `ts`, which is an *upper* bound on issuance: `built_at > ts` can only false-park a good build, never false-verify. A skew tolerance would push fail-open on inconclusive evidence — against the standing bias. The null fallback is already honest (logged + null in the ledger) and fail-closed; no change.
80
+ - **F-R2 (plausibility bound on `issued_at`)** — deferred. Fail-closed already holds and the worst case is a false park; the platform nonce is the designed fix. Revisit when the nonce lands.
81
+ - **F-S4 (`lib/cli.js` shared helper, subsuming F-F6's `--help` standardization)** — deferred. Real cut, clean shape (~−100 lines), but refactor churn across 8+ files right before publish for a non-correctness issue. File as follow-up; do not do piecemeal.
82
+
83
+ Panel consensus: no REJECT anywhere; no fail-open path found by any critic. After the 9 accepted dispositions are applied and the suite re-run green, this change set is publishable.