muse-crew 0.14.3 → 0.14.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +1 -1
- package/docs/decisions/AGENTS.md +1 -0
- package/docs/decisions/publish-path.md +43 -3
- package/docs/publish-verification.md +9 -1
- package/docs/release-integrity.md +60 -0
- package/docs/reviews/critic-0144.md +83 -0
- package/lib/AGENTS.md +10 -3
- 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 +54 -21
- 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/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 +77 -45
- package/lib/sample-project.js +22 -6
- package/lib/scaffold-crew.js +11 -2
- package/lib/see-act.js +17 -8
- package/lib/serve-artifact.js +12 -6
- package/lib/setup-project-repo.js +26 -7
- package/lib/update-watch.js +34 -17
- package/lib/ux-doctrine.js +31 -6
- package/lib/verify-publish.js +38 -2
- package/lib/write-ooda-verdict.js +12 -3
- package/package.json +1 -1
- package/workflows/bugfix.js +33 -35
- package/workflows/chore.js +33 -35
- package/workflows/standard.js +33 -16
- package/workflows/upgrade.js +4 -2
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,22 +1728,34 @@ function latestPublishNote(db, taskId) {
|
|
|
1721
1728
|
).get(taskId) || null;
|
|
1722
1729
|
}
|
|
1723
1730
|
|
|
1724
|
-
// The trigger anchor for an unknown attempt: the
|
|
1725
|
-
// entry for the same attempt (the trigger issuance)
|
|
1726
|
-
//
|
|
1727
|
-
//
|
|
1728
|
-
//
|
|
1729
|
-
//
|
|
1730
|
-
//
|
|
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.
|
|
1731
1742
|
function findTriggerEntry(entries, commit, attempt) {
|
|
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 = [];
|
|
1732
1750
|
for (let i = 0; i < entries.length; i++) {
|
|
1733
1751
|
const e = entries[i];
|
|
1734
1752
|
if (e.outcome === "submitted" && e.commit === commit &&
|
|
1735
1753
|
(e.attempt || null) === (attempt || null)) {
|
|
1736
|
-
|
|
1754
|
+
matches.push(e);
|
|
1737
1755
|
}
|
|
1738
1756
|
}
|
|
1739
|
-
|
|
1757
|
+
matches.sort((a, b) => (a.ts || "").localeCompare(b.ts || ""));
|
|
1758
|
+
return matches[0] || null;
|
|
1740
1759
|
}
|
|
1741
1760
|
|
|
1742
1761
|
// The unknown ledger entry's detail says whether the trigger went out.
|
|
@@ -1887,10 +1906,9 @@ commands["resolve-publish-unknown"] = (db, args, ctx) => {
|
|
|
1887
1906
|
// "publish: retry-issued" (not-before = intended+20m); NEVER re-trigger here
|
|
1888
1907
|
// "publish: retry-issued" w/o mirrored → mirror the missing publish: verification-requested
|
|
1889
1908
|
// verification-requested
|
|
1890
|
-
//
|
|
1891
|
-
//
|
|
1892
|
-
//
|
|
1893
|
-
// "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"
|
|
1894
1912
|
// "publish: verification-requested" → already routed; ignored (the verification scan owns it)
|
|
1895
1913
|
//
|
|
1896
1914
|
// Unknown parks whose trigger was never issued (preflight/toolcheck
|
|
@@ -1925,7 +1943,9 @@ commands["scan-publish-unknown"] = (db, args, ctx) => {
|
|
|
1925
1943
|
return;
|
|
1926
1944
|
}
|
|
1927
1945
|
const trigger = findTriggerEntry(derived.entries, derived.commit, derived.attempt);
|
|
1928
|
-
|
|
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)) {
|
|
1929
1949
|
skip(taskId, "no-trigger-anchor",
|
|
1930
1950
|
"no submitted ledger entry for this attempt — the trigger instant is unknowable; stays parked for human attention");
|
|
1931
1951
|
return;
|
|
@@ -1959,7 +1979,7 @@ commands["scan-publish-unknown"] = (db, args, ctx) => {
|
|
|
1959
1979
|
}
|
|
1960
1980
|
due.push({
|
|
1961
1981
|
task_id: taskId, commit: derived.commit, base,
|
|
1962
|
-
trigger_ts: trigger.ts, park_ts: parkNote.timestamp,
|
|
1982
|
+
trigger_ts: trigger.issued_at || trigger.ts, park_ts: parkNote.timestamp,
|
|
1963
1983
|
slug: derived.slug, repo_path: derived.repoPath,
|
|
1964
1984
|
project_id: derived.projectId, claim_expiry: expiry,
|
|
1965
1985
|
ledger_path: derived.ledgerPath,
|
|
@@ -1989,11 +2009,13 @@ commands["scan-publish-unknown"] = (db, args, ctx) => {
|
|
|
1989
2009
|
const has = (s) => msg.includes(s);
|
|
1990
2010
|
const historyHas = (s) => notes.some((n) => n.message.includes(s));
|
|
1991
2011
|
|
|
1992
|
-
// Terminal states — the machine never revisits these.
|
|
1993
|
-
|
|
1994
|
-
|
|
1995
|
-
|
|
1996
|
-
|
|
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]}`);
|
|
1997
2019
|
continue;
|
|
1998
2020
|
}
|
|
1999
2021
|
// Already routed into the verification pipeline — the verification
|
|
@@ -2065,7 +2087,7 @@ commands["scan-publish-unknown"] = (db, args, ctx) => {
|
|
|
2065
2087
|
task_id: taskId, commit: derived.commit, attempt: derived.attempt,
|
|
2066
2088
|
slug: derived.slug, repo_path: derived.repoPath, project_id: derived.projectId,
|
|
2067
2089
|
ledger_path: derived.ledgerPath, base: retryBase,
|
|
2068
|
-
trigger_ts: (retryTrigger && retryTrigger.ts) || null,
|
|
2090
|
+
trigger_ts: (retryTrigger && (retryTrigger.issued_at || retryTrigger.ts)) || null,
|
|
2069
2091
|
manifest_before: (retryTrigger && retryTrigger.manifest_before &&
|
|
2070
2092
|
typeof retryTrigger.manifest_before === "object") ? retryTrigger.manifest_before : null,
|
|
2071
2093
|
});
|
|
@@ -2802,6 +2824,17 @@ function main() {
|
|
|
2802
2824
|
}
|
|
2803
2825
|
}
|
|
2804
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
|
+
|
|
2805
2838
|
try {
|
|
2806
2839
|
main();
|
|
2807
2840
|
} catch (e) {
|
package/lib/crew-release.sh
CHANGED
|
@@ -81,7 +81,8 @@ _validate_workflows() {
|
|
|
81
81
|
# --- Parse gate (restored 2026-09-18, finding B2) ---
|
|
82
82
|
# The workflow runtime accepts top-level `export`, `return`, and `await`
|
|
83
83
|
# (it wraps scripts in an async function), so neither `node --check` on the
|
|
84
|
-
# raw .js (
|
|
84
|
+
# raw .js (goal confusion: parsed as a module it masks breakage the
|
|
85
|
+
# async-wrapped loader rejects — a false negative, not a true check) nor a
|
|
85
86
|
# .mjs check (rejects the runtime-legal top-level `return`) is correct.
|
|
86
87
|
# Emulate the runtime instead: strip `export`, wrap the script in an async
|
|
87
88
|
# function, then node --check the result. Tokenizer errors (e.g. an
|
|
@@ -210,6 +211,210 @@ _link_dep_node_modules() {
|
|
|
210
211
|
echo "DEP-NODE-MODULES: $link -> $src"
|
|
211
212
|
}
|
|
212
213
|
|
|
214
|
+
# ── lib entry gate (blocker 21) ─────────────────────────────────────
|
|
215
|
+
# Every shipped lib entry must actually EXECUTE (JS) — node --check is
|
|
216
|
+
# banned from this gate. The true reason, not the folklore: node --check
|
|
217
|
+
# can check a file under a different PARSE GOAL than the real loader uses
|
|
218
|
+
# (blocker 21: checked as a script, loaded as a module), and V8's
|
|
219
|
+
# preparser skips function bodies — a parse check proves nothing about the
|
|
220
|
+
# loader's path. Real execution is the only check that uses the loader's
|
|
221
|
+
# goal. Parse goal: whether Node reads a .js file as a module
|
|
222
|
+
# (import/export, no top-level return) or as a script — set by the nearest
|
|
223
|
+
# package.json's `type` field (shipped lib/package.json pins
|
|
224
|
+
# {"type":"module"}).
|
|
225
|
+
# What runs how:
|
|
226
|
+
# lib/*.js with a first line carrying a node shebang token
|
|
227
|
+
# (#!/usr/bin/env node, #!/bin/node, env -S variants) →
|
|
228
|
+
# `node <file> --help` through the $tmp/liblink symlink, which
|
|
229
|
+
# mirrors production's $CREW_HOME/current path shape (the
|
|
230
|
+
# ff3fb53 realpath main-guard exists exactly for this shape; the
|
|
231
|
+
# gate runs through the link so it can tell the broken guard
|
|
232
|
+
# from the fixed one). shebang⇔CLI contract: exit 0, non-empty
|
|
233
|
+
# stdout.
|
|
234
|
+
# lib/*.js without a shebang → bare `node <file>` through the link
|
|
235
|
+
# (import-safe module contract: exit 0, no side effects).
|
|
236
|
+
# Residual, documented: import-safety itself is unchecked — an
|
|
237
|
+
# import-dirty-but-exit-0 module passes. No cheap mechanism.
|
|
238
|
+
# lib/*.sh (except test-*.sh) → `bash -n <file>` — parse-only by
|
|
239
|
+
# necessity: real .sh execution risks side effects. Test scripts
|
|
240
|
+
# are exercised by tests/run.sh, not shipped as library entries
|
|
241
|
+
# (they get a `skipped` evidence row, not silence).
|
|
242
|
+
# lib/*.py → `python3 -m py_compile <file>` (bytecode goes to a temp
|
|
243
|
+
# PYTHONPYCACHEPREFIX, never into the staging dir) — parse-only
|
|
244
|
+
# by the same necessity.
|
|
245
|
+
# So the headline is honest: the gate EXECUTES every JS entry and
|
|
246
|
+
# PARSE-CHECKS sh/py.
|
|
247
|
+
# Boundary: `--help` short-circuits before argument parsing — the gate
|
|
248
|
+
# proves an entry LOADS, not that its main path behaves. The suite covers
|
|
249
|
+
# behavior. workflows/*.js are not covered by this gate at all; their true
|
|
250
|
+
# gate is the suite's loader emulation (publish-verdict-first.test.js —
|
|
251
|
+
# see root AGENTS.md).
|
|
252
|
+
# Every entry gets one ENTRY-GATE pass/FAIL line and one row in
|
|
253
|
+
# $staging_dir/entry-gate.json {release, node, entries:[{file,kind,verdict,exit,reason}]}.
|
|
254
|
+
# A FAIL reason folds the first 10 stderr lines in, so the row names the
|
|
255
|
+
# actual error instead of a bare exit code.
|
|
256
|
+
# On gate failure cmd_deploy copies entry-gate.json to $CREW_HOME before
|
|
257
|
+
# removing the staging dir, and the rejection names the failing entries —
|
|
258
|
+
# the evidence survives the burn.
|
|
259
|
+
# All verdicts are aggregated (no early stop) so the evidence names every
|
|
260
|
+
# failure. Returns 30 when any entry failed; any other exit is a gate
|
|
261
|
+
# mechanism failure (fail closed — the release is rejected either way).
|
|
262
|
+
_validate_lib_entries() {
|
|
263
|
+
local dir="$1" release="$2"
|
|
264
|
+
local tmp base f kind st reason verdict
|
|
265
|
+
tmp="$(mktemp -d)"
|
|
266
|
+
local rows="$tmp/rows.jsonl"
|
|
267
|
+
: > "$rows"
|
|
268
|
+
local failed=0
|
|
269
|
+
local node_version
|
|
270
|
+
node_version="$(node --version 2>/dev/null || echo "node-missing")"
|
|
271
|
+
# Invoke JS entries through a symlink mirroring production's
|
|
272
|
+
# $CREW_HOME/current path shape — the ff3fb53 realpath main-guard exists
|
|
273
|
+
# exactly for that shape, and the gate can only tell the broken guard
|
|
274
|
+
# from the fixed one when it exercises the symlinked path.
|
|
275
|
+
local link="$tmp/liblink"
|
|
276
|
+
if ! ln -s "$dir/lib" "$link" 2>/dev/null || [ ! -L "$link" ]; then
|
|
277
|
+
echo "ENTRY-GATE FAIL: cannot create lib invocation symlink ($link) — failing closed" >&2
|
|
278
|
+
return 31
|
|
279
|
+
fi
|
|
280
|
+
|
|
281
|
+
_gate_row() { # file kind verdict exit reason
|
|
282
|
+
node -e 'console.log(JSON.stringify({file:process.argv[1],kind:process.argv[2],verdict:process.argv[3],exit:+process.argv[4],reason:process.argv[5]}))' \
|
|
283
|
+
"$1" "$2" "$3" "$4" "$5" >> "$rows"
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
_gate_js() { # $1 = real file path; invoked through $link (the $CREW_HOME/current-shaped symlink)
|
|
287
|
+
local first
|
|
288
|
+
first="$(head -n 1 "$1")"
|
|
289
|
+
local cli=0
|
|
290
|
+
# Shebang match on a node token: #!/usr/bin/env node, #!/bin/node,
|
|
291
|
+
# env -S variants all classify as CLI.
|
|
292
|
+
case "$first" in
|
|
293
|
+
"#!"*"node"*) cli=1 ;;
|
|
294
|
+
esac
|
|
295
|
+
st=0
|
|
296
|
+
reason="ok"
|
|
297
|
+
local target="$link/$base"
|
|
298
|
+
if [ "$cli" = 1 ]; then
|
|
299
|
+
timeout 30 node "$target" --help >"$tmp/out" 2>"$tmp/err" || st=$?
|
|
300
|
+
kind="js-cli"
|
|
301
|
+
if [ "$st" -eq 0 ] && [ ! -s "$tmp/out" ]; then
|
|
302
|
+
reason="empty-stdout"
|
|
303
|
+
st=1
|
|
304
|
+
fi
|
|
305
|
+
else
|
|
306
|
+
timeout 30 node "$target" >"$tmp/out" 2>"$tmp/err" || st=$?
|
|
307
|
+
kind="js-module"
|
|
308
|
+
fi
|
|
309
|
+
if [ "$st" -eq 0 ]; then
|
|
310
|
+
reason="ok"
|
|
311
|
+
else
|
|
312
|
+
if grep -q "SyntaxError" "$tmp/err" 2>/dev/null; then
|
|
313
|
+
reason="syntax-error"
|
|
314
|
+
elif [ "$reason" != "empty-stdout" ]; then
|
|
315
|
+
reason="exit=$st"
|
|
316
|
+
fi
|
|
317
|
+
# Fold the first 10 stderr lines into the reason: a bare exit code
|
|
318
|
+
# names nothing, and the staging dir (with $tmp/err) is removed on
|
|
319
|
+
# failure — the row is the surviving evidence.
|
|
320
|
+
local errhead
|
|
321
|
+
errhead="$(head -n 10 "$tmp/err" 2>/dev/null | tr '\n' ';' | cut -c1-500)"
|
|
322
|
+
[ -n "$errhead" ] && reason="$reason | $errhead"
|
|
323
|
+
fi
|
|
324
|
+
verdict="pass"
|
|
325
|
+
[ "$st" -eq 0 ] || { verdict="FAIL"; failed=1; }
|
|
326
|
+
if [ "$verdict" = "pass" ]; then
|
|
327
|
+
echo "ENTRY-GATE pass $base ($kind)"
|
|
328
|
+
else
|
|
329
|
+
echo "ENTRY-GATE FAIL $base ($kind): $reason" >&2
|
|
330
|
+
fi
|
|
331
|
+
_gate_row "$base" "$kind" "$verdict" "$st" "$reason"
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
for f in "$dir"/lib/*.js; do
|
|
335
|
+
[ -f "$f" ] || continue
|
|
336
|
+
base="$(basename "$f")"
|
|
337
|
+
if ! command -v node >/dev/null 2>&1; then
|
|
338
|
+
echo "ENTRY-GATE FAIL $base (js): node missing" >&2
|
|
339
|
+
_gate_row "$base" "js" "FAIL" 127 "node-missing"
|
|
340
|
+
failed=1
|
|
341
|
+
continue
|
|
342
|
+
fi
|
|
343
|
+
_gate_js "$f"
|
|
344
|
+
done
|
|
345
|
+
|
|
346
|
+
for f in "$dir"/lib/*.sh; do
|
|
347
|
+
[ -f "$f" ] || continue
|
|
348
|
+
base="$(basename "$f")"
|
|
349
|
+
case "$base" in
|
|
350
|
+
test-*)
|
|
351
|
+
# Out of scope for the gate — but never silent: a skipped row, so
|
|
352
|
+
# "0 rows" is distinguishable from "all skipped".
|
|
353
|
+
_gate_row "$base" "sh" "skipped" 0 "test scripts are exercised by tests/run.sh, not shipped as library entries"
|
|
354
|
+
echo "ENTRY-GATE skipped $base (sh)"
|
|
355
|
+
continue ;;
|
|
356
|
+
esac
|
|
357
|
+
st=0
|
|
358
|
+
bash -n "$f" 2>"$tmp/err" || st=$?
|
|
359
|
+
reason="ok"; verdict="pass"
|
|
360
|
+
if [ "$st" -ne 0 ]; then
|
|
361
|
+
reason="bash-n exit=$st"
|
|
362
|
+
# Same evidence rule as the JS gate: the row names the actual error.
|
|
363
|
+
local errhead
|
|
364
|
+
errhead="$(head -n 10 "$tmp/err" 2>/dev/null | tr '\n' ';' | cut -c1-500)"
|
|
365
|
+
[ -n "$errhead" ] && reason="$reason | $errhead"
|
|
366
|
+
verdict="FAIL"; failed=1
|
|
367
|
+
echo "ENTRY-GATE FAIL $base (sh): $reason" >&2
|
|
368
|
+
else
|
|
369
|
+
echo "ENTRY-GATE pass $base (sh)"
|
|
370
|
+
fi
|
|
371
|
+
_gate_row "$base" "sh" "$verdict" "$st" "$reason"
|
|
372
|
+
done
|
|
373
|
+
|
|
374
|
+
for f in "$dir"/lib/*.py; do
|
|
375
|
+
[ -f "$f" ] || continue
|
|
376
|
+
base="$(basename "$f")"
|
|
377
|
+
st=0
|
|
378
|
+
if ! command -v python3 >/dev/null 2>&1; then
|
|
379
|
+
st=127; reason="python3-missing"
|
|
380
|
+
else
|
|
381
|
+
# Bytecode goes to a temp prefix — never into the staging dir, or it
|
|
382
|
+
# would be shipped inside the release.
|
|
383
|
+
PYTHONPYCACHEPREFIX="$tmp/pyc" python3 -m py_compile "$f" 2>"$tmp/err" || st=$?
|
|
384
|
+
reason="ok"
|
|
385
|
+
[ "$st" -eq 0 ] || reason="py-compile exit=$st"
|
|
386
|
+
fi
|
|
387
|
+
verdict="pass"
|
|
388
|
+
if [ "$st" -ne 0 ]; then
|
|
389
|
+
local errhead
|
|
390
|
+
errhead="$(head -n 10 "$tmp/err" 2>/dev/null | tr '\n' ';' | cut -c1-500)"
|
|
391
|
+
[ -n "$errhead" ] && reason="$reason | $errhead"
|
|
392
|
+
verdict="FAIL"; failed=1
|
|
393
|
+
fi
|
|
394
|
+
if [ "$verdict" = "pass" ]; then
|
|
395
|
+
echo "ENTRY-GATE pass $base (py)"
|
|
396
|
+
else
|
|
397
|
+
echo "ENTRY-GATE FAIL $base (py): $reason" >&2
|
|
398
|
+
fi
|
|
399
|
+
_gate_row "$base" "py" "$verdict" "$st" "$reason"
|
|
400
|
+
done
|
|
401
|
+
|
|
402
|
+
# Evidence: one row per enumerated entry.
|
|
403
|
+
node -e '
|
|
404
|
+
const fs = require("fs");
|
|
405
|
+
const rows = fs.readFileSync(process.argv[1], "utf8").split("\n").filter(Boolean).map(l => JSON.parse(l));
|
|
406
|
+
fs.writeFileSync(process.argv[4], JSON.stringify({ release: process.argv[2], node: process.argv[3], entries: rows }, null, 2) + "\n");
|
|
407
|
+
' "$rows" "$release" "$node_version" "$dir/entry-gate.json"
|
|
408
|
+
rm -rf "$tmp"
|
|
409
|
+
|
|
410
|
+
if [ "$failed" -ne 0 ]; then
|
|
411
|
+
echo "ENTRY-GATE: $failed entr(y/ies) failed — release rejected" >&2
|
|
412
|
+
return 30
|
|
413
|
+
fi
|
|
414
|
+
echo "ENTRY-GATE: all lib entries executed cleanly"
|
|
415
|
+
return 0
|
|
416
|
+
}
|
|
417
|
+
|
|
213
418
|
# ── deploy ────────────────────────────────────────────────────────────
|
|
214
419
|
# Build, validate, and atomically activate a release from repo HEAD.
|
|
215
420
|
# Single command — no cross-step lock needed.
|
|
@@ -302,6 +507,29 @@ cmd_deploy() {
|
|
|
302
507
|
*) die "release $hash rejected: workflow validation failed (unexpected exit $_vw_status)" ;;
|
|
303
508
|
esac
|
|
304
509
|
fi
|
|
510
|
+
# Gate: every shipped lib entry must actually execute (blocker 21).
|
|
511
|
+
# _validate_lib_entries returns 30 when any entry failed, anything else
|
|
512
|
+
# on a gate mechanism failure; either way the release is rejected. The
|
|
513
|
+
# rejection names the failing entries (not just the gate) and points at
|
|
514
|
+
# the preserved evidence — the evidence is copied to $CREW_HOME before
|
|
515
|
+
# the staging dir is removed, so it survives the burn.
|
|
516
|
+
_le_status=0
|
|
517
|
+
_validate_lib_entries "$staging_dir" "$hash" || _le_status=$?
|
|
518
|
+
if [ "$_le_status" -ne 0 ]; then
|
|
519
|
+
# The staging dir (and its entry-gate.json) is about to be removed —
|
|
520
|
+
# preserve the evidence first, then name the failing entries in the
|
|
521
|
+
# rejection instead of a bare exit code.
|
|
522
|
+
local _le_evidence="$CREW_HOME/entry-gate-$hash.json"
|
|
523
|
+
[ -f "$staging_dir/entry-gate.json" ] && cp "$staging_dir/entry-gate.json" "$_le_evidence"
|
|
524
|
+
local _le_fails="(no entry rows — gate mechanism failure)"
|
|
525
|
+
if [ -f "$staging_dir/entry-gate.json" ]; then
|
|
526
|
+
_le_fails="$(node -e 'const j=JSON.parse(require("fs").readFileSync(process.argv[1],"utf8"));const f=j.entries.filter(e=>e.verdict==="FAIL").map(e=>e.file+" ("+e.kind+")");console.log(f.length?f.join(", "):"(no failing entries — gate mechanism failure)")' "$staging_dir/entry-gate.json" 2>/dev/null)"
|
|
527
|
+
fi
|
|
528
|
+
local _le_meaning="gate mechanism failure"
|
|
529
|
+
[ "$_le_status" = 30 ] && _le_meaning="at least one entry failed"
|
|
530
|
+
rm -rf "$staging_dir"
|
|
531
|
+
die "release $hash rejected: lib entry gate failed (exit $_le_status = $_le_meaning) — failing entries: $_le_fails; evidence: $_le_evidence"
|
|
532
|
+
fi
|
|
305
533
|
# Atomic rename into place
|
|
306
534
|
mv "$staging_dir" "$release_dir"
|
|
307
535
|
echo "INSTALLED: $hash"
|
|
@@ -404,6 +632,9 @@ _prune_releases() {
|
|
|
404
632
|
}
|
|
405
633
|
|
|
406
634
|
# ── dispatch ──────────────────────────────────────────────────────────
|
|
635
|
+
# Sourced (never executed) by tests/entry-gate.test.js to drive
|
|
636
|
+
# _validate_lib_entries directly — dispatch only when run as the main script.
|
|
637
|
+
if [[ "${BASH_SOURCE[0]}" == "$0" ]]; then
|
|
407
638
|
case "${1:-}" in
|
|
408
639
|
init) shift; cmd_init "$@" ;;
|
|
409
640
|
deploy) shift; cmd_deploy "$@" ;;
|
|
@@ -412,3 +643,4 @@ case "${1:-}" in
|
|
|
412
643
|
list) shift; cmd_list "$@" ;;
|
|
413
644
|
*) die "usage: crew-release.sh {init|deploy|rollback|current|list} [args...]" ;;
|
|
414
645
|
esac
|
|
646
|
+
fi
|
package/lib/gitignore.js
CHANGED
|
@@ -16,8 +16,10 @@
|
|
|
16
16
|
// - Never removes, reorders, or reformats existing content.
|
|
17
17
|
// - Returns { created, appended[], skipped[] }.
|
|
18
18
|
|
|
19
|
-
|
|
20
|
-
|
|
19
|
+
import fs from "node:fs";
|
|
20
|
+
import path from "node:path";
|
|
21
|
+
import { realpathSync } from "node:fs";
|
|
22
|
+
import { fileURLToPath } from "node:url";
|
|
21
23
|
|
|
22
24
|
const CREW_GITIGNORE_ENTRIES = [".worktrees/", ".orchestration/user/"];
|
|
23
25
|
|
|
@@ -118,21 +120,37 @@ function describeGitignoreChange(repoPath, entries) {
|
|
|
118
120
|
};
|
|
119
121
|
}
|
|
120
122
|
|
|
121
|
-
|
|
123
|
+
export {
|
|
122
124
|
CREW_GITIGNORE_ENTRIES,
|
|
123
125
|
ensureGitignoreEntries,
|
|
124
126
|
describeGitignoreChange,
|
|
125
127
|
};
|
|
126
128
|
|
|
129
|
+
const USAGE = "Usage: node lib/gitignore.js --repo <path> [--dry-run]";
|
|
130
|
+
|
|
127
131
|
// CLI: node lib/gitignore.js --repo <path> [--dry-run]
|
|
128
132
|
// Used by crew-init and by tests. --dry-run prints the diff without writing.
|
|
129
|
-
|
|
133
|
+
// --help (and the shebang⇔CLI contract): prints usage, exits 0, before
|
|
134
|
+
// required-arg parsing.
|
|
135
|
+
const isMainModule = (() => {
|
|
136
|
+
try {
|
|
137
|
+
return !!process.argv[1] && realpathSync(process.argv[1]) === fileURLToPath(import.meta.url);
|
|
138
|
+
} catch {
|
|
139
|
+
return false;
|
|
140
|
+
}
|
|
141
|
+
})();
|
|
142
|
+
|
|
143
|
+
if (isMainModule) {
|
|
130
144
|
const argv = process.argv.slice(2);
|
|
145
|
+
if (argv.includes("--help")) {
|
|
146
|
+
console.log(USAGE);
|
|
147
|
+
process.exit(0);
|
|
148
|
+
}
|
|
131
149
|
const repoIdx = argv.indexOf("--repo");
|
|
132
150
|
const repoPath = repoIdx >= 0 ? argv[repoIdx + 1] : null;
|
|
133
151
|
const dryRun = argv.includes("--dry-run");
|
|
134
152
|
if (!repoPath) {
|
|
135
|
-
console.error(
|
|
153
|
+
console.error(USAGE);
|
|
136
154
|
process.exit(2);
|
|
137
155
|
}
|
|
138
156
|
if (dryRun) {
|
package/lib/package.json
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"type":"module"}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
// Terminal-note vocabulary registry (D7, 2026-09-19). Explicit state, not
|
|
2
|
+
// prose: every `publish: …` note the state machine treats as terminally
|
|
3
|
+
// parked. Readers (scan-publish-unknown in lib/crew-api.js) consult this;
|
|
4
|
+
// writers assert against it (lib/verify-publish.js's terminal() fails loud
|
|
5
|
+
// before emitting a verb that is not in the registry). Add a note here when
|
|
6
|
+
// a new terminal verb is introduced — the scan recognizes it with no other
|
|
7
|
+
// change.
|
|
8
|
+
//
|
|
9
|
+
// This module is the closed-enum pattern's first instance: the audit
|
|
10
|
+
// (room23-design/blanket-call-audit.md §3) lists the other state-carrying
|
|
11
|
+
// string families (the full `publish: <transition>` note shapes, the
|
|
12
|
+
// publish-ledger `outcome` vocabulary, worker-report marker lines) that a
|
|
13
|
+
// later unification pass extends the enum-guard pattern to. The contract
|
|
14
|
+
// for any new family: one registry module, readers consult it, writers
|
|
15
|
+
// assert membership before writing, closed by default and open by edit.
|
|
16
|
+
//
|
|
17
|
+
// ESM, no shebang, no side effects on import (import-safe module — bare
|
|
18
|
+
// `node publish-note-vocabulary.js` exits 0).
|
|
19
|
+
export const TERMINAL_PUBLISH_NOTES = [
|
|
20
|
+
"publish: ambiguous",
|
|
21
|
+
"publish: retry-superseded",
|
|
22
|
+
"publish: retry-refused",
|
|
23
|
+
"publish: verified",
|
|
24
|
+
"publish: superseded",
|
|
25
|
+
// Verification pipeline's terminal content verdict (verify-publish.js).
|
|
26
|
+
// The task stays parked for human attention; unknown-recovery never
|
|
27
|
+
// re-enters it. Added 2026-09-19 (Room #23 J1/J3: was mislogged as
|
|
28
|
+
// unrecognized-publish-note).
|
|
29
|
+
"publish: verification-failed",
|
|
30
|
+
];
|
|
31
|
+
export const TERMINAL_NOTE_MEANINGS = {
|
|
32
|
+
"publish: ambiguous": "unknown-recovery terminal: could not prove the drop; parked for human attention",
|
|
33
|
+
"publish: retry-superseded": "retry protocol terminal: superseded; parked for human attention",
|
|
34
|
+
"publish: retry-refused": "retry protocol terminal: platform refused the re-issued edit; parked for human attention",
|
|
35
|
+
"publish: verified": "publish verified and stamped; task re-queued (terminal for the scan)",
|
|
36
|
+
"publish: superseded": "HEAD moved past the attempt; recovery aborted; parked for human attention",
|
|
37
|
+
"publish: verification-failed": "verification pipeline's terminal content verdict — parked for human attention",
|
|
38
|
+
};
|
|
39
|
+
export function matchTerminalPublishNote(message) {
|
|
40
|
+
for (const note of TERMINAL_PUBLISH_NOTES) {
|
|
41
|
+
if (message.includes(note)) return note; // same substring semantics the scan already uses
|
|
42
|
+
}
|
|
43
|
+
return null;
|
|
44
|
+
}
|
package/lib/read-ooda-verdict.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
1
2
|
// read-ooda-verdict.js — deterministic cross-checker for the OODA terminal
|
|
2
3
|
// verdict (2026-09-15).
|
|
3
4
|
//
|
|
@@ -50,10 +51,9 @@
|
|
|
50
51
|
// Determinism: no wall-clock reads, no randomness. The script never judges
|
|
51
52
|
// report content — the agent states the reason; the machine enforces its
|
|
52
53
|
// presence and its agreement with the prose verdict.
|
|
53
|
-
"use strict";
|
|
54
54
|
|
|
55
|
-
|
|
56
|
-
|
|
55
|
+
import { readFileSync, existsSync } from "node:fs";
|
|
56
|
+
import { join, resolve } from "node:path";
|
|
57
57
|
|
|
58
58
|
function fail(code, error) {
|
|
59
59
|
process.stdout.write(JSON.stringify({ ok: false, code: code, error: error }) + "\n");
|
|
@@ -72,6 +72,14 @@ function parseArgs(argv) {
|
|
|
72
72
|
}
|
|
73
73
|
|
|
74
74
|
function main() {
|
|
75
|
+
// --help: before required-arg parsing (shebang⇔CLI contract).
|
|
76
|
+
if (process.argv.slice(2).includes("--help")) {
|
|
77
|
+
console.log(
|
|
78
|
+
"usage: node read-ooda-verdict.js --dir <phase-dir> --expect <PASS|FAIL>\n" +
|
|
79
|
+
"cross-checks the prose VERDICT: line against <phase-dir>/verdict.json"
|
|
80
|
+
);
|
|
81
|
+
process.exit(0);
|
|
82
|
+
}
|
|
75
83
|
const args = parseArgs(process.argv.slice(2));
|
|
76
84
|
if (!args.dir) fail("bad_input", "missing --dir <phase-dir>");
|
|
77
85
|
if (!args.expect) fail("bad_input", "missing --expect <PASS|FAIL>");
|
package/lib/readback-disk.js
CHANGED
|
@@ -53,6 +53,15 @@ import { existsSync, readFileSync, realpathSync, statSync } from "node:fs";
|
|
|
53
53
|
import { homedir } from "node:os";
|
|
54
54
|
import { join, resolve } from "node:path";
|
|
55
55
|
|
|
56
|
+
// --help: before required-arg parsing (shebang⇔CLI contract).
|
|
57
|
+
if (process.argv.slice(2).includes("--help")) {
|
|
58
|
+
console.log(
|
|
59
|
+
"usage: node readback-disk.js --repo-path <path> --commit <sha> --base <sha> --task-id <uuid> --slug <artifact-slug> [--spaces-root <dir>]\n" +
|
|
60
|
+
"the deterministic disk read-back SENSOR: emits the machine-readable findings block (FILE:/ADDED:/REMOVED:/END_FILE)"
|
|
61
|
+
);
|
|
62
|
+
process.exit(0);
|
|
63
|
+
}
|
|
64
|
+
|
|
56
65
|
const EMPTY_TREE = "4b825dc642cb6eb9a060e54bf8d69288fbee4904";
|
|
57
66
|
|
|
58
67
|
function arg(name) {
|
package/lib/render-html.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
1
2
|
// render-html.js — render an HTML evidence layout to PNG via headless Chromium.
|
|
2
3
|
//
|
|
3
4
|
// The reef-qa pattern (2026-09-14): the agent composes evidence as HTML —
|
|
@@ -26,11 +27,13 @@
|
|
|
26
27
|
// The rendered composition is evidence: log it with append-ooda-step.js
|
|
27
28
|
// (--action compose) and READ it — a composition you did not read is not
|
|
28
29
|
// evidence.
|
|
29
|
-
"use strict";
|
|
30
30
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
31
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
32
|
+
import { resolve, dirname } from "node:path";
|
|
33
|
+
import { pathToFileURL } from "node:url";
|
|
34
|
+
import { createRequire } from "node:module";
|
|
35
|
+
|
|
36
|
+
const cjsRequire = createRequire(import.meta.url);
|
|
34
37
|
|
|
35
38
|
const DEFAULT_WIDTH = 1200;
|
|
36
39
|
|
|
@@ -54,16 +57,17 @@ function parseArgs(argv) {
|
|
|
54
57
|
function loadPlaywright() {
|
|
55
58
|
// playwright-core is a declared dependency (package.json), so a normal
|
|
56
59
|
// npm install resolves it from the package's own node_modules.
|
|
60
|
+
// createRequire keeps the synchronous resolution the CLI relies on.
|
|
57
61
|
try {
|
|
58
|
-
return
|
|
62
|
+
return cjsRequire("playwright-core");
|
|
59
63
|
} catch (e) { /* fall through */ }
|
|
60
64
|
const envDir = (process.env.PLAYWRIGHT_CORE_DIR || "").trim();
|
|
61
65
|
if (envDir) {
|
|
62
66
|
try {
|
|
63
|
-
return
|
|
67
|
+
return cjsRequire(envDir + "/playwright-core");
|
|
64
68
|
} catch (e) { /* fall through */ }
|
|
65
69
|
try {
|
|
66
|
-
return
|
|
70
|
+
return cjsRequire(envDir);
|
|
67
71
|
} catch (e) { /* fall through */ }
|
|
68
72
|
}
|
|
69
73
|
fail(3, {
|
|
@@ -86,6 +90,12 @@ function findChromium() {
|
|
|
86
90
|
}
|
|
87
91
|
|
|
88
92
|
async function main() {
|
|
93
|
+
// --help: before required-arg parsing (shebang⇔CLI contract).
|
|
94
|
+
if (process.argv.slice(2).includes("--help")) {
|
|
95
|
+
console.log("usage: node render-html.js --in <page.html> --out <shot.png> [--width <px>]\n" +
|
|
96
|
+
"renders a local HTML evidence layout to PNG via headless Chromium");
|
|
97
|
+
process.exit(0);
|
|
98
|
+
}
|
|
89
99
|
const args = parseArgs(process.argv.slice(2));
|
|
90
100
|
if (!args.in) fail(2, { ok: false, error: "missing --in <page.html>" });
|
|
91
101
|
if (!args.out) fail(2, { ok: false, error: "missing --out <shot.png>" });
|
|
@@ -14,8 +14,10 @@
|
|
|
14
14
|
// Seeding uses no-clobber semantics: existing project customizations are
|
|
15
15
|
// never overwritten. Idempotent: re-running is a no-op.
|
|
16
16
|
|
|
17
|
-
|
|
18
|
-
|
|
17
|
+
import fs from "node:fs";
|
|
18
|
+
import path from "node:path";
|
|
19
|
+
import { realpathSync } from "node:fs";
|
|
20
|
+
import { fileURLToPath } from "node:url";
|
|
19
21
|
|
|
20
22
|
function ensureDir(dirPath) {
|
|
21
23
|
if (!fs.existsSync(dirPath)) {
|
|
@@ -106,19 +108,33 @@ function scaffoldRepoOrchestration(repoPath, crewRepoPath) {
|
|
|
106
108
|
return result;
|
|
107
109
|
}
|
|
108
110
|
|
|
109
|
-
|
|
111
|
+
export {
|
|
110
112
|
scaffoldRepoOrchestration,
|
|
111
113
|
};
|
|
112
114
|
|
|
115
|
+
const USAGE = "Usage: node lib/repo-orchestration.js --repo <path> --crew-repo <path>";
|
|
116
|
+
|
|
117
|
+
const isMainModule = (() => {
|
|
118
|
+
try {
|
|
119
|
+
return !!process.argv[1] && realpathSync(process.argv[1]) === fileURLToPath(import.meta.url);
|
|
120
|
+
} catch {
|
|
121
|
+
return false;
|
|
122
|
+
}
|
|
123
|
+
})();
|
|
124
|
+
|
|
113
125
|
// CLI: node lib/repo-orchestration.js --repo <path> --crew-repo <path>
|
|
114
|
-
if (
|
|
126
|
+
if (isMainModule) {
|
|
115
127
|
const argv = process.argv.slice(2);
|
|
128
|
+
if (argv.includes("--help")) {
|
|
129
|
+
console.log(USAGE);
|
|
130
|
+
process.exit(0);
|
|
131
|
+
}
|
|
116
132
|
const repoIdx = argv.indexOf("--repo");
|
|
117
133
|
const crewRepoIdx = argv.indexOf("--crew-repo");
|
|
118
134
|
const repoPath = repoIdx >= 0 ? argv[repoIdx + 1] : null;
|
|
119
135
|
const crewRepoPath = crewRepoIdx >= 0 ? argv[crewRepoIdx + 1] : null;
|
|
120
136
|
if (!repoPath || !crewRepoPath) {
|
|
121
|
-
console.error(
|
|
137
|
+
console.error(USAGE);
|
|
122
138
|
process.exit(2);
|
|
123
139
|
}
|
|
124
140
|
const r = scaffoldRepoOrchestration(repoPath, crewRepoPath);
|