muse-crew 0.14.2 → 0.14.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +1 -1
- package/docs/decisions/AGENTS.md +4 -0
- package/docs/decisions/publish-path.md +229 -0
- package/docs/publish-verification.md +14 -3
- package/docs/release-integrity.md +60 -0
- package/docs/reviews/critic-0143-redo.md +94 -0
- package/docs/reviews/critic-0143.md +108 -0
- package/docs/reviews/critic-0144.md +83 -0
- package/lib/AGENTS.md +14 -6
- package/lib/advance-publish-base.js +8 -0
- package/lib/append-ooda-step.js +12 -3
- package/lib/build-readback-request.js +10 -0
- package/lib/build-registry.js +8 -3
- package/lib/classify-publish-absence.js +11 -0
- package/lib/classify-surface.js +12 -2
- package/lib/commit-scaffold.js +22 -6
- package/lib/compose-evidence-caption.js +15 -4
- package/lib/compute-publish-diff.js +9 -0
- package/lib/crew-api.js +55 -20
- package/lib/crew-release.sh +233 -1
- package/lib/gitignore.js +23 -5
- package/lib/package.json +1 -0
- package/lib/publish-note-vocabulary.js +44 -0
- package/lib/publish-npm.sh +39 -10
- package/lib/read-ooda-verdict.js +11 -3
- package/lib/readback-disk.js +9 -0
- package/lib/render-html.js +17 -7
- package/lib/repo-orchestration.js +21 -5
- package/lib/retry-publish.js +79 -44
- package/lib/sample-project.js +22 -6
- package/lib/scaffold-crew.js +11 -2
- package/lib/see-act.js +17 -8
- package/lib/serve-artifact.js +12 -6
- package/lib/setup-project-repo.js +26 -7
- package/lib/test-detached-integrate.sh +79 -3
- package/lib/test-publish-preflight.sh +94 -5
- package/lib/update-watch.js +34 -17
- package/lib/ux-doctrine.js +31 -6
- package/lib/verify-publish.js +41 -6
- package/lib/worktree-lifecycle.sh +67 -25
- package/lib/write-ooda-verdict.js +12 -3
- package/package.json +1 -1
- package/workflows/bugfix.js +60 -61
- package/workflows/chore.js +60 -61
- package/workflows/standard.js +60 -42
- package/workflows/upgrade.js +4 -2
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/publish-npm.sh
CHANGED
|
@@ -82,7 +82,42 @@ if [ "$PREFLIGHT_CHECK_CODE" -ne 0 ] \
|
|
|
82
82
|
fi
|
|
83
83
|
# PREFLIGHT-END
|
|
84
84
|
|
|
85
|
-
#
|
|
85
|
+
# PUSH-TARGET-ANCHOR: resolve the push destination before ANY mutation.
|
|
86
|
+
# The destination is the integration target resolved by the lifecycle, not a
|
|
87
|
+
# hardcoded `main` (2026-09-19 REVIEW: the old `git push origin main` was a
|
|
88
|
+
# stale clone of the same hardcoding the integrate fix removed). A detached
|
|
89
|
+
# HEAD pushes as HEAD:<destination>, with the destination resolved explicitly
|
|
90
|
+
# by the lifecycle's push-destination command (2026-09-19 detached-HEAD audit
|
|
91
|
+
# REDO: the old code refused every detached checkout; detached publication
|
|
92
|
+
# is legitimate and must work). Fail here, before the version-bump commit
|
|
93
|
+
# and the registry publish, only when the destination is genuinely
|
|
94
|
+
# unknowable -- never after a side effect you cannot roll back. (The old
|
|
95
|
+
# position of this check -- step 13, after the npm publish -- left a shipped
|
|
96
|
+
# version with a failed phase and a retry loop.)
|
|
97
|
+
# No-origin is checked for BOTH shapes (2026-09-19 critic finding F-A1):
|
|
98
|
+
# step 13 pushes unconditionally, so an attached checkout with no origin
|
|
99
|
+
# would otherwise fail PUBLISH_FAILED=push after the version-bump commit
|
|
100
|
+
# and the registry publish. A version-bump commit with nowhere to record it
|
|
101
|
+
# would ship an npm version with no git provenance, so no-remote fails
|
|
102
|
+
# closed here rather than skipping like the integrate path (F-F6).
|
|
103
|
+
[ -n "${LIFECYCLE:-}" ] || fail "env" "LIFECYCLE is not set -- cannot resolve the push destination before mutation (F-R4)"
|
|
104
|
+
PUSH_TARGET="$(CREW_HOME="${CREW_HOME:-}" CREW_REPO="$REPO_PATH" bash "$LIFECYCLE" integration-target 2>&1)" \
|
|
105
|
+
|| fail "integration-target" "$PUSH_TARGET"
|
|
106
|
+
if ! git -C "$REPO_PATH" remote get-url origin >/dev/null 2>&1; then
|
|
107
|
+
fail "env" "no origin remote configured: the version-bump commit could not be pushed anywhere -- no version-bump commit was created and nothing was published"
|
|
108
|
+
fi
|
|
109
|
+
PUSH_REFSPEC="$PUSH_TARGET"
|
|
110
|
+
if [ "$PUSH_TARGET" = "HEAD" ]; then
|
|
111
|
+
PUSH_DEST="$(CREW_HOME="${CREW_HOME:-}" CREW_REPO="$REPO_PATH" bash "$LIFECYCLE" push-destination 2>&1)" \
|
|
112
|
+
|| fail "env" "$PUSH_DEST"
|
|
113
|
+
PUSH_REFSPEC="HEAD:$PUSH_DEST"
|
|
114
|
+
fi
|
|
115
|
+
# PUSH-TARGET-END
|
|
116
|
+
|
|
117
|
+
# 3. Dry run: markers only, no lock, no deploy, no mutation. The push
|
|
118
|
+
# preflight above already ran, so even a dry run fails closed on a
|
|
119
|
+
# genuinely unknowable push destination (see case 7 in
|
|
120
|
+
# lib/test-publish-preflight.sh -- F-R9).
|
|
86
121
|
if [ "${DRY_RUN:-0}" = "1" ]; then
|
|
87
122
|
echo "PUBLISH_DRY_RUN=1"
|
|
88
123
|
exit 0
|
|
@@ -281,15 +316,9 @@ LOCK_OUT2="$(CREW_HOME="${CREW_HOME:-}" CREW_REPO="$REPO_PATH" bash "${LIFECYCLE
|
|
|
281
316
|
|| fail "lock-refresh" "$LOCK_OUT2"
|
|
282
317
|
|
|
283
318
|
# 13. Push the version-bump commit (idempotent: no-op if already pushed).
|
|
284
|
-
#
|
|
285
|
-
#
|
|
286
|
-
|
|
287
|
-
PUSH_TARGET="$(CREW_HOME="${CREW_HOME:-}" CREW_REPO="$REPO_PATH" bash "${LIFECYCLE:?LIFECYCLE is required}" integration-target 2>&1)" \
|
|
288
|
-
|| fail "integration-target" "$PUSH_TARGET"
|
|
289
|
-
if [ "$PUSH_TARGET" = "HEAD" ]; then
|
|
290
|
-
fail "push" "detached HEAD: publish-npm cannot push a version-bump commit from a detached checkout"
|
|
291
|
-
fi
|
|
292
|
-
PUSH_OUT="$(git -C "$REPO_PATH" push origin "$PUSH_TARGET" 2>&1)" \
|
|
319
|
+
# PUSH_REFSPEC was resolved in the preflight above -- a branch name, or
|
|
320
|
+
# HEAD:<destination> on a detached checkout (never a bare HEAD).
|
|
321
|
+
PUSH_OUT="$(git -C "$REPO_PATH" push origin "$PUSH_REFSPEC" 2>&1)" \
|
|
293
322
|
|| fail "push" "$PUSH_OUT"
|
|
294
323
|
echo "PUBLISH_PUSHED=1"
|
|
295
324
|
|
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);
|