muse-crew 0.14.3 → 0.14.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. package/AGENTS.md +2 -2
  2. package/docs/decisions/AGENTS.md +1 -0
  3. package/docs/decisions/publish-path.md +96 -3
  4. package/docs/guide.md +2 -2
  5. package/docs/publish-unknown-recovery.md +10 -3
  6. package/docs/publish-verification.md +126 -62
  7. package/docs/release-integrity.md +62 -0
  8. package/docs/reviews/critic-0144.md +83 -0
  9. package/docs/reviews/critic-0145.md +106 -0
  10. package/lib/AGENTS.md +12 -4
  11. package/lib/advance-publish-base.js +8 -0
  12. package/lib/append-ooda-step.js +12 -3
  13. package/lib/build-readback-request.js +10 -0
  14. package/lib/build-registry.js +8 -3
  15. package/lib/check-intent-freshness.js +101 -0
  16. package/lib/classify-publish-absence.js +11 -0
  17. package/lib/classify-surface.js +12 -2
  18. package/lib/commit-scaffold.js +22 -6
  19. package/lib/compose-evidence-caption.js +15 -4
  20. package/lib/compute-publish-diff.js +41 -11
  21. package/lib/crew-api.js +478 -26
  22. package/lib/crew-release.sh +233 -1
  23. package/lib/gitignore.js +23 -5
  24. package/lib/package.json +1 -0
  25. package/lib/publish-note-vocabulary.js +64 -0
  26. package/lib/read-ooda-verdict.js +11 -3
  27. package/lib/readback-disk.js +9 -0
  28. package/lib/render-html.js +17 -7
  29. package/lib/repo-orchestration.js +21 -5
  30. package/lib/retry-publish.js +116 -72
  31. package/lib/sample-project.js +22 -6
  32. package/lib/scaffold-crew.js +11 -2
  33. package/lib/see-act.js +17 -8
  34. package/lib/serve-artifact.js +12 -6
  35. package/lib/setup-project-repo.js +26 -7
  36. package/lib/update-watch.js +34 -17
  37. package/lib/ux-doctrine.js +31 -6
  38. package/lib/verify-publish.js +44 -6
  39. package/lib/write-ooda-verdict.js +12 -3
  40. package/package.json +1 -1
  41. package/seed/cron-body-template.md +50 -16
  42. package/workflows/bugfix.js +110 -643
  43. package/workflows/chore.js +110 -643
  44. package/workflows/crew-dispatch.js +36 -0
  45. package/workflows/standard.js +118 -633
  46. package/workflows/upgrade.js +4 -2
@@ -0,0 +1,106 @@
1
+ # Critic review — 0.14.5 release candidate (one-party worker-owned publish)
2
+
3
+ Change set: `v0.14.4..HEAD` worktree (32 files, +1444/−4013). Full suite green on the final tree ("All test suites passed.", exit 0, 90 suites). Read-only review; no repo modifications made by the panel. Coordinator empirically verified the load-bearing claims before disposition.
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) unknown outcomes fail closed — never stamp/verify on inconclusive evidence; (d) don't proliferate tests — reuse the existing suite; (e) implementation and documentation ship together; (f) only `submitted` ledger entries with `issuer: "tick-worker"` bind as real issuance; (g) take critic blessings skeptically — every disposition below is checked against the rules and the code, not the panel's enthusiasm. Two panel claims were verified empirically by the coordinator before acceptance (F-O1, F-O2); one high claim was rejected on the code (F-R1).
6
+
7
+ ## Verdicts
8
+
9
+ **Architect: CONCERN** — the one-party shape is the right architecture and trust boundaries are clean in code (no workflow path writes `submitted`; no tick path issues without a claim/lock; the scan never emits `retry_due` with `retry-issued` in history; the verified re-entry guard is sound — no TOCTOU constructible). Four concrete defects: one stale string reintroducing blocker 22 into the retry path (F-A1), one false absolute guarantee (F-A2), one forced outcome ternary (F-A3), one new park→issuance base-staleness window (F-A4). None is fail-open at the stamp — the content read-back backstop holds everywhere — hence CONCERN, not REJECT.
10
+
11
+ **Subtractor: ACCEPT** — the change set does what it claims; the new code is mechanically disciplined; contracts (a), (d), (e), (f) hold. Three low-severity deletable leftovers (F-S1–S3), two info notes (F-S4–S5). Notably, the Subtractor did NOT claim the retry protocol as a must-cut — the step-back disposition (keep for 0.14.5) stands unchallenged.
12
+
13
+ **Reliability: CONCERN** — sequential operation is fail-closed and well-tested (10 intent-path probes + 22 validateRegistry probes green; scan-publish-unknown.test.js 45/45). The headline "never double-issue" guarantee is carried by prose (F-R2) and has a null-baseline hole with no terminal exit (F-R3). F-R1 (non-atomic lease) was REJECTED by the coordinator on the code — see below.
14
+
15
+ **Room Operator: REJECT** — two high findings in the 4.4b crash-recovery path (F-O1: the `recovered` outcome can never pass verification — self-defeating; F-O2: `--commit` silently ignored by the diff regenerator, and the mismatch path loops forever with no terminal exit). Both are cause-level and both were confirmed by the coordinator. The REJECT converts to ship-with-fixes: both have small mechanical fixes.
16
+
17
+ ## Findings and dispositions
18
+
19
+ ### Must-fix before ship
20
+
21
+ **F-O1 (high) — ACCEPTED. The `recovered` outcome can never pass verification.**
22
+ `record-intent-issuance` writes `issued_at = null` for `recovered` (lib/crew-api.js:2363); the verifier binds `triggerTs = issued_at || ts` = the *recovery-record* time T2 (lib/verify-publish.js:356), then requires `built_at > T2` (:376-378). The build completed at T1 < T2, so every genuine recovery ends terminal `unverifiable-content` with the false message "no new build for this attempt". Coordinator-verified on the code. The author's comment ("the verifier falls back to ts") shows the confusion: the recovery-record ts is necessarily *after* the build — it is not a usable bound.
23
+ *Fix:* for `recovered`, set `issued_at` to the dead tick's claim time (the earliest `publish: publish-intent-claimed` note's ts — `writePublishNote` takes an explicit claimedAt, and the intent-claimed note carries it). Issuance ∈ [claim, expiry] and built_at > issuance, so built_at > claim_time is a sound lower bound. Small, mechanical, testable.
24
+
25
+ **F-O2 (high) — ACCEPTED. 4.4b's regeneration passes `--commit`; `compute-publish-diff.js` silently ignores it.**
26
+ Only `--repo-path`/`--base`/`--out` are parsed (lib/compute-publish-diff.js:60-75); regeneration is `base..HEAD`, not the documented `base..commit` (docs/publish-verification.md:43). Coordinator-ran the command: exit 0, `--commit deadbeef…` ignored, stdout reports HEAD. Worse: on sha256 mismatch the template says only "log loudly and STOP" — the claim expires, the next scan re-claims, identical result, forever. The outcome enum has no terminal branch for this case.
27
+ *Fix:* teach `compute-publish-diff.js` a `--commit` flag (diff `base..commit`, default HEAD) so the template command does what the doc claims; add a terminal park (`publish: publish-unissuable` or equivalent vocabulary + parked for human attention) for the unresolvable-regeneration case instead of the silent reclaim loop.
28
+
29
+ **F-A1 (medium) — ACCEPTED. Stale "spawn the artifact_edit child" message in `retry-publish.js:323`.**
30
+ The prepare output — the actual handoff the tick reads — still instructs the blocker-22-forbidden shape, while the cron body (4.4) and the recovery doc were updated to tick-mediated direct issuance. Coordinator-confirmed verbatim. A tick following the tool message spawns a child → `missing session id` → the "otherwise `--edit-outcome accepted`" contract mints a phantom issuer-stamped `submitted` for an edit that never went out, spending the single retry budget on a non-attempt. The verification backstop fails closed (no stamp), but the ledger is poisoned.
31
+ *Fix:* one line — change the message to the tick-mediated direct-issuance instruction. Contract (a) violation at the exact handoff the tick reads.
32
+
33
+ ### Should-fix for 0.14.5
34
+
35
+ **F-A4 (medium) — ACCEPTED. No base-currency check between intent-park and issuance.**
36
+ The intent entry carries `diff_path`/`diff_sha256` but no base; the lock releases at the intent park, so issuance happens ≥1 tick later, lock-free. If provenance moved in between (another task's publish verified), the tick issues a diff against a superseded base: overlapping hunks → spurious terminal `publish: publish-refused`; partial application → `content-mismatch` park. Not stamp-unsafe, but a liveness regression in the multi-task pipeline the dispatcher supports. New in 0.14.5 (0.14.4 computed and issued in one lock-held session).
37
+ *Fix:* record `base` in the intent ledger entry; the scan's intent branch (or the tick's step 1) fails the claim loudly when `base !==` current stamped provenance — re-queue the task (the workflow's Publish re-prepares against the new base), never issue the stale diff.
38
+
39
+ **F-O3 / F-R2 (medium) — ACCEPTED (merged). The reclaim freshness check is prose, not mechanism.**
40
+ Template 4.4b step 2 tells the tick to compare `artifact_status` fields the repo never establishes against `manifest_before`, but `record-intent-issuance` takes no freshness evidence and records `accepted` unconditionally. The crash-window guarantee therefore rests on tick discipline — the "wording is not a mechanism" class. A null `manifest_before` (best-effort baseline, explicitly never-gating) leaves the worker guessing between blind re-issue and false `recovered`.
41
+ *Fix:* a deterministic freshness helper (lib script reading the on-disk manifest at the path the verifier already uses, comparing `content_sha256`/`built_at` against `manifest_before`) whose evidence is passed to / recorded by `record-intent-issuance`; the template names the manifest path and the exact comparison instead of `artifact_status` fields.
42
+
43
+ **F-A3 / F-O5 (low-medium) — ACCEPTED (merged). The outcome enum has no "unknown": inconclusive issuance is forced into `accepted`.**
44
+ Template 4.4b never defines `accepted` (the definition lives only in a code comment), and 4.4b lacks the tool-unavailable clause 4.4 has. Transport timeouts, apply-failures on already-applied hunks, namespace-load failures — none are "explicit refusal", so they land in `accepted` → issuer-stamped `submitted` on unproven issuance. Contained at the stamp (verification still requires a content match), but it weakens contract (f)'s meaning.
45
+ *Fix:* define `accepted` precisely in the template ("the direct call completed without error or explicit refusal"); add the inconclusive branch — on ambiguous outcome or tool-unavailable, do NOT record, log loudly, and exit: the claim expires and the reclaim path (with the mechanized freshness check) is exactly the designed handler for "maybe issued". Add 4.4's tool-unavailable clause to 4.4b.
46
+
47
+ **F-O4 (medium) — ACCEPTED. The known-blockers snapshot shipped already stale; no refresh mechanism.**
48
+ The template's blocker-23 snapshot says "re-anchor or remove the retry machinery" while the authoritative file says "Fixed in BUILD (0.14.5): ported to node:sqlite + crew-state.db". Grep over docs/, workflows/upgrade.js, tests/run.sh: no script, release step, or test performs the promised "refreshed at release time" — prose only. A fixed blocker keeps telling pollers to "log one line and move on".
49
+ *Fix for 0.14.5:* update the snapshot text to match the file now. Follow-up: a mechanical refresh (release-checklist step or a test that diffs the template snapshot against known-blockers.md's settled list).
50
+
51
+ **F-R3 (medium) — ACCEPTED. Null `manifest_before` on reclaim has no terminal exit.**
52
+ First claim + null baseline is safe to issue (no prior issuance possible under a fresh claim). Reclaim + null baseline is not: the freshness comparison is unperformable, the tick's safe move (stop without recording) yields an hourly reclaim loop with no human escalation, the unsafe move is a blind re-issue.
53
+ *Fix:* on reclaim with null baseline, fail closed with a terminal human-attention park — never blind-issue, never silent-loop. Contract (c).
54
+
55
+ **F-R5 (low) — ACCEPTED. Intent bucket can carry `repo_path: null` without failing closed.**
56
+ The scan fails closed on missing `deploy_slug` but emits `repo_path: null`; the tick then fails at sha256sum/regen and the task loops on hourly re-claims.
57
+ *Fix:* fail closed at emission — terminal/human park, same as the slug check.
58
+
59
+ **F-S1 / F-S2 / F-S3 (low) — ACCEPTED. Trivial cleanups.**
60
+ Dead `repo` param on `releaseLock` (lib/retry-publish.js:192, :356 — all 8 call sites pass nothing); dead `notes` param on `claimForIntent` (lib/crew-api.js:2014); four stray `echo "` debris lines in tests/run.sh (:698, :700, :702, :704) from the deleted-test-block removals.
61
+
62
+ ### Downgraded / rejected / deferred
63
+
64
+ **F-R1 (high) — REJECTED as stated. The "non-atomic lease" does not survive the code.**
65
+ The claim write is a read-then-write inside a single `BEGIN`/`COMMIT` (lib/crew-api.js:2115/2264) with `ROLLBACK` + throw on any error, under WAL + `busy_timeout = 5000` (:252-254). Two overlapping scans cannot both commit: the loser's write waits, then its COMMIT fails on the stale snapshot (`SQLITE_BUSY_SNAPSHOT`) and rolls back entirely. Coordinator-verified against the transaction code; the Architect's I-3 (fail-closed, not silent) was the correct read. The residual double-issuance risk is F-A2's *sequential* lease-expiry path only — no advisory-lock fix needed.
66
+
67
+ **F-A2 (medium) — ACCEPTED as a documentation fix.**
68
+ Slow-but-alive tick past the 1h lease + manifest lag (build normally still in flight exactly when the check matters — fire-and-forget, no receipt) = double *physical* issuance of the identical diff. Consequence is bounded: at most one *recorded* issuance (CAS), the verifier certifies content not provenance-of-issue, and identical bytes are idempotent-ish. Exactly-once physical issuance is not mechanically achievable with receipt-less semantics — the honest disposition is to reword the change set's "the diff is never issued twice" to what the mechanism provides: at-most-one recorded issuance; duplicate physical issuance possible in the lease-expiry window, content-identical, verifier-bounded. The F-O3/F-R2 freshness mechanization narrows the window; it cannot close it.
69
+
70
+ **F-R4 (low) — ACCEPTED as follow-up.** The exactly-one-retry `historyHas("publish: retry-issued")` scans only the last 40 publish notes (pre-existing shape, also in v0.14.4). Unbound it when convenient; not ship-blocking.
71
+
72
+ **F-O6 (low) — ACCEPTED as follow-up.** Skip/record-failure reasons are stdout-only; DB-alone debugging has blind spots. Nice-to-have.
73
+
74
+ **F-S4 / F-S5 (info) — noted, no action.** Pre-existing duplicated comment; legacy fixtures mixing the new issuer stamp with old workflow labels are cosmetic (the bound fields are exactly producible).
75
+
76
+ ## Coordinator's empirical verifications
77
+
78
+ 1. **F-O1 confirmed on the code:** `issued_at = null` for `recovered` (crew-api.js:2363) → verifier binds `triggerTs = issued_at || ts` = recovery-record time (verify-publish.js:356) → `built_at > triggerTs` (:376-378) strictly false in the normal case. Every genuine recovery ends terminal `unverifiable-content`. The dead tick's claim time is recoverable from note history (`writePublishNote` takes an explicit claimedAt), giving the fix a sound lower bound.
79
+ 2. **F-O2 confirmed by execution:** `node lib/compute-publish-diff.js --repo-path . --base <empty-tree> --commit deadbeef… --out /tmp/x.diff` → exit 0, `--commit` silently ignored, stdout reports HEAD. The template's regeneration command does not do what the doc claims.
80
+ 3. **F-R1 rejected on the code:** single BEGIN/COMMIT + ROLLBACK/throw, WAL, busy_timeout=5000 — the concurrent-scan double-claim cannot commit; the loser rolls back. The critic's sequential probes were valid; the concurrent mechanism was not.
81
+ 4. **F-A1 confirmed verbatim:** `lib/retry-publish.js:323` still reads "spawn the artifact_edit child with the diff".
82
+ 5. **F-A4's premise confirmed:** `parkTask` → `terminalCleanup` releases the merge lock at the intent park (prior BUILD proof) — the park→issuance window is lock-free and new in 0.14.5.
83
+
84
+ ## Step-back round (run by the coordinator, not delegated)
85
+
86
+ Question: is one-party tick-worker-owned publish the right shape, or is there a simpler architecture that dissolves it?
87
+
88
+ **A. Synchronous publish inside the workflow via a new platform capability.** Requires a platform change that doesn't exist. The current shape is the minimal adaptation to the platform as it is. Not actionable — external dependency, not a design flaw. *Coordinator: agree. Nothing in the review changes this.*
89
+
90
+ **B. Tick does everything including diff computation; workflow parks with bare coordinates.** Not simpler, just relocated: moves deterministic, testable preparation into the 90-minute tick where failures are harder to attribute. *Coordinator: agree, and the review reinforces it — F-O3/F-R2's fix is to mechanize the freshness check as a lib script, i.e. keep deterministic work in code, not move it into the tick's prose.*
91
+
92
+ **C. Tick applies the diff directly to `~/workspace/ts-spaces/<slug>/` via filesystem, bypassing `artifact_edit`.** Rejected: bypasses the supported product interface, and nothing proves the platform rebuilds on raw filesystem writes. *Coordinator: agree firmly. The read-back sensor reads the working copy, but writing to it directly would be the same cleverness-in-the-wrong-direction the loop keeps burning rooms on.*
93
+
94
+ **D. Route verified tasks straight to QA; delete the verified re-entry guard.** The guard is ~30 lines, mechanical, proven non-gating; the alternative is dispatcher special-casing. *Coordinator: agree — and the Architect verified the guard sound (no TOCTOU constructible; QA is an independent backstop).*
95
+
96
+ **E. Delete the retry protocol as subsumed by intent recovery.** The one live candidate. Under one-party, every new issuance is synchronous with immediate accepted/refused; the dropped shape is handled by intent reclaim + freshness → `recovered`. *Coordinator: KEEP for 0.14.5.* The review reinforces this: F-A1 lives in the retry path (fix the message, keep the path), F-R4 notes the retry budget, and the retry protocol is the only backstop for legacy "Publish outcome unknown" parks. Deleting it now, on zero clean-room evidence, would remove defense-in-depth for aesthetics. Filed as designed follow-up after the first clean room runs, when `retry_due` can be proven never to fire for new-model publishes. The Subtractor did not claim it as a must-cut — the guard held.
97
+
98
+ **F. Collapse the publish-intent ledger entry and the park note into one record.** Reject: the ledger is the audit trail, the note is the state machine — different consumers; merging tangles the two. *Coordinator: agree.*
99
+
100
+ **Step-back conclusion:** the one-party shape is the right architecture given the platform constraint; the workflow-prepares/tick-issues split isolates the privileged act minimally; the lease/reclaim machinery is proportional to the exactly-once-recorded requirement (overlapping ticks are possible: 90-min timeout vs 15-min interval); the retry protocol is the sole redundancy candidate and stays as a backstop for 0.14.5. One gap the step-back missed and the panel caught: F-A4's park→issuance base-staleness window — a genuine liveness hole in the multi-task pipeline, with a small mechanical fix. No better architecture found — the design stands, with the retry-deletion filed as post-room follow-up.
101
+
102
+ ## Recommendation: SHIP WITH FIXES
103
+
104
+ Fix the 3 must-fix (F-O1, F-O2, F-A1) and the 7 should-fix (F-A4, F-O3/F-R2, F-A3/F-O5, F-O4-text, F-R3, F-R5, F-S1/S2/S3), re-run the full suite, then publish. No finding is fail-open at the stamp — all four critics agree the content read-back backstop holds on every path — the architecture is sound, and every fix is small and mechanical. F-R4 and F-O6 ride as follow-ups; the retry-protocol deletion rides as a post-room follow-up.
105
+
106
+ What "done" looks like before PUBLISH: F-O1's `issued_at` bound in `record-intent-issuance` + test; `--commit` in `compute-publish-diff.js` + terminal `publish-unissuable` vocabulary + test; F-A1's one-line message fix; intent entry carries `base` + stale-base re-queue + test; mechanized freshness helper + template rewrite of step 2; `accepted` defined + inconclusive branch + 4.4b tool-unavailable clause; blocker-23 snapshot text corrected; null-baseline-reclaim and null-repo_path terminal parks + tests; dead params and stray echo lines removed. Then one fresh uninterrupted `bash tests/run.sh`, then the release sequence.
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,18 +33,20 @@ 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>`.
36
42
  - `setup-project-repo.js` — repo-local setup composer for a registered project (2026-09-17, setup-commit 2026-09-18): runs the `.orchestration/` scaffold plus the `.gitignore` update (only with explicit `--consent`), then COMMITS both via `commit-scaffold.js` so the repo's primary checkout is clean when setup finishes (room #13: an uncommitted scaffold deterministically broke the first task's Integrate). Refuses loudly when `.gitignore` has pre-existing uncommitted changes (never sweeps the user's edits into the setup commit); `.orchestration/` is crew-owned so pre-existing content there is committed, not refused. The CLI-path equivalent of crew-init's dashboard-mode repo setup; the dashboard calls the same script. No new API endpoint — composition of existing deterministic modules. CLI: `--repo <path> --crew-repo <path> [--consent]`; prints `{ orchestration, gitignore, commit }`, exits non-zero on any failure.
37
43
  - `commit-scaffold.js` — deterministic setup-scaffold commit (2026-09-18): stages only `.orchestration/` (+ `.gitignore` with `--gitignore`) and commits with a message naming exactly what was staged. Idempotent (`nothing-to-commit`, never an empty commit); not-a-git-repo is a clean no-op; missing `.orchestration/` and commit failures (e.g. no git identity) throw loudly — the commit belongs to the user, so no identity is ever invented here. Exports `isPathDirty` (porcelain check for one path) for the composer's pre-mutation guard. CLI: `--repo <path> [--gitignore]`.
38
44
  - `sample-project.js` — optional sample-project setup (2026-09-17): extracts `seed/sample-project.tar.gz` (stripping the `sample-project/` prefix), `git init`, initial commit. No remote required. Registration happens via the existing `create-project` action — no sample-specific endpoint. CLI: `--tarball <path> --dest <dir> --name <project-name>`.
39
- - `compute-publish-diff.js` — deterministic publish-diff computer (2026-09-18, room #14): replaces the agent-ferried `git diff` JSON string field (the JSON ferry dropped a valid 700-line diff — "Publish diff parsed to zero files"). Runs `git diff <base> <HEAD>` via argv (no shell), writes the raw diff bytes to `--out`, and prints only a small JSON summary (`commit`, `base`, `diff_file`, `sha256`, `bytes`, `file_count`, `changed_lines` = added+removed, never raw diff output lines, `files`, `has_binary`, `has_rename`). Empty-tree base is the first-publish ancestor (hardcoded, never `merge-base` on the empty tree); non-ancestor is semantic JSON (`ok:false, error:"not-ancestor"`, exit 0); usage errors exit 2 with nothing on stdout. Pinned per-run alongside the other lifecycle files (`COMPUTE_DIFF` in `PIN_BASENAMES`) so a mid-run release swap cannot change it under the workflow.
45
+ - `compute-publish-diff.js` — deterministic publish-diff computer (2026-09-18, room #14): replaces the agent-ferried `git diff` JSON string field (the JSON ferry dropped a valid 700-line diff — "Publish diff parsed to zero files"). Runs `git diff <base> <commit>` via argv (no shell) — `--commit <40-hex>` (2026-09-20, critic-0145 F-O2) pins the diff target deterministically; omitting it defaults to HEAD for the workflow's initial preparation. Writes the raw diff bytes to `--out`, and prints only a small JSON summary (`commit`, `base`, `diff_file`, `sha256`, `bytes`, `file_count`, `changed_lines` = added+removed, never raw diff output lines, `files`, `has_binary`, `has_rename`). Empty-tree base is the first-publish ancestor (hardcoded, never `merge-base` on the empty tree); non-ancestor is semantic JSON (`ok:false, error:"not-ancestor"`, exit 0); usage errors exit 2 with nothing on stdout. Pinned per-run alongside the other lifecycle files (`COMPUTE_DIFF` in `PIN_BASENAMES`) so a mid-run release swap cannot change it under the workflow.
46
+ - `check-intent-freshness.js` — deterministic intent-freshness helper (2026-09-20, critic-0145 F-O3/F-R2): on a re-claimed publish intent, the tick must decide whether the dead tick's edit already landed before re-issuing — prose judgment is forbidden. `node check-intent-freshness.js --slug <slug> --manifest-before '<json>' [--spaces-root <path>]` reads `<spaces-root>/<slug>/.space-build/manifest.json` (the same path the verifier uses) and prints exactly one JSON object `{ok, advanced, baseline:{built_at,content_sha256}, current:{built_at,content_sha256}}`; `advanced` is true when the content hash changed or `built_at` advanced past the baseline. Unreadable/missing manifest is `ok:false` (exit 1) — the freshness question is unanswerable, so the tick records nothing and lets the claim expire. The helper's stdout JSON is passed verbatim as `freshness_evidence` to `record-intent-issuance ... edit_outcome=recovered`; the command refuses a recovery without `advanced:true` evidence.
40
47
  - `classify-surface.js` — deterministic project-surface classifier (2026-09-18, room #14 defect): a pure, never-throwing scan of the registered repo — `space.json` present → `artifact` surface (deploy_slug suggestion = `space.json`'s valid slug, verbatim — presence alone still means "web app"), `package.json` with a non-empty `bin` → `terminal` surface, otherwise null (unclassified); artifact wins conflicts (the deployed artifact is what users see, same rule as workflows/standard.js). CLI `--repo-path` prints one JSON object (`{surface, deploy_slug, signals}`), always exit 0 (classification failure is a null result, exit 2 only for usage). `create-project` fills omitted `environment_type`/`deploy_type`/`deploy_slug` from the classification; explicit values (including explicit null) always win and drive the effective surface.
41
48
  - `advance-publish-base.js` — publish-base advance past the setup scaffold (2026-09-18, room #14; testimony-free 2026-09-19 REVIEW): `create-project` stamps provenance at the pre-scaffold repo HEAD, then crew-init commits `.orchestration/` (+ the consented `.gitignore`) on top — the first task's publish diff would carry the whole scaffold (517 insertions) and blow the 200-line diff-transport budget. The script resolves the scaffold commit itself (`git rev-parse HEAD` in `--repo`; `--scaffold-sha` is an explicit override only) and moves `source_commit` to it only when every fail-closed condition holds deterministically: the existing stamp is not a real publish's (provenance.task_id is set by every real publish and null for initial stamps/base advances — the task_id invariant documented on stampProvenance; the old `--project-created` testimony flag is gone), the scaffold commit's parent is exactly the stamped base, and the scaffold touches only crew-owned paths (`.orchestration/`, `.gitignore`); no provenance + root scaffold commit stamps fresh. Every other case skips without touching provenance; `published_at` is preserved on advance. Every stdout JSON object also carries `repo_clean` (informational — whether `git status --porcelain` is empty — so the caller can warn loudly about a dirty tree; the first task's Integrate refuses to merge onto one). Called from crew-init.js (best-effort, non-fatal). Provenance is per-project (room #15 blocker 8): `--project-id` is required and every read/stamp is project-scoped. CLI: `--crew-home <path> --repo <path> --project-id <id> [--scaffold-sha <sha>]`.
42
49
  - `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
50
  or empty identities/, personas/, or seed/workflows/ source refuses loudly (exit 1,
44
51
  naming the source) before creating anything — never an empty 0/0 receipt. No wall-clock reads, no randomness.
52
+ - `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) {
@@ -0,0 +1,101 @@
1
+ #!/usr/bin/env node
2
+ // check-intent-freshness.js — deterministic manifest-freshness check for a
3
+ // re-claimed one-party publish intent (2026-09-20, critic-0145 F-O3/F-R2).
4
+ //
5
+ // A re-claiming tick must decide: did the dead tick issue the edit (record
6
+ // "recovered") or not (issue it now)? The decision is mechanical, never
7
+ // prose: read the on-disk artifact manifest at the same path the verifier
8
+ // uses (<spaces-root>/<slug>/.space-build/manifest.json — see
9
+ // lib/verify-publish.js) and compare its content_sha256 / built_at against
10
+ // the intent entry's pre-issuance baseline (manifest_before).
11
+ //
12
+ // The manifest ADVANCED — a new build identity, so the dead tick issued —
13
+ // when either:
14
+ // current.content_sha256 !== baseline.content_sha256 (a new content hash), OR
15
+ // Date.parse(current.built_at) > Date.parse(baseline.built_at) (a later build).
16
+ // Otherwise it still shows the pre-edit state and the tick issues fresh.
17
+ //
18
+ // Stdout contract: exactly one JSON object, always small:
19
+ // { ok:true, advanced:<bool>, baseline:{built_at, content_sha256}, current:{built_at, content_sha256} }
20
+ // { ok:false, error:"no-baseline"|"unreadable-manifest"|"unparseable-manifest" }
21
+ //
22
+ // Exit codes: 0 ok (JSON on stdout) · 2 usage · 1 IO failure (nothing on
23
+ // stdout — a caller must never treat stdout as a result). Errors go to
24
+ // stderr as JSON.
25
+ //
26
+ // A non-ok result is never a verdict — the caller fails closed: record
27
+ // nothing and let the claim expire into the reclaim path (the scan's F-R3
28
+ // terminal park covers the null-baseline reclaim; an unreadable manifest
29
+ // leaves the task parked for the next tick). Reclaim decisions never rest
30
+ // on prose.
31
+
32
+ import { readFileSync } from "node:fs";
33
+ import { homedir } from "node:os";
34
+ import { join } from "node:path";
35
+
36
+ // --help: before required-arg parsing (shebang⇔CLI contract).
37
+ if (process.argv.slice(2).includes("--help")) {
38
+ console.log(
39
+ "usage: node check-intent-freshness.js --slug <slug> --manifest-before '<json>' [--spaces-root <path>]\n" +
40
+ "compares the on-disk artifact manifest's content_sha256/built_at against the pre-issuance baseline; prints {ok, advanced} JSON on stdout"
41
+ );
42
+ process.exit(0);
43
+ }
44
+
45
+ function arg(name, required = true) {
46
+ const i = process.argv.indexOf(name);
47
+ if (i < 0 || i + 1 >= process.argv.length) {
48
+ if (!required) return null;
49
+ fail("usage", `${name} is required.`, 2);
50
+ }
51
+ return process.argv[i + 1];
52
+ }
53
+ function fail(code, message, exitCode) {
54
+ process.stderr.write(JSON.stringify({ ok: false, error: code, message }) + "\n");
55
+ process.exit(exitCode);
56
+ }
57
+
58
+ const slug = arg("--slug");
59
+ const baselineRaw = arg("--manifest-before");
60
+ const spacesRoot = arg("--spaces-root", false) || join(homedir(), "workspace", "ts-spaces");
61
+
62
+ if (!slug || /[/\\]/.test(slug)) fail("usage", "--slug must be a non-empty slug (no path separators).", 2);
63
+
64
+ let baseline;
65
+ try {
66
+ baseline = JSON.parse(baselineRaw);
67
+ } catch {
68
+ fail("usage", "--manifest-before must be JSON.", 2);
69
+ }
70
+ if (!baseline || typeof baseline !== "object") fail("no-baseline", "--manifest-before is not an object.", 1);
71
+ const baselineSha = typeof baseline.content_sha256 === "string" ? baseline.content_sha256 : null;
72
+ const baselineBuiltAt = typeof baseline.built_at === "string" ? baseline.built_at : null;
73
+ if (!baselineSha && !baselineBuiltAt) {
74
+ fail("no-baseline", "the baseline carries neither content_sha256 nor built_at — freshness is unprovable.", 1);
75
+ }
76
+
77
+ let currentManifest;
78
+ try {
79
+ currentManifest = JSON.parse(readFileSync(join(spacesRoot, slug, ".space-build", "manifest.json"), "utf8"));
80
+ } catch (e) {
81
+ const code = e && e.code === "ENOENT" ? "unreadable-manifest" : "unreadable-manifest";
82
+ fail(code, `cannot read the on-disk manifest: ${(e && e.message) || e}`.slice(0, 300), 1);
83
+ }
84
+ if (!currentManifest || typeof currentManifest !== "object") {
85
+ fail("unparseable-manifest", "the on-disk manifest is not a JSON object.", 1);
86
+ }
87
+ const currentSha = typeof currentManifest.content_sha256 === "string" ? currentManifest.content_sha256 : null;
88
+ const currentBuiltAt = typeof currentManifest.built_at === "string" ? currentManifest.built_at : null;
89
+
90
+ const hashAdvanced = !!(baselineSha && currentSha && currentSha !== baselineSha);
91
+ const currentBuiltAtMs = currentBuiltAt ? Date.parse(currentBuiltAt) : NaN;
92
+ const baselineBuiltAtMs = baselineBuiltAt ? Date.parse(baselineBuiltAt) : NaN;
93
+ const timeAdvanced = !!(currentBuiltAtMs > 0 && baselineBuiltAtMs > 0 && currentBuiltAtMs > baselineBuiltAtMs);
94
+ const advanced = hashAdvanced || timeAdvanced;
95
+
96
+ process.stdout.write(JSON.stringify({
97
+ ok: true,
98
+ advanced,
99
+ baseline: { built_at: baselineBuiltAt, content_sha256: baselineSha },
100
+ current: { built_at: currentBuiltAt, content_sha256: currentSha },
101
+ }) + "\n");
@@ -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("--")) {