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/retry-publish.js
CHANGED
|
@@ -31,7 +31,7 @@
|
|
|
31
31
|
//
|
|
32
32
|
// Exit codes: 0 = success, 1 = terminal stop (no retry), 2 = usage error.
|
|
33
33
|
|
|
34
|
-
import { readFileSync, writeFileSync, appendFileSync, existsSync } from "fs";
|
|
34
|
+
import { readFileSync, writeFileSync, appendFileSync, existsSync, mkdirSync, unlinkSync } from "fs";
|
|
35
35
|
import { join } from "path";
|
|
36
36
|
import { execFileSync } from "child_process";
|
|
37
37
|
import { randomUUID as uuid } from "crypto";
|
|
@@ -50,6 +50,19 @@ function fail(code, msg, exitCode = 1) {
|
|
|
50
50
|
process.exit(exitCode);
|
|
51
51
|
}
|
|
52
52
|
|
|
53
|
+
// --help: handled before any required-arg parsing so
|
|
54
|
+
// `node retry-publish.js --help` always exits 0 (entry-gate contract).
|
|
55
|
+
if (args.includes("--help")) {
|
|
56
|
+
console.log(
|
|
57
|
+
"usage: node retry-publish.js --crew-home <path> --task-id <uuid> --phase <prepare|complete>\n" +
|
|
58
|
+
" prepare: acquire the merge lock, re-check HEAD, re-classify, journal intent, emit the diff\n" +
|
|
59
|
+
" complete: --edit-outcome <accepted|refused> [--refusal-text <text>] — write the outcome,\n" +
|
|
60
|
+
" append the ledger entry, mirror verification, and release the lock\n" +
|
|
61
|
+
"exit 0 = success, 1 = terminal stop, 2 = usage error"
|
|
62
|
+
);
|
|
63
|
+
process.exit(0);
|
|
64
|
+
}
|
|
65
|
+
|
|
53
66
|
const crewHome = arg("--crew-home");
|
|
54
67
|
const taskId = arg("--task-id");
|
|
55
68
|
const phase = arg("--phase");
|
|
@@ -59,6 +72,18 @@ if (!["prepare", "complete"].includes(phase)) {
|
|
|
59
72
|
process.exit(2);
|
|
60
73
|
}
|
|
61
74
|
|
|
75
|
+
// Retry protocol: phase-1 state is a persistent-disk pin
|
|
76
|
+
// ($CREW_HOME/.pins/ — never /tmp: tmpfs is wiped on cell reboot, and the
|
|
77
|
+
// lock release depends on phase 2 reading this state).
|
|
78
|
+
function pinsDir() {
|
|
79
|
+
const dir = join(crewHome, ".pins");
|
|
80
|
+
mkdirSync(dir, { recursive: true });
|
|
81
|
+
return dir;
|
|
82
|
+
}
|
|
83
|
+
function retryStatePath() {
|
|
84
|
+
return join(pinsDir(), `retry-publish-${taskId}.json`);
|
|
85
|
+
}
|
|
86
|
+
|
|
62
87
|
// --- Shared helpers ---------------------------------------------------------
|
|
63
88
|
|
|
64
89
|
function nowISO() { return new Date().toISOString(); }
|
|
@@ -180,12 +205,17 @@ if (phase === "prepare") {
|
|
|
180
205
|
}
|
|
181
206
|
|
|
182
207
|
const classifierPath = join(crewHome, "lib", "classify-publish-absence.js");
|
|
183
|
-
// Find the trigger entry for the manifest baseline: the OLDEST
|
|
184
|
-
//
|
|
208
|
+
// Find the trigger entry for the manifest baseline: the OLDEST submitted
|
|
209
|
+
// entry by ts (D1, 2026-09-19; entry_kind cut 2026-09-20 — the retired
|
|
210
|
+
// kind-preference could never change the bound anchor) — it binds
|
|
211
|
+
// issued_at (falling back to its ts when the capture was unobserved),
|
|
212
|
+
// the same anchor verify-publish.js and findTriggerEntry use.
|
|
185
213
|
// agent_id is null on the issuance entry (no receipt observed at trigger
|
|
186
214
|
// time) — the baseline comes from manifest_before, never agent_id.
|
|
187
|
-
const
|
|
215
|
+
const submitted = entries.filter((e) =>
|
|
188
216
|
e.outcome === "submitted" && e.commit === commit && (attempt == null || e.attempt === attempt));
|
|
217
|
+
submitted.sort((a, b) => (a.ts || "").localeCompare(b.ts || ""));
|
|
218
|
+
const trigger = submitted[0] || null;
|
|
189
219
|
const manifestBefore = trigger && trigger.manifest_before ? JSON.stringify(trigger.manifest_before) : null;
|
|
190
220
|
|
|
191
221
|
// Find the park note timestamp for quiesce calculation.
|
|
@@ -204,7 +234,7 @@ if (phase === "prepare") {
|
|
|
204
234
|
"--slug", slug,
|
|
205
235
|
"--commit", commit,
|
|
206
236
|
"--base", base,
|
|
207
|
-
"--trigger-ts", trigger ? trigger.ts : parkNote.timestamp,
|
|
237
|
+
"--trigger-ts", trigger ? (trigger.issued_at || trigger.ts) : parkNote.timestamp,
|
|
208
238
|
"--park-ts", parkNote.timestamp,
|
|
209
239
|
"--repo-path", repoPath,
|
|
210
240
|
];
|
|
@@ -269,7 +299,7 @@ if (phase === "prepare") {
|
|
|
269
299
|
// with the diff, then calls phase 2 (complete) to finish.
|
|
270
300
|
// Persist the phase-1 state for phase 2 (crash recovery reads the notes,
|
|
271
301
|
// but phase 2 needs the exact commit/attempt/baseline/ledger path).
|
|
272
|
-
const statePath =
|
|
302
|
+
const statePath = retryStatePath();
|
|
273
303
|
writeFileSync(statePath, JSON.stringify({
|
|
274
304
|
task_id: taskId, commit, attempt, slug, repoPath,
|
|
275
305
|
project_id: project.id, ledger_path: ledgerPath,
|
|
@@ -304,7 +334,7 @@ if (phase === "complete") {
|
|
|
304
334
|
}
|
|
305
335
|
const refusalText = arg("--refusal-text", false, "");
|
|
306
336
|
|
|
307
|
-
const statePath =
|
|
337
|
+
const statePath = retryStatePath();
|
|
308
338
|
if (!existsSync(statePath)) {
|
|
309
339
|
fail("no-prepare-state", `no phase-1 state at ${statePath} — the prepare phase did not complete or the state was cleaned`);
|
|
310
340
|
}
|
|
@@ -325,49 +355,51 @@ if (phase === "complete") {
|
|
|
325
355
|
writePublishNote(db, taskId,
|
|
326
356
|
`publish: retry-refused ${commit} — the platform refused the re-issued edit (${refusalText.split("\n")[0].slice(0, 200)}); not retrying again.`);
|
|
327
357
|
process.stdout.write(JSON.stringify({ ok: true, phase: "complete", outcome: "refused", commit }) + "\n");
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
358
|
+
} else {
|
|
359
|
+
// The child ended after the edit call with no explicit refusal —
|
|
360
|
+
// fire-and-forget, same shape as the first attempt.
|
|
361
|
+
writePublishNote(db, taskId,
|
|
362
|
+
`publish: retry-issued ${commit} — the dropped edit was re-issued once; no explicit refusal was observed.`);
|
|
363
|
+
|
|
364
|
+
// Mirror verification with a settle window.
|
|
365
|
+
const settleMs = Number(process.env.CREW_RETRY_SETTLE_MS) > 0
|
|
366
|
+
? Number(process.env.CREW_RETRY_SETTLE_MS)
|
|
367
|
+
: 20 * 60 * 1000;
|
|
368
|
+
const notBefore = new Date(Date.now() + settleMs).toISOString();
|
|
369
|
+
const retryAttempt = attempt ? `${attempt}-retry1` : "retry1";
|
|
370
|
+
writePublishNote(db, taskId,
|
|
371
|
+
`publish: verification-requested ${commit} attempt=${retryAttempt} not-before=${notBefore} — unknown-recovery retried the dropped edit once; content verification is the arbiter. Waiting on the manual read-back in docs/publish-verification.md.`);
|
|
372
|
+
|
|
373
|
+
// Append the -retry1 ledger entry with the pre-retry baseline (required).
|
|
374
|
+
const ledgerEntry = {
|
|
375
|
+
ts: nowISO(),
|
|
376
|
+
task_id: taskId,
|
|
377
|
+
workflow: "unknown-recovery",
|
|
378
|
+
slug,
|
|
379
|
+
commit,
|
|
380
|
+
attempt: retryAttempt,
|
|
381
|
+
agent_id: null,
|
|
382
|
+
applied_report: null,
|
|
383
|
+
outcome: "submitted",
|
|
384
|
+
manifest_before: retryBaseline,
|
|
385
|
+
detail: "retry trigger re-issued after provably-dropped classification",
|
|
386
|
+
};
|
|
387
|
+
try {
|
|
388
|
+
appendFileSync(ledgerPath, JSON.stringify(ledgerEntry) + "\n");
|
|
389
|
+
} catch (e) {
|
|
390
|
+
// Best-effort: the note journal is the source of truth, not the ledger.
|
|
391
|
+
console.error(`ledger append failed (non-fatal): ${e.message}`);
|
|
392
|
+
}
|
|
341
393
|
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
workflow: "unknown-recovery",
|
|
347
|
-
slug,
|
|
348
|
-
commit,
|
|
349
|
-
attempt: retryAttempt,
|
|
350
|
-
agent_id: null,
|
|
351
|
-
applied_report: null,
|
|
352
|
-
outcome: "submitted",
|
|
353
|
-
manifest_before: retryBaseline,
|
|
354
|
-
detail: "retry trigger re-issued after provably-dropped classification",
|
|
355
|
-
};
|
|
356
|
-
try {
|
|
357
|
-
appendFileSync(ledgerPath, JSON.stringify(ledgerEntry) + "\n");
|
|
358
|
-
} catch (e) {
|
|
359
|
-
// Best-effort: the note journal is the source of truth, not the ledger.
|
|
360
|
-
console.error(`ledger append failed (non-fatal): ${e.message}`);
|
|
394
|
+
process.stdout.write(JSON.stringify({
|
|
395
|
+
ok: true, phase: "complete", outcome: "issued",
|
|
396
|
+
commit, attempt: retryAttempt, not_before: notBefore,
|
|
397
|
+
}) + "\n");
|
|
361
398
|
}
|
|
362
|
-
|
|
363
|
-
process.stdout.write(JSON.stringify({
|
|
364
|
-
ok: true, phase: "complete", outcome: "issued",
|
|
365
|
-
commit, attempt: retryAttempt, not_before: notBefore,
|
|
366
|
-
}) + "\n");
|
|
367
399
|
} finally {
|
|
368
400
|
releaseLock();
|
|
369
401
|
// Clean up the phase-1 state file (best-effort).
|
|
370
|
-
try {
|
|
402
|
+
try { unlinkSync(statePath); } catch {}
|
|
371
403
|
}
|
|
372
404
|
process.exit(0);
|
|
373
405
|
}
|
package/lib/sample-project.js
CHANGED
|
@@ -15,9 +15,11 @@
|
|
|
15
15
|
// --name: directory/project name (e.g. "flashcards")
|
|
16
16
|
// Prints JSON: { path, committed, commitSha }
|
|
17
17
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
18
|
+
import fs from "node:fs";
|
|
19
|
+
import path from "node:path";
|
|
20
|
+
import { execFileSync } from "node:child_process";
|
|
21
|
+
import { realpathSync } from "node:fs";
|
|
22
|
+
import { fileURLToPath } from "node:url";
|
|
21
23
|
|
|
22
24
|
function run(cmd, args, cwd) {
|
|
23
25
|
return execFileSync(cmd, args, { cwd, encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] }).trim();
|
|
@@ -60,10 +62,24 @@ function setupSampleProject(tarballPath, destParent, name) {
|
|
|
60
62
|
return { path: destDir, committed: true, commitSha: sha };
|
|
61
63
|
}
|
|
62
64
|
|
|
63
|
-
|
|
65
|
+
export { setupSampleProject };
|
|
64
66
|
|
|
65
|
-
|
|
67
|
+
const USAGE = "Usage: node lib/sample-project.js --tarball <path> --dest <dir> --name <project-name>";
|
|
68
|
+
|
|
69
|
+
const isMainModule = (() => {
|
|
70
|
+
try {
|
|
71
|
+
return !!process.argv[1] && realpathSync(process.argv[1]) === fileURLToPath(import.meta.url);
|
|
72
|
+
} catch {
|
|
73
|
+
return false;
|
|
74
|
+
}
|
|
75
|
+
})();
|
|
76
|
+
|
|
77
|
+
if (isMainModule) {
|
|
66
78
|
const argv = process.argv.slice(2);
|
|
79
|
+
if (argv.includes("--help")) {
|
|
80
|
+
console.log(USAGE);
|
|
81
|
+
process.exit(0);
|
|
82
|
+
}
|
|
67
83
|
const get = (flag) => {
|
|
68
84
|
const i = argv.indexOf(flag);
|
|
69
85
|
return i >= 0 ? argv[i + 1] : null;
|
|
@@ -72,7 +88,7 @@ if (require.main === module) {
|
|
|
72
88
|
const dest = get("--dest");
|
|
73
89
|
const name = get("--name");
|
|
74
90
|
if (!tarball || !dest || !name) {
|
|
75
|
-
console.error(
|
|
91
|
+
console.error(USAGE);
|
|
76
92
|
process.exit(2);
|
|
77
93
|
}
|
|
78
94
|
try {
|
package/lib/scaffold-crew.js
CHANGED
|
@@ -18,8 +18,8 @@
|
|
|
18
18
|
// source (fail-closed: never stamp an empty 0/0 receipt). Exit 2 on usage
|
|
19
19
|
// errors. No wall-clock reads, no randomness.
|
|
20
20
|
|
|
21
|
-
|
|
22
|
-
|
|
21
|
+
import fs from "node:fs";
|
|
22
|
+
import path from "node:path";
|
|
23
23
|
|
|
24
24
|
function usageError(msg) {
|
|
25
25
|
process.stderr.write("scaffold-crew.js usage: --repo <crewRepoPath> --orch-dir <orchDir>\n" + msg + "\n");
|
|
@@ -27,6 +27,15 @@ function usageError(msg) {
|
|
|
27
27
|
}
|
|
28
28
|
|
|
29
29
|
const args = process.argv.slice(2);
|
|
30
|
+
// --help: before required-arg parsing (shebang⇔CLI contract).
|
|
31
|
+
if (args.includes("--help")) {
|
|
32
|
+
console.log(
|
|
33
|
+
"usage: node scaffold-crew.js --repo <crewRepoPath> --orch-dir <orchDir>\n" +
|
|
34
|
+
"copies the fixed scaffold file sets (no-clobber), verifies every file on disk,\n" +
|
|
35
|
+
"writes <orchDir>/.scaffold-receipt.json and prints {created, skipped}"
|
|
36
|
+
);
|
|
37
|
+
process.exit(0);
|
|
38
|
+
}
|
|
30
39
|
let repo = null;
|
|
31
40
|
let orchDir = null;
|
|
32
41
|
for (let i = 0; i < args.length; i++) {
|
package/lib/see-act.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
1
2
|
// see-act.js — single-step browser driver for experiential QA.
|
|
2
3
|
//
|
|
3
4
|
// The agent closes the OODA loop: run one step, READ the screenshot or aria
|
|
@@ -30,12 +31,14 @@
|
|
|
30
31
|
// Determinism: no wall-clock reads, no randomness. Archive names are sequential.
|
|
31
32
|
//
|
|
32
33
|
// Dependencies: playwright-core (resolved below), a Chromium binary.
|
|
33
|
-
"use strict";
|
|
34
34
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
35
|
+
import { createServer, request as httpRequest } from "node:http";
|
|
36
|
+
import { request as httpsRequest } from "node:https";
|
|
37
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync, copyFileSync } from "node:fs";
|
|
38
|
+
import { join, dirname, resolve } from "node:path";
|
|
39
|
+
import { createRequire } from "node:module";
|
|
40
|
+
|
|
41
|
+
const cjsRequire = createRequire(import.meta.url);
|
|
39
42
|
|
|
40
43
|
const VIEWPORTS = {
|
|
41
44
|
desktop: { width: 1440, height: 900 },
|
|
@@ -73,15 +76,15 @@ function loadPlaywright() {
|
|
|
73
76
|
// playwright-core is a declared dependency (package.json), so a normal
|
|
74
77
|
// npm install resolves it from the package's own node_modules.
|
|
75
78
|
try {
|
|
76
|
-
return
|
|
79
|
+
return cjsRequire("playwright-core");
|
|
77
80
|
} catch (e) { /* fall through */ }
|
|
78
81
|
const envDir = (process.env.PLAYWRIGHT_CORE_DIR || "").trim();
|
|
79
82
|
if (envDir) {
|
|
80
83
|
try {
|
|
81
|
-
return
|
|
84
|
+
return cjsRequire(join(envDir, "playwright-core"));
|
|
82
85
|
} catch (e) { /* fall through */ }
|
|
83
86
|
try {
|
|
84
|
-
return
|
|
87
|
+
return cjsRequire(envDir);
|
|
85
88
|
} catch (e) { /* fall through */ }
|
|
86
89
|
}
|
|
87
90
|
fail(3, {
|
|
@@ -188,6 +191,12 @@ function startProxy() {
|
|
|
188
191
|
}
|
|
189
192
|
|
|
190
193
|
async function main() {
|
|
194
|
+
// --help: before required-arg parsing (shebang⇔CLI contract).
|
|
195
|
+
if (process.argv.slice(2).includes("--help")) {
|
|
196
|
+
console.log("usage: node see-act.js --url <url> [--viewport desktop|mobile] [--timeout-ms <n>] <action> [args]\n" +
|
|
197
|
+
"actions: aria | shot --out <png> [--full] | click --out <png> --selector <css> | scroll --out <png> --y <px|bottom> | type --out <png> --selector <css> --text <text>");
|
|
198
|
+
process.exit(0);
|
|
199
|
+
}
|
|
191
200
|
const args = parseArgs(process.argv.slice(2));
|
|
192
201
|
if (!args.url) {
|
|
193
202
|
fail(2, { ok: false, error: "usage: node see-act.js --url <url> [--viewport desktop|mobile] <aria|shot|click|scroll|type> [args]" });
|
package/lib/serve-artifact.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
1
2
|
// serve-artifact.js — serve a built TS space locally for experiential QA.
|
|
2
3
|
//
|
|
3
4
|
// Serves <space-dir>/client/dist statically and dispatches POST */actions
|
|
@@ -20,13 +21,12 @@
|
|
|
20
21
|
// what the artifact's server needs before starting this.
|
|
21
22
|
//
|
|
22
23
|
// Determinism: no wall-clock reads, no randomness.
|
|
23
|
-
"use strict";
|
|
24
24
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
25
|
+
import { createServer } from "node:http";
|
|
26
|
+
import { join, normalize, dirname, sep } from "node:path";
|
|
27
|
+
import { existsSync } from "node:fs";
|
|
28
|
+
import { readFile, writeFile, mkdir, mkdtemp } from "node:fs/promises";
|
|
29
|
+
import { tmpdir } from "node:os";
|
|
30
30
|
|
|
31
31
|
function parseArgs(argv) {
|
|
32
32
|
const out = { spaceDir: null, port: 0, tag: null };
|
|
@@ -57,6 +57,12 @@ const CONTENT_TYPES = {
|
|
|
57
57
|
};
|
|
58
58
|
|
|
59
59
|
async function main() {
|
|
60
|
+
// --help: before required-arg parsing (shebang⇔CLI contract).
|
|
61
|
+
if (process.argv.slice(2).includes("--help")) {
|
|
62
|
+
console.log("usage: node serve-artifact.js --space-dir <path> [--port <n>]\n" +
|
|
63
|
+
"serves <space-dir>/client/dist statically, dispatches POST */actions to the built server actions");
|
|
64
|
+
process.exit(0);
|
|
65
|
+
}
|
|
60
66
|
const args = parseArgs(process.argv.slice(2));
|
|
61
67
|
if (!args.spaceDir) {
|
|
62
68
|
process.stderr.write("serve-artifact.js: --space-dir <path> is required\n");
|
|
@@ -30,14 +30,25 @@
|
|
|
30
30
|
// Prints JSON: { orchestration: {...}, gitignore: {...} | { declined: true }, commit: {...} }
|
|
31
31
|
// Exits non-zero on any failure (fail closed, loud).
|
|
32
32
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
33
|
+
import fs from "node:fs";
|
|
34
|
+
import path from "node:path";
|
|
35
|
+
import { scaffoldRepoOrchestration } from "./repo-orchestration.js";
|
|
36
|
+
import { ensureGitignoreEntries, describeGitignoreChange } from "./gitignore.js";
|
|
37
|
+
import { commitScaffold, isPathDirty } from "./commit-scaffold.js";
|
|
38
|
+
import { realpathSync } from "node:fs";
|
|
39
|
+
import { fileURLToPath } from "node:url";
|
|
38
40
|
|
|
39
41
|
function main() {
|
|
40
42
|
const argv = process.argv.slice(2);
|
|
43
|
+
// --help: before required-arg parsing (shebang⇔CLI contract).
|
|
44
|
+
if (argv.includes("--help")) {
|
|
45
|
+
console.log(
|
|
46
|
+
"usage: node setup-project-repo.js --repo <path> --crew-repo <path> [--consent]\n" +
|
|
47
|
+
"--consent: user approved the .gitignore change (from the setup conversation)\n" +
|
|
48
|
+
"prints JSON: { orchestration, gitignore, commit }"
|
|
49
|
+
);
|
|
50
|
+
process.exit(0);
|
|
51
|
+
}
|
|
41
52
|
const get = (flag) => {
|
|
42
53
|
const i = argv.indexOf(flag);
|
|
43
54
|
return i >= 0 ? argv[i + 1] : null;
|
|
@@ -91,6 +102,14 @@ function main() {
|
|
|
91
102
|
}
|
|
92
103
|
}
|
|
93
104
|
|
|
94
|
-
|
|
105
|
+
const isMainModule = (() => {
|
|
106
|
+
try {
|
|
107
|
+
return !!process.argv[1] && realpathSync(process.argv[1]) === fileURLToPath(import.meta.url);
|
|
108
|
+
} catch {
|
|
109
|
+
return false;
|
|
110
|
+
}
|
|
111
|
+
})();
|
|
112
|
+
|
|
113
|
+
if (isMainModule) main();
|
|
95
114
|
|
|
96
|
-
|
|
115
|
+
export { main };
|
package/lib/update-watch.js
CHANGED
|
@@ -22,9 +22,11 @@
|
|
|
22
22
|
// every path except a missing --crew-home (exit 2).
|
|
23
23
|
"use strict";
|
|
24
24
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
25
|
+
import fs from "node:fs";
|
|
26
|
+
import path from "node:path";
|
|
27
|
+
import { execFileSync } from "node:child_process";
|
|
28
|
+
import { realpathSync } from "node:fs";
|
|
29
|
+
import { fileURLToPath } from "node:url";
|
|
28
30
|
|
|
29
31
|
const STATE_FILE = ".update-watch.json";
|
|
30
32
|
const LOG_FILE = "update-watch.log";
|
|
@@ -653,6 +655,11 @@ function buildRealDeps(crewHome) {
|
|
|
653
655
|
|
|
654
656
|
function main() {
|
|
655
657
|
const args = process.argv.slice(2);
|
|
658
|
+
// --help: before required-arg parsing (entry-gate contract).
|
|
659
|
+
if (args.includes("--help")) {
|
|
660
|
+
console.log(USAGE);
|
|
661
|
+
process.exit(0);
|
|
662
|
+
}
|
|
656
663
|
let crewHome = null;
|
|
657
664
|
let enrollSha = null;
|
|
658
665
|
let enrollForce = false;
|
|
@@ -693,19 +700,29 @@ function main() {
|
|
|
693
700
|
);
|
|
694
701
|
}
|
|
695
702
|
|
|
696
|
-
|
|
697
|
-
parseSemver
|
|
698
|
-
compareSemver
|
|
699
|
-
channelAllows
|
|
700
|
-
crewDecision
|
|
701
|
-
dashboardDecision
|
|
702
|
-
crewVersionOrdering
|
|
703
|
-
fileCrewUpgrade
|
|
704
|
-
enrollDashboardBase
|
|
705
|
-
readState
|
|
706
|
-
writeState
|
|
707
|
-
appendLog
|
|
708
|
-
run
|
|
703
|
+
export {
|
|
704
|
+
parseSemver,
|
|
705
|
+
compareSemver,
|
|
706
|
+
channelAllows,
|
|
707
|
+
crewDecision,
|
|
708
|
+
dashboardDecision,
|
|
709
|
+
crewVersionOrdering,
|
|
710
|
+
fileCrewUpgrade,
|
|
711
|
+
enrollDashboardBase,
|
|
712
|
+
readState,
|
|
713
|
+
writeState,
|
|
714
|
+
appendLog,
|
|
715
|
+
run
|
|
709
716
|
};
|
|
710
717
|
|
|
711
|
-
|
|
718
|
+
const USAGE = "usage: node update-watch.js --crew-home <path> [--record-dashboard-sha <sha> [--force]]";
|
|
719
|
+
|
|
720
|
+
const isMainModule = (() => {
|
|
721
|
+
try {
|
|
722
|
+
return !!process.argv[1] && realpathSync(process.argv[1]) === fileURLToPath(import.meta.url);
|
|
723
|
+
} catch {
|
|
724
|
+
return false;
|
|
725
|
+
}
|
|
726
|
+
})();
|
|
727
|
+
|
|
728
|
+
if (isMainModule) main();
|
package/lib/ux-doctrine.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
1
2
|
// ux-doctrine.js — UX-surface doctrine page resolution.
|
|
2
3
|
//
|
|
3
4
|
// One canonical doctrine page per environment_type value: the crew reads
|
|
@@ -11,10 +12,16 @@
|
|
|
11
12
|
// against this file, so the two can never silently diverge.
|
|
12
13
|
//
|
|
13
14
|
// Pure and deterministic: no clock, no randomness, no I/O.
|
|
14
|
-
|
|
15
|
+
//
|
|
16
|
+
// Module shape (blocker 21): this file is an import-safe shared module —
|
|
17
|
+
// it has NO shebang, so the entry gate bare-loads it (`node ux-doctrine.js`
|
|
18
|
+
// exits 0). The CLI below runs only when the file itself is the process
|
|
19
|
+
// entrypoint.
|
|
15
20
|
|
|
16
|
-
|
|
17
|
-
|
|
21
|
+
import { existsSync } from "node:fs";
|
|
22
|
+
import { join } from "node:path";
|
|
23
|
+
import { realpathSync } from "node:fs";
|
|
24
|
+
import { fileURLToPath } from "node:url";
|
|
18
25
|
|
|
19
26
|
// THE MAP. environment_type value -> doctrine page filename, relative to
|
|
20
27
|
// the release docs dir ($CREW_HOME/current/docs/). null is unclassified —
|
|
@@ -44,12 +51,30 @@ function doctrinePageExists(crewHome, environmentType) {
|
|
|
44
51
|
return p ? existsSync(p) : false;
|
|
45
52
|
}
|
|
46
53
|
|
|
47
|
-
|
|
54
|
+
export { DOCTRINE_PAGES, doctrinePage, doctrinePath, doctrinePageExists };
|
|
55
|
+
|
|
56
|
+
// realpath: production spawns through $CREW_HOME/current/lib (a symlink),
|
|
57
|
+
// so argv[1] must be resolved before comparison with import.meta.url.
|
|
58
|
+
const isMainModule = (() => {
|
|
59
|
+
try {
|
|
60
|
+
return !!process.argv[1] && realpathSync(process.argv[1]) === fileURLToPath(import.meta.url);
|
|
61
|
+
} catch {
|
|
62
|
+
return false;
|
|
63
|
+
}
|
|
64
|
+
})();
|
|
48
65
|
|
|
49
|
-
|
|
66
|
+
const USAGE = "usage: node ux-doctrine.js --page <environment_type|null> | --path <crewHome> <environment_type|null>";
|
|
67
|
+
|
|
68
|
+
if (isMainModule) {
|
|
50
69
|
// CLI: node ux-doctrine.js --page <environment_type>
|
|
51
70
|
// node ux-doctrine.js --path <crewHome> <environment_type>
|
|
52
71
|
const args = process.argv.slice(2);
|
|
72
|
+
// Import-safe module contract: a bare invocation (no args) is a load
|
|
73
|
+
// check, not a usage error — it exits 0. --help always exits 0.
|
|
74
|
+
if (args.length === 0 || args.includes("--help")) {
|
|
75
|
+
console.log(USAGE);
|
|
76
|
+
process.exit(0);
|
|
77
|
+
}
|
|
53
78
|
if (args[0] === "--page" && args.length === 2) {
|
|
54
79
|
const page = doctrinePage(args[1] === "null" ? null : args[1]);
|
|
55
80
|
console.log(page === null ? "null" : page);
|
|
@@ -57,7 +82,7 @@ if (require.main === module) {
|
|
|
57
82
|
const p = doctrinePath(args[1], args[2] === "null" ? null : args[2]);
|
|
58
83
|
console.log(p === null ? "null" : p);
|
|
59
84
|
} else {
|
|
60
|
-
console.error(
|
|
85
|
+
console.error(USAGE);
|
|
61
86
|
process.exit(2);
|
|
62
87
|
}
|
|
63
88
|
}
|
package/lib/verify-publish.js
CHANGED
|
@@ -51,6 +51,19 @@ import {
|
|
|
51
51
|
makeOldCounter,
|
|
52
52
|
discriminatingLines,
|
|
53
53
|
} from "./publish-content.js";
|
|
54
|
+
// Terminal-note vocabulary (D7, 2026-09-19): the writer-side guard.
|
|
55
|
+
// terminal() asserts its emitted verb is a registry member before writing,
|
|
56
|
+
// so no future terminal verb ships unrecognized by the scans.
|
|
57
|
+
import { matchTerminalPublishNote } from "./publish-note-vocabulary.js";
|
|
58
|
+
|
|
59
|
+
// --help: before required-arg parsing (shebang⇔CLI contract).
|
|
60
|
+
if (process.argv.slice(2).includes("--help")) {
|
|
61
|
+
console.log(
|
|
62
|
+
"usage: node verify-publish.js --repo-path <path> --commit <sha> --base <sha> --task-id <uuid> --slug <artifact-slug> --project-id <id> [--spaces-root <dir>]\n" +
|
|
63
|
+
"the mechanical publish verification JUDGE: certifies read-back findings against git diff, checks manifest freshness, stamps provenance"
|
|
64
|
+
);
|
|
65
|
+
process.exit(0);
|
|
66
|
+
}
|
|
54
67
|
|
|
55
68
|
function arg(name, required = true) {
|
|
56
69
|
const i = process.argv.indexOf(name);
|
|
@@ -89,10 +102,25 @@ function api(command, json) {
|
|
|
89
102
|
});
|
|
90
103
|
return JSON.parse(out);
|
|
91
104
|
}
|
|
105
|
+
// The terminal verdict this writer emits. A single const (not an inline
|
|
106
|
+
// literal) so the emitted verb and the asserted verb can never drift apart.
|
|
107
|
+
const TERMINAL_VERB = "publish: verification-failed";
|
|
92
108
|
function terminal(reason, detail) {
|
|
93
109
|
// Always lands a terminal parent verdict, then exits 1 (failed) — the
|
|
94
110
|
// task stays parked for human attention. Only the verified path exits 0.
|
|
95
|
-
|
|
111
|
+
// Writer-side registry guard (D7): the verb is in the terminal-note
|
|
112
|
+
// registry before the note is written — a writer can never introduce a
|
|
113
|
+
// terminal verb the scans don't recognize.
|
|
114
|
+
if (!matchTerminalPublishNote(TERMINAL_VERB)) {
|
|
115
|
+
// Registry and writer disagree — fail loud, never emit an unrecognized
|
|
116
|
+
// terminal note.
|
|
117
|
+
process.stderr.write(JSON.stringify({
|
|
118
|
+
ok: false, verdict: "vocabulary-mismatch",
|
|
119
|
+
message: `terminal verb '${TERMINAL_VERB}' is not in the terminal-note registry`,
|
|
120
|
+
}) + "\n");
|
|
121
|
+
process.exit(2);
|
|
122
|
+
}
|
|
123
|
+
const msg = `${TERMINAL_VERB} ${commit} ${reason}${detail ? ` — ${detail}` : ""} (read-back ${inspectionId})`;
|
|
96
124
|
api("log-event", { task_id: taskId, type: "note", message: msg.slice(0, 1000) });
|
|
97
125
|
process.stderr.write(JSON.stringify({ ok: false, verdict: reason, message: msg }) + "\n");
|
|
98
126
|
process.exit(1);
|
|
@@ -301,6 +329,14 @@ if (head !== commit) {
|
|
|
301
329
|
// submitted entry (the trigger issuance; a receipt-bearing second "submitted"
|
|
302
330
|
// may follow it — the oldest binds the trigger instant, the latest carries
|
|
303
331
|
// the observed agent_id).
|
|
332
|
+
// D1 (2026-09-19; entry_kind cut 2026-09-20): the trigger instant is the
|
|
333
|
+
// ISSUANCE instant, not the ledger-write instant — a slow ledger write must
|
|
334
|
+
// not make a fresh build look stale (room #23 J1). The anchor is the OLDEST
|
|
335
|
+
// submitted entry by ts and the bound instant is its issued_at (falling back
|
|
336
|
+
// to its ts when the capture was unobserved). One anchor, one field — the
|
|
337
|
+
// retired entry_kind preference filter could never change the bound anchor
|
|
338
|
+
// (issuance and receipt entries carried the same captured issued_at), so the
|
|
339
|
+
// oldest-by-ts selection is the whole mechanism.
|
|
304
340
|
let triggerTs = null;
|
|
305
341
|
let manifestBefore = null;
|
|
306
342
|
try {
|
|
@@ -316,7 +352,7 @@ try {
|
|
|
316
352
|
if (candidates.length > 0) {
|
|
317
353
|
candidates.sort((a, b) => (a.ts || "").localeCompare(b.ts || ""));
|
|
318
354
|
const e = candidates[0];
|
|
319
|
-
triggerTs = e.ts || null;
|
|
355
|
+
triggerTs = e.issued_at || e.ts || null;
|
|
320
356
|
if (e.manifest_before && typeof e.manifest_before === "object") manifestBefore = e.manifest_before;
|
|
321
357
|
}
|
|
322
358
|
} catch (e) {
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
1
2
|
// write-ooda-verdict.js — deterministic writer for the OODA report's terminal state.
|
|
2
3
|
//
|
|
3
4
|
// After the see-act loop, the QA/repro agent records its verdict as
|
|
@@ -34,10 +35,9 @@
|
|
|
34
35
|
//
|
|
35
36
|
// Exit 0 on success, 2 on bad input. Determinism: no wall-clock reads, no
|
|
36
37
|
// randomness; ts comes only from --ts and is omitted when not passed.
|
|
37
|
-
"use strict";
|
|
38
38
|
|
|
39
|
-
|
|
40
|
-
|
|
39
|
+
import { writeFileSync, appendFileSync, mkdirSync, readFileSync, existsSync } from "node:fs";
|
|
40
|
+
import { join, resolve } from "node:path";
|
|
41
41
|
|
|
42
42
|
const VERDICTS = { PASS: 1, FAIL: 1, NOT_POSSIBLE: 1 };
|
|
43
43
|
|
|
@@ -85,6 +85,15 @@ function ledgerSeq(ledgerPath) {
|
|
|
85
85
|
}
|
|
86
86
|
|
|
87
87
|
function main() {
|
|
88
|
+
// --help: before required-arg parsing (shebang⇔CLI contract).
|
|
89
|
+
if (process.argv.slice(2).includes("--help")) {
|
|
90
|
+
console.log(
|
|
91
|
+
"usage: node write-ooda-verdict.js --dir <phase-dir> --attempt <id> --verdict <PASS|FAIL|NOT_POSSIBLE>\n" +
|
|
92
|
+
" [--summary <text>] [--expected <text>] [--actual <text>] [--missing <json-array>] [--reason <text>] [--ts <iso>]\n" +
|
|
93
|
+
"--reason is REQUIRED and must be non-empty for FAIL and NOT_POSSIBLE"
|
|
94
|
+
);
|
|
95
|
+
process.exit(0);
|
|
96
|
+
}
|
|
88
97
|
const args = parseArgs(process.argv.slice(2));
|
|
89
98
|
if (!args.dir) fail("missing --dir <phase-dir>");
|
|
90
99
|
if (args.attempt === undefined || String(args.attempt).trim() === "") {
|
package/package.json
CHANGED