muse-crew 0.14.4 → 0.14.6
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/API.md +61 -7
- package/docs/decisions/AGENTS.md +3 -0
- package/docs/decisions/publish-path.md +244 -1
- package/docs/guide.md +42 -6
- package/docs/publish-unknown-recovery.md +28 -113
- package/docs/publish-verification.md +148 -445
- package/docs/release-integrity.md +5 -3
- package/docs/reviews/critic-0145.md +106 -0
- package/lib/AGENTS.md +4 -8
- package/lib/advance-publish-base.js +3 -3
- package/lib/compose-evidence-caption.js +1 -2
- package/lib/compute-publish-diff.js +113 -15
- package/lib/crew-api.js +855 -725
- package/lib/publish-note-vocabulary.js +137 -24
- package/lib/schema.sql +19 -0
- package/package.json +1 -1
- package/seed/AGENTS.md +1 -0
- package/seed/cron-body-ack-scan.md +44 -0
- package/seed/cron-body-template.md +65 -65
- package/seed/crons.json +12 -0
- package/workflows/bugfix.js +146 -740
- package/workflows/chore.js +146 -740
- package/workflows/crew-dispatch.js +36 -0
- package/workflows/crew-init.js +23 -0
- package/workflows/crew-uninstall.js +7 -4
- package/workflows/standard.js +146 -763
- package/lib/build-readback-request.js +0 -140
- package/lib/classify-publish-absence.js +0 -462
- package/lib/publish-content.js +0 -154
- package/lib/readback-disk.js +0 -195
- package/lib/retry-publish.js +0 -405
- package/lib/verify-publish.js +0 -414
|
@@ -47,9 +47,11 @@ nearest `package.json`'s `type` field.
|
|
|
47
47
|
Boundaries, stated plainly: `--help` short-circuits before argument
|
|
48
48
|
parsing, so the gate proves an entry *loads*, not that its main path
|
|
49
49
|
*behaves* (the suite covers behavior); `workflows/*.js` are not gated
|
|
50
|
-
here — the suite's loader emulation (`
|
|
51
|
-
|
|
52
|
-
|
|
50
|
+
here — the suite's loader emulation (`workflow-size.test.js`, which
|
|
51
|
+
drives the release script's `_validate_workflows` behaviorally: export-strip
|
|
52
|
+
+ async-function-wrap parse plus the size budget) is their true gate; the
|
|
53
|
+
import-safe half of the shebang-less contract (no side effects on import)
|
|
54
|
+
is unchecked — an accepted residual with no
|
|
53
55
|
cheap mechanism.
|
|
54
56
|
|
|
55
57
|
Verdicts are aggregated and every entry gets one row in
|
|
@@ -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
|
@@ -9,7 +9,7 @@ Shipped library: ESM JavaScript CLIs and import-safe modules, shell scripts for
|
|
|
9
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`.
|
|
10
10
|
|
|
11
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
|
|
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-
|
|
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-ack-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.
|
|
13
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.
|
|
14
14
|
- `crew-release.sh` — immutable release manager: deploy, rollback, prune
|
|
15
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
|
|
@@ -31,21 +31,17 @@ Shipped library: ESM JavaScript CLIs and import-safe modules, shell scripts for
|
|
|
31
31
|
- `read-ooda-verdict.js` — deterministic cross-checker for the OODA terminal verdict (2026-09-15): `node read-ooda-verdict.js --dir <phase-dir> --expect <PASS|FAIL>` reads `<dir>/verdict.json`, prints one JSON line to stdout, exits 0 with `{ok:true, verdict, reason, summary, expected, actual, attempt}` when the record agrees with the prose expectation and a FAIL carries a non-empty reason, or exits 2 with `{ok:false, code}` — `missing|corrupt|contradiction|no_reason`. No wall-clock reads, no randomness. The bugfix QA closeout runs it against the prose `VERDICT:` line before any rework routing: a failed cross-check records the phase as failed for retry, never routes to rework.
|
|
32
32
|
- `ux-doctrine.js` — UX-surface doctrine page resolution (2026-09-17): the canonical map from `environment_type` to the crew's shared UX bar (`artifact` → `docs/artifact-ux.md`, `terminal` → `docs/terminal-ux.md`; null/unknown → no page). Pure and deterministic: `doctrinePage(env)`, `doctrinePath(crewHome, env)`, `doctrinePageExists(crewHome, env)`; CLI `--page <env>` / `--path <crewHome> <env>`. Workflows mirror the map inline (one line — the workflow runtime's relative-import support is unverified) and tests pin the mirror against this file.
|
|
33
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>`.
|
|
34
|
-
- `
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
34
|
+
- `publish-verification retired (0.14.6, 2026-09-20)` — the content-verdict/readback architecture is retired: `verify-publish.js`, `readback-disk.js`, `publish-content.js`, `classify-publish-absence.js`, `build-readback-request.js`, `check-intent-freshness.js`, and `retry-publish.js` are deleted. Eric's publication contract replaced it: exact per-attempt version acknowledgement is the sole positive completion criterion ("If we hear that the artifact acknowledges our version, that's it. We don't verify against content."). The version is derived deterministically (`compute-publish-diff.js --task-id/--attempt`), the tick worker issues the edit directly (`scan-publish-intent` → `record-intent-issuance`), and `scan-ack-pending` evaluates the version receipt with bounded retries. Provenance now certifies the issuance request was acknowledged, never byte equality.
|
|
39
35
|
- `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`.
|
|
40
36
|
- `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]`.
|
|
41
37
|
- `repo-orchestration.js` — repository-local `.orchestration/` scaffold (2026-09-17): `scaffoldRepoOrchestration(repoPath, crewRepoPath)` creates `$REPO/.orchestration/{workflows,identities,phases,user}/`, seeds workflows/identities/phases from the crew repo's platform defaults with no-clobber semantics (existing project customizations never overwritten), and writes a README in `user/` explaining it's for local config. Idempotent. CLI: `--repo <path> --crew-repo <path>`.
|
|
42
38
|
- `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.
|
|
43
39
|
- `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]`.
|
|
44
40
|
- `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>`.
|
|
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> <
|
|
41
|
+
- `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`, `version`). 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. `--task-id <id> --attempt <n>` (2026-09-20, 0.14.6 §1.1 publish version, paired flags — one without the other is a usage error): derives the per-attempt version `commit + ":" + attempt` (plain concatenation — no hash, no wall clock, no randomness; 2026-09-20 the sha256 "nonce" was cut — it defended against a forger that doesn't exist while the artifact's real failure mode defeats hash and counter identically; keyed on the resolved commit SHA so the tick's byte-identical regeneration yields identical bytes) and appends a SYNTHETIC new-file hunk for `.crew-publish-version-<task_id>` (per-task receipt scope — two same-project tasks can publish concurrently; content = version + "\n", index line carries the real receipt blob hash via `git hash-object`) to the staged diff — synthesized into the staged artifact only, never committed; the checksum covers the hunk; empty real diffs carry no receipt (the workflow parks empty diffs). 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
42
|
- `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.
|
|
47
43
|
- `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>]`.
|
|
48
44
|
- `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
|
|
49
45
|
or empty identities/, personas/, or seed/workflows/ source refuses loudly (exit 1,
|
|
50
46
|
naming the source) before creating anything — never an empty 0/0 receipt. No wall-clock reads, no randomness.
|
|
51
|
-
- `publish-note-vocabulary.js` —
|
|
47
|
+
- `publish-note-vocabulary.js` — publish-note vocabulary registry (2026-09-19, D7; 0.14.6 closed writer, 2026-09-20; code/prose write split 2026-09-20 REVIEW): the closed enum of every `publish: …` note the state machine treats as terminally parked (`TERMINAL_PUBLISH_NOTES`, `TERMINAL_NOTE_MEANINGS`, `matchTerminalPublishNote`) plus the transitional notes the 0.14.6 machine writes (`TRANSITIONAL_PUBLISH_NOTES`). Write authority is split: `WRITABLE_PUBLISH_NOTES` is transitional-only (tick prose via `record-publish-note` — prose can never mint a terminal); `CODE_WRITABLE_PUBLISH_NOTES` holds the current terminals (deterministic code via `assertCodeWritablePublishNote`); legacy terminals are recognized but never written anew. `log-event` mechanically rejects `type:"note"` messages containing `publish: <verb>` — they must route through `record-publish-note` or deterministic code. Readers (the ack/intent scans 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`; legacy verbs from the retired content-verdict machine stay recognized so old note history never becomes unrecognized. Writers assert via `assertWritablePublishNote` (prose) or `assertCodeWritablePublishNote` (code) before writing; the only prose path is the `record-publish-note` crew-api command, which asserts here. 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`. No side effects on import (bare `node` exits 0).
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
// This script moves provenance.source_commit to the scaffold commit only when
|
|
12
12
|
// every fail-closed condition holds:
|
|
13
13
|
// 1. The existing stamp is not a real publish's: provenance.task_id is set
|
|
14
|
-
// by every real publish (
|
|
14
|
+
// by every real publish (the version ack scan and the workflow
|
|
15
15
|
// provenance-refresh paths) and is null for initial stamps and for this
|
|
16
16
|
// script's own base advances. A stamp with task_id set is never moved.
|
|
17
17
|
// (2026-09-19 REVIEW: this deterministic check replaces the old
|
|
@@ -293,8 +293,8 @@ function main() {
|
|
|
293
293
|
console.error("set-provenance did not return ok");
|
|
294
294
|
process.exit(1);
|
|
295
295
|
}
|
|
296
|
-
// Exact
|
|
297
|
-
// (same discipline as
|
|
296
|
+
// Exact match: the stamp must match what we intended, or it is fiction
|
|
297
|
+
// (same discipline as the version ack scan's stamp).
|
|
298
298
|
let recheck;
|
|
299
299
|
try {
|
|
300
300
|
recheck = crewApi(crewHome, "get-provenance", JSON.stringify({ project_id: projectId })).provenance;
|
|
@@ -17,8 +17,7 @@
|
|
|
17
17
|
// below" for a terminal-surface task with no audit dir at all, and opened
|
|
18
18
|
// with "these screenshots are dropping in" when no screenshots existed. So
|
|
19
19
|
// it resolves the audit dir itself — <home>/workspace/ts-spaces/<deploy_slug>
|
|
20
|
-
// /audits/<auditDir>/, the same root lib/crew-api.js and
|
|
21
|
-
// resolve — and checks screenshot.png + screenshot-mobile.png are files.
|
|
20
|
+
// /audits/<auditDir>/, the same root lib/crew-api.js resolves — and checks screenshot.png + screenshot-mobile.png are files.
|
|
22
21
|
// Missing deploy_slug, missing dir, or missing PNGs gets the honest
|
|
23
22
|
// no-captures line; terminal surfaces are named, not assumed.
|
|
24
23
|
//
|
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
// never carries them:
|
|
16
16
|
//
|
|
17
17
|
// node compute-publish-diff.js --repo-path <path> --base <sha> --out <file>
|
|
18
|
+
// [--commit <sha>] [--task-id <id> --attempt <n>]
|
|
18
19
|
//
|
|
19
20
|
// --base is the previously-stamped provenance source_commit (or the
|
|
20
21
|
// empty-tree sha 4b825dc642cb6eb9a060e54bf8d69288fbee4904 for a first
|
|
@@ -22,9 +23,32 @@
|
|
|
22
23
|
// script echoes the base it used so the workflow can guard against the
|
|
23
24
|
// agent running the wrong command (fail-closed, as before).
|
|
24
25
|
//
|
|
26
|
+
// --commit is the diff's head end (2026-09-20, critic-0145 F-O2): the
|
|
27
|
+
// script used to ignore it and always diff base..HEAD. Optional, defaults
|
|
28
|
+
// to HEAD; the workflow's first-prepare call passes nothing (HEAD is the
|
|
29
|
+
// merge commit it just created), while the tick's byte-identical
|
|
30
|
+
// regeneration passes the intent's commit explicitly. The ancestor check,
|
|
31
|
+
// the diff range, and the reported `commit` all target --commit, never an
|
|
32
|
+
// implicit HEAD.
|
|
33
|
+
//
|
|
34
|
+
// --task-id and --attempt (2026-09-20, 0.14.6 §1.1 publish version): when
|
|
35
|
+
// BOTH are given, the script derives a per-attempt publish version
|
|
36
|
+
// version = commit + ":" + attempt (e.g. "abc123…:2")
|
|
37
|
+
// (deterministic — no wall clock, no randomness, no hash; the resolved
|
|
38
|
+
// --commit SHA, never an implicit HEAD, is the derivation input so the
|
|
39
|
+
// tick's byte-identical regeneration yields the identical version) and
|
|
40
|
+
// appends a SYNTHETIC new-file hunk for `.crew-publish-version-<task_id>`
|
|
41
|
+
// (content = version + "\n") to the staged diff. The version is protocol,
|
|
42
|
+
// not product: never in the repo — synthesized into the staged artifact
|
|
43
|
+
// only — and the staged diff's checksum covers the hunk. An empty real diff
|
|
44
|
+
// gets no hunk (the workflow parks empty diffs; a version-only publish is
|
|
45
|
+
// never a request). Either flag without the other, a non-positive-integer
|
|
46
|
+
// attempt, or an empty task id fails with usage (exit 2).
|
|
47
|
+
//
|
|
25
48
|
// Stdout contract: exactly one JSON object, always small (no diff text):
|
|
26
49
|
// { ok:true, commit, base, ancestor:"yes", diff_file, sha256, bytes,
|
|
27
|
-
// file_count, changed_lines, files:[<paths>], has_binary, has_rename
|
|
50
|
+
// file_count, changed_lines, files:[<paths>], has_binary, has_rename,
|
|
51
|
+
// version } (version is "<commit>:<attempt>" when synthesized, null otherwise)
|
|
28
52
|
// { ok:false, error:"not-ancestor", commit, base } (semantic, exit 0)
|
|
29
53
|
//
|
|
30
54
|
// Exit codes: 0 ok (JSON on stdout) · 2 usage/validation · 1 git/diff/IO
|
|
@@ -43,8 +67,8 @@ import { dirname, resolve } from "node:path";
|
|
|
43
67
|
// --help: before required-arg parsing (shebang⇔CLI contract).
|
|
44
68
|
if (process.argv.slice(2).includes("--help")) {
|
|
45
69
|
console.log(
|
|
46
|
-
"usage: node compute-publish-diff.js --repo-path <path> --base <sha> --out <file
|
|
47
|
-
"runs git diff <base> <HEAD
|
|
70
|
+
"usage: node compute-publish-diff.js --repo-path <path> --base <sha> --out <file> [--commit <sha>] [--task-id <id> --attempt <n>]\n" +
|
|
71
|
+
"runs git diff <base> <commit> (default <commit> = HEAD), writes the raw diff bytes to --out, prints the small JSON summary on stdout"
|
|
48
72
|
);
|
|
49
73
|
process.exit(0);
|
|
50
74
|
}
|
|
@@ -74,37 +98,108 @@ function git(repoPath, args) {
|
|
|
74
98
|
const repoPath = arg("--repo-path");
|
|
75
99
|
const base = arg("--base");
|
|
76
100
|
const outFile = arg("--out");
|
|
101
|
+
const commitArg = arg("--commit", false);
|
|
102
|
+
const taskIdArg = arg("--task-id", false);
|
|
103
|
+
const attemptArg = arg("--attempt", false);
|
|
104
|
+
|
|
105
|
+
// 0.14.6 §1.1: --task-id and --attempt are a pair — the version is derived
|
|
106
|
+
// from both, so one without the other is a caller bug, not a default.
|
|
107
|
+
if ((taskIdArg === null) !== (attemptArg === null)) {
|
|
108
|
+
fail("usage", "--task-id and --attempt must be provided together.", 2);
|
|
109
|
+
}
|
|
110
|
+
let taskId = null;
|
|
111
|
+
let attempt = null;
|
|
112
|
+
if (taskIdArg !== null) {
|
|
113
|
+
taskId = String(taskIdArg).trim();
|
|
114
|
+
if (!taskId) fail("usage", "--task-id must be a non-empty task id.", 2);
|
|
115
|
+
if (!/^[1-9][0-9]*$/.test(attemptArg)) {
|
|
116
|
+
fail("usage", "--attempt must be a positive integer (the 1-based issuance attempt).", 2);
|
|
117
|
+
}
|
|
118
|
+
attempt = attemptArg;
|
|
119
|
+
}
|
|
77
120
|
|
|
78
121
|
if (!/^[0-9a-f]{40}$/.test(base)) fail("usage", "--base must be a 40-char hex commit SHA (or the empty-tree SHA).", 2);
|
|
79
122
|
let st = null;
|
|
80
123
|
try { st = statSync(repoPath); } catch { /* falls through */ }
|
|
81
124
|
if (!st || !st.isDirectory()) fail("usage", "--repo-path must be an existing directory.", 2);
|
|
82
125
|
|
|
83
|
-
|
|
84
|
-
|
|
126
|
+
// The diff's head end: explicit --commit, or HEAD when omitted (the
|
|
127
|
+
// workflow's first-prepare call passes nothing — HEAD is the merge commit
|
|
128
|
+
// it just created). Validated as a 40-hex commit object in this repo;
|
|
129
|
+
// everything below (ancestor check, diff range, reported commit) targets
|
|
130
|
+
// it, never an implicit HEAD (2026-09-20, critic-0145 F-O2).
|
|
131
|
+
let commit;
|
|
132
|
+
if (commitArg === null) {
|
|
133
|
+
commit = git(repoPath, ["rev-parse", "HEAD"]).trim();
|
|
134
|
+
} else {
|
|
135
|
+
if (!/^[0-9a-f]{40}$/.test(commitArg)) fail("usage", "--commit must be a 40-char hex commit SHA.", 2);
|
|
136
|
+
commit = git(repoPath, ["rev-parse", "--verify", commitArg + "^{commit}"]).trim();
|
|
137
|
+
}
|
|
138
|
+
if (!/^[0-9a-f]{40}$/.test(commit)) fail("git-failed", "could not resolve the diff target to a commit SHA.", 1);
|
|
85
139
|
|
|
86
|
-
// Ancestor check — deterministic, in code
|
|
87
|
-
// git merge-base --is-ancestor fails on it, so
|
|
88
|
-
// ANCESTOR=yes here instead of executing the
|
|
89
|
-
// 2026-09-16: an agent ran merge-base on the empty
|
|
140
|
+
// Ancestor check — deterministic, in code, against the diff target. The
|
|
141
|
+
// empty tree is not a commit: git merge-base --is-ancestor fails on it, so
|
|
142
|
+
// a first publish hardcodes ANCESTOR=yes here instead of executing the
|
|
143
|
+
// conditional (clean-room 2026-09-16: an agent ran merge-base on the empty
|
|
144
|
+
// tree directly and parked).
|
|
90
145
|
let ancestor = "yes";
|
|
91
146
|
if (base !== EMPTY_TREE) {
|
|
92
147
|
try {
|
|
93
|
-
execFileSync("git", ["-C", repoPath, "merge-base", "--is-ancestor", base,
|
|
148
|
+
execFileSync("git", ["-C", repoPath, "merge-base", "--is-ancestor", base, commit], { stdio: "ignore" });
|
|
94
149
|
ancestor = "yes";
|
|
95
150
|
} catch {
|
|
96
151
|
ancestor = "no";
|
|
97
152
|
}
|
|
98
153
|
}
|
|
99
154
|
if (ancestor !== "yes") {
|
|
100
|
-
process.stdout.write(JSON.stringify({ ok: false, error: "not-ancestor", commit
|
|
155
|
+
process.stdout.write(JSON.stringify({ ok: false, error: "not-ancestor", commit, base }) + "\n");
|
|
101
156
|
process.exit(0);
|
|
102
157
|
}
|
|
103
158
|
|
|
104
159
|
const outPath = resolve(outFile);
|
|
105
160
|
mkdirSync(dirname(outPath), { recursive: true });
|
|
106
|
-
const diffText = git(repoPath, ["diff", base,
|
|
107
|
-
|
|
161
|
+
const diffText = git(repoPath, ["diff", base, commit]);
|
|
162
|
+
|
|
163
|
+
// 0.14.6 §1.1 — the publish version. The diff transport is the handoff, so
|
|
164
|
+
// the staged artifact carries its own receipt: a synthetic new-file hunk
|
|
165
|
+
// for `.crew-publish-version-<task_id>` whose content is the per-attempt
|
|
166
|
+
// version. The receipt is scoped per task (not per slug): two tasks of the
|
|
167
|
+
// same project can sit in `edit-issued` concurrently, and a shared
|
|
168
|
+
// per-slug receipt would let each issuance overwrite the other's
|
|
169
|
+
// acknowledgement. The derivation is a plain concatenation — commit + ":" +
|
|
170
|
+
// attempt — no wall clock, no randomness, no hash, keyed on the RESOLVED
|
|
171
|
+
// commit SHA, so the tick's byte-identical regeneration yields the
|
|
172
|
+
// identical version and the identical staged bytes. The index line
|
|
173
|
+
// carries the real git blob hash of the receipt content (via
|
|
174
|
+
// hash-object, deterministic) — the hunk is honest, not a mock-up. The
|
|
175
|
+
// receipt content is never committed to the repo: it is synthesized here,
|
|
176
|
+
// into the staged artifact only. The staged-diff checksum (below) covers
|
|
177
|
+
// the hunk by construction. An empty real diff carries no receipt — the
|
|
178
|
+
// workflow parks empty diffs before issuance, and a version-only staged
|
|
179
|
+
// diff would defeat that park.
|
|
180
|
+
let version = null;
|
|
181
|
+
let stagedDiff = diffText;
|
|
182
|
+
if (taskId !== null && diffText.length > 0) {
|
|
183
|
+
version = commit + ":" + attempt;
|
|
184
|
+
const receiptContent = version + "\n";
|
|
185
|
+
const blobSha = execFileSync("git", ["-C", repoPath, "hash-object", "--stdin"], {
|
|
186
|
+
input: receiptContent, encoding: "utf8", stdio: ["pipe", "pipe", "ignore"],
|
|
187
|
+
}).trim();
|
|
188
|
+
if (!/^[0-9a-f]{40}$/.test(blobSha)) {
|
|
189
|
+
fail("git-failed", "could not hash the synthetic version receipt.", 1);
|
|
190
|
+
}
|
|
191
|
+
const receiptName = ".crew-publish-version-" + taskId;
|
|
192
|
+
stagedDiff =
|
|
193
|
+
diffText +
|
|
194
|
+
"diff --git a/" + receiptName + " b/" + receiptName + "\n" +
|
|
195
|
+
"new file mode 100644\n" +
|
|
196
|
+
"index 0000000.." + blobSha + "\n" +
|
|
197
|
+
"--- /dev/null\n" +
|
|
198
|
+
"+++ b/" + receiptName + "\n" +
|
|
199
|
+
"@@ -0,0 +1 @@\n" +
|
|
200
|
+
"+" + version + "\n";
|
|
201
|
+
}
|
|
202
|
+
writeFileSync(outPath, stagedDiff, "utf8");
|
|
108
203
|
const bytes = readFileSync(outPath);
|
|
109
204
|
const sha256 = createHash("sha256").update(bytes).digest("hex");
|
|
110
205
|
|
|
@@ -122,7 +217,7 @@ let added = 0;
|
|
|
122
217
|
let removed = 0;
|
|
123
218
|
let has_binary = false;
|
|
124
219
|
let has_rename = false;
|
|
125
|
-
for (const line of
|
|
220
|
+
for (const line of stagedDiff.split("\n")) {
|
|
126
221
|
const m = /^diff --git a\/(.*) b\/(.*)$/.exec(line);
|
|
127
222
|
if (m) {
|
|
128
223
|
current = m[2];
|
|
@@ -140,7 +235,7 @@ for (const line of diffText.split("\n")) {
|
|
|
140
235
|
|
|
141
236
|
process.stdout.write(JSON.stringify({
|
|
142
237
|
ok: true,
|
|
143
|
-
commit
|
|
238
|
+
commit,
|
|
144
239
|
base,
|
|
145
240
|
ancestor: "yes",
|
|
146
241
|
diff_file: outPath,
|
|
@@ -151,4 +246,7 @@ process.stdout.write(JSON.stringify({
|
|
|
151
246
|
files,
|
|
152
247
|
has_binary,
|
|
153
248
|
has_rename,
|
|
249
|
+
// 0.14.6 §1.1: the per-attempt publish version ("<commit>:<attempt>" when synthesized,
|
|
250
|
+
// null when --task-id/--attempt were omitted or the diff was empty).
|
|
251
|
+
version,
|
|
154
252
|
}) + "\n");
|