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.
- package/AGENTS.md +2 -2
- package/docs/decisions/AGENTS.md +1 -0
- package/docs/decisions/publish-path.md +96 -3
- package/docs/guide.md +2 -2
- package/docs/publish-unknown-recovery.md +10 -3
- package/docs/publish-verification.md +126 -62
- package/docs/release-integrity.md +62 -0
- package/docs/reviews/critic-0144.md +83 -0
- package/docs/reviews/critic-0145.md +106 -0
- package/lib/AGENTS.md +12 -4
- package/lib/advance-publish-base.js +8 -0
- package/lib/append-ooda-step.js +12 -3
- package/lib/build-readback-request.js +10 -0
- package/lib/build-registry.js +8 -3
- package/lib/check-intent-freshness.js +101 -0
- package/lib/classify-publish-absence.js +11 -0
- package/lib/classify-surface.js +12 -2
- package/lib/commit-scaffold.js +22 -6
- package/lib/compose-evidence-caption.js +15 -4
- package/lib/compute-publish-diff.js +41 -11
- package/lib/crew-api.js +478 -26
- package/lib/crew-release.sh +233 -1
- package/lib/gitignore.js +23 -5
- package/lib/package.json +1 -0
- package/lib/publish-note-vocabulary.js +64 -0
- package/lib/read-ooda-verdict.js +11 -3
- package/lib/readback-disk.js +9 -0
- package/lib/render-html.js +17 -7
- package/lib/repo-orchestration.js +21 -5
- package/lib/retry-publish.js +116 -72
- package/lib/sample-project.js +22 -6
- package/lib/scaffold-crew.js +11 -2
- package/lib/see-act.js +17 -8
- package/lib/serve-artifact.js +12 -6
- package/lib/setup-project-repo.js +26 -7
- package/lib/update-watch.js +34 -17
- package/lib/ux-doctrine.js +31 -6
- package/lib/verify-publish.js +44 -6
- package/lib/write-ooda-verdict.js +12 -3
- package/package.json +1 -1
- package/seed/cron-body-template.md +50 -16
- package/workflows/bugfix.js +110 -643
- package/workflows/chore.js +110 -643
- package/workflows/crew-dispatch.js +36 -0
- package/workflows/standard.js +118 -633
- 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`
|
|
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.
|
package/docs/decisions/AGENTS.md
CHANGED
|
@@ -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
|
|
1324
|
-
|
|
1325
|
-
|
|
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
|
|
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
|
|
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
|
-
──
|
|
51
|
-
|
|
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.
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
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:**
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
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;
|
|
352
|
-
|
|
353
|
-
Attempt 7 parked at Publish with outcome `unknown`: the
|
|
354
|
-
child failed structured closeout and the
|
|
355
|
-
could not see the completed build — even
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
**
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
`
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
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.
|