muse-crew 0.11.1 → 0.12.0
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/API.md +5 -3
- package/docs/guide.md +1 -1
- package/docs/publish-verification.md +13 -0
- package/docs/state-boundary-spec.md +37 -0
- package/lib/AGENTS.md +4 -1
- package/lib/advance-publish-base.js +250 -0
- package/lib/classify-surface.js +109 -0
- package/lib/compose-evidence-caption.js +55 -7
- package/lib/compute-publish-diff.js +145 -0
- package/lib/crew-api.js +34 -3
- package/package.json +1 -1
- package/seed/cron-body-template.md +5 -4
- package/workflows/bugfix.js +58 -65
- package/workflows/chore.js +58 -65
- package/workflows/crew-dispatch.js +21 -6
- package/workflows/crew-init.js +50 -0
- package/workflows/standard.js +58 -65
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// compute-publish-diff.js — deterministic publish-diff computer.
|
|
3
|
+
//
|
|
4
|
+
// Room #14 (2026-09-17): the Publish phase asked an agent to run
|
|
5
|
+
// `git diff "$BASE" "$HEAD"` and return the raw unified diff as a JSON
|
|
6
|
+
// string field. The agent produced the diff correctly on disk but the
|
|
7
|
+
// JSON ferry dropped it — parseUnifiedDiff saw zero files and the task
|
|
8
|
+
// parked at "Publish diff parsed to zero files" for a real, valid
|
|
9
|
+
// 700-line / 23-file diff. General rule: NEVER ferry a deterministic
|
|
10
|
+
// unified diff through an LLM JSON response.
|
|
11
|
+
//
|
|
12
|
+
// This script is the deterministic replacement. The workflow's agent is
|
|
13
|
+
// pure hands — it runs exactly one command (this script) and returns the
|
|
14
|
+
// small JSON summary verbatim. The diff bytes live in a file; the LLM
|
|
15
|
+
// never carries them:
|
|
16
|
+
//
|
|
17
|
+
// node compute-publish-diff.js --repo-path <path> --base <sha> --out <file>
|
|
18
|
+
//
|
|
19
|
+
// --base is the previously-stamped provenance source_commit (or the
|
|
20
|
+
// empty-tree sha 4b825dc642cb6eb9a060e54bf8d69288fbee4904 for a first
|
|
21
|
+
// publish) — the SAME base the workflow read from get-provenance. The
|
|
22
|
+
// script echoes the base it used so the workflow can guard against the
|
|
23
|
+
// agent running the wrong command (fail-closed, as before).
|
|
24
|
+
//
|
|
25
|
+
// Stdout contract: exactly one JSON object, always small (no diff text):
|
|
26
|
+
// { ok:true, commit, base, ancestor:"yes", diff_file, sha256, bytes,
|
|
27
|
+
// file_count, changed_lines, files:[<paths>], has_binary, has_rename }
|
|
28
|
+
// { ok:false, error:"not-ancestor", commit, base } (semantic, exit 0)
|
|
29
|
+
//
|
|
30
|
+
// Exit codes: 0 ok (JSON on stdout) · 2 usage/validation · 1 git/diff/IO
|
|
31
|
+
// failure. On non-zero exit NOTHING is printed to stdout, so a caller
|
|
32
|
+
// must never treat stdout as a summary. Errors go to stderr as JSON.
|
|
33
|
+
//
|
|
34
|
+
// Security: --base must be 40-hex (or the empty tree); --repo-path must
|
|
35
|
+
// be an existing directory; git is invoked via execFileSync with an argv
|
|
36
|
+
// array (no shell). The diff file's parent directories are created.
|
|
37
|
+
|
|
38
|
+
import { execFileSync } from "node:child_process";
|
|
39
|
+
import { createHash } from "node:crypto";
|
|
40
|
+
import { existsSync, mkdirSync, statSync, writeFileSync, readFileSync } from "node:fs";
|
|
41
|
+
import { dirname, resolve } from "node:path";
|
|
42
|
+
|
|
43
|
+
const EMPTY_TREE = "4b825dc642cb6eb9a060e54bf8d69288fbee4904";
|
|
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
|
+
function git(repoPath, args) {
|
|
58
|
+
try {
|
|
59
|
+
return execFileSync("git", ["-C", repoPath, ...args], { encoding: "utf8", maxBuffer: 256 * 1024 * 1024 });
|
|
60
|
+
} catch (e) {
|
|
61
|
+
fail("git-failed", `git ${args[0]} failed: ${(e && e.message) || e}`.slice(0, 500), 1);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const repoPath = arg("--repo-path");
|
|
66
|
+
const base = arg("--base");
|
|
67
|
+
const outFile = arg("--out");
|
|
68
|
+
|
|
69
|
+
if (!/^[0-9a-f]{40}$/.test(base)) fail("usage", "--base must be a 40-char hex commit SHA (or the empty-tree SHA).", 2);
|
|
70
|
+
let st = null;
|
|
71
|
+
try { st = statSync(repoPath); } catch { /* falls through */ }
|
|
72
|
+
if (!st || !st.isDirectory()) fail("usage", "--repo-path must be an existing directory.", 2);
|
|
73
|
+
|
|
74
|
+
const head = git(repoPath, ["rev-parse", "HEAD"]).trim();
|
|
75
|
+
if (!/^[0-9a-f]{40}$/.test(head)) fail("git-failed", "rev-parse HEAD did not return a commit SHA.", 1);
|
|
76
|
+
|
|
77
|
+
// Ancestor check — deterministic, in code. The empty tree is not a commit:
|
|
78
|
+
// git merge-base --is-ancestor fails on it, so a first publish hardcodes
|
|
79
|
+
// ANCESTOR=yes here instead of executing the conditional (clean-room
|
|
80
|
+
// 2026-09-16: an agent ran merge-base on the empty tree directly and parked).
|
|
81
|
+
let ancestor = "yes";
|
|
82
|
+
if (base !== EMPTY_TREE) {
|
|
83
|
+
try {
|
|
84
|
+
execFileSync("git", ["-C", repoPath, "merge-base", "--is-ancestor", base, head], { stdio: "ignore" });
|
|
85
|
+
ancestor = "yes";
|
|
86
|
+
} catch {
|
|
87
|
+
ancestor = "no";
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
if (ancestor !== "yes") {
|
|
91
|
+
process.stdout.write(JSON.stringify({ ok: false, error: "not-ancestor", commit: head, base }) + "\n");
|
|
92
|
+
process.exit(0);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const outPath = resolve(outFile);
|
|
96
|
+
mkdirSync(dirname(outPath), { recursive: true });
|
|
97
|
+
const diffText = git(repoPath, ["diff", base, head]);
|
|
98
|
+
writeFileSync(outPath, diffText, "utf8");
|
|
99
|
+
const bytes = readFileSync(outPath);
|
|
100
|
+
const sha256 = createHash("sha256").update(bytes).digest("hex");
|
|
101
|
+
|
|
102
|
+
// summarizeDiff — the parseUnifiedDiff logic, moved out of the workflows
|
|
103
|
+
// (canary run 4, 2026-09-11: the builder's source tree was stale and the
|
|
104
|
+
// workflow stamped a new hash on it — provenance fiction; the parse
|
|
105
|
+
// exists so Publish verifies the artifact builder applied the carried
|
|
106
|
+
// change). Counts changed lines (added + removed), never raw diff output
|
|
107
|
+
// lines: context lines and file headers inflated the old split("\n").length
|
|
108
|
+
// count ~2x, parking a 95-line change against the 200-line budget
|
|
109
|
+
// (Gate 1 Journey 3 attempt 3, 2026-09-13).
|
|
110
|
+
let files = [];
|
|
111
|
+
let current = null;
|
|
112
|
+
let added = 0;
|
|
113
|
+
let removed = 0;
|
|
114
|
+
let has_binary = false;
|
|
115
|
+
let has_rename = false;
|
|
116
|
+
for (const line of diffText.split("\n")) {
|
|
117
|
+
const m = /^diff --git a\/(.*) b\/(.*)$/.exec(line);
|
|
118
|
+
if (m) {
|
|
119
|
+
current = m[2];
|
|
120
|
+
files.push(current);
|
|
121
|
+
continue;
|
|
122
|
+
}
|
|
123
|
+
if (/^Binary files /.test(line)) { has_binary = true; continue; }
|
|
124
|
+
if (/^rename from /.test(line)) { has_rename = true; continue; }
|
|
125
|
+
if (current === null) continue;
|
|
126
|
+
if (/^--- /.test(line) || /^\+\+\+ /.test(line)) continue;
|
|
127
|
+
if (/^@@ /.test(line)) continue;
|
|
128
|
+
if (line.charAt(0) === "+") added++;
|
|
129
|
+
else if (line.charAt(0) === "-") removed++;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
process.stdout.write(JSON.stringify({
|
|
133
|
+
ok: true,
|
|
134
|
+
commit: head,
|
|
135
|
+
base,
|
|
136
|
+
ancestor: "yes",
|
|
137
|
+
diff_file: outPath,
|
|
138
|
+
sha256,
|
|
139
|
+
bytes: bytes.length,
|
|
140
|
+
file_count: files.length,
|
|
141
|
+
changed_lines: added + removed,
|
|
142
|
+
files,
|
|
143
|
+
has_binary,
|
|
144
|
+
has_rename,
|
|
145
|
+
}) + "\n");
|
package/lib/crew-api.js
CHANGED
|
@@ -28,6 +28,7 @@ import { randomUUID } from "node:crypto";
|
|
|
28
28
|
import { execFileSync } from "node:child_process";
|
|
29
29
|
import { DatabaseSync } from "node:sqlite";
|
|
30
30
|
import { homedir } from "node:os";
|
|
31
|
+
import { classifySurface } from "./classify-surface.js";
|
|
31
32
|
|
|
32
33
|
// ---------------------------------------------------------------------------
|
|
33
34
|
// Errors
|
|
@@ -1618,17 +1619,47 @@ commands["create-project"] = (db, args, ctx) => {
|
|
|
1618
1619
|
const repoPath = (args.repo_path ?? "").trim();
|
|
1619
1620
|
if (!repoPath) throw usageError("repo_path is required.");
|
|
1620
1621
|
if (!isGitRepoPath(repoPath)) throw usageError("repo_path must be a valid Git repository path.");
|
|
1621
|
-
|
|
1622
|
+
let deployType = args.deploy_type ?? "";
|
|
1622
1623
|
if (!["npm", "artifact", "vercel", ""].includes(deployType)) throw usageError("bad deploy_type.");
|
|
1623
1624
|
if (db.prepare("SELECT id FROM projects WHERE id = ?").get(id)) {
|
|
1624
1625
|
throw conflict("A project with this ID already exists.");
|
|
1625
1626
|
}
|
|
1626
1627
|
const timestamp = now();
|
|
1627
1628
|
const visualProtocol = validateVisualProtocol(args.visual_protocol);
|
|
1628
|
-
|
|
1629
|
+
// Surface classification default (room #14, 2026-09-17): the setup
|
|
1630
|
+
// conversation never classified the flashcards sample — environment_type
|
|
1631
|
+
// stayed null and no deploy target was set, so Capture skipped
|
|
1632
|
+
// ("surface unclassified"), Publish had nothing to build, and QA had no
|
|
1633
|
+
// runnable surface (J1/J2 parked). Fill OMITTED fields from the
|
|
1634
|
+
// deterministic repo scan (lib/classify-surface.js); explicit values
|
|
1635
|
+
// always win — including an explicit null environment_type, which still
|
|
1636
|
+
// means "unclassified". An artifact surface needs a deploy target:
|
|
1637
|
+
// classification alone doesn't close the loop, because Publish skips
|
|
1638
|
+
// when deploy_type is empty and QA can only run against the built
|
|
1639
|
+
// artifact. The initial-provenance stamp below then keys off the
|
|
1640
|
+
// defaulted deploy_type, so the sample's first publish diffs only the
|
|
1641
|
+
// task's changes instead of the whole tree.
|
|
1642
|
+
let environmentType = validateEnvironmentType(args.environment_type);
|
|
1643
|
+
let deploySlug = args.deploy_slug ?? null;
|
|
1644
|
+
if (args.environment_type === undefined || args.deploy_type === undefined) {
|
|
1645
|
+
let c = null;
|
|
1646
|
+
try { c = classifySurface(repoPath); } catch { c = null; }
|
|
1647
|
+
// Effective surface: an explicit environment_type always wins; the
|
|
1648
|
+
// classifier only speaks for omitted fields. Deploy defaults follow
|
|
1649
|
+
// the effective surface — an explicit "terminal" (or an explicit
|
|
1650
|
+
// null = "unclassified") never triggers artifact deploy defaults.
|
|
1651
|
+
// The slug is only auto-filled when we auto-set the artifact deploy;
|
|
1652
|
+
// a caller who chose their own deploy_type owns their slug.
|
|
1653
|
+
const effectiveSurface = args.environment_type === undefined ? (c && c.surface) : environmentType;
|
|
1654
|
+
if (args.environment_type === undefined && c && c.surface) environmentType = c.surface;
|
|
1655
|
+
if (effectiveSurface === "artifact" && args.deploy_type === undefined) {
|
|
1656
|
+
deployType = "artifact";
|
|
1657
|
+
if (args.deploy_slug === undefined && c && c.deploy_slug) deploySlug = c.deploy_slug;
|
|
1658
|
+
}
|
|
1659
|
+
}
|
|
1629
1660
|
const row = {
|
|
1630
1661
|
id, display_name: displayName, repo_path: repoPath, deploy_type: deployType,
|
|
1631
|
-
deploy_slug:
|
|
1662
|
+
deploy_slug: deploySlug,
|
|
1632
1663
|
description: (args.description ?? "").trim().slice(0, 3000),
|
|
1633
1664
|
simultaneity: args.simultaneity ?? 2,
|
|
1634
1665
|
quiesced: args.quiesced ? 1 : 0,
|
package/package.json
CHANGED
|
@@ -70,13 +70,14 @@ You are the dispatch trigger for Muse Crew. Run the authoritative dispatcher wor
|
|
|
70
70
|
- Dedupe: for each candidate, check `get-events --json '{"task_id": "<id>", "limit": 50}'` for an event whose message starts with `evidence: delivered`. Skip tasks already delivered.
|
|
71
71
|
- For each remaining task:
|
|
72
72
|
- Read its QA verdict: from get-state's `sessions`, the completed session for this task whose notes mention Hazel/QA. Quote one verdict line (pass/fail and the one-line reason).
|
|
73
|
-
- Find the screenshots (platform audit harness): `AUDIT=$(readlink ~/workspace/ts-spaces/<project>/audits/latest)` where `<project>` is the task's project field. Confirm `$AUDIT/screenshot.png` and `$AUDIT/screenshot-mobile.png` exist AND are newer than the task's `created_at` (compare with `stat -c %Y`). If the audit dir predates the task, the capture is stale from an earlier run — say so and do NOT attach it. Never attach screenshots from a different run.
|
|
74
|
-
- Write the transition BEFORE the attachments (2026-09-14 — Eric: screenshots arriving out of context need a problem→solution handoff). Do not hand-write it: run the deterministic composer and paste its stdout verbatim
|
|
73
|
+
- Find the screenshots (platform audit harness): `AUDIT=$(readlink ~/workspace/ts-spaces/<project>/audits/latest)` where `<project>` is the task's project field. Confirm `$AUDIT/screenshot.png` and `$AUDIT/screenshot-mobile.png` exist AND are newer than the task's `created_at` (compare with `stat -c %Y`). Set `CAPTURES_OK=1` only when both PNGs exist and are fresh; otherwise `CAPTURES_OK=0`. If the audit dir predates the task, the capture is stale from an earlier run — say so and do NOT attach it. Never attach screenshots from a different run. Terminal-surface tasks never have harness captures — expect CAPTURES_OK=0 for them.
|
|
74
|
+
- Write the transition BEFORE the attachments (2026-09-14 — Eric: screenshots arriving out of context need a problem→solution handoff). Do not hand-write it: run the deterministic composer and paste its stdout verbatim:
|
|
75
75
|
`node {crewHome}/lib/compose-evidence-caption.js --crew-home {crewHome} --task-id "<id>" --audit-dir "<resolved_dir>"`
|
|
76
|
-
The composer emits: the out-of-context acknowledgment, the task title, Problem: (from the task description), Shipped: (the merge commit's subject + short-sha, or an honest "provenance not yet stamped"), Evidence: (the audit dir + QA verdict, or "
|
|
77
|
-
- Attach
|
|
76
|
+
The composer emits: the out-of-context acknowledgment, the task title, Problem: (from the task description), Shipped: (the merge commit's subject + short-sha, or an honest "provenance not yet stamped"), Evidence: (the audit dir + QA verdict when captures exist, or the honest no-captures line — 2026-09-17: the composer verifies the captures exist before claiming them, so a terminal-surface task with no audit dir gets the honest line, never "desktop + mobile captures below"), and the visual-protocol-unavailable honest label when the run recorded one. If the composer exits non-zero, say so in one line and attach nothing for that task.
|
|
77
|
+
- Attach the two screenshot lines ONLY when CAPTURES_OK=1, each on its own plain line, using the RESOLVED dir name (not the `latest` symlink) so the evidence is pinned to this run:
|
|
78
78
|
``
|
|
79
79
|
``
|
|
80
|
+
When CAPTURES_OK=0, attach nothing — the composer's caption already says so honestly.
|
|
80
81
|
- Log the delivery so the next tick skips it: `node {crewHome}/lib/crew-api.js --crew-home {crewHome} log-event --json '{"task_id": "<id>", "type": "note", "message": "evidence: delivered <id> — screenshots attached to tick report"}'`.
|
|
81
82
|
- If a done task has no fresh screenshots, say so in one line — never claim evidence you don't have.
|
|
82
83
|
|
package/workflows/bugfix.js
CHANGED
|
@@ -81,9 +81,15 @@ const PUBLISH_NPM = RUN_LIB + "/publish-npm.sh";
|
|
|
81
81
|
const CREW_API_PINNED = RUN_LIB + "/crew-api.js";
|
|
82
82
|
const SCHEMA_SQL_SRC = crewHome + "/lib/schema.sql";
|
|
83
83
|
const SCHEMA_SQL_PINNED = RUN_LIB + "/schema.sql";
|
|
84
|
-
|
|
84
|
+
const COMPUTE_DIFF_SRC = crewHome + "/current/lib/compute-publish-diff.js";
|
|
85
|
+
const COMPUTE_DIFF = RUN_LIB + "/compute-publish-diff.js";
|
|
86
|
+
// The six basenames the pin step must materialize — asserted mechanically
|
|
85
87
|
// by workflow code from the verbatim listing, never from agent prose.
|
|
86
|
-
|
|
88
|
+
// COMPUTE_DIFF is the deterministic publish-diff computer (room #14,
|
|
89
|
+
// 2026-09-17): the diff is computed by this script, never ferried as an
|
|
90
|
+
// agent JSON string. Pinned like the other publish-critical modules so a
|
|
91
|
+
// mid-run release swap cannot change it under the workflow.
|
|
92
|
+
const PIN_BASENAMES = [LIFECYCLE, MERGE_LOCK, PUBLISH_NPM, CREW_API_PINNED, SCHEMA_SQL_PINNED, COMPUTE_DIFF].map(function (p) { return p.split("/").pop(); });
|
|
87
93
|
|
|
88
94
|
// Project config — passed by dispatcher, falls back to dashboard defaults
|
|
89
95
|
const projectConfig = inputs.project_config || {};
|
|
@@ -259,7 +265,7 @@ function attemptKey(base, reworkCount) {
|
|
|
259
265
|
return base + (reworkCount > 0 ? "-r" + reworkCount : "");
|
|
260
266
|
}
|
|
261
267
|
// pinLifecycle(key) — snapshot the lifecycle scripts into RUN_LIB and return
|
|
262
|
-
// the verbatim `ls -1` listing so WORKFLOW CODE asserts the
|
|
268
|
+
// the verbatim `ls -1` listing so WORKFLOW CODE asserts the six pinned
|
|
263
269
|
// basenames; the agent cannot self-certify. (The pin step was the one place
|
|
264
270
|
// the workflows trusted agent prose: task 24be1cd6 walked to Publish on an
|
|
265
271
|
// empty pin dir.) Byte-identical across standard/bugfix/chore — pinned by
|
|
@@ -267,7 +273,7 @@ function attemptKey(base, reworkCount) {
|
|
|
267
273
|
function pinLifecycle(key) {
|
|
268
274
|
return agent(
|
|
269
275
|
"Snapshot lifecycle scripts for version pinning.\n" +
|
|
270
|
-
"Run: mkdir -p " + RUN_LIB + " && cp " + LIFECYCLE_SRC + " " + LIFECYCLE + " && cp " + MERGE_LOCK_SRC + " " + MERGE_LOCK + " && cp " + PUBLISH_NPM_SRC + " " + PUBLISH_NPM + " && cp " + CREW_API_SRC + " " + CREW_API_PINNED + " && cp " + SCHEMA_SQL_SRC + " " + SCHEMA_SQL_PINNED + " && chmod +x " + LIFECYCLE + " " + MERGE_LOCK + " " + PUBLISH_NPM + " && ls -1 " + RUN_LIB + "\n" +
|
|
276
|
+
"Run: mkdir -p " + RUN_LIB + " && cp " + LIFECYCLE_SRC + " " + LIFECYCLE + " && cp " + MERGE_LOCK_SRC + " " + MERGE_LOCK + " && cp " + PUBLISH_NPM_SRC + " " + PUBLISH_NPM + " && cp " + CREW_API_SRC + " " + CREW_API_PINNED + " && cp " + SCHEMA_SQL_SRC + " " + SCHEMA_SQL_PINNED + " && cp " + COMPUTE_DIFF_SRC + " " + COMPUTE_DIFF + " && chmod +x " + LIFECYCLE + " " + MERGE_LOCK + " " + PUBLISH_NPM + " && ls -1 " + RUN_LIB + "\n" +
|
|
271
277
|
"Return the verbatim output of the ls -1 command as { \"listing\": \"<verbatim output>\" } and nothing else.",
|
|
272
278
|
{ key: key, label: "Pinning lifecycle scripts",
|
|
273
279
|
schema: { type: "object", properties: { listing: { type: "string" } }, required: ["listing"] } }
|
|
@@ -448,37 +454,6 @@ function hydrateReleaseDecision(rec) {
|
|
|
448
454
|
return null;
|
|
449
455
|
}
|
|
450
456
|
|
|
451
|
-
// Publish diff transport: parse a unified diff into per-file {path, added,
|
|
452
|
-
// removed} line lists. Pure function — no I/O, no clock. Used by Publish to
|
|
453
|
-
// verify the artifact builder applied the carried change (canary run 4,
|
|
454
|
-
// 2026-09-11: the builder's source tree was stale and disconnected from the
|
|
455
|
-
// crew's repo; "rebuild from current source" rebuilt stale code and the
|
|
456
|
-
// workflow stamped the new commit hash on it — provenance fiction).
|
|
457
|
-
function parseUnifiedDiff(diffText) {
|
|
458
|
-
var files = [];
|
|
459
|
-
var current = null;
|
|
460
|
-
var lines = (diffText || "").split("\n");
|
|
461
|
-
for (var i = 0; i < lines.length; i++) {
|
|
462
|
-
var line = lines[i];
|
|
463
|
-
var m = /^diff --git a\/(.*) b\/(.*)$/.exec(line);
|
|
464
|
-
if (m) {
|
|
465
|
-
current = { path: m[2], added: [], removed: [] };
|
|
466
|
-
files.push(current);
|
|
467
|
-
continue;
|
|
468
|
-
}
|
|
469
|
-
if (!current) continue;
|
|
470
|
-
if (/^--- /.test(line) || /^\+\+\+ /.test(line)) continue;
|
|
471
|
-
if (/^@@ /.test(line)) continue;
|
|
472
|
-
if (line.charAt(0) === "+") {
|
|
473
|
-
current.added.push(line.slice(1));
|
|
474
|
-
} else if (line.charAt(0) === "-") {
|
|
475
|
-
current.removed.push(line.slice(1));
|
|
476
|
-
}
|
|
477
|
-
}
|
|
478
|
-
return files;
|
|
479
|
-
}
|
|
480
|
-
|
|
481
|
-
|
|
482
457
|
// Publish read-back request (currently unavailable): the verbatim_request
|
|
483
458
|
// the parent protocol (docs/publish-verification.md) would hand to an
|
|
484
459
|
// independent read-back tool after the artifact build lands. artifact_inspect
|
|
@@ -1586,59 +1561,77 @@ while (i < STEPS.length) {
|
|
|
1586
1561
|
} else if (!/^[0-9a-f]{40}$/.test(publishBase)) {
|
|
1587
1562
|
return await parkTask("Publish base '" + publishBase + "' is not a valid commit SHA — cannot compute the publish diff. Human attention needed.");
|
|
1588
1563
|
}
|
|
1589
|
-
//
|
|
1590
|
-
//
|
|
1591
|
-
//
|
|
1592
|
-
//
|
|
1593
|
-
//
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
1564
|
+
// Publish diff transport (room #14, 2026-09-17): the diff used to be
|
|
1565
|
+
// ferried as a JSON string field in the agent's response — the agent
|
|
1566
|
+
// produced a valid 700-line diff on disk but the JSON ferry dropped
|
|
1567
|
+
// it, and the parse saw zero files ("Publish diff parsed to zero
|
|
1568
|
+
// files"). The diff now travels git -> file -> deterministic script
|
|
1569
|
+
// summary; the LLM never carries diff bytes. The agent is pure hands:
|
|
1570
|
+
// it runs exactly one command (the PINNED compute-publish-diff.js —
|
|
1571
|
+
// a mid-run release swap cannot change it under the workflow) and
|
|
1572
|
+
// returns the small JSON summary verbatim. All fail-closed parks
|
|
1573
|
+
// below are unchanged in meaning.
|
|
1574
|
+
var publishDiffFile = crewHome + "/.publish-diffs/" + taskId + (totalReworkCount > 0 ? "-r" + totalReworkCount : "") + ".diff";
|
|
1597
1575
|
var diffResult = await agent(
|
|
1598
|
-
"Run
|
|
1599
|
-
|
|
1600
|
-
"
|
|
1601
|
-
"if [ \"$ANCESTOR\" = yes ]; then echo '---DIFF---' && git diff \"$BASE\" \"$HEAD\" && echo '---NAMES---' && git diff-tree --no-commit-id --name-only -r \"$BASE\" \"$HEAD\"; fi\n" +
|
|
1602
|
-
"Return JSON { \"commit\": \"<HEAD trimmed>\", \"base\": \"<BASE trimmed>\", \"ancestor\": \"<yes|no>\", \"diff\": \"<raw unified diff, may be multi-line>\", \"files\": \"<newline-separated paths>\" } and nothing else.",
|
|
1576
|
+
"Run exactly one command and nothing else:\n" +
|
|
1577
|
+
"node " + COMPUTE_DIFF + " --repo-path \"" + REPO_PATH + "\" --base " + publishBase + " --out \"" + publishDiffFile + "\"\n" +
|
|
1578
|
+
"The script prints one small JSON object to stdout. Return the stdout verbatim as { \"output\": \"<verbatim stdout>\" } and nothing else. Do not interpret it.",
|
|
1603
1579
|
{ key: attemptKey("publish-artifact-diff-" + taskId, totalReworkCount), label: "Computing publish diff from stamped base",
|
|
1604
|
-
schema: { type: "object", properties: {
|
|
1580
|
+
schema: { type: "object", properties: { output: { type: "string" } }, required: ["output"] } }
|
|
1605
1581
|
);
|
|
1606
|
-
|
|
1607
|
-
|
|
1582
|
+
var diffSummary = null;
|
|
1583
|
+
try {
|
|
1584
|
+
diffSummary = JSON.parse((diffResult.output || "").trim());
|
|
1585
|
+
} catch (e) {
|
|
1586
|
+
return await parkTask("Publish diff computation returned unparseable output — cannot verify application. Human attention needed.");
|
|
1587
|
+
}
|
|
1588
|
+
if (!diffSummary || diffSummary.ok !== true) {
|
|
1589
|
+
if (diffSummary && diffSummary.error === "not-ancestor") {
|
|
1590
|
+
return await parkTask("Publish base " + publishBase.slice(0, 12) + " is not an ancestor of HEAD " + String(diffSummary.commit || "").slice(0, 12) + " — the stamped provenance does not lead to the integrated commit. Human attention needed.");
|
|
1591
|
+
}
|
|
1592
|
+
return await parkTask("Publish diff computation failed (" + ((diffSummary && diffSummary.error) || "unknown error") + ") — cannot verify application. Human attention needed.");
|
|
1608
1593
|
}
|
|
1609
|
-
if (
|
|
1610
|
-
return await parkTask("Publish diff base mismatch:
|
|
1594
|
+
if (diffSummary.base !== publishBase) {
|
|
1595
|
+
return await parkTask("Publish diff base mismatch: script reported '" + String(diffSummary.base || "").slice(0, 12) + "' but the stamped base is '" + publishBase.slice(0, 12) + "'. Human attention needed.");
|
|
1611
1596
|
}
|
|
1612
|
-
var mergeCommitForPublish = (
|
|
1613
|
-
var
|
|
1614
|
-
if (!
|
|
1597
|
+
var mergeCommitForPublish = String(diffSummary.commit || "").trim();
|
|
1598
|
+
var publishDiffSha256 = String(diffSummary.sha256 || "");
|
|
1599
|
+
if (!diffSummary.bytes) {
|
|
1615
1600
|
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.");
|
|
1616
1601
|
}
|
|
1617
|
-
if (
|
|
1618
|
-
return await parkTask("Publish diff contains binary files — the
|
|
1602
|
+
if (diffSummary.has_binary) {
|
|
1603
|
+
return await parkTask("Publish diff contains binary files — the diff transport cannot carry them. Human attention needed.");
|
|
1619
1604
|
}
|
|
1620
|
-
if (
|
|
1605
|
+
if (diffSummary.has_rename) {
|
|
1621
1606
|
return await parkTask("Publish diff contains a rename — the diff transport cannot carry renames. Human attention needed.");
|
|
1622
1607
|
}
|
|
1623
|
-
var expectedChanges = parseUnifiedDiff(mergeDiff);
|
|
1624
1608
|
// Budget counts CHANGED lines (added + removed), not raw unified-diff
|
|
1625
1609
|
// output lines: context lines and file headers inflated the old
|
|
1626
1610
|
// split("\n").length count ~2x, parking a 95-line change against a
|
|
1627
|
-
// 200-line budget (Gate 1 Journey 3 attempt 3, 2026-09-13).
|
|
1628
|
-
|
|
1611
|
+
// 200-line budget (Gate 1 Journey 3 attempt 3, 2026-09-13). The count
|
|
1612
|
+
// now comes from the deterministic script summary, never from the LLM.
|
|
1613
|
+
var mergeDiffChangedLines = diffSummary.changed_lines || 0;
|
|
1629
1614
|
if (mergeDiffChangedLines > 200) {
|
|
1630
1615
|
return await parkTask("Publish diff changes " + mergeDiffChangedLines + " lines (budget 200) — too large for the diff transport. Human attention needed.");
|
|
1631
1616
|
}
|
|
1632
|
-
if (
|
|
1617
|
+
if (!diffSummary.file_count) {
|
|
1633
1618
|
return await parkTask("Publish diff parsed to zero files for commit " + (mergeCommitForPublish || "unknown") + " — cannot verify application. Human attention needed.");
|
|
1634
1619
|
}
|
|
1620
|
+
// The trigger agent reads the diff from the checksummed file — the
|
|
1621
|
+
// workflow never holds diff bytes (room #14, 2026-09-17). The file
|
|
1622
|
+
// is small by construction: the 200-line budget above gates the
|
|
1623
|
+
// trigger. The sha256 check is the agent's only verification step;
|
|
1624
|
+
// a mismatch stops the trigger before any artifact_edit call.
|
|
1635
1625
|
var rebuildPrompt =
|
|
1636
1626
|
ARTIFACT_LOAD_PREAMBLE +
|
|
1637
|
-
"
|
|
1627
|
+
"The change to apply is the unified diff in the file \"" + publishDiffFile + "\" (sha256 " + publishDiffSha256 + ").\n" +
|
|
1628
|
+
"1. Verify the file: run sha256sum on it. If the printed hash is not exactly " + publishDiffSha256 + ", STOP and end your turn — do not call artifact_edit.\n" +
|
|
1629
|
+
"2. Read the file's full content.\n" +
|
|
1630
|
+
"3. Call artifact_edit with slug \"" + PUBLISH_SLUG + "\" and verbatim_request:\n" +
|
|
1638
1631
|
"'Apply the following change to your source tree, then rebuild and deploy.\n" +
|
|
1639
1632
|
"\n" +
|
|
1640
1633
|
"UNIFIED DIFF (relative to your source tree):\n" +
|
|
1641
|
-
"```diff\n
|
|
1634
|
+
"```diff\n<the full content of the verified file, pasted verbatim>\n```\n" +
|
|
1642
1635
|
"\n" +
|
|
1643
1636
|
"Rules:\n" +
|
|
1644
1637
|
"- For each file in the diff, apply its hunks to the same path in your source tree (use git apply or equivalent).\n" +
|
package/workflows/chore.js
CHANGED
|
@@ -138,9 +138,15 @@ const PUBLISH_NPM = RUN_LIB + "/publish-npm.sh";
|
|
|
138
138
|
const CREW_API_PINNED = RUN_LIB + "/crew-api.js";
|
|
139
139
|
const SCHEMA_SQL_SRC = crewHome + "/lib/schema.sql";
|
|
140
140
|
const SCHEMA_SQL_PINNED = RUN_LIB + "/schema.sql";
|
|
141
|
-
|
|
141
|
+
const COMPUTE_DIFF_SRC = crewHome + "/current/lib/compute-publish-diff.js";
|
|
142
|
+
const COMPUTE_DIFF = RUN_LIB + "/compute-publish-diff.js";
|
|
143
|
+
// The six basenames the pin step must materialize — asserted mechanically
|
|
142
144
|
// by workflow code from the verbatim listing, never from agent prose.
|
|
143
|
-
|
|
145
|
+
// COMPUTE_DIFF is the deterministic publish-diff computer (room #14,
|
|
146
|
+
// 2026-09-17): the diff is computed by this script, never ferried as an
|
|
147
|
+
// agent JSON string. Pinned like the other publish-critical modules so a
|
|
148
|
+
// mid-run release swap cannot change it under the workflow.
|
|
149
|
+
const PIN_BASENAMES = [LIFECYCLE, MERGE_LOCK, PUBLISH_NPM, CREW_API_PINNED, SCHEMA_SQL_PINNED, COMPUTE_DIFF].map(function (p) { return p.split("/").pop(); });
|
|
144
150
|
|
|
145
151
|
// Project config — passed by dispatcher, falls back to dashboard defaults
|
|
146
152
|
const projectConfig = inputs.project_config || {};
|
|
@@ -311,7 +317,7 @@ function attemptKey(base, reworkCount) {
|
|
|
311
317
|
return base + (reworkCount > 0 ? "-r" + reworkCount : "");
|
|
312
318
|
}
|
|
313
319
|
// pinLifecycle(key) — snapshot the lifecycle scripts into RUN_LIB and return
|
|
314
|
-
// the verbatim `ls -1` listing so WORKFLOW CODE asserts the
|
|
320
|
+
// the verbatim `ls -1` listing so WORKFLOW CODE asserts the six pinned
|
|
315
321
|
// basenames; the agent cannot self-certify. (The pin step was the one place
|
|
316
322
|
// the workflows trusted agent prose: task 24be1cd6 walked to Publish on an
|
|
317
323
|
// empty pin dir.) Byte-identical across standard/bugfix/chore — pinned by
|
|
@@ -319,7 +325,7 @@ function attemptKey(base, reworkCount) {
|
|
|
319
325
|
function pinLifecycle(key) {
|
|
320
326
|
return agent(
|
|
321
327
|
"Snapshot lifecycle scripts for version pinning.\n" +
|
|
322
|
-
"Run: mkdir -p " + RUN_LIB + " && cp " + LIFECYCLE_SRC + " " + LIFECYCLE + " && cp " + MERGE_LOCK_SRC + " " + MERGE_LOCK + " && cp " + PUBLISH_NPM_SRC + " " + PUBLISH_NPM + " && cp " + CREW_API_SRC + " " + CREW_API_PINNED + " && cp " + SCHEMA_SQL_SRC + " " + SCHEMA_SQL_PINNED + " && chmod +x " + LIFECYCLE + " " + MERGE_LOCK + " " + PUBLISH_NPM + " && ls -1 " + RUN_LIB + "\n" +
|
|
328
|
+
"Run: mkdir -p " + RUN_LIB + " && cp " + LIFECYCLE_SRC + " " + LIFECYCLE + " && cp " + MERGE_LOCK_SRC + " " + MERGE_LOCK + " && cp " + PUBLISH_NPM_SRC + " " + PUBLISH_NPM + " && cp " + CREW_API_SRC + " " + CREW_API_PINNED + " && cp " + SCHEMA_SQL_SRC + " " + SCHEMA_SQL_PINNED + " && cp " + COMPUTE_DIFF_SRC + " " + COMPUTE_DIFF + " && chmod +x " + LIFECYCLE + " " + MERGE_LOCK + " " + PUBLISH_NPM + " && ls -1 " + RUN_LIB + "\n" +
|
|
323
329
|
"Return the verbatim output of the ls -1 command as { \"listing\": \"<verbatim output>\" } and nothing else.",
|
|
324
330
|
{ key: key, label: "Pinning lifecycle scripts",
|
|
325
331
|
schema: { type: "object", properties: { listing: { type: "string" } }, required: ["listing"] } }
|
|
@@ -500,37 +506,6 @@ function hydrateReleaseDecision(rec) {
|
|
|
500
506
|
return null;
|
|
501
507
|
}
|
|
502
508
|
|
|
503
|
-
// Publish diff transport: parse a unified diff into per-file {path, added,
|
|
504
|
-
// removed} line lists. Pure function — no I/O, no clock. Used by Publish to
|
|
505
|
-
// verify the artifact builder applied the carried change (canary run 4,
|
|
506
|
-
// 2026-09-11: the builder's source tree was stale and disconnected from the
|
|
507
|
-
// crew's repo; "rebuild from current source" rebuilt stale code and the
|
|
508
|
-
// workflow stamped the new commit hash on it — provenance fiction).
|
|
509
|
-
function parseUnifiedDiff(diffText) {
|
|
510
|
-
var files = [];
|
|
511
|
-
var current = null;
|
|
512
|
-
var lines = (diffText || "").split("\n");
|
|
513
|
-
for (var i = 0; i < lines.length; i++) {
|
|
514
|
-
var line = lines[i];
|
|
515
|
-
var m = /^diff --git a\/(.*) b\/(.*)$/.exec(line);
|
|
516
|
-
if (m) {
|
|
517
|
-
current = { path: m[2], added: [], removed: [] };
|
|
518
|
-
files.push(current);
|
|
519
|
-
continue;
|
|
520
|
-
}
|
|
521
|
-
if (!current) continue;
|
|
522
|
-
if (/^--- /.test(line) || /^\+\+\+ /.test(line)) continue;
|
|
523
|
-
if (/^@@ /.test(line)) continue;
|
|
524
|
-
if (line.charAt(0) === "+") {
|
|
525
|
-
current.added.push(line.slice(1));
|
|
526
|
-
} else if (line.charAt(0) === "-") {
|
|
527
|
-
current.removed.push(line.slice(1));
|
|
528
|
-
}
|
|
529
|
-
}
|
|
530
|
-
return files;
|
|
531
|
-
}
|
|
532
|
-
|
|
533
|
-
|
|
534
509
|
// Publish read-back request (currently unavailable): the verbatim_request
|
|
535
510
|
// the parent protocol (docs/publish-verification.md) would hand to an
|
|
536
511
|
// independent read-back tool after the artifact build lands. artifact_inspect
|
|
@@ -1473,59 +1448,77 @@ while (i < STEPS.length) {
|
|
|
1473
1448
|
} else if (!/^[0-9a-f]{40}$/.test(publishBase)) {
|
|
1474
1449
|
return await parkTask("Publish base '" + publishBase + "' is not a valid commit SHA — cannot compute the publish diff. Human attention needed.");
|
|
1475
1450
|
}
|
|
1476
|
-
//
|
|
1477
|
-
//
|
|
1478
|
-
//
|
|
1479
|
-
//
|
|
1480
|
-
//
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1451
|
+
// Publish diff transport (room #14, 2026-09-17): the diff used to be
|
|
1452
|
+
// ferried as a JSON string field in the agent's response — the agent
|
|
1453
|
+
// produced a valid 700-line diff on disk but the JSON ferry dropped
|
|
1454
|
+
// it, and the parse saw zero files ("Publish diff parsed to zero
|
|
1455
|
+
// files"). The diff now travels git -> file -> deterministic script
|
|
1456
|
+
// summary; the LLM never carries diff bytes. The agent is pure hands:
|
|
1457
|
+
// it runs exactly one command (the PINNED compute-publish-diff.js —
|
|
1458
|
+
// a mid-run release swap cannot change it under the workflow) and
|
|
1459
|
+
// returns the small JSON summary verbatim. All fail-closed parks
|
|
1460
|
+
// below are unchanged in meaning.
|
|
1461
|
+
var publishDiffFile = crewHome + "/.publish-diffs/" + taskId + (reworkCount > 0 ? "-r" + reworkCount : "") + ".diff";
|
|
1484
1462
|
var diffResult = await agent(
|
|
1485
|
-
"Run
|
|
1486
|
-
|
|
1487
|
-
"
|
|
1488
|
-
"if [ \"$ANCESTOR\" = yes ]; then echo '---DIFF---' && git diff \"$BASE\" \"$HEAD\" && echo '---NAMES---' && git diff-tree --no-commit-id --name-only -r \"$BASE\" \"$HEAD\"; fi\n" +
|
|
1489
|
-
"Return JSON { \"commit\": \"<HEAD trimmed>\", \"base\": \"<BASE trimmed>\", \"ancestor\": \"<yes|no>\", \"diff\": \"<raw unified diff, may be multi-line>\", \"files\": \"<newline-separated paths>\" } and nothing else.",
|
|
1463
|
+
"Run exactly one command and nothing else:\n" +
|
|
1464
|
+
"node " + COMPUTE_DIFF + " --repo-path \"" + REPO_PATH + "\" --base " + publishBase + " --out \"" + publishDiffFile + "\"\n" +
|
|
1465
|
+
"The script prints one small JSON object to stdout. Return the stdout verbatim as { \"output\": \"<verbatim stdout>\" } and nothing else. Do not interpret it.",
|
|
1490
1466
|
{ key: attemptKey("publish-artifact-diff-" + taskId, reworkCount), label: "Computing publish diff from stamped base",
|
|
1491
|
-
schema: { type: "object", properties: {
|
|
1467
|
+
schema: { type: "object", properties: { output: { type: "string" } }, required: ["output"] } }
|
|
1492
1468
|
);
|
|
1493
|
-
|
|
1494
|
-
|
|
1469
|
+
var diffSummary = null;
|
|
1470
|
+
try {
|
|
1471
|
+
diffSummary = JSON.parse((diffResult.output || "").trim());
|
|
1472
|
+
} catch (e) {
|
|
1473
|
+
return await parkTask("Publish diff computation returned unparseable output — cannot verify application. Human attention needed.");
|
|
1474
|
+
}
|
|
1475
|
+
if (!diffSummary || diffSummary.ok !== true) {
|
|
1476
|
+
if (diffSummary && diffSummary.error === "not-ancestor") {
|
|
1477
|
+
return await parkTask("Publish base " + publishBase.slice(0, 12) + " is not an ancestor of HEAD " + String(diffSummary.commit || "").slice(0, 12) + " — the stamped provenance does not lead to the integrated commit. Human attention needed.");
|
|
1478
|
+
}
|
|
1479
|
+
return await parkTask("Publish diff computation failed (" + ((diffSummary && diffSummary.error) || "unknown error") + ") — cannot verify application. Human attention needed.");
|
|
1495
1480
|
}
|
|
1496
|
-
if (
|
|
1497
|
-
return await parkTask("Publish diff base mismatch:
|
|
1481
|
+
if (diffSummary.base !== publishBase) {
|
|
1482
|
+
return await parkTask("Publish diff base mismatch: script reported '" + String(diffSummary.base || "").slice(0, 12) + "' but the stamped base is '" + publishBase.slice(0, 12) + "'. Human attention needed.");
|
|
1498
1483
|
}
|
|
1499
|
-
var mergeCommitForPublish = (
|
|
1500
|
-
var
|
|
1501
|
-
if (!
|
|
1484
|
+
var mergeCommitForPublish = String(diffSummary.commit || "").trim();
|
|
1485
|
+
var publishDiffSha256 = String(diffSummary.sha256 || "");
|
|
1486
|
+
if (!diffSummary.bytes) {
|
|
1502
1487
|
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.");
|
|
1503
1488
|
}
|
|
1504
|
-
if (
|
|
1505
|
-
return await parkTask("Publish diff contains binary files — the
|
|
1489
|
+
if (diffSummary.has_binary) {
|
|
1490
|
+
return await parkTask("Publish diff contains binary files — the diff transport cannot carry them. Human attention needed.");
|
|
1506
1491
|
}
|
|
1507
|
-
if (
|
|
1492
|
+
if (diffSummary.has_rename) {
|
|
1508
1493
|
return await parkTask("Publish diff contains a rename — the diff transport cannot carry renames. Human attention needed.");
|
|
1509
1494
|
}
|
|
1510
|
-
var expectedChanges = parseUnifiedDiff(mergeDiff);
|
|
1511
1495
|
// Budget counts CHANGED lines (added + removed), not raw unified-diff
|
|
1512
1496
|
// output lines: context lines and file headers inflated the old
|
|
1513
1497
|
// split("\n").length count ~2x, parking a 95-line change against a
|
|
1514
|
-
// 200-line budget (Gate 1 Journey 3 attempt 3, 2026-09-13).
|
|
1515
|
-
|
|
1498
|
+
// 200-line budget (Gate 1 Journey 3 attempt 3, 2026-09-13). The count
|
|
1499
|
+
// now comes from the deterministic script summary, never from the LLM.
|
|
1500
|
+
var mergeDiffChangedLines = diffSummary.changed_lines || 0;
|
|
1516
1501
|
if (mergeDiffChangedLines > 200) {
|
|
1517
1502
|
return await parkTask("Publish diff changes " + mergeDiffChangedLines + " lines (budget 200) — too large for the diff transport. Human attention needed.");
|
|
1518
1503
|
}
|
|
1519
|
-
if (
|
|
1504
|
+
if (!diffSummary.file_count) {
|
|
1520
1505
|
return await parkTask("Publish diff parsed to zero files for commit " + (mergeCommitForPublish || "unknown") + " — cannot verify application. Human attention needed.");
|
|
1521
1506
|
}
|
|
1507
|
+
// The trigger agent reads the diff from the checksummed file — the
|
|
1508
|
+
// workflow never holds diff bytes (room #14, 2026-09-17). The file
|
|
1509
|
+
// is small by construction: the 200-line budget above gates the
|
|
1510
|
+
// trigger. The sha256 check is the agent's only verification step;
|
|
1511
|
+
// a mismatch stops the trigger before any artifact_edit call.
|
|
1522
1512
|
var rebuildPrompt =
|
|
1523
1513
|
ARTIFACT_LOAD_PREAMBLE +
|
|
1524
|
-
"
|
|
1514
|
+
"The change to apply is the unified diff in the file \"" + publishDiffFile + "\" (sha256 " + publishDiffSha256 + ").\n" +
|
|
1515
|
+
"1. Verify the file: run sha256sum on it. If the printed hash is not exactly " + publishDiffSha256 + ", STOP and end your turn — do not call artifact_edit.\n" +
|
|
1516
|
+
"2. Read the file's full content.\n" +
|
|
1517
|
+
"3. Call artifact_edit with slug \"" + PUBLISH_SLUG + "\" and verbatim_request:\n" +
|
|
1525
1518
|
"'Apply the following change to your source tree, then rebuild and deploy.\n" +
|
|
1526
1519
|
"\n" +
|
|
1527
1520
|
"UNIFIED DIFF (relative to your source tree):\n" +
|
|
1528
|
-
"```diff\n
|
|
1521
|
+
"```diff\n<the full content of the verified file, pasted verbatim>\n```\n" +
|
|
1529
1522
|
"\n" +
|
|
1530
1523
|
"Rules:\n" +
|
|
1531
1524
|
"- For each file in the diff, apply its hunks to the same path in your source tree (use git apply or equivalent).\n" +
|