muse-crew 0.14.3 → 0.14.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. package/AGENTS.md +1 -1
  2. package/docs/decisions/AGENTS.md +1 -0
  3. package/docs/decisions/publish-path.md +43 -3
  4. package/docs/publish-verification.md +9 -1
  5. package/docs/release-integrity.md +60 -0
  6. package/docs/reviews/critic-0144.md +83 -0
  7. package/lib/AGENTS.md +10 -3
  8. package/lib/advance-publish-base.js +8 -0
  9. package/lib/append-ooda-step.js +12 -3
  10. package/lib/build-readback-request.js +10 -0
  11. package/lib/build-registry.js +8 -3
  12. package/lib/classify-publish-absence.js +11 -0
  13. package/lib/classify-surface.js +12 -2
  14. package/lib/commit-scaffold.js +22 -6
  15. package/lib/compose-evidence-caption.js +15 -4
  16. package/lib/compute-publish-diff.js +9 -0
  17. package/lib/crew-api.js +54 -21
  18. package/lib/crew-release.sh +233 -1
  19. package/lib/gitignore.js +23 -5
  20. package/lib/package.json +1 -0
  21. package/lib/publish-note-vocabulary.js +44 -0
  22. package/lib/read-ooda-verdict.js +11 -3
  23. package/lib/readback-disk.js +9 -0
  24. package/lib/render-html.js +17 -7
  25. package/lib/repo-orchestration.js +21 -5
  26. package/lib/retry-publish.js +77 -45
  27. package/lib/sample-project.js +22 -6
  28. package/lib/scaffold-crew.js +11 -2
  29. package/lib/see-act.js +17 -8
  30. package/lib/serve-artifact.js +12 -6
  31. package/lib/setup-project-repo.js +26 -7
  32. package/lib/update-watch.js +34 -17
  33. package/lib/ux-doctrine.js +31 -6
  34. package/lib/verify-publish.js +38 -2
  35. package/lib/write-ooda-verdict.js +12 -3
  36. package/package.json +1 -1
  37. package/workflows/bugfix.js +33 -35
  38. package/workflows/chore.js +33 -35
  39. package/workflows/standard.js +33 -16
  40. 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)
@@ -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
@@ -171,7 +171,15 @@ LLM never judges. The division:
171
171
  stamp back exactly, logs the terminal verdict, and re-queues to
172
172
  `in_progress`. Unparseable findings, mismatches, supersession, and stamp
173
173
  failures all fail CLOSED with a terminal `publish: verification-failed`
174
- verdict — never a stamp.
174
+ verdict — never a stamp. The terminal vocabulary is a closed registry in
175
+ `lib/publish-note-vocabulary.js` (D7, 2026-09-19): `scan-publish-unknown`
176
+ skips a recognized terminal note as terminal with its meaning named (never
177
+ as `unrecognized-publish-note`), and `verify-publish.js`'s `terminal()`
178
+ asserts its emitted verb is in the registry before writing.
179
+ `tests/publish-note-vocabulary.test.js` closes the enum structurally:
180
+ every `publish: <verb>` literal in lib/ must be declared in the terminal
181
+ registry or the pinned transitional set, so no future terminal verb ships
182
+ unrecognized.
175
183
  - **Envelope:** the tick saves the COMPLETE handoff — the full prose
176
184
  report AND the full JSON result, both verbatim (raw prose, JSON, or
177
185
  both concatenated are all accepted). Observed 2026-09-14: the
@@ -0,0 +1,60 @@
1
+ # Release integrity: the lib entry gate
2
+
3
+ Every JS file shipped in a release's `lib/` must actually execute;
4
+ `sh`/`py` entries are parse-checked (real `.sh` execution would risk side
5
+ effects). This page is the contract; the mechanism is
6
+ `_validate_lib_entries` in `lib/crew-release.sh`, wired into `cmd_deploy`
7
+ right after `_validate_workflows`. Behavioral pins live in
8
+ `tests/entry-gate.test.js`.
9
+
10
+ ## ESM-only lib
11
+
12
+ `lib/` is ESM-only, pinned by the shipped `lib/package.json` containing
13
+ exactly `{"type": "module"}`. The root `package.json` stays CommonJS —
14
+ the test suite is CJS and loads the ESM lib through Node 24's
15
+ `require(esm)` (the six modules the suite imports —
16
+ `commit-scaffold`, `gitignore`, `repo-orchestration`, `sample-project`,
17
+ `update-watch`, `ux-doctrine` — carry no top-level `await`, which is what
18
+ keeps `require(esm)` working).
19
+
20
+ ## Shebang ⇔ CLI contract
21
+
22
+ - `#!/usr/bin/env node` as the first line means the file is a **CLI**.
23
+ Every CLI answers `--help` with a usage line on stdout and exit 0,
24
+ handled **before** required-argument parsing.
25
+ - No shebang means the file is an **import-safe module**: importing it
26
+ has no side effects, and a bare `node <file>` exits 0.
27
+
28
+ ## The gate
29
+
30
+ The mechanism is `_validate_lib_entries` in `lib/crew-release.sh` — its
31
+ code header is the full contract (what runs how, verdict aggregation,
32
+ evidence, exit codes). In short: the gate **executes every JS entry for
33
+ real** (CLIs via `--help` through a `$CREW_HOME/current`-shaped symlink,
34
+ shebang-less modules bare) and **parse-checks `sh`/`py`** — real `.sh`
35
+ execution would risk side effects, so the parse check is an accepted
36
+ residual, not a guarantee.
37
+
38
+ `node --check` is banned from the gate for the true reason: it can check
39
+ a file under a different parse goal than the real loader uses (blocker 21
40
+ was checked as a script but loaded as a module), and V8's preparser skips
41
+ function bodies. Only real execution uses the loader's goal.
42
+
43
+ **Parse goal:** whether Node reads a `.js` file as a module
44
+ (`import`/`export`, no top-level `return`) or as a script — set by the
45
+ nearest `package.json`'s `type` field.
46
+
47
+ Boundaries, stated plainly: `--help` short-circuits before argument
48
+ parsing, so the gate proves an entry *loads*, not that its main path
49
+ *behaves* (the suite covers behavior); `workflows/*.js` are not gated
50
+ here — the suite's loader emulation (`publish-verdict-first.test.js`) is
51
+ their true gate; the import-safe half of the shebang-less contract (no
52
+ side effects on import) is unchecked — an accepted residual with no
53
+ cheap mechanism.
54
+
55
+ Verdicts are aggregated and every entry gets one row in
56
+ `entry-gate.json` (excluded `test-*.sh` scripts get `skipped` rows, never
57
+ silence); a FAIL reason folds the first 10 stderr lines in so the row
58
+ names the actual error. On rejection the JSON is preserved in
59
+ `$CREW_HOME` before the staging dir is removed, and the rejection names
60
+ the failing entries.
@@ -0,0 +1,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.
package/lib/AGENTS.md CHANGED
@@ -1,6 +1,12 @@
1
1
  # AGENTS.md
2
2
 
3
- Shell scripts for the crew's infrastructure. Called by workflow scripts, cron, and the release system.
3
+ Shipped library: ESM JavaScript CLIs and import-safe modules, shell scripts for release/merge/worktree/cleanup, and Python evidence tools. Called by workflow scripts, cron, and the release system.
4
+
5
+ ## Contracts (blocker 21, 2026-09-20)
6
+
7
+ - **ESM-only.** `lib/package.json` contains exactly `{"type": "module"}` — every `lib/*.js` is ESM. The root `package.json` stays CommonJS for the CJS test suite, which loads the ESM lib through Node 24 `require(esm)`. The six suite-imported modules (`commit-scaffold`, `gitignore`, `repo-orchestration`, `sample-project`, `update-watch`, `ux-doctrine`) carry no top-level `await`, which is what keeps `require(esm)` working.
8
+ - **Shebang ⇔ CLI.** `#!/usr/bin/env node` means CLI: the file answers `--help` with a usage line on stdout and exit 0, handled before required-argument parsing. No shebang means import-safe module: no side effects on import, bare `node <file>` exits 0.
9
+ - **Release entry gate.** `_validate_lib_entries` in `crew-release.sh` (wired into `cmd_deploy` after `_validate_workflows`) executes every shipped JS entry for real through a `$CREW_HOME/current`-shaped symlink: shebang'd JS as `node <file> --help`, shebang-less JS as bare `node <file>`; `lib/*.sh` (minus `test-*.sh`) via `bash -n` and `lib/*.py` via `python3 -m py_compile` are parse-checked only (real `.sh` execution risks side effects), with bytecode kept out of the staging dir. `node --check` is banned — it can check under a different parse goal than the real loader uses (blocker 21), and V8's preparser skips function bodies; only real execution uses the loader's goal. Per-entry `ENTRY-GATE pass/FAIL` lines (FAIL reasons fold the first 10 stderr lines), one row per entry in `<staging>/entry-gate.json` (`test-*.sh` exclusions get `skipped` rows), exit 30 on any entry failure, deploy fails closed naming the failing entries and preserving the evidence in `$CREW_HOME`. Contract page: `docs/release-integrity.md`; behavioral pins: `tests/entry-gate.test.js`.
4
10
 
5
11
  - `build-registry.js` — deterministic extractor that generates `workflows/registry.json` (workflow step registry) from the workflow files' `meta` blocks at release time; invoked by `crew-release.sh` deploy
6
12
  - `crew-api.js` — the crew-owned task-service API (dependency inversion, 2026-09-11): a zero-dependency Node CLI implementing the API.md contract against `$CREW_HOME/crew-state.db` (schema in `schema.sql`). Workflows call it through their agents' shell; the dashboard delegates to it. All state-machine invariants live as CHECK constraints in the schema, never in client prose. Includes the `record-phase` composite (session + event in one transaction) and a one-time `migrate` import from a dashboard app.db. Active-release resolution is split in two (room #15): `resolveActiveReleaseName` (symlink-only — writers like the initial-provenance stamp record the active release without proving they are it) and `resolveActiveRelease` (symlink + self-path cross-check — verifiers like `scan-verification-pending` refuse to stamp claims when the running code isn't the active release's own). Provenance is per-project (2026-09-18, room #15 blocker 8): nullable `provenance_*` columns on the projects row, a single `stampProvenance()` writer, `set-provenance`/`get-provenance` require `project_id` (no silent global fallback), and a watermarked openDb backfill that attributes the legacy `config.provenance.*` triple to exactly-one ancestor match — never fabricated, otherwise deferred.
@@ -27,9 +33,9 @@ Shell scripts for the crew's infrastructure. Called by workflow scripts, cron, a
27
33
  - `serve-artifact.js` — local server for a built TS space for experiential QA (2026-09-14): serves `<space-dir>/client/dist` statically and dispatches POST `*/actions` to the compiled server actions with a locally-built Ctx. Prints `READY port=<n>` then serves until killed. Read-only w.r.t. the space directory. Fidelity: the served client and action handlers are the artifact's own built code; the Ctx is locally built (privileged handlers run from the space's own `server/dist/privileged.js` when present; blobs are stored in a per-run temp dir and served back at `/__blobs/<key>`); environment is inherited from the caller. It is not the hosted runtime — tasks that cannot be judged under it must report `NOT POSSIBLE: <reason>`.
28
34
  - `readback-disk.js` — deterministic publish content sensor (2026-09-16): reads the on-disk tree the artifact is built/served from and emits the machine-readable findings block (`FILE:`/`ADDED:`/`REMOVED:`/`END_FILE`) that `verify-publish.js` judges. The primary sensor — the LLM-inspector path (`build-readback-request.js`) is manual-fallback only since `artifact_inspect` was removed by the platform 2026-09-14.
29
35
  - `publish-content.js` — shared ESM content-primitives for publish verification (2026-09-18, blocker 15): diff parsing (`parseDiff`), findings parsing (`parseFindings`), old-tree occurrence counting (`makeOldCounter`), and the discriminating-line / collision-exemption logic (`discriminatingLines`). Unifies `verify-publish.js` and the unknown-recovery classifier on one judgment so the two paths can never disagree about what a diff proves.
30
- - `classify-publish-absence.js` — deterministic six-way classifier for publish-parked UNKNOWN outcomes (2026-09-18, blocker 15): decides from durable signals only — the pre-trigger manifest baseline (captured by the workflow into the submitted ledger entry; design §1.9) vs the current manifest's `built_at`/`content_sha256`, `git diff <base> <commit>` discriminating lines against the on-disk source tree, and HEAD vs the publish commit. Outcomes: `provably-dropped` (source shows pre-edit state, manifest NOT advanced past the trigger, HEAD == commit, aged past quiesce — retry once), `verified` (manifest advanced past the trigger AND content_sha256 differs from the pre-trigger baseline — a new build identity, not a replayed manifest; falls back to the time-based advance check with a note when the baseline is absent, and the Step 4.5 verifier fails closed without a baseline), `applied-not-built` (platform build-emission failure; no retry — the 2026-09-12 re-trigger hazard), `ambiguous` (any inconclusive shape — no retry by design), `deferred` (build in flight — manifest changed during the content read, or built within the settle window — or park below quiesce; not a verdict, retry later), `superseded` (HEAD != commit — never retry the old commit). Retry budget is consumed by the classification itself, never by the edit attempt. Never reads the wall clock except for recovery timing; addition-only and removal-only diffs are vacuously satisfied on their empty side.
36
+ - `classify-publish-absence.js` — deterministic six-way classifier for publish-parked UNKNOWN outcomes (2026-09-18, blocker 15): decides from durable signals only — the pre-trigger manifest baseline (captured by the workflow into the submitted ledger entry; design §1.9) vs the current manifest's `built_at`/`content_sha256`, where "the trigger" is the D1 issuance instant (2026-09-19; entry_kind cut 2026-09-20): the oldest submitted ledger entry by `ts`, binding its `issued_at` (falling back to its `ts` when the capture was unobserved), `git diff <base> <commit>` discriminating lines against the on-disk source tree, and HEAD vs the publish commit. Outcomes: `provably-dropped` (source shows pre-edit state, manifest NOT advanced past the trigger, HEAD == commit, aged past quiesce — retry once), `verified` (manifest advanced past the trigger AND content_sha256 differs from the pre-trigger baseline — a new build identity, not a replayed manifest; falls back to the time-based advance check with a note when the baseline is absent, and the Step 4.5 verifier fails closed without a baseline), `applied-not-built` (platform build-emission failure; no retry — the 2026-09-12 re-trigger hazard), `ambiguous` (any inconclusive shape — no retry by design), `deferred` (build in flight — manifest changed during the content read, or built within the settle window — or park below quiesce; not a verdict, retry later), `superseded` (HEAD != commit — never retry the old commit). Retry budget is consumed by the classification itself, never by the edit attempt. Never reads the wall clock except for recovery timing; addition-only and removal-only diffs are vacuously satisfied on their empty side.
31
37
  - `build-readback-request.js` — builds the LLM-inspector read-back `verbatim_request` from the merge commit's diff (2026-09-14): carries the merged diff as the expected change and asks for an independent read of the artifact's actual source. Retained as the manual fallback; the deterministic `readback-disk.js` is the primary sensor.
32
- - `verify-publish.js` — mechanical publish verification judge (2026-09-14/16; shared primitives 2026-09-18): certifies the read-back findings block against `git diff` (strict `FILE:`/`ADDED:`/`REMOVED:`/`END_FILE` parsing, every discriminating added line PRESENT / every discriminating removed line ABSENT, HEAD==commit supersession check), then the design §1.9 manifest-freshness gate (current manifest `built_at` advanced past the trigger AND `content_sha256` differs from the workflow's pre-trigger baseline in the submitted ledger entry — a new build identity, not a replayed manifest; missing baseline fails closed), and only then stamps provenance. Diff parsing, findings parsing, and the collision-exemption rules come from the shared `lib/publish-content.js` (the unknown-recovery classifier's own judgment — one definition, never two). Binary files, mode-only changes, and fully-colliding added hunks fail closed as `unverifiable-content` (2026-09-16, critic findings 1/5) — they can never vacuously stamp. Content-mismatch, unreadable-result, superseded, and stamp failures exit 1 with `publish: verification-failed` and no stamp.
38
+ - `verify-publish.js` — mechanical publish verification judge (2026-09-14/16; shared primitives 2026-09-18): certifies the read-back findings block against `git diff` (strict `FILE:`/`ADDED:`/`REMOVED:`/`END_FILE` parsing, every discriminating added line PRESENT / every discriminating removed line ABSENT, HEAD==commit supersession check), then the design §1.9 manifest-freshness gate (current manifest `built_at` advanced past the trigger AND `content_sha256` differs from the workflow's pre-trigger baseline in the submitted ledger entry — a new build identity, not a replayed manifest; missing baseline fails closed), and only then stamps provenance. The trigger anchor is the D1 issuance instant (2026-09-19; entry_kind cut 2026-09-20): the oldest submitted ledger entry by `ts`, binding its `issued_at` (falling back to its `ts` when the capture was unobserved). Diff parsing, findings parsing, and the collision-exemption rules come from the shared `lib/publish-content.js` (the unknown-recovery classifier's own judgment — one definition, never two). Binary files, mode-only changes, and fully-colliding added hunks fail closed as `unverifiable-content` (2026-09-16, critic findings 1/5) — they can never vacuously stamp. Content-mismatch, unreadable-result, superseded, and stamp failures exit 1 with `publish: verification-failed` and no stamp.
33
39
  - `update-watch.js` — deterministic automatic update watcher (2026-09-16, zero deps): `node update-watch.js --crew-home <path>` (missing arg → usage, exit 2; every other path exits 0). Watches the public npm registry (`npm view muse-crew version` pinned to `https://registry.npmjs.org/`) vs `crew-release.sh current` and files a `workflow: "upgrade"` task with `source: npm@<version>` when policy (`auto_update_crew`, `update_channel`) and channel gating allow; watches `git ls-remote origin HEAD` on the first `deploy_type=artifact` project vs `$CREW_HOME/.update-watch.json` and files a `workflow: "chore"` task carrying the mechanical dashboard-upgrade journey. Reads the `.crew-version` compatibility anchor at the new ref via `git fetch` + `git show <sha>:.crew-version` (never the working tree) and orders dashboard-led: a declared newer crew files the crew upgrade task FIRST and the dashboard task notes it follows the crew upgrade (declaration bypasses `update_channel`, not the `auto_update_crew=false` opt-out); a declared older crew skips the dashboard leg entirely as a human decision; a missing/invalid/unfetchable anchor fails open to the dashboard leg as today. Idempotency via the same state file (records at file time); check failures log to `$CREW_HOME/update-watch.log` and are never thrown. Safety: only files tasks — never deploys, never touches the artifact/config/scheduler. Run by the daily `crew-update-watch` cron through the `current` symlink (latest release); deliberately NOT in the lib-pinning `PIN_BASENAMES`.
34
40
  - `gitignore.js` — deterministic .gitignore management for crew-owned paths (2026-09-17): the crew touches exactly one user-owned file outside `.orchestration/` — the repo's `.gitignore`. `ensureGitignoreEntries(repoPath, entries)` creates the file when missing, appends missing entries (exact line match, no duplicates), preserves existing content byte-for-byte, and is idempotent. `describeGitignoreChange(repoPath, entries)` renders the exact diff for the setup consent conversation. Crew-owned entries: `.worktrees/`, `.orchestration/user/`. CLI: `--repo <path> [--dry-run]`.
35
41
  - `repo-orchestration.js` — repository-local `.orchestration/` scaffold (2026-09-17): `scaffoldRepoOrchestration(repoPath, crewRepoPath)` creates `$REPO/.orchestration/{workflows,identities,phases,user}/`, seeds workflows/identities/phases from the crew repo's platform defaults with no-clobber semantics (existing project customizations never overwritten), and writes a README in `user/` explaining it's for local config. Idempotent. CLI: `--repo <path> --crew-repo <path>`.
@@ -42,3 +48,4 @@ Shell scripts for the crew's infrastructure. Called by workflow scripts, cron, a
42
48
  - `scaffold-crew.js` — deterministic crew-home scaffold composer (2026-09-19, Room #19 D1): replaces the agent-authored scaffold report (the agent returned prose instead of the schema'd JSON and the platform rejected it, killing room #19's init on minute one — the copying itself always succeeded). The script copies the fixed file sets (identities *.md+*.png, personas *.md, seed/workflows *.md, seed/feedback/README.md) into `<orchDir>/{identities,personas,workflows,feedback}/` with mkdir -p + no-clobber semantics (existing files never overwritten), verifies every expected file on disk (the decision rule), then writes `<orchDir>/.scaffold-receipt.json` `{created, skipped}` (integer counts, durable evidence) and prints the same JSON to stdout. Idempotent: all-skipped is exit 0. Exit 1 with the reason on stderr on any failure; exit 2 on usage errors. crew-init's scaffold-1 call is a byte courier only — it runs this script and prints the receipt's exact bytes; a paraphrased ferry fails the mechanical parse loudly as a blocked init (tripwire, never a fabricated 0/0). The receipt bytes are the agent's report of script output, not proof the script ran: crew-init corroborates with a witness re-run of this same idempotent composer (a witness that still has files to create proves the ferry's work didn't land; the witness's own disk-verification then holds the end state). Fail-closed source guards (2026-09-19 REVIEW): a missing
43
49
  or empty identities/, personas/, or seed/workflows/ source refuses loudly (exit 1,
44
50
  naming the source) before creating anything — never an empty 0/0 receipt. No wall-clock reads, no randomness.
51
+ - `publish-note-vocabulary.js` — terminal-note vocabulary registry (2026-09-19, D7): the closed enum of every `publish: …` note the state machine treats as terminally parked (`TERMINAL_PUBLISH_NOTES`, `TERMINAL_NOTE_MEANINGS`, `matchTerminalPublishNote`). Readers (`scan-publish-unknown` in `crew-api.js`) consult it instead of an inline pattern list — a recognized terminal note is skipped as terminal with its meaning named, never as `unrecognized-publish-note`; writers (`verify-publish.js`'s `terminal()`) assert their emitted verb is in the registry before writing, so no future terminal verb ships unrecognized. The closed enum is pinned structurally by `tests/publish-note-vocabulary.test.js` (every `publish: <verb>` literal in lib/ declared in the registry or the pinned transitional set). Pinned with the other runtime-relative dependencies of `crew-api.js` in the workflows' `pinLifecycle` (`NOTE_VOCAB`), enforced by `tests/pin-closure.test.js`. Extensible closed-enum pattern: the first instance of the enum-guard family the audit (§3) extends to the other state-carrying string vocabularies. No side effects on import (bare `node` exits 0).
@@ -107,6 +107,14 @@ function skip(reason, repoClean) {
107
107
  }
108
108
 
109
109
  function main() {
110
+ // --help: before required-arg parsing (shebang⇔CLI contract).
111
+ if (process.argv.slice(2).includes("--help")) {
112
+ console.log(
113
+ "usage: node advance-publish-base.js --crew-home <path> --repo <path> --project-id <id> [--scaffold-sha <sha>]\n" +
114
+ "advances the provenance base past the setup scaffold; every stdout JSON also carries repo_clean"
115
+ );
116
+ process.exit(0);
117
+ }
110
118
  const parsed = parseArgv(process.argv.slice(2));
111
119
  const { crewHome, repo, projectId } = parsed;
112
120
  let scaffoldSha = parsed.scaffoldSha || null;
@@ -1,3 +1,4 @@
1
+ #!/usr/bin/env node
1
2
  // append-ooda-step.js — deterministic writer for the OODA report log.
2
3
  //
3
4
  // The QA/repro agent runs the see-act loop itself; after each step it logs
@@ -36,10 +37,9 @@
36
37
  // Exit 0 on success, 2 on bad input. Determinism: no wall-clock reads, no
37
38
  // randomness; ts comes only from --ts (the agent's own clock) and is omitted
38
39
  // when not passed.
39
- "use strict";
40
40
 
41
- const { appendFileSync, mkdirSync, readFileSync, existsSync } = require("node:fs");
42
- const { dirname, resolve } = require("node:path");
41
+ import { appendFileSync, mkdirSync, readFileSync, existsSync } from "node:fs";
42
+ import { dirname, resolve } from "node:path";
43
43
 
44
44
  const ACTIONS = {
45
45
  aria: 1, shot: 1, click: 1, scroll: 1, type: 1,
@@ -117,6 +117,15 @@ function maxStepForAttempt(logPath, attempt) {
117
117
  }
118
118
 
119
119
  function main() {
120
+ // --help: before required-arg parsing (shebang⇔CLI contract).
121
+ if (process.argv.slice(2).includes("--help")) {
122
+ console.log(
123
+ "usage: node append-ooda-step.js --log <path> --attempt <id> --step <n> --action <a> --exit <code>\n" +
124
+ " [--args <json>] [--screenshot <path>] [--transcript <path>] [--observation <text>] [--ts <iso>]\n" +
125
+ "actions: aria|shot|click|scroll|type (browser) crop|zoom|label|nup|compose (image) terminal"
126
+ );
127
+ process.exit(0);
128
+ }
120
129
  const args = parseArgs(process.argv.slice(2));
121
130
  if (!args.log) fail("missing --log <path>");
122
131
  if (args.attempt === undefined || String(args.attempt).trim() === "") {
@@ -25,6 +25,16 @@
25
25
 
26
26
  import { execFileSync } from "node:child_process";
27
27
 
28
+ // --help: before required-arg parsing (shebang⇔CLI contract).
29
+ if (process.argv.slice(2).includes("--help")) {
30
+ console.log(
31
+ "usage: node build-readback-request.js --repo-path <path> --commit <sha>\n" +
32
+ " --base <sha> --task-id <uuid> --slug <artifact-slug> [--build-agent-id <uuid>]\n" +
33
+ "builds the read-back inspection request from the merge commit's diff"
34
+ );
35
+ process.exit(0);
36
+ }
37
+
28
38
  const EMPTY_TREE = "4b825dc642cb6eb9a060e54bf8d69288fbee4904";
29
39
 
30
40
  function arg(name) {
@@ -11,9 +11,9 @@
11
11
  // Usage: node lib/build-registry.js <workflows-dir> <output-json>
12
12
  "use strict";
13
13
 
14
- const fs = require("fs");
15
- const path = require("path");
16
- const vm = require("vm");
14
+ import fs from "node:fs";
15
+ import path from "node:path";
16
+ import vm from "node:vm";
17
17
 
18
18
  const WORKFLOWS = ["standard", "bugfix", "chore", "docs", "upgrade"];
19
19
 
@@ -23,6 +23,11 @@ function fail(msg) {
23
23
  }
24
24
 
25
25
  function main() {
26
+ // --help: before required-arg parsing (shebang⇔CLI contract).
27
+ if (process.argv.slice(2).includes("--help")) {
28
+ console.log("usage: node build-registry.js <workflows-dir> <output-json>");
29
+ process.exit(0);
30
+ }
26
31
  const workflowsDir = process.argv[2];
27
32
  const outputJson = process.argv[3];
28
33
  if (!workflowsDir || !outputJson) {
@@ -71,6 +71,17 @@ import {
71
71
  discriminatingLines,
72
72
  } from "./publish-content.js";
73
73
 
74
+ // --help: before required-arg parsing (shebang⇔CLI contract).
75
+ if (process.argv.slice(2).includes("--help")) {
76
+ console.log(
77
+ "usage: node classify-publish-absence.js --repo-path <path> --commit <sha>\n" +
78
+ " --base <sha> --slug <slug> --trigger-ts <ISO> --park-ts <ISO>\n" +
79
+ " [--spaces-root <dir>] [--task-id <uuid>] [--quiesce-mins <n>] [--in-flight-window-mins <n>] [--manifest-before <json>]\n" +
80
+ "six-way classifier for publish attempts parked with an UNKNOWN outcome; prints the decision JSON on stdout"
81
+ );
82
+ process.exit(0);
83
+ }
84
+
74
85
  function arg(name, required = true, def = null) {
75
86
  const i = process.argv.lastIndexOf(name); // last occurrence wins
76
87
  if (i < 0 || i + 1 >= process.argv.length) {
@@ -35,7 +35,7 @@
35
35
  // Exit codes: 0 ok (always — classification failure is a null result) ·
36
36
  // 2 usage/validation. On exit 2 nothing is printed to stdout.
37
37
 
38
- import { existsSync, readFileSync, statSync } from "node:fs";
38
+ import { existsSync, readFileSync, statSync, realpathSync } from "node:fs";
39
39
  import { join } from "node:path";
40
40
  import { fileURLToPath } from "node:url";
41
41
 
@@ -87,15 +87,25 @@ export function classifySurface(repoPath) {
87
87
  return { surface: null, deploy_slug: null, signals };
88
88
  }
89
89
 
90
+ const USAGE =
91
+ "usage: node classify-surface.js --repo-path <path> — prints {surface, deploy_slug, signals} as JSON, always exit 0";
92
+
90
93
  const isMainModule = (() => {
91
94
  try {
92
- return process.argv[1] === fileURLToPath(import.meta.url);
95
+ // realpath: production spawns through $CREW_HOME/current/lib (a symlink),
96
+ // so argv[1] must be resolved before comparison with import.meta.url.
97
+ return !!process.argv[1] && realpathSync(process.argv[1]) === fileURLToPath(import.meta.url);
93
98
  } catch {
94
99
  return false;
95
100
  }
96
101
  })();
97
102
 
98
103
  if (isMainModule) {
104
+ // --help: before required-arg parsing (shebang⇔CLI contract).
105
+ if (process.argv.slice(2).includes("--help")) {
106
+ console.log(USAGE);
107
+ process.exit(0);
108
+ }
99
109
  const i = process.argv.indexOf("--repo-path");
100
110
  const repoPath = i >= 0 ? process.argv[i + 1] : null;
101
111
  if (!repoPath) {
@@ -32,9 +32,11 @@
32
32
  // include it in the commit.
33
33
  // Prints JSON: { committed, sha, reason }
34
34
 
35
- const fs = require("fs");
36
- const path = require("path");
37
- const { execFileSync } = require("child_process");
35
+ import fs from "node:fs";
36
+ import path from "node:path";
37
+ import { execFileSync } from "node:child_process";
38
+ import { realpathSync } from "node:fs";
39
+ import { fileURLToPath } from "node:url";
38
40
 
39
41
  function git(repoPath, args, env) {
40
42
  try {
@@ -99,17 +101,31 @@ function commitScaffold(repoPath, opts) {
99
101
  return { committed: true, sha: sha, reason: null };
100
102
  }
101
103
 
102
- module.exports = { commitScaffold, isPathDirty };
104
+ export { commitScaffold, isPathDirty };
103
105
 
104
- if (require.main === module) {
106
+ const USAGE = "Usage: node lib/commit-scaffold.js --repo <path> [--gitignore]";
107
+
108
+ const isMainModule = (() => {
109
+ try {
110
+ return !!process.argv[1] && realpathSync(process.argv[1]) === fileURLToPath(import.meta.url);
111
+ } catch {
112
+ return false;
113
+ }
114
+ })();
115
+
116
+ if (isMainModule) {
105
117
  const argv = process.argv.slice(2);
118
+ if (argv.includes("--help")) {
119
+ console.log(USAGE);
120
+ process.exit(0);
121
+ }
106
122
  const get = (flag) => {
107
123
  const i = argv.indexOf(flag);
108
124
  return i >= 0 ? argv[i + 1] : null;
109
125
  };
110
126
  const repoPath = get("--repo");
111
127
  if (!repoPath) {
112
- console.error("Usage: node lib/commit-scaffold.js --repo <path> [--gitignore]");
128
+ console.error(USAGE);
113
129
  process.exit(2);
114
130
  }
115
131
  try {
@@ -36,10 +36,13 @@
36
36
  //
37
37
  // Prints the caption to stdout. Exits non-zero on any missing input.
38
38
 
39
- const { execFileSync } = require("node:child_process");
40
- const fs = require("node:fs");
41
- const os = require("node:os");
42
- const path = require("node:path");
39
+ import { execFileSync } from "node:child_process";
40
+ import fs from "node:fs";
41
+ import os from "node:os";
42
+ import path from "node:path";
43
+ import { fileURLToPath } from "node:url";
44
+
45
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
43
46
 
44
47
  function usage(msg) {
45
48
  process.stderr.write("compose-evidence-caption: " + msg + "\n");
@@ -47,6 +50,14 @@ function usage(msg) {
47
50
  }
48
51
 
49
52
  const argv = process.argv.slice(2);
53
+ // --help: before required-arg parsing (shebang⇔CLI contract).
54
+ if (argv.includes("--help")) {
55
+ console.log(
56
+ "usage: node compose-evidence-caption.js --crew-home <home> --task-id <id> --audit-dir <resolved-dir-name>\n" +
57
+ "prints the QA-evidence caption to stdout"
58
+ );
59
+ process.exit(0);
60
+ }
50
61
  const args = {};
51
62
  for (let i = 0; i < argv.length; i++) {
52
63
  if (argv[i].startsWith("--")) {
@@ -40,6 +40,15 @@ import { createHash } from "node:crypto";
40
40
  import { existsSync, mkdirSync, statSync, writeFileSync, readFileSync } from "node:fs";
41
41
  import { dirname, resolve } from "node:path";
42
42
 
43
+ // --help: before required-arg parsing (shebang⇔CLI contract).
44
+ if (process.argv.slice(2).includes("--help")) {
45
+ console.log(
46
+ "usage: node compute-publish-diff.js --repo-path <path> --base <sha> --out <file>\n" +
47
+ "runs git diff <base> <HEAD>, writes the raw diff bytes to --out, prints the small JSON summary on stdout"
48
+ );
49
+ process.exit(0);
50
+ }
51
+
43
52
  const EMPTY_TREE = "4b825dc642cb6eb9a060e54bf8d69288fbee4904";
44
53
 
45
54
  function arg(name, required = true) {