muse-crew 0.14.3 → 0.14.5
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 +2 -2
- package/docs/decisions/AGENTS.md +1 -0
- package/docs/decisions/publish-path.md +96 -3
- package/docs/guide.md +2 -2
- package/docs/publish-unknown-recovery.md +10 -3
- package/docs/publish-verification.md +126 -62
- package/docs/release-integrity.md +62 -0
- package/docs/reviews/critic-0144.md +83 -0
- package/docs/reviews/critic-0145.md +106 -0
- package/lib/AGENTS.md +12 -4
- 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/check-intent-freshness.js +101 -0
- 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 +41 -11
- package/lib/crew-api.js +478 -26
- 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 +64 -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 +116 -72
- 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 +44 -6
- package/lib/write-ooda-verdict.js +12 -3
- package/package.json +1 -1
- package/seed/cron-body-template.md +50 -16
- package/workflows/bugfix.js +110 -643
- package/workflows/chore.js +110 -643
- package/workflows/crew-dispatch.js +36 -0
- package/workflows/standard.js +118 -633
- package/workflows/upgrade.js +4 -2
package/lib/retry-publish.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
// retry-publish.js — Deterministic unknown-recovery retry protocol (2026-09-18, should-fix 5
|
|
2
|
+
// retry-publish.js — Deterministic unknown-recovery retry protocol (2026-09-18, should-fix 5;
|
|
3
|
+
// ported 2026-09-20: node:sqlite + crew-state.db (blocker 23) and direct
|
|
4
|
+
// tick-worker issuance (blocker 22 — children cannot reach artifact_edit).
|
|
3
5
|
//
|
|
4
6
|
// The cron Step 4.4 retry protocol was prose-owned: the cron agent performed
|
|
5
7
|
// lock handling, journaling, diff generation, ledger writes, mirroring, and
|
|
@@ -8,19 +10,20 @@
|
|
|
8
10
|
//
|
|
9
11
|
// This command owns all deterministic transitions with try/finally lock
|
|
10
12
|
// release. Only the unavoidable `artifact_edit` tool call remains
|
|
11
|
-
//
|
|
12
|
-
// cannot load)
|
|
13
|
+
// tick-mediated (it requires the artifact namespace, which this process
|
|
14
|
+
// cannot load): the session-carrying tick worker issues the edit directly
|
|
15
|
+
// in its own turn — NEVER a child (blocker 22, room #24).
|
|
13
16
|
//
|
|
14
17
|
// Two phases:
|
|
15
18
|
// Phase 1 (prepare): acquire merge lock, re-check HEAD, re-classify,
|
|
16
19
|
// journal intent, generate diff. Outputs JSON with the diff path/sha
|
|
17
|
-
// for the
|
|
18
|
-
// (the
|
|
20
|
+
// for the tick worker's direct artifact_edit call. The lock is HELD on
|
|
21
|
+
// success (the tick must call phase 2 to release it).
|
|
19
22
|
// Phase 2 (complete): given the artifact_edit outcome (accepted/refused),
|
|
20
23
|
// write the outcome notes, append the ledger entry, mirror verification,
|
|
21
24
|
// and RELEASE the lock in a finally block.
|
|
22
25
|
//
|
|
23
|
-
// Crash safety: if the
|
|
26
|
+
// Crash safety: if the tick dies between phases, the lock expires after
|
|
24
27
|
// 600s and the note journal (`publish: retry-intended` without
|
|
25
28
|
// `publish: retry-issued`) lets the next scan mirror verification-requested
|
|
26
29
|
// without re-triggering — exactly one retry, enforced from notes.
|
|
@@ -31,7 +34,7 @@
|
|
|
31
34
|
//
|
|
32
35
|
// Exit codes: 0 = success, 1 = terminal stop (no retry), 2 = usage error.
|
|
33
36
|
|
|
34
|
-
import { readFileSync, writeFileSync, appendFileSync, existsSync } from "fs";
|
|
37
|
+
import { readFileSync, writeFileSync, appendFileSync, existsSync, mkdirSync, unlinkSync } from "fs";
|
|
35
38
|
import { join } from "path";
|
|
36
39
|
import { execFileSync } from "child_process";
|
|
37
40
|
import { randomUUID as uuid } from "crypto";
|
|
@@ -50,6 +53,19 @@ function fail(code, msg, exitCode = 1) {
|
|
|
50
53
|
process.exit(exitCode);
|
|
51
54
|
}
|
|
52
55
|
|
|
56
|
+
// --help: handled before any required-arg parsing so
|
|
57
|
+
// `node retry-publish.js --help` always exits 0 (entry-gate contract).
|
|
58
|
+
if (args.includes("--help")) {
|
|
59
|
+
console.log(
|
|
60
|
+
"usage: node retry-publish.js --crew-home <path> --task-id <uuid> --phase <prepare|complete>\n" +
|
|
61
|
+
" prepare: acquire the merge lock, re-check HEAD, re-classify, journal intent, emit the diff\n" +
|
|
62
|
+
" complete: --edit-outcome <accepted|refused> [--refusal-text <text>] — write the outcome,\n" +
|
|
63
|
+
" append the ledger entry, mirror verification, and release the lock\n" +
|
|
64
|
+
"exit 0 = success, 1 = terminal stop, 2 = usage error"
|
|
65
|
+
);
|
|
66
|
+
process.exit(0);
|
|
67
|
+
}
|
|
68
|
+
|
|
53
69
|
const crewHome = arg("--crew-home");
|
|
54
70
|
const taskId = arg("--task-id");
|
|
55
71
|
const phase = arg("--phase");
|
|
@@ -59,6 +75,18 @@ if (!["prepare", "complete"].includes(phase)) {
|
|
|
59
75
|
process.exit(2);
|
|
60
76
|
}
|
|
61
77
|
|
|
78
|
+
// Retry protocol: phase-1 state is a persistent-disk pin
|
|
79
|
+
// ($CREW_HOME/.pins/ — never /tmp: tmpfs is wiped on cell reboot, and the
|
|
80
|
+
// lock release depends on phase 2 reading this state).
|
|
81
|
+
function pinsDir() {
|
|
82
|
+
const dir = join(crewHome, ".pins");
|
|
83
|
+
mkdirSync(dir, { recursive: true });
|
|
84
|
+
return dir;
|
|
85
|
+
}
|
|
86
|
+
function retryStatePath() {
|
|
87
|
+
return join(pinsDir(), `retry-publish-${taskId}.json`);
|
|
88
|
+
}
|
|
89
|
+
|
|
62
90
|
// --- Shared helpers ---------------------------------------------------------
|
|
63
91
|
|
|
64
92
|
function nowISO() { return new Date().toISOString(); }
|
|
@@ -70,25 +98,24 @@ function writePublishNote(db, taskId, message) {
|
|
|
70
98
|
).run(uuid(), taskId, message, nowISO());
|
|
71
99
|
}
|
|
72
100
|
|
|
73
|
-
// Load the crew DB
|
|
74
|
-
//
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
} catch {
|
|
79
|
-
fail("db-unavailable", "better-sqlite3 is not available to retry-publish.js");
|
|
80
|
-
}
|
|
81
|
-
const dbPath = join(crewHome, "crew.db");
|
|
101
|
+
// Load the crew DB via node:sqlite (the runtime's built-in — 2026-09-20,
|
|
102
|
+
// blocker 23: better-sqlite3 is not available and the old code pointed at
|
|
103
|
+
// the wrong database name). crew-api.js opens the same path the same way.
|
|
104
|
+
import { DatabaseSync } from "node:sqlite";
|
|
105
|
+
const dbPath = join(crewHome, "crew-state.db");
|
|
82
106
|
if (!existsSync(dbPath)) fail("no-db", `crew database not found at ${dbPath}`);
|
|
83
|
-
const db = new
|
|
107
|
+
const db = new DatabaseSync(dbPath);
|
|
84
108
|
|
|
85
109
|
const EMPTY_TREE = "4b825dc642cb6eb9a060e54bf8d69288fbee4904";
|
|
86
110
|
|
|
87
|
-
|
|
111
|
+
// The merge lock is per-repo (merge-lock.sh fails closed without
|
|
112
|
+
// CREW_REPO — 2026-09-20: the old code never set it, so the lock could
|
|
113
|
+
// never be acquired). The retry protocol locks the task's own repo.
|
|
114
|
+
function runMergeLock(action, repoPath) {
|
|
88
115
|
const lockScript = join(crewHome, "lib", "merge-lock.sh");
|
|
89
116
|
try {
|
|
90
117
|
execFileSync(lockScript, [action, taskId], {
|
|
91
|
-
env: { ...process.env, CREW_HOME: crewHome },
|
|
118
|
+
env: { ...process.env, CREW_HOME: crewHome, CREW_REPO: repoPath },
|
|
92
119
|
encoding: "utf8",
|
|
93
120
|
timeout: 30000,
|
|
94
121
|
});
|
|
@@ -101,7 +128,7 @@ function runMergeLock(action) {
|
|
|
101
128
|
// --- Phase 1: prepare --------------------------------------------------------
|
|
102
129
|
// Acquires the lock, re-checks HEAD, re-classifies, journals intent,
|
|
103
130
|
// generates the diff. On success the lock is HELD and the output carries
|
|
104
|
-
// the diff path/sha for the
|
|
131
|
+
// the diff path/sha for the tick worker's direct artifact_edit call.
|
|
105
132
|
|
|
106
133
|
if (phase === "prepare") {
|
|
107
134
|
// Derive the unknown attempt (same derivation as scan-publish-unknown).
|
|
@@ -113,6 +140,7 @@ if (phase === "prepare") {
|
|
|
113
140
|
const slug = project && project.deploy_slug;
|
|
114
141
|
if (!slug) fail("no-slug", "project has no deploy_slug");
|
|
115
142
|
const repoPath = (project && project.repo_path) || null;
|
|
143
|
+
if (!repoPath) fail("no-repo", "project has no repo_path — the retry protocol locks the task's repo");
|
|
116
144
|
|
|
117
145
|
const ledgerPath = join(crewHome, ".publish-ledger", slug + ".jsonl");
|
|
118
146
|
let entries = [];
|
|
@@ -149,7 +177,7 @@ if (phase === "prepare") {
|
|
|
149
177
|
}
|
|
150
178
|
|
|
151
179
|
// 2. Acquire the merge lock.
|
|
152
|
-
const lock = runMergeLock("acquire");
|
|
180
|
+
const lock = runMergeLock("acquire", repoPath);
|
|
153
181
|
if (!lock.ok) {
|
|
154
182
|
process.stdout.write(JSON.stringify({
|
|
155
183
|
ok: true, phase: "prepare", outcome: "lock-held",
|
|
@@ -163,7 +191,7 @@ if (phase === "prepare") {
|
|
|
163
191
|
let lockHeld = true;
|
|
164
192
|
const releaseLock = () => {
|
|
165
193
|
if (lockHeld) {
|
|
166
|
-
runMergeLock("release");
|
|
194
|
+
runMergeLock("release", repoPath);
|
|
167
195
|
lockHeld = false;
|
|
168
196
|
}
|
|
169
197
|
};
|
|
@@ -180,12 +208,21 @@ if (phase === "prepare") {
|
|
|
180
208
|
}
|
|
181
209
|
|
|
182
210
|
const classifierPath = join(crewHome, "lib", "classify-publish-absence.js");
|
|
183
|
-
// Find the trigger entry for the manifest baseline: the OLDEST
|
|
184
|
-
// "submitted"
|
|
211
|
+
// Find the trigger entry for the manifest baseline: the OLDEST
|
|
212
|
+
// tick-worker "submitted" entry by ts (D1, 2026-09-19; entry_kind cut
|
|
213
|
+
// 2026-09-20; issuer anchor 2026-09-20, one-party worker-owned publish —
|
|
214
|
+
// only the session-carrying tick worker performs issuance, so only its
|
|
215
|
+
// stamped writes bind; a workflow-written "submitted" without issuer is
|
|
216
|
+
// a phantom claim, never a baseline source) — it binds issued_at
|
|
217
|
+
// (falling back to its ts when the capture was unobserved), the same
|
|
218
|
+
// anchor verify-publish.js and findTriggerEntry use.
|
|
185
219
|
// agent_id is null on the issuance entry (no receipt observed at trigger
|
|
186
220
|
// time) — the baseline comes from manifest_before, never agent_id.
|
|
187
|
-
const
|
|
188
|
-
e.outcome === "submitted" && e.
|
|
221
|
+
const submitted = entries.filter((e) =>
|
|
222
|
+
e.outcome === "submitted" && e.issuer === "tick-worker" &&
|
|
223
|
+
e.commit === commit && (attempt == null || e.attempt === attempt));
|
|
224
|
+
submitted.sort((a, b) => (a.ts || "").localeCompare(b.ts || ""));
|
|
225
|
+
const trigger = submitted[0] || null;
|
|
189
226
|
const manifestBefore = trigger && trigger.manifest_before ? JSON.stringify(trigger.manifest_before) : null;
|
|
190
227
|
|
|
191
228
|
// Find the park note timestamp for quiesce calculation.
|
|
@@ -204,7 +241,7 @@ if (phase === "prepare") {
|
|
|
204
241
|
"--slug", slug,
|
|
205
242
|
"--commit", commit,
|
|
206
243
|
"--base", base,
|
|
207
|
-
"--trigger-ts", trigger ? trigger.ts : parkNote.timestamp,
|
|
244
|
+
"--trigger-ts", trigger ? (trigger.issued_at || trigger.ts) : parkNote.timestamp,
|
|
208
245
|
"--park-ts", parkNote.timestamp,
|
|
209
246
|
"--repo-path", repoPath,
|
|
210
247
|
];
|
|
@@ -265,11 +302,11 @@ if (phase === "prepare") {
|
|
|
265
302
|
fail("diff-not-ok", `compute-publish-diff.js returned ok:false — intent note stands; next tick mirrors verification-requested`);
|
|
266
303
|
}
|
|
267
304
|
|
|
268
|
-
// Success: lock remains HELD. The
|
|
269
|
-
// with the diff, then calls phase 2 (complete) to finish.
|
|
305
|
+
// Success: lock remains HELD. The tick worker issues the edit directly
|
|
306
|
+
// in its own turn with the diff, then calls phase 2 (complete) to finish.
|
|
270
307
|
// Persist the phase-1 state for phase 2 (crash recovery reads the notes,
|
|
271
308
|
// but phase 2 needs the exact commit/attempt/baseline/ledger path).
|
|
272
|
-
const statePath =
|
|
309
|
+
const statePath = retryStatePath();
|
|
273
310
|
writeFileSync(statePath, JSON.stringify({
|
|
274
311
|
task_id: taskId, commit, attempt, slug, repoPath,
|
|
275
312
|
project_id: project.id, ledger_path: ledgerPath,
|
|
@@ -283,7 +320,7 @@ if (phase === "prepare") {
|
|
|
283
320
|
task_id: taskId, commit, attempt,
|
|
284
321
|
diff_path: diffPath, diff_sha256: diffMeta.sha256,
|
|
285
322
|
slug,
|
|
286
|
-
message: "lock held;
|
|
323
|
+
message: "lock held; issue the artifact_edit directly in this tick (the session-carrying tick worker owns issuance), then call phase 2 (complete)",
|
|
287
324
|
}) + "\n");
|
|
288
325
|
process.exit(0);
|
|
289
326
|
} catch (e) {
|
|
@@ -304,17 +341,21 @@ if (phase === "complete") {
|
|
|
304
341
|
}
|
|
305
342
|
const refusalText = arg("--refusal-text", false, "");
|
|
306
343
|
|
|
307
|
-
const statePath =
|
|
344
|
+
const statePath = retryStatePath();
|
|
308
345
|
if (!existsSync(statePath)) {
|
|
309
346
|
fail("no-prepare-state", `no phase-1 state at ${statePath} — the prepare phase did not complete or the state was cleaned`);
|
|
310
347
|
}
|
|
311
348
|
const state = JSON.parse(readFileSync(statePath, "utf8"));
|
|
312
|
-
const { commit, attempt, slug, ledger_path: ledgerPath, retry_baseline: retryBaseline } = state;
|
|
349
|
+
const { commit, attempt, slug, repoPath, ledger_path: ledgerPath, retry_baseline: retryBaseline } = state;
|
|
350
|
+
// (2026-09-20, one-party publish) The phase-1 pin carries repoPath; the
|
|
351
|
+
// merge lock is repo-scoped, so a missing value is a hard failure — the
|
|
352
|
+
// finally's release must never run against an undefined repo.
|
|
353
|
+
if (!repoPath) fail("no-repo-in-state", "phase-1 state has no repoPath — cannot release the merge lock safely; the lock lease expires on its own");
|
|
313
354
|
|
|
314
355
|
let lockHeld = true;
|
|
315
356
|
const releaseLock = () => {
|
|
316
357
|
if (lockHeld) {
|
|
317
|
-
runMergeLock("release");
|
|
358
|
+
runMergeLock("release", repoPath);
|
|
318
359
|
lockHeld = false;
|
|
319
360
|
}
|
|
320
361
|
};
|
|
@@ -325,49 +366,52 @@ if (phase === "complete") {
|
|
|
325
366
|
writePublishNote(db, taskId,
|
|
326
367
|
`publish: retry-refused ${commit} — the platform refused the re-issued edit (${refusalText.split("\n")[0].slice(0, 200)}); not retrying again.`);
|
|
327
368
|
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
|
-
|
|
369
|
+
} else {
|
|
370
|
+
// The tick worker's direct call ended with no explicit refusal —
|
|
371
|
+
// fire-and-forget, same shape as the first attempt.
|
|
372
|
+
writePublishNote(db, taskId,
|
|
373
|
+
`publish: retry-issued ${commit} — the dropped edit was re-issued once; no explicit refusal was observed.`);
|
|
374
|
+
|
|
375
|
+
// Mirror verification with a settle window.
|
|
376
|
+
const settleMs = Number(process.env.CREW_RETRY_SETTLE_MS) > 0
|
|
377
|
+
? Number(process.env.CREW_RETRY_SETTLE_MS)
|
|
378
|
+
: 20 * 60 * 1000;
|
|
379
|
+
const notBefore = new Date(Date.now() + settleMs).toISOString();
|
|
380
|
+
const retryAttempt = attempt ? `${attempt}-retry1` : "retry1";
|
|
381
|
+
writePublishNote(db, taskId,
|
|
382
|
+
`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.`);
|
|
383
|
+
|
|
384
|
+
// Append the -retry1 ledger entry with the pre-retry baseline (required).
|
|
385
|
+
const ledgerEntry = {
|
|
386
|
+
ts: nowISO(),
|
|
387
|
+
task_id: taskId,
|
|
388
|
+
workflow: "unknown-recovery",
|
|
389
|
+
slug,
|
|
390
|
+
commit,
|
|
391
|
+
attempt: retryAttempt,
|
|
392
|
+
agent_id: null,
|
|
393
|
+
applied_report: null,
|
|
394
|
+
outcome: "submitted",
|
|
395
|
+
issuer: "tick-worker",
|
|
396
|
+
manifest_before: retryBaseline,
|
|
397
|
+
detail: "retry trigger re-issued after provably-dropped classification",
|
|
398
|
+
};
|
|
399
|
+
try {
|
|
400
|
+
appendFileSync(ledgerPath, JSON.stringify(ledgerEntry) + "\n");
|
|
401
|
+
} catch (e) {
|
|
402
|
+
// Best-effort: the note journal is the source of truth, not the ledger.
|
|
403
|
+
console.error(`ledger append failed (non-fatal): ${e.message}`);
|
|
404
|
+
}
|
|
341
405
|
|
|
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}`);
|
|
406
|
+
process.stdout.write(JSON.stringify({
|
|
407
|
+
ok: true, phase: "complete", outcome: "issued",
|
|
408
|
+
commit, attempt: retryAttempt, not_before: notBefore,
|
|
409
|
+
}) + "\n");
|
|
361
410
|
}
|
|
362
|
-
|
|
363
|
-
process.stdout.write(JSON.stringify({
|
|
364
|
-
ok: true, phase: "complete", outcome: "issued",
|
|
365
|
-
commit, attempt: retryAttempt, not_before: notBefore,
|
|
366
|
-
}) + "\n");
|
|
367
411
|
} finally {
|
|
368
412
|
releaseLock();
|
|
369
413
|
// Clean up the phase-1 state file (best-effort).
|
|
370
|
-
try {
|
|
414
|
+
try { unlinkSync(statePath); } catch {}
|
|
371
415
|
}
|
|
372
416
|
process.exit(0);
|
|
373
417
|
}
|
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();
|