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.
Files changed (46) hide show
  1. package/AGENTS.md +2 -2
  2. package/docs/decisions/AGENTS.md +1 -0
  3. package/docs/decisions/publish-path.md +96 -3
  4. package/docs/guide.md +2 -2
  5. package/docs/publish-unknown-recovery.md +10 -3
  6. package/docs/publish-verification.md +126 -62
  7. package/docs/release-integrity.md +62 -0
  8. package/docs/reviews/critic-0144.md +83 -0
  9. package/docs/reviews/critic-0145.md +106 -0
  10. package/lib/AGENTS.md +12 -4
  11. package/lib/advance-publish-base.js +8 -0
  12. package/lib/append-ooda-step.js +12 -3
  13. package/lib/build-readback-request.js +10 -0
  14. package/lib/build-registry.js +8 -3
  15. package/lib/check-intent-freshness.js +101 -0
  16. package/lib/classify-publish-absence.js +11 -0
  17. package/lib/classify-surface.js +12 -2
  18. package/lib/commit-scaffold.js +22 -6
  19. package/lib/compose-evidence-caption.js +15 -4
  20. package/lib/compute-publish-diff.js +41 -11
  21. package/lib/crew-api.js +478 -26
  22. package/lib/crew-release.sh +233 -1
  23. package/lib/gitignore.js +23 -5
  24. package/lib/package.json +1 -0
  25. package/lib/publish-note-vocabulary.js +64 -0
  26. package/lib/read-ooda-verdict.js +11 -3
  27. package/lib/readback-disk.js +9 -0
  28. package/lib/render-html.js +17 -7
  29. package/lib/repo-orchestration.js +21 -5
  30. package/lib/retry-publish.js +116 -72
  31. package/lib/sample-project.js +22 -6
  32. package/lib/scaffold-crew.js +11 -2
  33. package/lib/see-act.js +17 -8
  34. package/lib/serve-artifact.js +12 -6
  35. package/lib/setup-project-repo.js +26 -7
  36. package/lib/update-watch.js +34 -17
  37. package/lib/ux-doctrine.js +31 -6
  38. package/lib/verify-publish.js +44 -6
  39. package/lib/write-ooda-verdict.js +12 -3
  40. package/package.json +1 -1
  41. package/seed/cron-body-template.md +50 -16
  42. package/workflows/bugfix.js +110 -643
  43. package/workflows/chore.js +110 -643
  44. package/workflows/crew-dispatch.js +36 -0
  45. package/workflows/standard.js +118 -633
  46. package/workflows/upgrade.js +4 -2
@@ -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
- // agent-mediated (it requires the artifact namespace, which this process
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 agent to feed to artifact_edit. The lock is HELD on success
18
- // (the agent must call phase 2 to release it).
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 agent dies between phases, the lock expires after
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. We use better-sqlite3 via the crew-api's own dependency
74
- // resolution — fall back to a clear error if unavailable.
75
- let Database;
76
- try {
77
- Database = (await import("better-sqlite3")).default;
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 Database(dbPath);
107
+ const db = new DatabaseSync(dbPath);
84
108
 
85
109
  const EMPTY_TREE = "4b825dc642cb6eb9a060e54bf8d69288fbee4904";
86
110
 
87
- function runMergeLock(action) {
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 agent's artifact_edit child.
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 matching
184
- // "submitted" (the trigger issuance), matching verify-publish.js's anchor.
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 trigger = entries.find((e) =>
188
- e.outcome === "submitted" && e.commit === commit && (attempt == null || e.attempt === attempt));
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 agent spawns the artifact_edit child
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 = `/tmp/retry-publish-${taskId}.json`;
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; spawn the artifact_edit child with the diff, then call phase 2 (complete)",
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 = `/tmp/retry-publish-${taskId}.json`;
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
- return;
329
- }
330
-
331
- // The child ended after the edit call with no explicit refusal —
332
- // fire-and-forget, same shape as the first attempt.
333
- writePublishNote(db, taskId,
334
- `publish: retry-issued ${commit} — the dropped edit was re-issued once; no explicit refusal was observed.`);
335
-
336
- // Mirror verification with a settle window.
337
- const notBefore = new Date(Date.now() + 20 * 60 * 1000).toISOString();
338
- const retryAttempt = attempt ? `${attempt}-retry1` : "retry1";
339
- writePublishNote(db, taskId,
340
- `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.`);
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
- // Append the -retry1 ledger entry with the pre-retry baseline (required).
343
- const ledgerEntry = {
344
- ts: nowISO(),
345
- task_id: taskId,
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 { (await import("fs")).unlinkSync(statePath); } catch {}
414
+ try { unlinkSync(statePath); } catch {}
371
415
  }
372
416
  process.exit(0);
373
417
  }
@@ -15,9 +15,11 @@
15
15
  // --name: directory/project name (e.g. "flashcards")
16
16
  // Prints JSON: { path, committed, commitSha }
17
17
 
18
- const fs = require("fs");
19
- const path = require("path");
20
- const { execFileSync } = require("child_process");
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
- module.exports = { setupSampleProject };
65
+ export { setupSampleProject };
64
66
 
65
- if (require.main === module) {
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("Usage: node lib/sample-project.js --tarball <path> --dest <dir> --name <project-name>");
91
+ console.error(USAGE);
76
92
  process.exit(2);
77
93
  }
78
94
  try {
@@ -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
- const fs = require("node:fs");
22
- const path = require("node:path");
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
- const { createServer, request: httpRequest } = require("node:http");
36
- const { request: httpsRequest } = require("node:https");
37
- const { existsSync, mkdirSync, readFileSync, writeFileSync, copyFileSync } = require("node:fs");
38
- const { join, dirname, resolve } = require("node:path");
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 require("playwright-core");
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 require(join(envDir, "playwright-core"));
84
+ return cjsRequire(join(envDir, "playwright-core"));
82
85
  } catch (e) { /* fall through */ }
83
86
  try {
84
- return require(envDir);
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]" });
@@ -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
- const { createServer } = require("node:http");
26
- const { join, normalize, dirname, sep } = require("node:path");
27
- const { existsSync } = require("node:fs");
28
- const { readFile, writeFile, mkdir, mkdtemp } = require("node:fs/promises");
29
- const { tmpdir } = require("node:os");
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
- const fs = require("fs");
34
- const path = require("path");
35
- const { scaffoldRepoOrchestration } = require("./repo-orchestration");
36
- const { ensureGitignoreEntries, describeGitignoreChange } = require("./gitignore");
37
- const { commitScaffold, isPathDirty } = require("./commit-scaffold");
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
- if (require.main === module) main();
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
- module.exports = { main };
115
+ export { main };
@@ -22,9 +22,11 @@
22
22
  // every path except a missing --crew-home (exit 2).
23
23
  "use strict";
24
24
 
25
- const fs = require("fs");
26
- const path = require("path");
27
- const { execFileSync } = require("child_process");
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
- module.exports = {
697
- parseSemver: parseSemver,
698
- compareSemver: compareSemver,
699
- channelAllows: channelAllows,
700
- crewDecision: crewDecision,
701
- dashboardDecision: dashboardDecision,
702
- crewVersionOrdering: crewVersionOrdering,
703
- fileCrewUpgrade: fileCrewUpgrade,
704
- enrollDashboardBase: enrollDashboardBase,
705
- readState: readState,
706
- writeState: writeState,
707
- appendLog: appendLog,
708
- run: 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
- if (require.main === module) main();
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();