muse-crew 0.14.5 → 0.14.6

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.
@@ -245,14 +245,6 @@ var TOOL_CHECK_PREAMBLE =
245
245
  "2. Write exactly one line: artifact_tools: ok - or artifact_tools: missing if the call failed or the tool does not exist.\n" +
246
246
  "3. Run the shell command: echo tool-probe-ok - then write exactly one line: shell_transport: ok - or shell_transport: unavailable if you cannot run shell commands.\n" +
247
247
  "Then do the assignment below.\n\n";
248
- // ARTIFACT_LOAD_PREAMBLE - the one-line load instruction that precedes every
249
- // special-purpose prompt calling artifact tools (rebuild trigger, provenance
250
- // stamp, publish verification). The artifact tool namespace is deferred for
251
- // workflow children: present but invisible until the child loads it via
252
- // tool_search.load_tool_namespace (bug 3472bf36 root cause). Byte-identical
253
- // across standard/bugfix/chore - pinned by tests/artifact-tools.test.js.
254
- var ARTIFACT_LOAD_PREAMBLE =
255
- "First call tool_search.load_tool_namespace with paths [\"artifact\"] - the artifact tool namespace is deferred and invisible until loaded.\n";
256
248
  function buildTransportRetryTrailer(stepName, repoPath, taskId, attempt, reason) {
257
249
  // reason: "discarded" (the runtime threw the output away - it could not be
258
250
  // machine-read), "empty" (agent() returned without throwing but produced
@@ -399,28 +391,6 @@ function hydrateReleaseDecision(rec) {
399
391
  return null;
400
392
  }
401
393
 
402
- // Publish read-back request (agent path currently unavailable): the
403
- // verbatim_request the parent protocol (docs/publish-verification.md) would
404
- // hand to an independent read-back tool after the artifact build lands.
405
- // artifact_inspect was removed by the platform (2026-09-14);
406
- // artifact.inspect is malfunction diagnosis, not a substitute — so no
407
- // agent-callable read-back tool exists and this LLM-inspector request
408
- // cannot currently be issued. The primary sensor is now the deterministic
409
- // lib/readback-disk.js (reads the on-disk tree the artifact is served
410
- // from); this request builder is retained only as the manual fallback.
411
- // Pure function — no I/O, no clock. The request carries the merged diff as the expected change and asks
412
- // for an independent read of the artifact's actual source: for each file, the
413
- // exact current text of the changed regions plus a per-line present/absent
414
- // finding. The primary sensor is the deterministic lib/readback-disk.js;
415
- // after the tick worker issues the edit, the parent's scan mirrors the
416
- // verification request park note and the verifier certifies content
417
- // before stamping (see docs/publish-verification.md). This preserves the
418
- // circularity break that hollowed canary run 8 (2026-09-11): the old
419
- // verifyAppliedChanges compared the builder's applied-report against the
420
- // diff the report was derived from — a fabricated report passed by
421
- // construction. Independent read-back cannot be fabricated from the diff;
422
- // it must match the artifact's real content.
423
-
424
394
  // See docs/decisions/publish-path.md#publish-attempt-ledger: every trigger outcome is recorded in the durable ledger.
425
395
  // (2026-09-20, one-party worker-owned publish) No keyTag: every ledger
426
396
  // write in a Publish pass is followed by a return, so no pass can write two
@@ -452,6 +422,11 @@ async function recordPublishLedger(entry, rework) {
452
422
  diff_path: entry.diff_path || null,
453
423
  diff_sha256: entry.diff_sha256 || null,
454
424
  base: entry.base || null,
425
+ // (0.14.6) Numeric publication attempt + the attempt's deterministic
426
+ // version. Attempt 1 is staged here by the workflow; attempts 2-3 are
427
+ // staged by scan-ack-pending's re-issue path with fresh versions.
428
+ publish_attempt: entry.publish_attempt || null,
429
+ version: entry.version || null,
455
430
  });
456
431
  var sq = function(s) { return "'" + String(s).split("'").join("'\\''") + "'"; };
457
432
  var res = await agent(
@@ -463,9 +438,15 @@ async function recordPublishLedger(entry, rework) {
463
438
  label: "Recording publish attempt in ledger",
464
439
  schema: { type: "object", properties: { result: { type: "string" } }, required: ["result"] } }
465
440
  );
466
- log("Noted publish outcome '" + entry.outcome + "' for task " + taskId + " in ledger");
441
+ // (2026-09-20 REVIEW) The intent entry is the machine's anchor — the
442
+ // caller gates on this return. LEDGER_OK in the agent's verbatim output
443
+ // is the only mechanical proof the line landed.
444
+ var ok = res && typeof res.result === "string" && res.result.indexOf("LEDGER_OK") !== -1;
445
+ log("Noted publish outcome '" + entry.outcome + "' for task " + taskId + " in ledger" + (ok ? "" : " (WRITE UNCONFIRMED)"));
446
+ return ok;
467
447
  } catch (e) {
468
448
  log("Publish ledger: write failed for task " + taskId + " (non-fatal, observability only): " + (e && e.message ? e.message : e));
449
+ return false;
469
450
  }
470
451
  }
471
452
 
@@ -1396,7 +1377,7 @@ while (i < STEPS.length) {
1396
1377
  var publishDiffFile = crewHome + "/.publish-diffs/" + taskId + (totalReworkCount > 0 ? "-r" + totalReworkCount : "") + ".diff";
1397
1378
  var diffResult = await agent(
1398
1379
  "Run exactly one command and nothing else:\n" +
1399
- "node " + COMPUTE_DIFF + " --repo-path \"" + REPO_PATH + "\" --base " + publishBase + " --out \"" + publishDiffFile + "\"\n" +
1380
+ "node " + COMPUTE_DIFF + " --repo-path \"" + REPO_PATH + "\" --base " + publishBase + " --task-id \"" + taskId + "\" --attempt 1 --out \"" + publishDiffFile + "\"\n" +
1400
1381
  "The script prints one small JSON object to stdout. Return the stdout verbatim as { \"output\": \"<verbatim stdout>\" } and nothing else. Do not interpret it.",
1401
1382
  { key: attemptKey("publish-artifact-diff-" + taskId, totalReworkCount), label: "Computing publish diff from stamped base",
1402
1383
  schema: { type: "object", properties: { output: { type: "string" } }, required: ["output"] } }
@@ -1419,6 +1400,18 @@ while (i < STEPS.length) {
1419
1400
  var mergeCommitForPublish = String(diffSummary.commit || "").trim();
1420
1401
  var mergeCommitShortForPublish = String(mergeCommitForPublish).substring(0, 7) || "unknown";
1421
1402
  var publishDiffSha256 = String(diffSummary.sha256 || "");
1403
+ // (0.14.6) The per-attempt publish version, synthesized into the
1404
+ // staged diff as .crew-publish-version-<task_id> (per-task receipt —
1405
+ // two same-project tasks can publish concurrently) by the
1406
+ // deterministic script.
1407
+ // The workflow never invents it: the script derives it from
1408
+ // task+commit+attempt, and the intent scan re-derives it before
1409
+ // claiming. A missing/malformed version parks — never publish blind.
1410
+ var publishVersion = String(diffSummary.version || "");
1411
+ // Mirrors isPublishVersion() in lib/crew-api.js: <40-hex commit>:<attempt>.
1412
+ if (!/^[0-9a-f]{40}:[1-9][0-9]*$/.test(publishVersion)) {
1413
+ return await parkTask("Publish diff for commit " + (mergeCommitForPublish || "unknown") + " carries no usable publish version — cannot stage a verifiable intent. Human attention needed.");
1414
+ }
1422
1415
  if (!diffSummary.bytes) {
1423
1416
  return await parkTask("Publish diff is empty for commit " + (mergeCommitForPublish || "unknown") + " — a merge lock was held but there is no change to carry. Human attention needed.");
1424
1417
  }
@@ -1446,131 +1439,53 @@ while (i < STEPS.length) {
1446
1439
  // worker verifies the sha256 from the intent entry before issuing;
1447
1440
  // a mismatch stops issuance. (2026-09-20, one-party worker-owned
1448
1441
  // publish: the workflow no longer issues the edit itself.)
1449
- // See docs/decisions/publish-path.md#durable-evidence-snapshot: snapshot the audit-dir listing BEFORE the trigger; fallback diffs before/after.
1450
- var auditDirsBeforeTrigger = [];
1451
- var auditBeforeOk = false;
1452
- // Pre-trigger baselines (design §1.9): the audit-dir listing and the
1453
- // manifest snapshot. The verified-path freshness check compares the
1454
- // post-trigger manifest against the baseline (built_at advance +
1455
- // content_sha256 change). Best-effort, never gates — a missing
1456
- // manifest baseline fails the verified path closed.
1457
- var preTriggerManifest = null;
1458
- try {
1459
- var auditBefore = await agent(
1460
- "Capture pre-trigger baselines for slug \"" + PUBLISH_SLUG + "\" (best-effort snapshots, never gates).\n" +
1461
- "Run: ls -1 ~/workspace/ts-spaces/" + PUBLISH_SLUG + "/audits/ 2>/dev/null; echo ---MANIFEST---; cat ~/workspace/ts-spaces/" + PUBLISH_SLUG + "/.space-build/manifest.json 2>/dev/null\n" +
1462
- "Return JSON { \"dirs\": \"<newline-separated names, empty string when missing>\", \"manifest\": \"<the manifest's full text, or empty string when missing/unreadable>\" } and nothing else.",
1463
- { key: attemptKey("publish-baseline-before-" + taskId, totalReworkCount), label: "Snapshotting baselines before rebuild trigger",
1464
- schema: { type: "object", properties: { dirs: { type: "string" }, manifest: { type: "string" } }, required: ["dirs", "manifest"] } }
1465
- );
1466
- auditDirsBeforeTrigger = String((auditBefore && auditBefore.dirs) || "").split("\n").map(function (s) { return s.trim(); }).filter(function (s) { return s.length > 0; });
1467
- var manifestText = String((auditBefore && auditBefore.manifest) || "").trim();
1468
- if (manifestText) {
1469
- var manifestJson = JSON.parse(manifestText);
1470
- preTriggerManifest = {
1471
- built_at: typeof manifestJson.built_at === "string" ? manifestJson.built_at : null,
1472
- content_sha256: typeof manifestJson.content_sha256 === "string" ? manifestJson.content_sha256 : null,
1473
- };
1474
- }
1475
- // Arm only after both baselines parse.
1476
- auditBeforeOk = true;
1477
- log("Publish pre-trigger baselines for task " + taskId + ": " + auditDirsBeforeTrigger.length + " audit dirs, manifest " + (preTriggerManifest ? "built_at=" + preTriggerManifest.built_at : "none"));
1478
- } catch (auditBeforeErr) {
1479
- log("Publish pre-trigger baselines failed for task " + taskId + " (non-fatal): audit fallback DISABLED for this attempt — without a baseline, historical dirs would look new: " + (auditBeforeErr && auditBeforeErr.message ? auditBeforeErr.message : auditBeforeErr));
1480
- }
1481
-
1482
- // See docs/decisions/publish-path.md#fire-and-forget-trigger: the trigger child returns immediately; the workflow owns observation and verdict.
1483
- var publishToolsOk = false;
1484
- var publishToolsMissing = false;
1485
- for (var toolcheckAttempt = 1; toolcheckAttempt <= 2 && !publishToolsOk; toolcheckAttempt++) {
1486
- try {
1487
- var toolcheckResult = await agent(
1488
- "Check whether the artifact tool namespace is available.\n" +
1489
- "Call tool_search.load_tool_namespace with paths [\"artifact\"].\n" +
1490
- "Then emit exactly one line and nothing else: ARTIFACT_TOOLS: <ok if the load succeeded and artifact_edit and artifact_status are now functions, missing otherwise>.\n" +
1491
- "Return JSON { \"signal\": \"<the exact ARTIFACT_TOOLS line>\" } and nothing else.",
1492
- { key: attemptKey("publish-artifact-toolcheck-" + taskId + (toolcheckAttempt === 1 ? "" : "-retry2"), totalReworkCount),
1493
- label: "Checking artifact tool availability" + (toolcheckAttempt === 1 ? "" : " (retry)"),
1494
- schema: { type: "object", properties: { signal: { type: "string" } }, required: ["signal"] } }
1495
- );
1496
- var toolSignal = String((toolcheckResult && toolcheckResult.signal) || "");
1497
- if (/ARTIFACT_TOOLS:\s*ok/.test(toolSignal)) {
1498
- publishToolsOk = true;
1499
- } else if (/ARTIFACT_TOOLS:\s*missing/.test(toolSignal)) {
1500
- publishToolsMissing = true;
1501
- }
1502
- log("Publish artifact toolcheck for task " + taskId + " (attempt " + toolcheckAttempt + " of 2): " +
1503
- (publishToolsOk ? "tools ok" : publishToolsMissing ? "tools missing (explicit parsed signal)" : "inconclusive (no ARTIFACT_TOOLS signal parsed)"));
1504
- } catch (toolcheckErr) {
1505
- log("Publish artifact toolcheck for task " + taskId + " (attempt " + toolcheckAttempt + " of 2) threw (" + (toolcheckErr && toolcheckErr.message ? toolcheckErr.message : toolcheckErr) + ") — inconclusive: a throw proves nothing about tool availability, never counted as missing");
1506
- }
1507
- }
1508
- if (!publishToolsOk && !publishToolsMissing) {
1509
- await recordPublishLedger({
1510
- commit: mergeCommitForPublish,
1511
- attempt: rebuildAttemptKey,
1512
- agent_id: null,
1513
- applied_report: null,
1514
- outcome: "unknown",
1515
- detail: "artifact toolcheck inconclusive after two attempts (throws or unparseable signals — never an explicit ARTIFACT_TOOLS: missing): tool availability unproven, so the trigger was NOT issued; unknown parks fail closed with no blind retry"
1516
- }, totalReworkCount);
1517
- return await parkTask("Publish cannot proceed for task " + taskId + ": the artifact toolcheck was inconclusive after two attempts (no explicit ARTIFACT_TOOLS signal parsed — a throw is transport noise, not evidence). Tool availability is unproven, so no edit was issued and nothing was retried blindly. Human attention needed.");
1518
- }
1519
- if (!publishToolsOk) {
1520
- await recordPublishLedger({
1521
- commit: mergeCommitForPublish,
1522
- attempt: rebuildAttemptKey,
1523
- agent_id: null,
1524
- applied_report: null,
1525
- outcome: "rejected",
1526
- detail: "artifact tool namespace explicitly missing (parsed ARTIFACT_TOOLS: missing signal, one bounded retry spent): the edit provably did not go through — no trigger issued, no blind retry"
1527
- }, totalReworkCount);
1528
- return await parkTask("Publish cannot proceed for task " + taskId + ": the artifact tool namespace was explicitly missing (parsed signal — the edit provably did not go through, so no trigger was issued and nothing was retried blindly). Human attention needed.");
1529
- }
1530
- // See docs/decisions/publish-path.md#pretrigger-baseline: a tiny schema'd baseline is captured before the trigger.
1531
- var baselineAgentId = null;
1532
- var baselineFailed = false;
1533
- try {
1534
- var publishBaseline = await agent(
1535
- ARTIFACT_LOAD_PREAMBLE +
1536
- "Call artifact_status with slug \"" + PUBLISH_SLUG + "\" once.\n" +
1537
- "Return JSON { \"build\": <the raw \"build\" value exactly as returned, or null when there is none> } and nothing else.",
1538
- { key: attemptKey("publish-artifact-baseline-" + taskId, totalReworkCount), label: "Reading pre-trigger build state",
1539
- schema: { type: "object", properties: { build: { type: ["object", "null"] } }, required: ["build"] } }
1540
- );
1541
- baselineAgentId = (publishBaseline && publishBaseline.build && typeof publishBaseline.build.agent_id === "string" && publishBaseline.build.agent_id) || null;
1542
- log("Publish pre-trigger baseline for task " + taskId + ": " + (baselineAgentId ? "build " + baselineAgentId + " already in flight (predates the trigger — never attributed to this edit)" : "no build in flight"));
1543
- } catch (baselineErr) {
1544
- baselineFailed = true;
1545
- log("Publish pre-trigger baseline read failed for task " + taskId + " (" + (baselineErr && baselineErr.message ? baselineErr.message : baselineErr) + ") — receipt attribution skipped; durable audit-dir evidence is the only positive signal");
1546
- }
1547
1442
  // (2026-09-20, one-party worker-owned publish) The workflow exits
1548
1443
  // Publish at intent. Everything above — preflight, provenance base,
1444
+ // checksummed diff — is read-only. (2026-09-20 REVIEW: the
1445
+ // pre-trigger baselines, artifact toolcheck, and pre-trigger build
1446
+ // state agent steps were deleted — their outputs fed only the
1447
+ // retired content-verdict/receipt-attribution machinery, and the
1448
+ // toolcheck ran in a workflow child that cannot reach artifact_edit.
1449
+ // Issuance belongs to the session-carrying tick worker, which
1450
+ // handles tool-unavailability as inconclusive per the tick protocol.)
1451
+ // Publish at intent. Everything above — preflight, provenance base,
1549
1452
  // checksummed diff, toolcheck, pre-trigger baselines — is read-only.
1550
1453
  // Issuance belongs to the session-carrying tick worker
1551
- // (scan-publish-unknown's intent bucket), the only caller class that
1454
+ // (scan-publish-intent's intent bucket), the only caller class that
1552
1455
  // can reach artifact_edit. This entry is issuer-written BY THE
1553
1456
  // WORKFLOW and names the intent: outcome "publish-intent", never
1554
1457
  // "submitted" — the workflow issued nothing. The worker claims it
1555
1458
  // via the park note below.
1556
- await recordPublishLedger({
1459
+ // (2026-09-20 REVIEW) The intent ledger entry is the anchor the
1460
+ // whole machine depends on. If the ferry drops it, the task would
1461
+ // park with publish-requested and no anchor — unclaimable forever.
1462
+ // Fail loudly instead of parking an orphan intent.
1463
+ var intentAnchored = await recordPublishLedger({
1557
1464
  commit: mergeCommitForPublish,
1558
1465
  attempt: rebuildAttemptKey,
1466
+ // (0.14.6) Attempt 1, staged here. The version is the attempt's
1467
+ // deterministic receipt string, synthesized into the staged diff;
1468
+ // the ack scan re-derives it from task+commit+attempt before the
1469
+ // tick worker may issue the edit.
1470
+ publish_attempt: 1,
1471
+ version: publishVersion,
1559
1472
  agent_id: null,
1560
1473
  applied_report: null,
1561
- manifest_before: preTriggerManifest,
1562
1474
  outcome: "publish-intent",
1563
1475
  issuer: "workflow",
1564
1476
  diff_path: publishDiffFile,
1565
1477
  diff_sha256: publishDiffSha256,
1566
1478
  // (2026-09-20, critic-0145 F-A4) The base the diff was computed
1567
- // against: scan-publish-unknown's intent branch compares it with
1479
+ // against: scan-publish-intent's intent branch compares it with
1568
1480
  // current stamped provenance and re-queues the task when it went
1569
1481
  // stale — never issues the stale diff.
1570
1482
  base: publishBase,
1571
- detail: "publish intent: preflight, checksummed diff, toolcheck, and pre-trigger baselines are complete; issuance is owned by the session-carrying tick worker. The workflow performed no artifact_edit and wrote no issuance."
1483
+ detail: "publish intent: preflight and checksummed diff are complete; issuance is owned by the session-carrying tick worker. The workflow performed no artifact_edit and wrote no issuance."
1572
1484
  }, totalReworkCount);
1573
- return await parkTask("publish: publish-requested " + mergeCommitForPublish + " " + rebuildAttemptKey + " — Publish intent recorded for task " + taskId + ": the merged change is staged as a checksummed diff (" + publishDiffFile + ", sha256 " + publishDiffSha256 + ") for the crew's publish worker to issue. The worker verifies the build and stamps provenance before this task resumes; no action needed.");
1485
+ if (!intentAnchored) {
1486
+ throw new Error("publish-intent ledger anchor write unconfirmed for task " + taskId + " — refusing to park an unclaimable intent; the Publish phase fails loudly instead");
1487
+ }
1488
+ return await parkTask("publish: publish-requested " + mergeCommitForPublish + " attempt=1 — Publish intent recorded for task " + taskId + ": the merged change is staged as a checksummed diff (" + publishDiffFile + ", sha256 " + publishDiffSha256 + ") carrying the per-attempt publish version for the crew's publish worker to issue. The worker issues the edit directly and the acknowledgement scan owns the task from there; no action needed.");
1574
1489
  } // end: publishSkippedNoLock — no rebuild, no stamp, nothing to ship
1575
1490
  // STEP 2 (mechanical, always — skip path included): post-deploy
1576
1491
  // commits builder leftovers if any, removes the worktree, and releases
@@ -2127,10 +2042,10 @@ while (i < STEPS.length) {
2127
2042
  } // end: !npmPublishSkipped — a skipped publish has nothing to verify
2128
2043
  }
2129
2044
 
2130
- // Parent-owned verification (docs/publish-verification.md): the parent's
2131
- // scan builds the request via lib/build-readback-request.js and ferries
2132
- // the inspection; a skipped or failed publish has nothing to verify.
2133
- // See docs/decisions/publish-path.md#parent-owned-verification for history.
2045
+ // Version acknowledgement (docs/publish-verification.md): the tick worker
2046
+ // issues the edit directly and the ack scan owns the task from there —
2047
+ // the exact per-attempt version on the artifact's disk is the sole positive
2048
+ // completion criterion. A skipped or failed publish has nothing to ack.
2134
2049
 
2135
2050
  // Session notes. Machine-readable marker lines are extracted from the full
2136
2051
  // worker report and appended AFTER the slice so a long report can never
@@ -1,140 +0,0 @@
1
- #!/usr/bin/env node
2
- // build-readback-request.js — deterministic builder for the parent publish
3
- // content read-back inspection request.
4
- //
5
- // The parent (tick worker) never hand-writes the inspection prompt: this
6
- // script builds the EXACT request from the merge commit's diff, so the
7
- // read-back asks about the real expected change, not an LLM's paraphrase.
8
- // Output is JSON on stdout: { verbatim_request }.
9
- //
10
- // Usage:
11
- // node build-readback-request.js --repo-path <path> --commit <sha>
12
- // --base <sha> --task-id <uuid> --slug <artifact-slug>
13
- // [--build-agent-id <uuid>]
14
- //
15
- // --base is the previously-stamped provenance source_commit (the tree the
16
- // last verification certified), or the empty-tree sha
17
- // 4b825dc642cb6eb9a060e54bf8d69288fbee4904 for a first publish with no
18
- // prior stamp. It is REQUIRED because the merge commit's first parent is
19
- // NOT necessarily the previously-published tree: push-time reconcile
20
- // merges (2026-09-14, task 0c53af4e) put the task's own changes behind an
21
- // intermediate merge, so `commit^1..commit` covers only the reconcile
22
- // delta and the request would certify content it never checked.
23
- //
24
- // Exit codes: 0 ok · 2 usage/validation · 1 git/diff failure.
25
-
26
- import { execFileSync } from "node:child_process";
27
-
28
- // --help: before required-arg parsing (shebang⇔CLI contract).
29
- if (process.argv.slice(2).includes("--help")) {
30
- console.log(
31
- "usage: node build-readback-request.js --repo-path <path> --commit <sha>\n" +
32
- " --base <sha> --task-id <uuid> --slug <artifact-slug> [--build-agent-id <uuid>]\n" +
33
- "builds the read-back inspection request from the merge commit's diff"
34
- );
35
- process.exit(0);
36
- }
37
-
38
- const EMPTY_TREE = "4b825dc642cb6eb9a060e54bf8d69288fbee4904";
39
-
40
- function arg(name) {
41
- const i = process.argv.indexOf(name);
42
- if (i < 0 || i + 1 >= process.argv.length) {
43
- process.stderr.write(JSON.stringify({ ok: false, error: "usage", message: `${name} is required.` }) + "\n");
44
- process.exit(2);
45
- }
46
- return process.argv[i + 1];
47
- }
48
-
49
- const repoPath = arg("--repo-path");
50
- const commit = arg("--commit");
51
- const base = arg("--base");
52
- const taskId = arg("--task-id");
53
- const slug = arg("--slug");
54
- const buildAgentId = process.argv.includes("--build-agent-id")
55
- ? arg("--build-agent-id") : null;
56
-
57
- if (!/^[0-9a-f]{40}$/.test(commit)) {
58
- process.stderr.write(JSON.stringify({ ok: false, error: "usage", message: "commit must be a 40-char hex sha." }) + "\n");
59
- process.exit(2);
60
- }
61
- if (!/^[0-9a-f]{40}$/.test(base)) {
62
- process.stderr.write(JSON.stringify({ ok: false, error: "usage", message: "base must be a 40-char hex sha." }) + "\n");
63
- process.exit(2);
64
- }
65
- // The publish delta is base..commit. Refuse a base that is not an ancestor:
66
- // certifying a diff against an unrelated tree is worse than no request.
67
- if (base !== EMPTY_TREE) {
68
- try {
69
- execFileSync("git", ["-C", repoPath, "merge-base", "--is-ancestor", base, commit], { stdio: "ignore" });
70
- } catch (e) {
71
- process.stderr.write(JSON.stringify({ ok: false, error: "usage", message: `base ${base} is not an ancestor of commit ${commit} — refusing to build a request that could certify an unrelated tree.` }) + "\n");
72
- process.exit(2);
73
- }
74
- }
75
-
76
- let diff;
77
- try {
78
- diff = execFileSync("git", ["-C", repoPath, "diff", base, commit, "--"], {
79
- encoding: "utf8", maxBuffer: 4 * 1024 * 1024,
80
- });
81
- } catch (e) {
82
- process.stderr.write(JSON.stringify({ ok: false, error: "git", message: `git diff failed: ${e.message}` }) + "\n");
83
- process.exit(1);
84
- }
85
- if (!diff.trim()) {
86
- process.stderr.write(JSON.stringify({ ok: false, error: "git", message: "empty diff for commit." }) + "\n");
87
- process.exit(1);
88
- }
89
-
90
- // Parse the diff into per-file added/removed lines (the contract the
91
- // verifier will check the inspector's findings against).
92
- const files = [];
93
- let current = null;
94
- for (const line of diff.split("\n")) {
95
- if (line.startsWith("diff --git")) {
96
- const m = line.match(/^diff --git a\/(.+) b\/(.+)$/);
97
- current = { path: m ? m[2] : "unknown", added: [], removed: [] };
98
- files.push(current);
99
- } else if (current && line.startsWith("+") && !line.startsWith("+++")) {
100
- current.added.push(line.slice(1));
101
- } else if (current && line.startsWith("-") && !line.startsWith("---")) {
102
- current.removed.push(line.slice(1));
103
- }
104
- }
105
-
106
- const buildIdLine = buildAgentId
107
- ? `Expected builder build agent_id: ${buildAgentId} (the artifact system's in-flight correlation ID for this publish attempt — not a durable post-completion identifier).\nReport the live build's agent_id as seen in artifact_status (or state explicitly that no build/agent_id is visible). If an expected agent_id is given above and the live one differs, say so exactly — the read-back may be inspecting a different build's output.\n\n`
108
- : "";
109
-
110
- const verbatim_request =
111
- `Publish content read-back for task ${taskId}, merge commit ${commit}.
112
- Expected change: the publish delta ${base}..${commit} (base = previously-stamped source commit, or the empty tree for a first publish).
113
- The unified diff below was supposed to be applied to this artifact's source tree and deployed. Do NOT modify anything.
114
- Do NOT rely on the builder's applied-changes report — it is derived from this same diff, so it cannot confirm the content. Read the artifact's CURRENT source directly.
115
-
116
- ${buildIdLine}UNIFIED DIFF (expected change):
117
- \`\`\`diff
118
- ${diff}\`\`\`
119
-
120
- For each file in the diff:
121
- 1. Read the file's CURRENT content in the artifact source tree.
122
- 2. Quote the exact current text of the regions around the changed lines.
123
- 3. For every added (+) line in the diff, state whether that exact line is PRESENT in the current source.
124
- 4. For every removed (-) line in the diff, state whether that exact line is ABSENT from the current source.
125
- 5. Report build/deploy health and the console error count.
126
-
127
- MACHINE-READABLE FINDINGS (REQUIRED — the parent parses this block mechanically; if it is missing or malformed the verification FAILS CLOSED as blocked):
128
- For each file, emit exactly these lines, one per changed line, with the verdict at the end of the line:
129
- FILE: <path>
130
- ADDED: <exact added line from the diff> :: PRESENT
131
- ADDED: <exact added line from the diff> :: ABSENT
132
- REMOVED: <exact removed line from the diff> :: PRESENT
133
- REMOVED: <exact removed line from the diff> :: ABSENT
134
- END_FILE
135
- Emit one ADDED line per (+) diff line and one REMOVED line per (-) diff line — no more, no fewer. The verdict is PRESENT if that exact line occurs in the file's current source, ABSENT otherwise.
136
-
137
- Return the per-file present/absent findings with the quoted observed lines. Do not modify anything.
138
- This read-back feeds the parent content-verification protocol (docs/publish-verification.md): the parent stamps provenance only when every added line is present and every removed line is absent.`;
139
-
140
- process.stdout.write(JSON.stringify({ verbatim_request, files_changed: files.map((f) => f.path) }, null, 2) + "\n");
@@ -1,101 +0,0 @@
1
- #!/usr/bin/env node
2
- // check-intent-freshness.js — deterministic manifest-freshness check for a
3
- // re-claimed one-party publish intent (2026-09-20, critic-0145 F-O3/F-R2).
4
- //
5
- // A re-claiming tick must decide: did the dead tick issue the edit (record
6
- // "recovered") or not (issue it now)? The decision is mechanical, never
7
- // prose: read the on-disk artifact manifest at the same path the verifier
8
- // uses (<spaces-root>/<slug>/.space-build/manifest.json — see
9
- // lib/verify-publish.js) and compare its content_sha256 / built_at against
10
- // the intent entry's pre-issuance baseline (manifest_before).
11
- //
12
- // The manifest ADVANCED — a new build identity, so the dead tick issued —
13
- // when either:
14
- // current.content_sha256 !== baseline.content_sha256 (a new content hash), OR
15
- // Date.parse(current.built_at) > Date.parse(baseline.built_at) (a later build).
16
- // Otherwise it still shows the pre-edit state and the tick issues fresh.
17
- //
18
- // Stdout contract: exactly one JSON object, always small:
19
- // { ok:true, advanced:<bool>, baseline:{built_at, content_sha256}, current:{built_at, content_sha256} }
20
- // { ok:false, error:"no-baseline"|"unreadable-manifest"|"unparseable-manifest" }
21
- //
22
- // Exit codes: 0 ok (JSON on stdout) · 2 usage · 1 IO failure (nothing on
23
- // stdout — a caller must never treat stdout as a result). Errors go to
24
- // stderr as JSON.
25
- //
26
- // A non-ok result is never a verdict — the caller fails closed: record
27
- // nothing and let the claim expire into the reclaim path (the scan's F-R3
28
- // terminal park covers the null-baseline reclaim; an unreadable manifest
29
- // leaves the task parked for the next tick). Reclaim decisions never rest
30
- // on prose.
31
-
32
- import { readFileSync } from "node:fs";
33
- import { homedir } from "node:os";
34
- import { join } from "node:path";
35
-
36
- // --help: before required-arg parsing (shebang⇔CLI contract).
37
- if (process.argv.slice(2).includes("--help")) {
38
- console.log(
39
- "usage: node check-intent-freshness.js --slug <slug> --manifest-before '<json>' [--spaces-root <path>]\n" +
40
- "compares the on-disk artifact manifest's content_sha256/built_at against the pre-issuance baseline; prints {ok, advanced} JSON on stdout"
41
- );
42
- process.exit(0);
43
- }
44
-
45
- function arg(name, required = true) {
46
- const i = process.argv.indexOf(name);
47
- if (i < 0 || i + 1 >= process.argv.length) {
48
- if (!required) return null;
49
- fail("usage", `${name} is required.`, 2);
50
- }
51
- return process.argv[i + 1];
52
- }
53
- function fail(code, message, exitCode) {
54
- process.stderr.write(JSON.stringify({ ok: false, error: code, message }) + "\n");
55
- process.exit(exitCode);
56
- }
57
-
58
- const slug = arg("--slug");
59
- const baselineRaw = arg("--manifest-before");
60
- const spacesRoot = arg("--spaces-root", false) || join(homedir(), "workspace", "ts-spaces");
61
-
62
- if (!slug || /[/\\]/.test(slug)) fail("usage", "--slug must be a non-empty slug (no path separators).", 2);
63
-
64
- let baseline;
65
- try {
66
- baseline = JSON.parse(baselineRaw);
67
- } catch {
68
- fail("usage", "--manifest-before must be JSON.", 2);
69
- }
70
- if (!baseline || typeof baseline !== "object") fail("no-baseline", "--manifest-before is not an object.", 1);
71
- const baselineSha = typeof baseline.content_sha256 === "string" ? baseline.content_sha256 : null;
72
- const baselineBuiltAt = typeof baseline.built_at === "string" ? baseline.built_at : null;
73
- if (!baselineSha && !baselineBuiltAt) {
74
- fail("no-baseline", "the baseline carries neither content_sha256 nor built_at — freshness is unprovable.", 1);
75
- }
76
-
77
- let currentManifest;
78
- try {
79
- currentManifest = JSON.parse(readFileSync(join(spacesRoot, slug, ".space-build", "manifest.json"), "utf8"));
80
- } catch (e) {
81
- const code = e && e.code === "ENOENT" ? "unreadable-manifest" : "unreadable-manifest";
82
- fail(code, `cannot read the on-disk manifest: ${(e && e.message) || e}`.slice(0, 300), 1);
83
- }
84
- if (!currentManifest || typeof currentManifest !== "object") {
85
- fail("unparseable-manifest", "the on-disk manifest is not a JSON object.", 1);
86
- }
87
- const currentSha = typeof currentManifest.content_sha256 === "string" ? currentManifest.content_sha256 : null;
88
- const currentBuiltAt = typeof currentManifest.built_at === "string" ? currentManifest.built_at : null;
89
-
90
- const hashAdvanced = !!(baselineSha && currentSha && currentSha !== baselineSha);
91
- const currentBuiltAtMs = currentBuiltAt ? Date.parse(currentBuiltAt) : NaN;
92
- const baselineBuiltAtMs = baselineBuiltAt ? Date.parse(baselineBuiltAt) : NaN;
93
- const timeAdvanced = !!(currentBuiltAtMs > 0 && baselineBuiltAtMs > 0 && currentBuiltAtMs > baselineBuiltAtMs);
94
- const advanced = hashAdvanced || timeAdvanced;
95
-
96
- process.stdout.write(JSON.stringify({
97
- ok: true,
98
- advanced,
99
- baseline: { built_at: baselineBuiltAt, content_sha256: baselineSha },
100
- current: { built_at: currentBuiltAt, content_sha256: currentSha },
101
- }) + "\n");