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,3 +1,4 @@
1
+ #!/usr/bin/env node
1
2
  // ux-doctrine.js — UX-surface doctrine page resolution.
2
3
  //
3
4
  // One canonical doctrine page per environment_type value: the crew reads
@@ -11,10 +12,16 @@
11
12
  // against this file, so the two can never silently diverge.
12
13
  //
13
14
  // Pure and deterministic: no clock, no randomness, no I/O.
14
- "use strict";
15
+ //
16
+ // Module shape (blocker 21): this file is an import-safe shared module —
17
+ // it has NO shebang, so the entry gate bare-loads it (`node ux-doctrine.js`
18
+ // exits 0). The CLI below runs only when the file itself is the process
19
+ // entrypoint.
15
20
 
16
- const { existsSync } = require("node:fs");
17
- const { join } = require("node:path");
21
+ import { existsSync } from "node:fs";
22
+ import { join } from "node:path";
23
+ import { realpathSync } from "node:fs";
24
+ import { fileURLToPath } from "node:url";
18
25
 
19
26
  // THE MAP. environment_type value -> doctrine page filename, relative to
20
27
  // the release docs dir ($CREW_HOME/current/docs/). null is unclassified —
@@ -44,12 +51,30 @@ function doctrinePageExists(crewHome, environmentType) {
44
51
  return p ? existsSync(p) : false;
45
52
  }
46
53
 
47
- module.exports = { DOCTRINE_PAGES, doctrinePage, doctrinePath, doctrinePageExists };
54
+ export { DOCTRINE_PAGES, doctrinePage, doctrinePath, doctrinePageExists };
55
+
56
+ // realpath: production spawns through $CREW_HOME/current/lib (a symlink),
57
+ // so argv[1] must be resolved before comparison with import.meta.url.
58
+ const isMainModule = (() => {
59
+ try {
60
+ return !!process.argv[1] && realpathSync(process.argv[1]) === fileURLToPath(import.meta.url);
61
+ } catch {
62
+ return false;
63
+ }
64
+ })();
48
65
 
49
- if (require.main === module) {
66
+ const USAGE = "usage: node ux-doctrine.js --page <environment_type|null> | --path <crewHome> <environment_type|null>";
67
+
68
+ if (isMainModule) {
50
69
  // CLI: node ux-doctrine.js --page <environment_type>
51
70
  // node ux-doctrine.js --path <crewHome> <environment_type>
52
71
  const args = process.argv.slice(2);
72
+ // Import-safe module contract: a bare invocation (no args) is a load
73
+ // check, not a usage error — it exits 0. --help always exits 0.
74
+ if (args.length === 0 || args.includes("--help")) {
75
+ console.log(USAGE);
76
+ process.exit(0);
77
+ }
53
78
  if (args[0] === "--page" && args.length === 2) {
54
79
  const page = doctrinePage(args[1] === "null" ? null : args[1]);
55
80
  console.log(page === null ? "null" : page);
@@ -57,7 +82,7 @@ if (require.main === module) {
57
82
  const p = doctrinePath(args[1], args[2] === "null" ? null : args[2]);
58
83
  console.log(p === null ? "null" : p);
59
84
  } else {
60
- console.error("usage: node ux-doctrine.js --page <environment_type|null> | --path <crewHome> <environment_type|null>");
85
+ console.error(USAGE);
61
86
  process.exit(2);
62
87
  }
63
88
  }
@@ -51,6 +51,19 @@ import {
51
51
  makeOldCounter,
52
52
  discriminatingLines,
53
53
  } from "./publish-content.js";
54
+ // Terminal-note vocabulary (D7, 2026-09-19): the writer-side guard.
55
+ // terminal() asserts its emitted verb is a registry member before writing,
56
+ // so no future terminal verb ships unrecognized by the scans.
57
+ import { matchTerminalPublishNote } from "./publish-note-vocabulary.js";
58
+
59
+ // --help: before required-arg parsing (shebang⇔CLI contract).
60
+ if (process.argv.slice(2).includes("--help")) {
61
+ console.log(
62
+ "usage: node verify-publish.js --repo-path <path> --commit <sha> --base <sha> --task-id <uuid> --slug <artifact-slug> --project-id <id> [--spaces-root <dir>]\n" +
63
+ "the mechanical publish verification JUDGE: certifies read-back findings against git diff, checks manifest freshness, stamps provenance"
64
+ );
65
+ process.exit(0);
66
+ }
54
67
 
55
68
  function arg(name, required = true) {
56
69
  const i = process.argv.indexOf(name);
@@ -89,10 +102,25 @@ function api(command, json) {
89
102
  });
90
103
  return JSON.parse(out);
91
104
  }
105
+ // The terminal verdict this writer emits. A single const (not an inline
106
+ // literal) so the emitted verb and the asserted verb can never drift apart.
107
+ const TERMINAL_VERB = "publish: verification-failed";
92
108
  function terminal(reason, detail) {
93
109
  // Always lands a terminal parent verdict, then exits 1 (failed) — the
94
110
  // task stays parked for human attention. Only the verified path exits 0.
95
- const msg = `publish: verification-failed ${commit} ${reason}${detail ? ` — ${detail}` : ""} (read-back ${inspectionId})`;
111
+ // Writer-side registry guard (D7): the verb is in the terminal-note
112
+ // registry before the note is written — a writer can never introduce a
113
+ // terminal verb the scans don't recognize.
114
+ if (!matchTerminalPublishNote(TERMINAL_VERB)) {
115
+ // Registry and writer disagree — fail loud, never emit an unrecognized
116
+ // terminal note.
117
+ process.stderr.write(JSON.stringify({
118
+ ok: false, verdict: "vocabulary-mismatch",
119
+ message: `terminal verb '${TERMINAL_VERB}' is not in the terminal-note registry`,
120
+ }) + "\n");
121
+ process.exit(2);
122
+ }
123
+ const msg = `${TERMINAL_VERB} ${commit} ${reason}${detail ? ` — ${detail}` : ""} (read-back ${inspectionId})`;
96
124
  api("log-event", { task_id: taskId, type: "note", message: msg.slice(0, 1000) });
97
125
  process.stderr.write(JSON.stringify({ ok: false, verdict: reason, message: msg }) + "\n");
98
126
  process.exit(1);
@@ -298,25 +326,35 @@ if (head !== commit) {
298
326
  // 2026-09-18, blocker 4: bind by exact (task_id, commit, attempt) when the
299
327
  // attempt is known — the original and retry attempts share a commit, and
300
328
  // positional selection is luck. Within one attempt, select the OLDEST
301
- // submitted entry (the trigger issuance; a receipt-bearing second "submitted"
302
- // may follow it — the oldest binds the trigger instant, the latest carries
303
- // the observed agent_id).
329
+ // tick-worker "submitted" entry (the worker's issuance; issuer anchor
330
+ // 2026-09-20). A workflow-written "submitted" (issuer absent, the
331
+ // pre-one-party shape) is a phantom claim — never a baseline source, never
332
+ // an anchor — so skip it silently, exactly as findTriggerEntry does.
333
+ // D1 (2026-09-19; entry_kind cut 2026-09-20): the trigger instant is the
334
+ // ISSUANCE instant, not the ledger-write instant — a slow ledger write must
335
+ // not make a fresh build look stale (room #23 J1). The anchor is the OLDEST
336
+ // tick-worker "submitted" entry by ts and the bound instant is its issued_at
337
+ // (falling back to its ts when the capture was unobserved). One anchor, one
338
+ // field — the retired entry_kind preference filter could never change the
339
+ // bound anchor (issuance and receipt entries carried the same captured
340
+ // issued_at), so the oldest-by-ts selection is the whole mechanism.
304
341
  let triggerTs = null;
305
342
  let manifestBefore = null;
306
343
  try {
307
344
  const ledgerText = readFileSync(join(crewHome, ".publish-ledger", slug + ".jsonl"), "utf8");
308
345
  const entries = ledgerText.split("\n").filter((l) => l.trim()).map((l) => JSON.parse(l));
309
- // Collect all submitted entries matching (task_id, commit[, attempt]).
346
+ // Collect the tick-worker's issuance entries matching (task_id, commit[, attempt]).
310
347
  const candidates = entries.filter((e) =>
311
348
  e.task_id === taskId &&
312
349
  e.outcome === "submitted" &&
350
+ e.issuer === "tick-worker" &&
313
351
  e.commit === commit &&
314
352
  (attempt == null || e.attempt === attempt)
315
353
  );
316
354
  if (candidates.length > 0) {
317
355
  candidates.sort((a, b) => (a.ts || "").localeCompare(b.ts || ""));
318
356
  const e = candidates[0];
319
- triggerTs = e.ts || null;
357
+ triggerTs = e.issued_at || e.ts || null;
320
358
  if (e.manifest_before && typeof e.manifest_before === "object") manifestBefore = e.manifest_before;
321
359
  }
322
360
  } catch (e) {
@@ -1,3 +1,4 @@
1
+ #!/usr/bin/env node
1
2
  // write-ooda-verdict.js — deterministic writer for the OODA report's terminal state.
2
3
  //
3
4
  // After the see-act loop, the QA/repro agent records its verdict as
@@ -34,10 +35,9 @@
34
35
  //
35
36
  // Exit 0 on success, 2 on bad input. Determinism: no wall-clock reads, no
36
37
  // randomness; ts comes only from --ts and is omitted when not passed.
37
- "use strict";
38
38
 
39
- const { writeFileSync, appendFileSync, mkdirSync, readFileSync, existsSync } = require("node:fs");
40
- const { join, resolve } = require("node:path");
39
+ import { writeFileSync, appendFileSync, mkdirSync, readFileSync, existsSync } from "node:fs";
40
+ import { join, resolve } from "node:path";
41
41
 
42
42
  const VERDICTS = { PASS: 1, FAIL: 1, NOT_POSSIBLE: 1 };
43
43
 
@@ -85,6 +85,15 @@ function ledgerSeq(ledgerPath) {
85
85
  }
86
86
 
87
87
  function main() {
88
+ // --help: before required-arg parsing (shebang⇔CLI contract).
89
+ if (process.argv.slice(2).includes("--help")) {
90
+ console.log(
91
+ "usage: node write-ooda-verdict.js --dir <phase-dir> --attempt <id> --verdict <PASS|FAIL|NOT_POSSIBLE>\n" +
92
+ " [--summary <text>] [--expected <text>] [--actual <text>] [--missing <json-array>] [--reason <text>] [--ts <iso>]\n" +
93
+ "--reason is REQUIRED and must be non-empty for FAIL and NOT_POSSIBLE"
94
+ );
95
+ process.exit(0);
96
+ }
88
97
  const args = parseArgs(process.argv.slice(2));
89
98
  if (!args.dir) fail("missing --dir <phase-dir>");
90
99
  if (args.attempt === undefined || String(args.attempt).trim() === "") {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "muse-crew",
3
- "version": "0.14.3",
3
+ "version": "0.14.5",
4
4
  "description": "Opinionated orchestration for Muse — workflows, identities, and tooling for autonomous software development.",
5
5
  "license": "UNLICENSED",
6
6
  "private": false,
@@ -2,6 +2,15 @@
2
2
 
3
3
  You are the dispatch trigger for Muse Crew. Run the authoritative dispatcher workflow and launch its claims autonomously.
4
4
 
5
+ ### Known blockers — read first, do not re-derive or re-report
6
+
7
+ The failures below are settled and recorded in the Gate 1 OODA state. The authoritative list lives at `~/workspace/goals/gate-1-onboarding-canary-passes/hidden_files/ooda/known-blockers.md` — if this copy ever disagrees with that file, the file wins. Their fixes are already queued in the main repo BUILD. If you observe one, log exactly one line — `known-blocker: <id> — <one-line observation>` — and move on. Do NOT write root-cause analysis and do NOT write "follow-up needed"; the follow-up is already owned.
8
+
9
+ - **Blocker 22 — wrong publication caller (settled 2026-09-20):** workflow children cannot call `artifact_edit` (the builder requires a parent-conversation session id they don't have). The 0.14.x trigger/observation path is dead by architecture; `submitted` writes from it are phantom. Fix in BUILD: one-party worker-owned artifact publication.
10
+ - **Blocker 23 — retry harness cannot access Crew state (settled 2026-09-20):** `lib/retry-publish.js` imported `better-sqlite3` (not shipped; the release uses `node:sqlite`) and targeted `crew.db` instead of the real `crew-state.db`. Every retry prepare returned `db-unavailable` and failed closed before any lock, edit, or journal write. Fixed in BUILD (0.14.5): ported to `node:sqlite` + `$CREW_HOME/crew-state.db`, the missing `CREW_REPO` for the merge lock supplied, and the trigger step converted from a spawned child to the session-carrying tick worker's direct `artifact_edit` call (blocker 22). The retry path is worker-owned issuance now; it is subsumed under one-party publish, not a separate queue.
11
+
12
+ (Maintainer note: this snapshot is refreshed from known-blockers.md at release time. The poller always treats the file as authoritative.)
13
+
5
14
  ### Steps
6
15
 
7
16
  0. **Check for platform workflow failures (opacity killer):** The platform records workflow run failures in `runtime.workflow_runs` — these are invisible in the crew DB unless you check. A workflow that dies on a fatal `agent()` error (e.g. "subagent bootstrap is no longer authorized") leaves its task stranded with no explanation.
@@ -39,7 +48,7 @@ You are the dispatch trigger for Muse Crew. Run the authoritative dispatcher wor
39
48
 
40
49
  4.4. **Unknown-recovery (docs/publish-unknown-recovery.md):** tasks parked with "Publish outcome unknown" are the receipt-less trigger gap (2026-09-18, blocker 15). The note is the state machine; deterministic code owns every transition; you are the ferry (scan → classifier → record / retry protocol). Run this BEFORE Step 4.5 so a re-triggered build's mirrored verification-requested is visible to the verification scan on a later tick. Log every list the scan returns.
41
50
  - Scan (code): `node {crewHome}/lib/crew-api.js --crew-home {crewHome} scan-publish-unknown`
42
- Returns `{ waiting: [...], due: [...], retry_due: [...], mirrored: [...], skipped: [...] }`. `waiting` = parked < 30m — leave alone, no claim. `mirrored`/`skipped` = already handled by code — log only, take no action.
51
+ Returns `{ waiting: [...], due: [...], intent: [...], retry_due: [...], mirrored: [...], skipped: [...], requeued: [...] }`. `waiting` = parked < 30m — leave alone, no claim. `mirrored`/`skipped` = already handled by code — log only, take no action. `requeued` = stale-base intents sent back to the workflow's Publish for re-preparation — log only, take no action. `intent` = claimed publish intents — handle in Step 4.4b.
43
52
  - For each entry in `due`: classify (code), then record the decision (code). Use the entry's fields verbatim.
44
53
  - `node {crewHome}/lib/classify-publish-absence.js --repo-path "<repo_path>" --commit <commit> --base <base> --slug "<slug>" --trigger-ts "<trigger_ts>" --park-ts "<park_ts>" --task-id <task_id> --manifest-before '<manifest_before JSON>'`
45
54
  Serialize the entry's `manifest_before` field to compact JSON for the `--manifest-before` value; when the entry's `manifest_before` is null, omit the flag (the classifier falls back to the time-based advance check and the Step 4.5 verifier fails closed without a baseline).
@@ -56,12 +65,8 @@ You are the dispatch trigger for Muse Crew. Run the authoritative dispatcher wor
56
65
  - Exit 0 with `{"ok": true, "outcome": "superseded"|"lock-held"|"not-dropped", ...}`: terminal for this tick. Do NOT proceed to the trigger. If `outcome` is `not-dropped`, the output carries the full `classification` — pass it to `record-retry-recheck`: `node {crewHome}/lib/crew-api.js --crew-home {crewHome} record-retry-recheck --json '{"task_id": "<task_id>", "decision": <classification JSON>}'` and STOP for this task.
57
66
  - Exit non-zero: the command failed (lock released via try/finally). Log the stderr line and STOP — do NOT trigger.
58
67
 
59
- Trigger (agent-mediated — the ONLY prose-owned step):
60
- Spawn ONE child (subagent) whose first instruction loads the artifact namespace (`tool_search.load_tool_namespace` with paths `["artifact"]`), with this exact brief — the same shape as the workflow's first attempt:
61
- "The change to apply is the unified diff in the file \"<diff_path>\" (sha256 <diff_sha256>).
62
- 1. Verify the file: run sha256sum on it. If the printed hash is not exactly <diff_sha256>, STOP and end your turn — do not call artifact_edit.
63
- 2. Read the file's full content.
64
- 3. Call artifact_edit with slug \"<slug>\" and verbatim_request:
68
+ Trigger (tick-mediated — the ONLY prose-owned step):
69
+ Load the artifact namespace in YOUR OWN turn (`tool_search.load_tool_namespace` with paths `["artifact"]`) and call `artifact_edit` directly — NEVER spawn a child (blocker 22, room #24: children cannot reach artifact_edit). First verify the staged diff: `sha256sum "<diff_path>"` must print exactly `<diff_sha256>`; if it does not, STOP and end this task's retry (log loudly — the claim expires, the next tick re-claims; a mismatched diff never becomes an edit). Read the verified file's full content and call artifact_edit with slug `"<slug>"` and this verbatim_request:
65
70
  'Apply the following change to your source tree, then rebuild and deploy.
66
71
 
67
72
  UNIFIED DIFF (relative to your source tree):
@@ -76,22 +81,51 @@ You are the dispatch trigger for Muse Crew. Run the authoritative dispatcher wor
76
81
  - If any hunk does not apply cleanly, STOP and report the failure — do not improvise or skip hunks.
77
82
  - Do not make any other source changes.
78
83
  - After applying, rebuild and deploy.'
79
- Edit-request contract (read carefully):
80
- - Call artifact_edit exactly once with the slug and verbatim_request above. Never retry the edit yourself: if the edit is not accepted, do NOT call artifact_edit again — end your turn.
81
- - If artifact_edit explicitly refuses the edit (the call is rejected — e.g. the artifact does not exist), do NOT call artifact_edit again: end your turn with exactly one line and nothing else: ARTIFACT_EDIT_REFUSED: <the refusal text, one line>.
82
- - If artifact_edit is not available after the load, do NOT improvise — end your turn.
83
- - You do NOT call setprovenance, artifact_inspect, or post-deploy yourself.
84
- No report is needed: do not return JSON, do not summarize what you did, do not echo the diff. End your turn after the artifact_edit call."
85
- Wait for the child to finish. The child's final text is your only signal.
84
+ Edit-request contract:
85
+ - Call artifact_edit exactly once with the slug and verbatim_request above. Never retry the edit yourself: if the edit is not accepted, do NOT call artifact_edit again — proceed to the complete phase below.
86
+ - If artifact_edit explicitly refuses the edit (the call is rejected — e.g. the artifact does not exist), do NOT call artifact_edit again: record the first line of the refusal for the complete phase below.
87
+ - If artifact_edit is not available after the load, do NOT improvise — log it and proceed to the complete phase with `refused` and the refusal text "artifact tool namespace unavailable in tick".
86
88
 
87
89
  Phase 2 — complete (deterministic):
88
90
  `node {crewHome}/lib/retry-publish.js --crew-home {crewHome} --task-id <task_id> --phase complete --edit-outcome <accepted|refused> [--refusal-text "<first line>"]`
89
- - If the child's final text starts with `ARTIFACT_EDIT_REFUSED`: `--edit-outcome refused --refusal-text "<first line of the refusal>"`.
91
+ - If artifact_edit explicitly refused the edit: `--edit-outcome refused --refusal-text "<first line of the refusal>"`.
90
92
  - Otherwise: `--edit-outcome accepted`.
91
- - The command writes the outcome notes, appends the `-retry1` ledger entry (with the pre-retry `manifest_before` baseline), mirrors verification-requested, and RELEASES the lock in a finally block. Exit 0 means the protocol completed.
93
+ - The command writes the outcome notes, appends the `-retry1` ledger entry (issuer `tick-worker`, with the pre-retry `manifest_before` baseline), mirrors verification-requested, and RELEASES the lock in a finally block. Exit 0 means the protocol completed.
92
94
 
93
95
  - Crash rules (fail closed, never re-trigger blind): if this tick dies anywhere in the retry protocol, the note journal is the recovery state. `publish: retry-intended` without `retry-issued` → the next scan mirrors `verification-requested` (never re-triggers). `publish: retry-issued` without a mirrored request → the next scan mirrors it. A leaked merge lock expires after 600s. The next tick's scan never emits `retry_due` for a task with `retry-issued` in history — exactly one retry, enforced from notes, not from the ledger.
94
96
 
97
+ 4.4b. **One-party intent issuance (2026-09-20, blocker 22):** the workflow parks at Publish with a `publish: publish-requested` note and a checksummed `publish-intent` ledger entry; issuance belongs to YOU, the session-carrying tick worker, calling `artifact_edit` directly in this turn. The Step 4.4 scan returns the `intent: [...]` bucket — each entry is claimed for this tick (1-hour lease) and carries `task_id`, `commit`, `attempt`, `slug`, `repo_path`, `base`, `diff_path`, `diff_sha256`, `manifest_before`, `claim_expiry`, and `reclaimed`.
98
+ - For each intent entry:
99
+ 1. **Verify the staged diff (code):** `sha256sum "<diff_path>"` must print exactly `diff_sha256`. If the file is missing, regenerate deterministically and re-verify:
100
+ `node {crewHome}/lib/compute-publish-diff.js --repo-path "<repo_path>" --base <base> --commit <commit> --out "<diff_path>"`
101
+ then re-run the sha256sum. The `--commit` flag is honored: the script diffs `base..<commit>` (never an implicit HEAD) and reports that target as `commit`. If the regeneration exits non-zero, or the hash still does not match after regeneration, the diff is UNISSUABLE — record it terminally (code, never prose):
102
+ `node {crewHome}/lib/crew-api.js --crew-home {crewHome} record-intent-unissuable --json '{"task_id": "<task_id>", "claim_expiry": "<claim_expiry>", "reason": "<missing-staged-diff|diff-unregenerable|hash-mismatch-after-regeneration>"}'`
103
+ (copy `claim_expiry` verbatim from the scan entry — it is the compare-and-swap). This writes the terminal `publish: publish-unissuable` note and an issuer-stamped ledger entry — parked for human attention. Never re-claim, never loop on an hourly reclaim; a diff that cannot be checksummed never becomes an edit.
104
+ 2. **Freshness check on a re-claimed intent (code, never prose):** when the entry's `reclaimed` is true, a previous tick died mid-issuance and may have issued the edit without recording it. Run the deterministic freshness helper — it reads the on-disk artifact manifest at `~/workspace/ts-spaces/<slug>/.space-build/manifest.json` (the same path the verifier uses) and compares `content_sha256`/`built_at` against the entry's `manifest_before`:
105
+ `node {crewHome}/lib/check-intent-freshness.js --slug "<slug>" --manifest-before '<manifest_before JSON>'`
106
+ (copy the entry's `manifest_before` verbatim as JSON; use the entry's `slug` verbatim). The helper prints exactly one JSON object: `{ok:true, advanced:<true|false>, baseline:{...}, current:{...}}`. If `advanced` is true → the edit is already live: record `recovered` in step 4 below (passing the helper's stdout JSON as `freshness_evidence`) and NEVER re-issue. If `advanced` is false → the manifest still shows the pre-edit state: proceed to issuance. If the helper exits non-zero or prints `ok:false` → the freshness question is unanswerable: record NOTHING, log loudly in one line, and let the claim expire — the next scan re-claims. Never decide this from prose, from `artifact_status`, or from an inspector's summary.
107
+ 3. **Issue the edit directly — in this turn, never in a child.** Blocker 22 is proven: workflow children cannot reach `artifact_edit` (room #24's four journeys died on exactly this). Load the artifact namespace in your own turn and call `artifact_edit` with the entry's `slug` and this verbatim_request (paste the verified diff file's full content verbatim):
108
+ 'Apply the following change to your source tree, then rebuild and deploy.
109
+
110
+ UNIFIED DIFF (relative to your source tree):
111
+ ```diff
112
+ <the full content of the verified file, pasted verbatim>
113
+ ```
114
+
115
+ Rules:
116
+ - For each file in the diff, apply its hunks to the same path in your source tree (use git apply or equivalent).
117
+ - For a new file (--- /dev/null), create it with the added (+) lines as its full content.
118
+ - For a deleted file (+++ /dev/null), delete it.
119
+ - If any hunk does not apply cleanly, STOP and report the failure — do not improvise or skip hunks.
120
+ - Do not make any other source changes.
121
+ - After applying, rebuild and deploy.'
122
+ Call artifact_edit exactly once. Never retry it yourself. If the call is explicitly refused, note the first line of the refusal text.
123
+ `accepted` means exactly one thing: the direct `artifact_edit` call completed without error and without an explicit refusal. Tool unavailable (the artifact namespace would not load), a timeout, a transport error, or any otherwise ambiguous result is NOT accepted and NOT refused — it is inconclusive: record NOTHING for this task, log the ambiguity in one line, and let the claim expire; the next scan re-claims and step 2's freshness check decides. An unrecorded outcome is the only honest record of an unknown one.
124
+ 4. **Record the outcome (code):**
125
+ `node {crewHome}/lib/crew-api.js --crew-home {crewHome} record-intent-issuance --json '{"task_id": "<task_id>", "claim_expiry": "<claim_expiry>", "edit_outcome": "<accepted|refused|recovered>", "refusal_text": "<first line>"}'`
126
+ (omit `refusal_text` unless refused). For `recovered`, pass `freshness_evidence` with the step-2 helper's stdout JSON verbatim — the command refuses a recovery without machine evidence. Copy `claim_expiry` verbatim from the scan entry — it is the compare-and-swap. If the command returns `recorded: false`, log the reason and stop: another tick owns the task. `accepted`/`recovered` writes the issuer-stamped `submitted` ledger entry and mirrors `publish: verification-requested` (the Step 4.5 scan claims it on this or a later tick); `refused` writes `rejected` and the terminal `publish: publish-refused` note. There is no outcome for an inconclusive call (step 3) — do not call record-intent-issuance at all when the edit's fate is unknown.
127
+ - Crash rules (fail closed, never blind re-issue): if this tick dies between the artifact_edit call and the record step, the next scan re-claims with `reclaimed: true` and step 2's manifest-freshness check decides between `recovered` and a fresh issuance. The issuance contract is at-most-one RECORDED issuance per attempt: a duplicate physical issuance remains possible across a lease expiry when the first attempt's fate went unrecorded (the dead tick issued but died before recording), but it is content-identical — the same checksummed diff — and the verifier's content check bounds it. If the tick dies before issuing, the claim simply expires and the next tick claims it.
128
+
95
129
  4.5. **Parent publish verification (docs/publish-verification.md):** The publisher parks instead of stamping provenance; the parent — this tick, the live root agent — verifies content and stamps. Deterministic code detects, reads back, and certifies; you are only the ferry between the deterministic steps (scan → sensor → verifier).
96
130
  - Scan (code): `node {crewHome}/lib/crew-api.js --crew-home {crewHome} scan-verification-pending`
97
131
  This atomically claims each verification-pending task (1-hour lease, so a second tick cannot double-verify) and reconciles verified-but-still-parked tasks to `in_progress`. It returns `{ to_verify: [...], reconciled: [...] }`. Log both lists. If the scan exits 2 (e.g. the active release cannot be resolved), log the error loudly and continue — do NOT work around it.