muse-crew 0.14.4 → 0.14.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +1 -1
- package/docs/decisions/publish-path.md +53 -0
- package/docs/guide.md +2 -2
- package/docs/publish-unknown-recovery.md +10 -3
- package/docs/publish-verification.md +117 -61
- package/docs/release-integrity.md +5 -3
- package/docs/reviews/critic-0145.md +106 -0
- package/lib/AGENTS.md +2 -1
- package/lib/check-intent-freshness.js +101 -0
- package/lib/compute-publish-diff.js +34 -13
- package/lib/crew-api.js +437 -18
- package/lib/publish-note-vocabulary.js +20 -0
- package/lib/retry-publish.js +45 -33
- package/lib/verify-publish.js +11 -9
- package/package.json +1 -1
- package/seed/cron-body-template.md +50 -16
- package/workflows/bugfix.js +96 -627
- package/workflows/chore.js +96 -627
- package/workflows/crew-dispatch.js +36 -0
- package/workflows/standard.js +104 -636
package/lib/retry-publish.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
// retry-publish.js — Deterministic unknown-recovery retry protocol (2026-09-18, should-fix 5
|
|
2
|
+
// retry-publish.js — Deterministic unknown-recovery retry protocol (2026-09-18, should-fix 5;
|
|
3
|
+
// ported 2026-09-20: node:sqlite + crew-state.db (blocker 23) and direct
|
|
4
|
+
// tick-worker issuance (blocker 22 — children cannot reach artifact_edit).
|
|
3
5
|
//
|
|
4
6
|
// The cron Step 4.4 retry protocol was prose-owned: the cron agent performed
|
|
5
7
|
// lock handling, journaling, diff generation, ledger writes, mirroring, and
|
|
@@ -8,19 +10,20 @@
|
|
|
8
10
|
//
|
|
9
11
|
// This command owns all deterministic transitions with try/finally lock
|
|
10
12
|
// release. Only the unavoidable `artifact_edit` tool call remains
|
|
11
|
-
//
|
|
12
|
-
// cannot load)
|
|
13
|
+
// tick-mediated (it requires the artifact namespace, which this process
|
|
14
|
+
// cannot load): the session-carrying tick worker issues the edit directly
|
|
15
|
+
// in its own turn — NEVER a child (blocker 22, room #24).
|
|
13
16
|
//
|
|
14
17
|
// Two phases:
|
|
15
18
|
// Phase 1 (prepare): acquire merge lock, re-check HEAD, re-classify,
|
|
16
19
|
// journal intent, generate diff. Outputs JSON with the diff path/sha
|
|
17
|
-
// for the
|
|
18
|
-
// (the
|
|
20
|
+
// for the tick worker's direct artifact_edit call. The lock is HELD on
|
|
21
|
+
// success (the tick must call phase 2 to release it).
|
|
19
22
|
// Phase 2 (complete): given the artifact_edit outcome (accepted/refused),
|
|
20
23
|
// write the outcome notes, append the ledger entry, mirror verification,
|
|
21
24
|
// and RELEASE the lock in a finally block.
|
|
22
25
|
//
|
|
23
|
-
// Crash safety: if the
|
|
26
|
+
// Crash safety: if the tick dies between phases, the lock expires after
|
|
24
27
|
// 600s and the note journal (`publish: retry-intended` without
|
|
25
28
|
// `publish: retry-issued`) lets the next scan mirror verification-requested
|
|
26
29
|
// without re-triggering — exactly one retry, enforced from notes.
|
|
@@ -95,25 +98,24 @@ function writePublishNote(db, taskId, message) {
|
|
|
95
98
|
).run(uuid(), taskId, message, nowISO());
|
|
96
99
|
}
|
|
97
100
|
|
|
98
|
-
// Load the crew DB
|
|
99
|
-
//
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
} catch {
|
|
104
|
-
fail("db-unavailable", "better-sqlite3 is not available to retry-publish.js");
|
|
105
|
-
}
|
|
106
|
-
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");
|
|
107
106
|
if (!existsSync(dbPath)) fail("no-db", `crew database not found at ${dbPath}`);
|
|
108
|
-
const db = new
|
|
107
|
+
const db = new DatabaseSync(dbPath);
|
|
109
108
|
|
|
110
109
|
const EMPTY_TREE = "4b825dc642cb6eb9a060e54bf8d69288fbee4904";
|
|
111
110
|
|
|
112
|
-
|
|
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) {
|
|
113
115
|
const lockScript = join(crewHome, "lib", "merge-lock.sh");
|
|
114
116
|
try {
|
|
115
117
|
execFileSync(lockScript, [action, taskId], {
|
|
116
|
-
env: { ...process.env, CREW_HOME: crewHome },
|
|
118
|
+
env: { ...process.env, CREW_HOME: crewHome, CREW_REPO: repoPath },
|
|
117
119
|
encoding: "utf8",
|
|
118
120
|
timeout: 30000,
|
|
119
121
|
});
|
|
@@ -126,7 +128,7 @@ function runMergeLock(action) {
|
|
|
126
128
|
// --- Phase 1: prepare --------------------------------------------------------
|
|
127
129
|
// Acquires the lock, re-checks HEAD, re-classifies, journals intent,
|
|
128
130
|
// generates the diff. On success the lock is HELD and the output carries
|
|
129
|
-
// the diff path/sha for the
|
|
131
|
+
// the diff path/sha for the tick worker's direct artifact_edit call.
|
|
130
132
|
|
|
131
133
|
if (phase === "prepare") {
|
|
132
134
|
// Derive the unknown attempt (same derivation as scan-publish-unknown).
|
|
@@ -138,6 +140,7 @@ if (phase === "prepare") {
|
|
|
138
140
|
const slug = project && project.deploy_slug;
|
|
139
141
|
if (!slug) fail("no-slug", "project has no deploy_slug");
|
|
140
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");
|
|
141
144
|
|
|
142
145
|
const ledgerPath = join(crewHome, ".publish-ledger", slug + ".jsonl");
|
|
143
146
|
let entries = [];
|
|
@@ -174,7 +177,7 @@ if (phase === "prepare") {
|
|
|
174
177
|
}
|
|
175
178
|
|
|
176
179
|
// 2. Acquire the merge lock.
|
|
177
|
-
const lock = runMergeLock("acquire");
|
|
180
|
+
const lock = runMergeLock("acquire", repoPath);
|
|
178
181
|
if (!lock.ok) {
|
|
179
182
|
process.stdout.write(JSON.stringify({
|
|
180
183
|
ok: true, phase: "prepare", outcome: "lock-held",
|
|
@@ -188,7 +191,7 @@ if (phase === "prepare") {
|
|
|
188
191
|
let lockHeld = true;
|
|
189
192
|
const releaseLock = () => {
|
|
190
193
|
if (lockHeld) {
|
|
191
|
-
runMergeLock("release");
|
|
194
|
+
runMergeLock("release", repoPath);
|
|
192
195
|
lockHeld = false;
|
|
193
196
|
}
|
|
194
197
|
};
|
|
@@ -205,15 +208,19 @@ if (phase === "prepare") {
|
|
|
205
208
|
}
|
|
206
209
|
|
|
207
210
|
const classifierPath = join(crewHome, "lib", "classify-publish-absence.js");
|
|
208
|
-
// Find the trigger entry for the manifest baseline: the OLDEST
|
|
209
|
-
// entry by ts (D1, 2026-09-19; entry_kind cut
|
|
210
|
-
//
|
|
211
|
-
//
|
|
212
|
-
//
|
|
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.
|
|
213
219
|
// agent_id is null on the issuance entry (no receipt observed at trigger
|
|
214
220
|
// time) — the baseline comes from manifest_before, never agent_id.
|
|
215
221
|
const submitted = entries.filter((e) =>
|
|
216
|
-
e.outcome === "submitted" && e.
|
|
222
|
+
e.outcome === "submitted" && e.issuer === "tick-worker" &&
|
|
223
|
+
e.commit === commit && (attempt == null || e.attempt === attempt));
|
|
217
224
|
submitted.sort((a, b) => (a.ts || "").localeCompare(b.ts || ""));
|
|
218
225
|
const trigger = submitted[0] || null;
|
|
219
226
|
const manifestBefore = trigger && trigger.manifest_before ? JSON.stringify(trigger.manifest_before) : null;
|
|
@@ -295,8 +302,8 @@ if (phase === "prepare") {
|
|
|
295
302
|
fail("diff-not-ok", `compute-publish-diff.js returned ok:false — intent note stands; next tick mirrors verification-requested`);
|
|
296
303
|
}
|
|
297
304
|
|
|
298
|
-
// Success: lock remains HELD. The
|
|
299
|
-
// 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.
|
|
300
307
|
// Persist the phase-1 state for phase 2 (crash recovery reads the notes,
|
|
301
308
|
// but phase 2 needs the exact commit/attempt/baseline/ledger path).
|
|
302
309
|
const statePath = retryStatePath();
|
|
@@ -313,7 +320,7 @@ if (phase === "prepare") {
|
|
|
313
320
|
task_id: taskId, commit, attempt,
|
|
314
321
|
diff_path: diffPath, diff_sha256: diffMeta.sha256,
|
|
315
322
|
slug,
|
|
316
|
-
message: "lock held;
|
|
323
|
+
message: "lock held; issue the artifact_edit directly in this tick (the session-carrying tick worker owns issuance), then call phase 2 (complete)",
|
|
317
324
|
}) + "\n");
|
|
318
325
|
process.exit(0);
|
|
319
326
|
} catch (e) {
|
|
@@ -339,12 +346,16 @@ if (phase === "complete") {
|
|
|
339
346
|
fail("no-prepare-state", `no phase-1 state at ${statePath} — the prepare phase did not complete or the state was cleaned`);
|
|
340
347
|
}
|
|
341
348
|
const state = JSON.parse(readFileSync(statePath, "utf8"));
|
|
342
|
-
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");
|
|
343
354
|
|
|
344
355
|
let lockHeld = true;
|
|
345
356
|
const releaseLock = () => {
|
|
346
357
|
if (lockHeld) {
|
|
347
|
-
runMergeLock("release");
|
|
358
|
+
runMergeLock("release", repoPath);
|
|
348
359
|
lockHeld = false;
|
|
349
360
|
}
|
|
350
361
|
};
|
|
@@ -356,7 +367,7 @@ if (phase === "complete") {
|
|
|
356
367
|
`publish: retry-refused ${commit} — the platform refused the re-issued edit (${refusalText.split("\n")[0].slice(0, 200)}); not retrying again.`);
|
|
357
368
|
process.stdout.write(JSON.stringify({ ok: true, phase: "complete", outcome: "refused", commit }) + "\n");
|
|
358
369
|
} else {
|
|
359
|
-
// The
|
|
370
|
+
// The tick worker's direct call ended with no explicit refusal —
|
|
360
371
|
// fire-and-forget, same shape as the first attempt.
|
|
361
372
|
writePublishNote(db, taskId,
|
|
362
373
|
`publish: retry-issued ${commit} — the dropped edit was re-issued once; no explicit refusal was observed.`);
|
|
@@ -381,6 +392,7 @@ if (phase === "complete") {
|
|
|
381
392
|
agent_id: null,
|
|
382
393
|
applied_report: null,
|
|
383
394
|
outcome: "submitted",
|
|
395
|
+
issuer: "tick-worker",
|
|
384
396
|
manifest_before: retryBaseline,
|
|
385
397
|
detail: "retry trigger re-issued after provably-dropped classification",
|
|
386
398
|
};
|
package/lib/verify-publish.js
CHANGED
|
@@ -326,26 +326,28 @@ if (head !== commit) {
|
|
|
326
326
|
// 2026-09-18, blocker 4: bind by exact (task_id, commit, attempt) when the
|
|
327
327
|
// attempt is known — the original and retry attempts share a commit, and
|
|
328
328
|
// positional selection is luck. Within one attempt, select the OLDEST
|
|
329
|
-
// submitted entry (the
|
|
330
|
-
//
|
|
331
|
-
//
|
|
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.
|
|
332
333
|
// D1 (2026-09-19; entry_kind cut 2026-09-20): the trigger instant is the
|
|
333
334
|
// ISSUANCE instant, not the ledger-write instant — a slow ledger write must
|
|
334
335
|
// not make a fresh build look stale (room #23 J1). The anchor is the OLDEST
|
|
335
|
-
// submitted entry by ts and the bound instant is its issued_at
|
|
336
|
-
// to its ts when the capture was unobserved). One anchor, one
|
|
337
|
-
// retired entry_kind preference filter could never change the
|
|
338
|
-
// (issuance and receipt entries carried the same captured
|
|
339
|
-
// oldest-by-ts selection is the whole mechanism.
|
|
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.
|
|
340
341
|
let triggerTs = null;
|
|
341
342
|
let manifestBefore = null;
|
|
342
343
|
try {
|
|
343
344
|
const ledgerText = readFileSync(join(crewHome, ".publish-ledger", slug + ".jsonl"), "utf8");
|
|
344
345
|
const entries = ledgerText.split("\n").filter((l) => l.trim()).map((l) => JSON.parse(l));
|
|
345
|
-
// Collect
|
|
346
|
+
// Collect the tick-worker's issuance entries matching (task_id, commit[, attempt]).
|
|
346
347
|
const candidates = entries.filter((e) =>
|
|
347
348
|
e.task_id === taskId &&
|
|
348
349
|
e.outcome === "submitted" &&
|
|
350
|
+
e.issuer === "tick-worker" &&
|
|
349
351
|
e.commit === commit &&
|
|
350
352
|
(attempt == null || e.attempt === attempt)
|
|
351
353
|
);
|
package/package.json
CHANGED
|
@@ -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 (
|
|
60
|
-
|
|
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
|
|
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 —
|
|
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:
|
|
82
|
-
- If artifact_edit is not available after the load, do NOT improvise —
|
|
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
|
|
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.
|