muse-crew 0.14.2 → 0.14.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +1 -1
- package/docs/decisions/AGENTS.md +4 -0
- package/docs/decisions/publish-path.md +229 -0
- package/docs/publish-verification.md +14 -3
- package/docs/release-integrity.md +60 -0
- package/docs/reviews/critic-0143-redo.md +94 -0
- package/docs/reviews/critic-0143.md +108 -0
- package/docs/reviews/critic-0144.md +83 -0
- package/lib/AGENTS.md +14 -6
- package/lib/advance-publish-base.js +8 -0
- package/lib/append-ooda-step.js +12 -3
- package/lib/build-readback-request.js +10 -0
- package/lib/build-registry.js +8 -3
- package/lib/classify-publish-absence.js +11 -0
- package/lib/classify-surface.js +12 -2
- package/lib/commit-scaffold.js +22 -6
- package/lib/compose-evidence-caption.js +15 -4
- package/lib/compute-publish-diff.js +9 -0
- package/lib/crew-api.js +55 -20
- package/lib/crew-release.sh +233 -1
- package/lib/gitignore.js +23 -5
- package/lib/package.json +1 -0
- package/lib/publish-note-vocabulary.js +44 -0
- package/lib/publish-npm.sh +39 -10
- package/lib/read-ooda-verdict.js +11 -3
- package/lib/readback-disk.js +9 -0
- package/lib/render-html.js +17 -7
- package/lib/repo-orchestration.js +21 -5
- package/lib/retry-publish.js +79 -44
- package/lib/sample-project.js +22 -6
- package/lib/scaffold-crew.js +11 -2
- package/lib/see-act.js +17 -8
- package/lib/serve-artifact.js +12 -6
- package/lib/setup-project-repo.js +26 -7
- package/lib/test-detached-integrate.sh +79 -3
- package/lib/test-publish-preflight.sh +94 -5
- package/lib/update-watch.js +34 -17
- package/lib/ux-doctrine.js +31 -6
- package/lib/verify-publish.js +41 -6
- package/lib/worktree-lifecycle.sh +67 -25
- package/lib/write-ooda-verdict.js +12 -3
- package/package.json +1 -1
- package/workflows/bugfix.js +60 -61
- package/workflows/chore.js +60 -61
- package/workflows/standard.js +60 -42
- package/workflows/upgrade.js +4 -2
|
@@ -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,19 +1,26 @@
|
|
|
1
1
|
# AGENTS.md
|
|
2
2
|
|
|
3
|
-
|
|
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.
|
|
7
13
|
- `schema.sql` — the crew-owned state schema: projects, tasks, poll_state, config, agent_sessions, events. Vocabularies enforced by CHECK constraints; `rejected` is a valid event type (the 2026-09-11 crash was a stored session whose event was rejected). Column names match the historical dashboard tables for a verbatim migration.
|
|
8
14
|
- `crew-release.sh` — immutable release manager: deploy, rollback, prune
|
|
9
15
|
- `merge-lock.sh` — serialized merge lock for concurrent agents: time-based holder lease (bug 2fc8f52f — an unexpired lease is held regardless of process liveness; only an expired lease may be broken). Requires both `CREW_REPO` and `CREW_HOME` (fail closed: BLOCKED, exit 2 when either is unset). Lock file is key=value: task_id, opaque holder identity (never a PID), acquired_at epoch, lease_seconds (default 600, override via MERGE_LOCK_LEASE_SECONDS). acquire/refresh/release/status/force-release; holder-only refresh and release; every op appends to $CREW_HOME/.merge-lock.log
|
|
10
|
-
- `worktree-lifecycle.sh` — the worktree lifecycle seam: prepare/cleanup/inspect/integrate/verify-merge/status/post-deploy/terminal-cleanup/refresh-lock/lock-status over git worktrees (the integration target is the repo's current checkout — a branch name, or HEAD when detached — resolved by `integration_target()`; nothing in the lifecycle ever checks out a branch. `integrate` merges into the target, reconciles `origin/<target>` under the merge lock after the task merge — canary `a6d8b0c8`, 2026-09-11 — then pushes inline via `do_push` (no agent round-trip; 2026-09-19 REVIEW collapsed the old STEP-2 agent run, which added only a lock refresh). `do_push`: PUSHED
|
|
11
|
-
- `test-detached-integrate.sh` — behavioral regression tests for the detached-HEAD Integrate fix (room #21 J3, 2026-09-19; extended 2026-09-19 REVIEW): fixture A stages a detached HEAD (reviewed SHA + crew-init commit) and proves integrate leaves HEAD detached, keeps the reviewed SHA and crew-init as ancestors, lands the task merge, never moves main, records `integration_target=HEAD`, verify-merge VERIFIEDs, and push-target
|
|
16
|
+
- `worktree-lifecycle.sh` — the worktree lifecycle seam: prepare/cleanup/inspect/integrate/verify-merge/status/post-deploy/terminal-cleanup/refresh-lock/lock-status over git worktrees (the integration target is the repo's current checkout — a branch name, or HEAD when detached — resolved by `integration_target()`; nothing in the lifecycle ever checks out a branch. `integrate` merges into the target, reconciles `origin/<target>` under the merge lock after the task merge — canary `a6d8b0c8`, 2026-09-11 — then pushes inline via `do_push` (no agent round-trip; 2026-09-19 REVIEW collapsed the old STEP-2 agent run, which added only a lock refresh). `do_push`: PUSHED (the refspec is always explicit — a branch pushes as `<branch>`, a detached HEAD as `HEAD:<destination>` where the destination is the remote's default branch from `resolve_push_destination()`; 2026-09-19 detached-HEAD audit REDO) / NO_REMOTE_PUSH (no origin — fail-soft, 2026-09-19 REVIEW: the old unconditional push exited 128 and mislabeled it); an unresolvable detached destination fails closed (never a silent skip, never a refusal of a knowable push); on push failure one fetch+merge retry while the lock is held, then fail closed without asserting a cause (never force-push). PUSH_SKIPPED survives only for the nothing-merged R5 path (no merge record + no lock held). Merged-but-unpushed retry recovery: a record whose merge commit IS the live HEAD takes the lock and pushes instead of reporting MERGED_EMPTY. `CREW_STAGED_BASE` guard (ferried from get-provenance): the line must descend from the staged publish base — STAGED_BASE_MISMATCH fails closed before the merge (room #21 J3 shape). `verify-merge` always re-resolves the target from the current checkout and checks the recorded commit against the LIVE tip — never the record's word for the tip (2026-09-19 REVIEW: the old read-back compared the record to itself for target=HEAD). A detached HEAD skips the inline reconcile loudly (NO_REMOTE_RECONCILE) — the inline reconcile needs a static branch target, so on detached the destination is resolved explicitly in do_push, which reconciles lazily on push failure (2026-09-19 detached-HEAD audit REDO; the old "local by design" rationale is rejected); `push-target` is the thin R5 wrapper over `do_push`: refreshes the lock, distinguishes nothing-merged (no record + no lock → loud skip) from lock-lost (fail closed). (`.worktrees/<id>`, branch `task/<id>`). Branch resolution order: the crew registry (`<repo>/.worktrees/.registry/<id>`), then the canonical `task/<full-id>` ref, then a `task/<id-prefix>` ref matched by strict prefix enumeration (task 4e1a1bba — a Build agent may create the branch with raw git from an abbreviated id, bypassing prepare; prefix names are tolerated, never created; ambiguous prefixes fail closed). `resolve-branch` prints the resolved branch for agent-side one-liners. Nothing reconstructs the branch name — every command resolves it. Prepare fails closed on a dirty integration target; cleanup is forgiving. `terminal-cleanup` is the run's last act at every park/fail boundary (called from `parkTask` in standard/bugfix/chore): releases the merge lock unconditionally and reclaims the worktree+branch only when the task branch is fully merged into the integration target — unmerged work is preserved for the human by design, and a dirty worktree is reported, never force-removed. `post-deploy` reports worktree removal honestly (a lying "removed" echo hid real leftovers — canary run 9, 2026-09-12). Requires both `CREW_REPO` and `CREW_HOME` (fail closed: BLOCKED, exit 2 when either is unset); `CREW_REPO` is exported so internal merge-lock.sh calls inherit the repo being worked on; `LIB_DIR` defaults to `$CREW_HOME/lib` (`CREW_LIB` override). `lock-status` reports the merge-lock state explicitly (`UNLOCKED`, or key=value: locked=true, task_id, holder, acquired_at, lease_seconds, age_seconds, remaining_seconds — always exit 0) so Publish can distinguish an empty-diff Integrate (no lock taken) from a refresh failure. post-deploy's `.worktrees/` guard is add-then-reset, never an all-negative pathspec: `git add -A -- ':!.worktrees/'` still exits 1 on git 2.43.0 against the crew's own gitignored non-empty `.worktrees/` (room #15 J3), killing post-deploy under `set -e` — a plain `add -A` never errors on ignored paths and the follow-up `git reset -q -- .worktrees/` keeps stray lock files out of the integration target even on pre-gitignore-entry repos.
|
|
17
|
+
- `test-detached-integrate.sh` — behavioral regression tests for the detached-HEAD Integrate fix (room #21 J3, 2026-09-19; extended 2026-09-19 REVIEW): fixture A stages a detached HEAD (reviewed SHA + crew-init commit) and proves integrate leaves HEAD detached, keeps the reviewed SHA and crew-init as ancestors, lands the task merge, never moves main, records `integration_target=HEAD`, verify-merge VERIFIEDs, and push-target reports NO_REMOTE_PUSH (fixture A has no origin); fixture B proves the branch path still merges, reconciles, and pushes inline to a local origin; fixture C proves verify-merge checks the record against the LIVE tip (a detached line reset past the merge does NOT verify; the branch-gone path verifies only when the recorded commit is an ancestor of the live tip); fixture D proves the no-remote branch path fails soft with NO_REMOTE_PUSH (exit 0); fixture E proves the CREW_STAGED_BASE guard (non-ancestor base → STAGED_BASE_MISMATCH before any merge; true base → STAGED_BASE_OK); fixture F proves merged-but-unpushed retry recovery (re-running integrate after a crashed push takes the lock and pushes instead of parking on MERGED_EMPTY); fixture H proves the detached-HEAD audit REDO contract (2026-09-19) on a detached checkout with a local origin: push-destination fails closed when origin/HEAD is unset (genuinely unknowable destination), otherwise resolves the remote default branch; integrate pushes inline as `HEAD:main` (`PUSHED: origin/main (refspec HEAD:main)`), the remote ref advances to the merge commit, local main stays at the reviewed base, HEAD stays detached, verify-merge VERIFIEDs, and push-target re-pushes idempotently. Scratch dirs under /tmp only; CREW_HOME lives outside the scratch repos (as in real rooms) so lock files never trip the integrate preflight.
|
|
12
18
|
- `test-worktree-backend.sh` — regression tests for the lifecycle script (validate, prepare/reuse, inspect, status, cleanup, idempotent cleanup, dirty-main preflight, integrate remote-reconcile with fixture sensitivity + no-remote fail-soft) on scratch repos
|
|
13
19
|
- `test-version-write.sh` — regression tests for the escape-preserving step-8 version write in publish-npm.sh (fixture: current package.json with the \\u2014 escape; extracts the shipped block by anchor)
|
|
14
20
|
- `test-publish-verify.sh` — regression tests for the retry-tolerant step-12 verification in publish-npm.sh (canary 5a027278): extracts the shipped block by anchor and runs it against a fake npm whose read replica lags (non-zero exits, then the old version, then the target) — requires convergence on success, fail-closed `PUBLISH_FAILED=verify` on exhaustion, and `--prefer-online` on every read
|
|
15
21
|
- `test-publish-skip.sh` — regression tests for the no-lock graceful publish skip (park 2026-09-11): `lock-status` returns exact `UNLOCKED`/`LOCKED by <holder> since <ts> (pid <pid>)`; a fake empty-diff integrate takes the MERGED_EMPTY branch and never takes the lock; the shipped step-5 block from publish-npm.sh (extracted by anchor) skips gracefully on UNLOCKED (no park, no `PUBLISH_COMPLETE`), follows the refresh path when a lock is held, and fails closed on a `lock-status` query failure; the gate sits inside the ALREADY_PUBLISHED=0 branch and mutation steps are gated on NO_LOCK_HELD=0
|
|
16
|
-
- `publish-npm.sh` — deterministic npm publish: lock refresh, release install, version write/commit, pack, registry publish, verify, push, post-deploy. Takes TARGET_VERSION as input; idempotent on retry/resume.
|
|
22
|
+
- `publish-npm.sh` — deterministic npm publish: preflight (npm capability + push-destination resolution — the push refspec is resolved before any mutation: a branch name, or `HEAD:<destination>` on a detached checkout via the lifecycle's `push-destination` command; a genuinely unknowable destination fails fast here with `PUBLISH_FAILED=env`, before any mutation — 2026-09-19 detached-HEAD audit REDO: detached publication is legitimate and must work, not refuse), lock refresh, release install, version write/commit, pack, registry publish, verify, push, post-deploy. Takes TARGET_VERSION as input; idempotent on retry/resume.
|
|
23
|
+
- `test-publish-preflight.sh` — regression tests for the npm publish capability preflight: the PREFLIGHT block extracted by anchor, helper -f/-r checks, placement, full-script runs (helper missing / credential missing / credential present + DRY_RUN / old helper without --check / real-helper probe), plus the detached-HEAD audit REDO cases (2026-09-19): a detached fixture repo with a local origin — origin/HEAD set + DRY_RUN=1 passes through to PUBLISH_DRY_RUN=1 (no refusal), origin/HEAD unset fails closed with PUBLISH_FAILED=env before any mutation, repo untouched; and the attached+no-origin case (critic F-A1: the anchor used to check no-origin only on the detached branch — attached+no-origin now fails closed pre-mutation too). Runs against scratch repos; the real-helper probe is credential-dependent (author machine only).
|
|
17
24
|
- `compose-evidence.py` — deterministic visual-evidence compositor (Pillow): pairs identical PNG stems from baseline/ and postchange/ dirs, emits `<stem>-sidebyside.png` and amplified-difference `<stem>-overlay.png` into composites/, prints `PAIR`/`SKIP` manifests. Byte-deterministic; nonzero exit on errors.
|
|
18
25
|
- `compose-evidence-caption.js` — deterministic QA-evidence caption composer (2026-09-14): given `--crew-home`, `--task-id`, and the resolved `--audit-dir` name, extracts the task title/description, the QA verdict, the `publish: verified` commit (or the task's project-scoped provenance record when it belongs to this task), and the merge commit's subject line from the project repo — and prints the out-of-context transition Eric asked for (problem → solution → evidence handoff) to stdout. The caller appends the desktop/mobile attachment lines after this caption. Verdict source (2026-09-16): Hazel's `task-evidence/<taskId>/postchange/verdict.json` is authoritative, then the last line of the append-only `verdicts.jsonl` ledger, then the newest legacy `visual_verdict:` note (the parent visual-verdict protocol was retired 2026-09-15). Honest labels: "provenance not yet stamped" when no commit is known, "no QA verdict recorded" when none exists, and the visual-protocol-unavailable note only when no verdict evidence exists at all, the newest `baseline:` note is `baseline: none`, AND captures exist (a `baseline: none` note means no baseline captures to compare against — it never overrules a real QA verdict; the note's premise is the post-deploy capture, so without captures it would fabricate evidence again). Capture guard (2026-09-17, room #14): the composer resolves the audit dir itself (`<home>/workspace/ts-spaces/<deploy_slug>/audits/<auditDir>/`, the same root crew-api.js and readback-disk.js resolve) and verifies `screenshot.png` + `screenshot-mobile.png` are files before claiming "desktop + mobile captures below" — a bare `--audit-dir` name is never trusted. Missing deploy_slug, missing dir, or missing PNGs gets the honest no-captures opener and Evidence line (terminal surfaces named explicitly: "terminal surface — QA judges the CLI, not pixels"); the caller only appends attachment lines when its own freshness check passes. "Latest" is by event timestamp, sorted explicitly. Exits non-zero on missing input.
|
|
19
26
|
- `edit-image.py` — deterministic pixel-level evidence editing (2026-09-15, Pillow): `crop --region x,y,w,h` (pixel-exact, rejects out-of-bounds), `zoom --factor <f> [--center x,y]` (nearest-neighbor, returns to source frame size), `label --text <caption>` (caption bar), `nup --cols <n> --in <a.png> --in <b.png> [--labels "a|b"]` (side-by-side grid). Byte-deterministic PNG output; no network, no time, no randomness. Derivatives supplement raw frames — the source frame stays archived.
|
|
@@ -26,9 +33,9 @@ Shell scripts for the crew's infrastructure. Called by workflow scripts, cron, a
|
|
|
26
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>`.
|
|
27
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.
|
|
28
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.
|
|
29
|
-
- `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.
|
|
30
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.
|
|
31
|
-
- `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.
|
|
32
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`.
|
|
33
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]`.
|
|
34
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>`.
|
|
@@ -41,3 +48,4 @@ Shell scripts for the crew's infrastructure. Called by workflow scripts, cron, a
|
|
|
41
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
|
|
42
49
|
or empty identities/, personas/, or seed/workflows/ source refuses loudly (exit 1,
|
|
43
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;
|
package/lib/append-ooda-step.js
CHANGED
|
@@ -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
|
-
|
|
42
|
-
|
|
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) {
|
package/lib/build-registry.js
CHANGED
|
@@ -11,9 +11,9 @@
|
|
|
11
11
|
// Usage: node lib/build-registry.js <workflows-dir> <output-json>
|
|
12
12
|
"use strict";
|
|
13
13
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
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) {
|
package/lib/classify-surface.js
CHANGED
|
@@ -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
|
-
|
|
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) {
|
package/lib/commit-scaffold.js
CHANGED
|
@@ -32,9 +32,11 @@
|
|
|
32
32
|
// include it in the commit.
|
|
33
33
|
// Prints JSON: { committed, sha, reason }
|
|
34
34
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
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
|
-
|
|
104
|
+
export { commitScaffold, isPathDirty };
|
|
103
105
|
|
|
104
|
-
|
|
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(
|
|
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
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
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) {
|
package/lib/crew-api.js
CHANGED
|
@@ -30,6 +30,13 @@ import { execFileSync } from "node:child_process";
|
|
|
30
30
|
import { DatabaseSync } from "node:sqlite";
|
|
31
31
|
import { homedir } from "node:os";
|
|
32
32
|
import { classifySurface } from "./classify-surface.js";
|
|
33
|
+
// Terminal-note vocabulary (D7, 2026-09-19): the scan's terminal branch
|
|
34
|
+
// consults this closed registry instead of an inline pattern list, so a
|
|
35
|
+
// recognized terminal note is skipped as terminal with its meaning —
|
|
36
|
+
// never as unrecognized. Pin-adjacent: every runtime-relative dependency
|
|
37
|
+
// of crew-api.js is pinned in the workflows' pinLifecycle (PIN_BASENAMES),
|
|
38
|
+
// mechanically verified by tests/pin-closure.test.js.
|
|
39
|
+
import { matchTerminalPublishNote, TERMINAL_NOTE_MEANINGS } from "./publish-note-vocabulary.js";
|
|
33
40
|
|
|
34
41
|
// ---------------------------------------------------------------------------
|
|
35
42
|
// Errors
|
|
@@ -1721,20 +1728,34 @@ function latestPublishNote(db, taskId) {
|
|
|
1721
1728
|
).get(taskId) || null;
|
|
1722
1729
|
}
|
|
1723
1730
|
|
|
1724
|
-
// The trigger anchor for an unknown attempt: the "submitted"
|
|
1725
|
-
// for the same attempt
|
|
1726
|
-
//
|
|
1727
|
-
//
|
|
1728
|
-
//
|
|
1731
|
+
// The trigger anchor for an unknown attempt: the issuance-kind "submitted"
|
|
1732
|
+
// ledger entry for the same attempt (the trigger issuance), falling back to
|
|
1733
|
+
// the oldest by ts for pre-D1 ledgers. The issuance entry binds the trigger
|
|
1734
|
+
// instant — verify-publish.js and the decision doc prefer it the same way;
|
|
1735
|
+
// binding newest misclassifies builds that complete between issuance and
|
|
1736
|
+
// receipt — binding newest misclassifies builds that complete between
|
|
1737
|
+
// issuance and receipt (see docs/reviews/critic-0143.md findings A2/R2/O4).
|
|
1738
|
+
// Returns the entry or null. A null means the trigger
|
|
1739
|
+
// instant is unknowable (or the trigger was never issued) — classification
|
|
1740
|
+
// cannot run; the task stays parked for human attention. Fail closed, never
|
|
1741
|
+
// guess the anchor.
|
|
1729
1742
|
function findTriggerEntry(entries, commit, attempt) {
|
|
1730
|
-
|
|
1743
|
+
// D1 (2026-09-19; entry_kind cut 2026-09-20): the anchor is the OLDEST
|
|
1744
|
+
// submitted entry by ts and the bound instant is its issued_at || ts —
|
|
1745
|
+
// the same fallback verify-publish.js uses. The retired entry_kind
|
|
1746
|
+
// preference filter could never change the bound anchor (issuance and
|
|
1747
|
+
// receipt entries carried the same captured issued_at), so oldest-by-ts
|
|
1748
|
+
// is the whole mechanism.
|
|
1749
|
+
const matches = [];
|
|
1750
|
+
for (let i = 0; i < entries.length; i++) {
|
|
1731
1751
|
const e = entries[i];
|
|
1732
1752
|
if (e.outcome === "submitted" && e.commit === commit &&
|
|
1733
1753
|
(e.attempt || null) === (attempt || null)) {
|
|
1734
|
-
|
|
1754
|
+
matches.push(e);
|
|
1735
1755
|
}
|
|
1736
1756
|
}
|
|
1737
|
-
|
|
1757
|
+
matches.sort((a, b) => (a.ts || "").localeCompare(b.ts || ""));
|
|
1758
|
+
return matches[0] || null;
|
|
1738
1759
|
}
|
|
1739
1760
|
|
|
1740
1761
|
// The unknown ledger entry's detail says whether the trigger went out.
|
|
@@ -1885,10 +1906,9 @@ commands["resolve-publish-unknown"] = (db, args, ctx) => {
|
|
|
1885
1906
|
// "publish: retry-issued" (not-before = intended+20m); NEVER re-trigger here
|
|
1886
1907
|
// "publish: retry-issued" w/o mirrored → mirror the missing publish: verification-requested
|
|
1887
1908
|
// verification-requested
|
|
1888
|
-
//
|
|
1889
|
-
//
|
|
1890
|
-
//
|
|
1891
|
-
// "publish: superseded"
|
|
1909
|
+
// terminal notes (the closed registry in → terminal; skipped with the
|
|
1910
|
+
// lib/publish-note-vocabulary.js — the reason named, never
|
|
1911
|
+
// scan names the recognized note) "unrecognized"
|
|
1892
1912
|
// "publish: verification-requested" → already routed; ignored (the verification scan owns it)
|
|
1893
1913
|
//
|
|
1894
1914
|
// Unknown parks whose trigger was never issued (preflight/toolcheck
|
|
@@ -1923,7 +1943,9 @@ commands["scan-publish-unknown"] = (db, args, ctx) => {
|
|
|
1923
1943
|
return;
|
|
1924
1944
|
}
|
|
1925
1945
|
const trigger = findTriggerEntry(derived.entries, derived.commit, derived.attempt);
|
|
1926
|
-
|
|
1946
|
+
// D1 (2026-09-19): the anchor instant is the trigger-issuance instant
|
|
1947
|
+
// (issued_at), not the ledger-write instant (ts) — issued_at || ts.
|
|
1948
|
+
if (!trigger || !(trigger.issued_at || trigger.ts)) {
|
|
1927
1949
|
skip(taskId, "no-trigger-anchor",
|
|
1928
1950
|
"no submitted ledger entry for this attempt — the trigger instant is unknowable; stays parked for human attention");
|
|
1929
1951
|
return;
|
|
@@ -1957,7 +1979,7 @@ commands["scan-publish-unknown"] = (db, args, ctx) => {
|
|
|
1957
1979
|
}
|
|
1958
1980
|
due.push({
|
|
1959
1981
|
task_id: taskId, commit: derived.commit, base,
|
|
1960
|
-
trigger_ts: trigger.ts, park_ts: parkNote.timestamp,
|
|
1982
|
+
trigger_ts: trigger.issued_at || trigger.ts, park_ts: parkNote.timestamp,
|
|
1961
1983
|
slug: derived.slug, repo_path: derived.repoPath,
|
|
1962
1984
|
project_id: derived.projectId, claim_expiry: expiry,
|
|
1963
1985
|
ledger_path: derived.ledgerPath,
|
|
@@ -1987,11 +2009,13 @@ commands["scan-publish-unknown"] = (db, args, ctx) => {
|
|
|
1987
2009
|
const has = (s) => msg.includes(s);
|
|
1988
2010
|
const historyHas = (s) => notes.some((n) => n.message.includes(s));
|
|
1989
2011
|
|
|
1990
|
-
// Terminal states — the machine never revisits these.
|
|
1991
|
-
|
|
1992
|
-
|
|
1993
|
-
|
|
1994
|
-
|
|
2012
|
+
// Terminal states — the machine never revisits these. The vocabulary is
|
|
2013
|
+
// explicit state (lib/publish-note-vocabulary.js); a recognized terminal
|
|
2014
|
+
// note is skipped as terminal with its meaning, never as unrecognized.
|
|
2015
|
+
const terminalNote = matchTerminalPublishNote(msg);
|
|
2016
|
+
if (terminalNote) {
|
|
2017
|
+
skip(taskId, "terminal",
|
|
2018
|
+
`recognized terminal note '${terminalNote}' — ${TERMINAL_NOTE_MEANINGS[terminalNote]}`);
|
|
1995
2019
|
continue;
|
|
1996
2020
|
}
|
|
1997
2021
|
// Already routed into the verification pipeline — the verification
|
|
@@ -2063,7 +2087,7 @@ commands["scan-publish-unknown"] = (db, args, ctx) => {
|
|
|
2063
2087
|
task_id: taskId, commit: derived.commit, attempt: derived.attempt,
|
|
2064
2088
|
slug: derived.slug, repo_path: derived.repoPath, project_id: derived.projectId,
|
|
2065
2089
|
ledger_path: derived.ledgerPath, base: retryBase,
|
|
2066
|
-
trigger_ts: (retryTrigger && retryTrigger.ts) || null,
|
|
2090
|
+
trigger_ts: (retryTrigger && (retryTrigger.issued_at || retryTrigger.ts)) || null,
|
|
2067
2091
|
manifest_before: (retryTrigger && retryTrigger.manifest_before &&
|
|
2068
2092
|
typeof retryTrigger.manifest_before === "object") ? retryTrigger.manifest_before : null,
|
|
2069
2093
|
});
|
|
@@ -2800,6 +2824,17 @@ function main() {
|
|
|
2800
2824
|
}
|
|
2801
2825
|
}
|
|
2802
2826
|
|
|
2827
|
+
const CREW_API_USAGE =
|
|
2828
|
+
"usage: node crew-api.js --crew-home <path> <command> [--json <json>] [flags...]\n" +
|
|
2829
|
+
"the crew-owned task-service API (API.md): every state-machine invariant lives in the schema.\n" +
|
|
2830
|
+
"run with a command and no --help for command-specific usage; exit 0 ok · 2 usage · 3 not found · 4 conflict/guard";
|
|
2831
|
+
|
|
2832
|
+
// --help: before required-arg parsing (shebang⇔CLI contract).
|
|
2833
|
+
if (process.argv.slice(2).includes("--help")) {
|
|
2834
|
+
console.log(CREW_API_USAGE);
|
|
2835
|
+
process.exit(0);
|
|
2836
|
+
}
|
|
2837
|
+
|
|
2803
2838
|
try {
|
|
2804
2839
|
main();
|
|
2805
2840
|
} catch (e) {
|