muse-crew 0.7.11 → 0.7.13

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.
@@ -0,0 +1,94 @@
1
+ // read-ooda-verdict.js — deterministic cross-checker for the OODA terminal
2
+ // verdict (2026-09-15).
3
+ //
4
+ // The prose VERDICT: line in an agent's report is the routing signal, but
5
+ // verdict.json is the reason-carrying record the workflow closeout actually
6
+ // reads. This script cross-checks the two: the caller passes the verdict it
7
+ // read from prose (--expect) and the script fails loudly when the
8
+ // machine-readable record disagrees or cannot back the verdict.
9
+ //
10
+ // Usage:
11
+ // node read-ooda-verdict.js --dir <phase-dir> --expect <PASS|FAIL>
12
+ //
13
+ // Reads <phase-dir>/verdict.json and prints exactly one JSON line to stdout.
14
+ //
15
+ // Exit 0 with {ok:true, verdict, reason, summary, expected, actual, attempt}
16
+ // when the record exists, parses, has a verdict field, the verdict equals
17
+ // --expect, and a FAIL carries a non-empty reason.
18
+ //
19
+ // Exit 2 with {ok:false, code, error} when:
20
+ // missing — verdict.json is absent (the agent never wrote one)
21
+ // corrupt — unparseable JSON, or parsed but no verdict field
22
+ // contradiction — the record's verdict !== --expect (NOT_POSSIBLE can
23
+ // never equal PASS or FAIL, so it lands here)
24
+ // no_reason — the record is FAIL with an empty or missing reason
25
+ //
26
+ // Determinism: no wall-clock reads, no randomness. The script never judges
27
+ // report content — the agent states the reason; the machine enforces its
28
+ // presence and its agreement with the prose verdict.
29
+ "use strict";
30
+
31
+ const { readFileSync, existsSync } = require("node:fs");
32
+ const { join, resolve } = require("node:path");
33
+
34
+ function fail(code, error) {
35
+ process.stdout.write(JSON.stringify({ ok: false, code: code, error: error }) + "\n");
36
+ process.exit(2);
37
+ }
38
+
39
+ function parseArgs(argv) {
40
+ const out = {};
41
+ for (let i = 0; i < argv.length; i++) {
42
+ const a = argv[i];
43
+ if (a === "--dir") out.dir = argv[++i];
44
+ else if (a === "--expect") out.expect = argv[++i];
45
+ else fail("bad_input", "unknown flag: " + a);
46
+ }
47
+ return out;
48
+ }
49
+
50
+ function main() {
51
+ const args = parseArgs(process.argv.slice(2));
52
+ if (!args.dir) fail("bad_input", "missing --dir <phase-dir>");
53
+ if (!args.expect) fail("bad_input", "missing --expect <PASS|FAIL>");
54
+ if (args.expect !== "PASS" && args.expect !== "FAIL") {
55
+ fail("bad_input", "unknown --expect: " + args.expect + " (PASS|FAIL)");
56
+ }
57
+
58
+ const verdictPath = join(resolve(args.dir), "verdict.json");
59
+ if (!existsSync(verdictPath)) {
60
+ fail("missing", "verdict.json not found: " + verdictPath);
61
+ }
62
+
63
+ let record;
64
+ try {
65
+ record = JSON.parse(readFileSync(verdictPath, "utf8"));
66
+ } catch (e) {
67
+ fail("corrupt", "verdict.json is unparseable: " + e.message);
68
+ }
69
+ if (!record || typeof record.verdict !== "string" || record.verdict === "") {
70
+ fail("corrupt", "verdict.json has no verdict field: " + verdictPath);
71
+ }
72
+
73
+ if (record.verdict !== args.expect) {
74
+ fail("contradiction", "verdict.json verdict is " + record.verdict + " but prose verdict was " + args.expect);
75
+ }
76
+
77
+ if (record.verdict === "FAIL") {
78
+ if (record.reason === undefined || String(record.reason).trim() === "") {
79
+ fail("no_reason", "verdict.json verdict is FAIL with no machine-readable reason");
80
+ }
81
+ }
82
+
83
+ process.stdout.write(JSON.stringify({
84
+ ok: true,
85
+ verdict: record.verdict,
86
+ reason: record.reason === undefined ? "" : record.reason,
87
+ summary: record.summary === undefined ? "" : record.summary,
88
+ expected: record.expected === undefined ? "" : record.expected,
89
+ actual: record.actual === undefined ? "" : record.actual,
90
+ attempt: record.attempt === undefined ? "" : record.attempt,
91
+ }) + "\n");
92
+ }
93
+
94
+ main();
@@ -0,0 +1,186 @@
1
+ #!/usr/bin/env node
2
+ // readback-disk.js — deterministic disk read-back sensor for parent publish
3
+ // verification (docs/publish-verification.md).
4
+ //
5
+ // The read-back gap (2026-09-14): the platform removed `artifact_inspect`
6
+ // and no agent-callable content read-back existed, so every artifact publish
7
+ // parked at `publish: verification-requested` permanently. The platform's
8
+ // artifact edits land in its on-disk working copy of the artifact source
9
+ // (~/workspace/ts-spaces/<slug>/ — verified empirically 2026-09-15: added
10
+ // lines present, removed lines absent across real platform commits), so a
11
+ // deterministic local sensor can replace the removed inspection tool: no
12
+ // LLM, no async handoff, no prose to parse.
13
+ //
14
+ // This script is the SENSOR; lib/verify-publish.js stays the judge. It
15
+ // emits the exact machine-readable findings block the verifier already
16
+ // parses:
17
+ //
18
+ // FILE: <path>
19
+ // ADDED: <exact added line from the diff> :: PRESENT|ABSENT
20
+ // REMOVED: <exact removed line from the diff> :: PRESENT|ABSENT
21
+ // END_FILE
22
+ //
23
+ // one ADDED line per (+) diff line and one REMOVED line per (-) diff line,
24
+ // in diff order. The verdict is PRESENT iff that exact line occurs in the
25
+ // file's CURRENT on-disk source (whole-line match), ABSENT otherwise.
26
+ //
27
+ // Authority boundary: the sensor reads the platform's working copy of the
28
+ // artifact source — the tree the hosted artifact is built/served from. If
29
+ // the working copy is stale relative to a just-applied edit, the findings
30
+ // honestly report ABSENT and the verifier fails CLOSED (parked, never a
31
+ // false stamp). Staleness can only park, never certify.
32
+ //
33
+ // Usage:
34
+ // node readback-disk.js --repo-path <path> --commit <sha> --base <sha>
35
+ // --slug <artifact-slug> --task-id <uuid> [--spaces-root <dir>]
36
+ //
37
+ // --base is the previously-stamped provenance source_commit (or the
38
+ // empty-tree sha 4b825dc642cb6eb9a060e54bf8d69288fbee4904 for a first
39
+ // publish) — the SAME base the verifier uses. --spaces-root defaults to
40
+ // ~/workspace/ts-spaces.
41
+ //
42
+ // Exit codes: 0 ok (findings block on stdout) · 2 usage/validation ·
43
+ // 1 git/diff/read failure. On non-zero exit NOTHING is printed to stdout,
44
+ // so a caller must never save stdout as a findings file.
45
+ //
46
+ // Security: the slug is charset-validated and the resolved space directory
47
+ // must stay under the spaces root (realpath containment); every file path
48
+ // from the diff is likewise contained before reading. A path that escapes
49
+ // is a validation failure (exit 2), never a read.
50
+
51
+ import { execFileSync } from "node:child_process";
52
+ import { existsSync, readFileSync, realpathSync, statSync } from "node:fs";
53
+ import { homedir } from "node:os";
54
+ import { join, resolve } from "node:path";
55
+
56
+ const EMPTY_TREE = "4b825dc642cb6eb9a060e54bf8d69288fbee4904";
57
+
58
+ function arg(name) {
59
+ const i = process.argv.indexOf(name);
60
+ if (i < 0 || i + 1 >= process.argv.length) {
61
+ fail("usage", `${name} is required.`, 2);
62
+ }
63
+ return process.argv[i + 1];
64
+ }
65
+ function fail(code, message, exitCode) {
66
+ // stderr only — stdout must stay empty on failure.
67
+ process.stderr.write(JSON.stringify({ ok: false, error: code, message }) + "\n");
68
+ process.exit(exitCode);
69
+ }
70
+
71
+ const repoPath = arg("--repo-path");
72
+ const commit = arg("--commit");
73
+ const base = arg("--base");
74
+ const slug = arg("--slug");
75
+ const taskId = arg("--task-id");
76
+ const spacesRoot = process.argv.includes("--spaces-root")
77
+ ? arg("--spaces-root")
78
+ : join(homedir(), "workspace", "ts-spaces");
79
+
80
+ if (!/^[0-9a-f]{40}$/.test(commit)) fail("usage", "commit must be a 40-char hex sha.", 2);
81
+ if (!/^[0-9a-f]{40}$/.test(base)) fail("usage", "base must be a 40-char hex sha.", 2);
82
+ if (!/^[A-Za-z0-9][A-Za-z0-9_.-]*$/.test(slug)) {
83
+ fail("usage", `slug ${JSON.stringify(slug)} is not a safe artifact slug.`, 2);
84
+ }
85
+
86
+ // Resolve the space directory with realpath containment: the slug must not
87
+ // escape the spaces root (no "..", no symlinks pointing out).
88
+ let spaceDir;
89
+ try {
90
+ const rootReal = realpathSync(spacesRoot);
91
+ const candReal = realpathSync(join(spacesRoot, slug));
92
+ if (candReal !== rootReal && !candReal.startsWith(rootReal + "/")) {
93
+ fail("usage", `slug ${JSON.stringify(slug)} resolves outside the spaces root.`, 2);
94
+ }
95
+ spaceDir = candReal;
96
+ } catch (e) {
97
+ fail("usage", `cannot resolve space dir for slug ${JSON.stringify(slug)}: ${e.message}`, 2);
98
+ }
99
+ try {
100
+ if (!statSync(spaceDir).isDirectory()) fail("usage", `space dir ${spaceDir} is not a directory.`, 2);
101
+ } catch (e) {
102
+ fail("usage", `space dir ${spaceDir} unreadable: ${e.message}`, 2);
103
+ }
104
+
105
+ // The publish delta is base..commit — the same diff the verifier checks the
106
+ // findings against. Refuse a base that is not an ancestor: certifying a
107
+ // diff against an unrelated tree is worse than no read-back.
108
+ if (base !== EMPTY_TREE) {
109
+ try {
110
+ execFileSync("git", ["-C", repoPath, "merge-base", "--is-ancestor", base, commit], { stdio: "ignore" });
111
+ } catch (e) {
112
+ fail("usage", `base ${base} is not an ancestor of commit ${commit} — refusing to read back an unrelated tree.`, 2);
113
+ }
114
+ }
115
+
116
+ let diff;
117
+ try {
118
+ diff = execFileSync("git", ["-C", repoPath, "diff", base, commit, "--"], {
119
+ encoding: "utf8", maxBuffer: 4 * 1024 * 1024,
120
+ });
121
+ } catch (e) {
122
+ fail("git", `git diff failed: ${e.message}`, 1);
123
+ }
124
+ if (!diff.trim()) {
125
+ fail("git", "empty diff for commit — nothing to read back.", 1);
126
+ }
127
+
128
+ // Parse the diff into per-file added/removed lines (byte-identical parser
129
+ // to build-readback-request.js and verify-publish.js: the verifier looks
130
+ // the raw lines up verbatim, so all three must agree on the split).
131
+ const files = [];
132
+ let current = null;
133
+ for (const line of diff.split("\n")) {
134
+ if (line.startsWith("diff --git")) {
135
+ const m = line.match(/^diff --git a\/(.+) b\/(.+)$/);
136
+ current = { path: m ? m[2] : "unknown", added: [], removed: [] };
137
+ files.push(current);
138
+ } else if (current && line.startsWith("+") && !line.startsWith("+++")) {
139
+ current.added.push(line.slice(1));
140
+ } else if (current && line.startsWith("-") && !line.startsWith("---")) {
141
+ current.removed.push(line.slice(1));
142
+ }
143
+ }
144
+
145
+ // Whole-line content of a disk file, normalized for the membership test.
146
+ // Trailing-newline artifact popped (same convention as verify-publish.js
147
+ // oldTreeLines); one trailing CR stripped per line so CRLF working copies
148
+ // compare honestly against LF diffs. The RAW diff lines are still what get
149
+ // emitted — the verifier looks those up verbatim.
150
+ const norm = (s) => (s.endsWith("\r") ? s.slice(0, -1) : s);
151
+ function diskLines(absPath) {
152
+ if (!existsSync(absPath)) return new Set(); // missing file: every line ABSENT
153
+ let text;
154
+ try {
155
+ text = readFileSync(absPath, "utf8");
156
+ } catch (e) {
157
+ fail("read", `cannot read ${absPath}: ${e.message}`, 1);
158
+ }
159
+ const lines = text.split("\n");
160
+ if (lines.length > 0 && lines[lines.length - 1] === "") lines.pop();
161
+ return new Set(lines.map(norm));
162
+ }
163
+
164
+ const out = [];
165
+ for (const f of files) {
166
+ // Contain every diff path inside the space dir before reading.
167
+ const abs = resolve(spaceDir, f.path);
168
+ if (abs !== spaceDir && !abs.startsWith(spaceDir + "/")) {
169
+ fail("usage", `diff path ${JSON.stringify(f.path)} escapes the space dir.`, 2);
170
+ }
171
+ const present = diskLines(abs);
172
+ out.push(`FILE: ${f.path}`);
173
+ for (const line of f.added) {
174
+ out.push(`ADDED: ${line} :: ${present.has(norm(line)) ? "PRESENT" : "ABSENT"}`);
175
+ }
176
+ for (const line of f.removed) {
177
+ out.push(`REMOVED: ${line} :: ${present.has(norm(line)) ? "PRESENT" : "ABSENT"}`);
178
+ }
179
+ out.push("END_FILE");
180
+ }
181
+
182
+ process.stdout.write(out.join("\n") + "\n");
183
+ process.stderr.write(JSON.stringify({
184
+ ok: true, slug, task_id: taskId, commit, base,
185
+ files: files.map((f) => f.path),
186
+ } ) + "\n");
@@ -52,6 +52,8 @@ function parseArgs(argv) {
52
52
  }
53
53
 
54
54
  function loadPlaywright() {
55
+ // playwright-core is a declared dependency (package.json), so a normal
56
+ // npm install resolves it from the package's own node_modules.
55
57
  try {
56
58
  return require("playwright-core");
57
59
  } catch (e) { /* fall through */ }
@@ -64,12 +66,6 @@ function loadPlaywright() {
64
66
  return require(envDir);
65
67
  } catch (e) { /* fall through */ }
66
68
  }
67
- const conventional = "/home/hatch/workspace/crew-tools/node_modules/playwright-core";
68
- if (existsSync(conventional)) {
69
- try {
70
- return require(conventional);
71
- } catch (e) { /* fall through */ }
72
- }
73
69
  fail(3, {
74
70
  ok: false,
75
71
  not_possible: "NOT POSSIBLE: playwright-core is not installed or not resolvable. " +
package/lib/see-act.js CHANGED
@@ -70,6 +70,8 @@ function parseArgs(argv) {
70
70
  }
71
71
 
72
72
  function loadPlaywright() {
73
+ // playwright-core is a declared dependency (package.json), so a normal
74
+ // npm install resolves it from the package's own node_modules.
73
75
  try {
74
76
  return require("playwright-core");
75
77
  } catch (e) { /* fall through */ }
@@ -82,12 +84,6 @@ function loadPlaywright() {
82
84
  return require(envDir);
83
85
  } catch (e) { /* fall through */ }
84
86
  }
85
- const conventional = "/home/hatch/workspace/crew-tools/node_modules/playwright-core";
86
- if (existsSync(conventional)) {
87
- try {
88
- return require(conventional);
89
- } catch (e) { /* fall through */ }
90
- }
91
87
  fail(3, {
92
88
  ok: false,
93
89
  not_possible: "NOT POSSIBLE: playwright-core is not installed or not resolvable. " +
@@ -5,8 +5,10 @@
5
5
  // async read-back inspection's result JSON and MECHANICALLY decides the
6
6
  // verdict: it parses the inspector's machine-readable findings block,
7
7
  // compares every added/removed diff line against the reported
8
- // present/absent verdicts, checks supersession via git, and only then
9
- // stamps provenance, logs the terminal event, and re-queues the task.
8
+ // present/absent verdicts (with a mechanically computed collision
9
+ // exemption for removed lines that also occur in untouched code),
10
+ // checks supersession via git, and only then stamps provenance, logs
11
+ // the terminal event, and re-queues the task.
10
12
  //
11
13
  // The LLM is the sensor (it reads the artifact source); this code is the
12
14
  // judge. Unparseable findings, content mismatches, supersession, and stamp
@@ -207,6 +209,51 @@ if (!findings || bestScore <= 0) {
207
209
  }
208
210
 
209
211
  // --- 4. Mechanical comparison ----------------------------------------------
212
+ // Collision exemption (2026-09-15, task 00bca4b8): a removed diff line that
213
+ // also occurs verbatim in untouched code has zero discriminating power —
214
+ // its presence in the new file cannot tell "old block removed" from "old
215
+ // block present". Task 1d692d91 parked a valid publish because two removed
216
+ // lines (` return (`, ` </div>`) occur identically in
217
+ // the untouched WorkflowSteps component; the naive every-removed-line-ABSENT
218
+ // rule failed closed on zero signal.
219
+ //
220
+ // The exemption is mechanically computed and signal-preserving: for file F,
221
+ // line L is exempt iff L occurs in F's old tree (at <base>) strictly more
222
+ // times than the expected diff removes it. An exempted line must survive in
223
+ // untouched code no matter what, so exempting it can never turn a missed
224
+ // removal into a pass. No change to the sensor: build-readback-request.js
225
+ // still reports whole-file PRESENT/ABSENT honestly; only this judge gets
226
+ // smarter.
227
+ function oldTreeLines(path) {
228
+ // Exact whole-line contents of <path> at <base>; [] when the base is the
229
+ // empty tree or the file did not exist there (a new file has no removed
230
+ // lines, so the exemption is vacuous for it).
231
+ if (base === EMPTY_TREE) return [];
232
+ let text;
233
+ try {
234
+ text = execFileSync("git", ["-C", repoPath, "show", `${base}:${path}`], {
235
+ encoding: "utf8", maxBuffer: 4 * 1024 * 1024,
236
+ });
237
+ } catch {
238
+ return [];
239
+ }
240
+ if (text === "") return [];
241
+ const lines = text.split("\n");
242
+ if (lines[lines.length - 1] === "") lines.pop(); // drop the trailing-newline artifact
243
+ return lines;
244
+ }
245
+ const oldTreeCounts = new Map(); // path -> Map(line -> occurrences in old tree)
246
+ function oldCount(path, line) {
247
+ let counts = oldTreeCounts.get(path);
248
+ if (counts === undefined) {
249
+ counts = new Map();
250
+ for (const l of oldTreeLines(path)) counts.set(l, (counts.get(l) || 0) + 1);
251
+ oldTreeCounts.set(path, counts);
252
+ }
253
+ return counts.get(line) || 0;
254
+ }
255
+
256
+ let exempted = 0;
210
257
  for (const [path, exp] of expected) {
211
258
  const found = findings.get(path);
212
259
  if (!found) terminal("content-mismatch", `no findings for changed file ${path}`);
@@ -215,10 +262,19 @@ for (const [path, exp] of expected) {
215
262
  if (v === undefined) terminal("unreadable-result", `no ADDED finding for line in ${path}: ${line.slice(0, 60)}`);
216
263
  if (v !== "PRESENT") terminal("content-mismatch", `added line ABSENT in ${path}: ${line.slice(0, 80)}`);
217
264
  }
265
+ // The diff may remove the same line more than once; the old tree must
266
+ // account for every removal before a line counts as a collision.
267
+ const removedBudget = new Map();
268
+ for (const line of exp.removed) removedBudget.set(line, (removedBudget.get(line) || 0) + 1);
218
269
  for (const line of exp.removed) {
219
270
  const v = found.removed.get(line);
220
271
  if (v === undefined) terminal("unreadable-result", `no REMOVED finding for line in ${path}: ${line.slice(0, 60)}`);
221
- if (v !== "ABSENT") terminal("content-mismatch", `removed line PRESENT in ${path}: ${line.slice(0, 80)}`);
272
+ if (v === "ABSENT") continue;
273
+ if (oldCount(path, line) > removedBudget.get(line)) {
274
+ exempted += 1; // colliding pre-existing line: zero signal, cannot fail a good publish
275
+ continue;
276
+ }
277
+ terminal("content-mismatch", `removed line PRESENT in ${path}: ${line.slice(0, 80)}`);
222
278
  }
223
279
  }
224
280
 
@@ -259,7 +315,9 @@ if (!p || p.source_commit !== commit || p.task_id !== taskId || p.crew_release !
259
315
 
260
316
  // --- 8. Terminal verified event + re-queue -----------------------------------
261
317
  const verifiedMsg =
262
- `publish: verified ${commit} (${inspectionId}) — read-back: all added lines PRESENT, all removed lines ABSENT; ${buildNote}; no supersession (HEAD=${commit}).`;
318
+ `publish: verified ${commit} (${inspectionId}) — read-back: all added lines PRESENT, all removed lines ABSENT` +
319
+ (exempted > 0 ? ` (${exempted} colliding removed line${exempted === 1 ? "" : "s"} exempted)` : "") +
320
+ `; ${buildNote}; no supersession (HEAD=${commit}).`;
263
321
  api("log-event", { task_id: taskId, type: "note", message: verifiedMsg.slice(0, 1000) });
264
322
  api("update-task", { id: taskId, state: "in_progress" });
265
323
  process.stdout.write(JSON.stringify({ ok: true, verdict: "verified", commit, build_note: buildNote }) + "\n");
@@ -9,6 +9,11 @@
9
9
  // [--summary <text>] [--expected <text>] [--actual <text>]
10
10
  // [--missing <json-array>] [--reason <text>] [--ts <iso>]
11
11
  //
12
+ // --reason is REQUIRED and must be non-empty after trim when --verdict is
13
+ // FAIL or NOT_POSSIBLE; a missing or empty --reason fails with exit 2 and
14
+ // writes nothing (no verdict.json, no ledger line). A FAIL verdict must
15
+ // carry a machine-readable reason — an unreasoned FAIL can never be written.
16
+ //
12
17
  // Writes two records:
13
18
  // <phase-dir>/verdict.json — the LATEST verdict (what the workflow
14
19
  // closeout reads). Overwritten on each call.
@@ -24,7 +29,8 @@
24
29
  //
25
30
  // missing_evidence names evidence that should exist but honestly does not
26
31
  // (e.g. "no pixel-visual of the footer element: infinite scroll").
27
- // reason carries the NOT POSSIBLE explanation when verdict is NOT_POSSIBLE.
32
+ // reason carries the FAIL defect or the NOT POSSIBLE explanation required
33
+ // for both verdicts, never optional.
28
34
  //
29
35
  // Exit 0 on success, 2 on bad input. Determinism: no wall-clock reads, no
30
36
  // randomness; ts comes only from --ts and is omitted when not passed.
@@ -87,6 +93,17 @@ function main() {
87
93
  if (!args.verdict) fail("missing --verdict <PASS|FAIL|NOT_POSSIBLE>");
88
94
  if (!VERDICTS[args.verdict]) fail("unknown --verdict: " + args.verdict + " (PASS|FAIL|NOT_POSSIBLE)");
89
95
 
96
+ // A FAIL or NOT_POSSIBLE verdict without a machine-readable reason cannot
97
+ // be written — the workflow closeout reads verdict.json for the reason,
98
+ // and an unreasoned negative verdict is a broken record.
99
+ var reason = "";
100
+ if (args.verdict === "FAIL" || args.verdict === "NOT_POSSIBLE") {
101
+ if (args.reason === undefined || String(args.reason).trim() === "") {
102
+ fail("--reason is required for a " + args.verdict + " verdict and must be non-empty");
103
+ }
104
+ reason = String(args.reason).trim();
105
+ }
106
+
90
107
  const attempt = String(args.attempt).trim();
91
108
 
92
109
  let missing = [];
@@ -109,7 +126,7 @@ function main() {
109
126
  actual: args.actual || "",
110
127
  missing_evidence: missing,
111
128
  };
112
- if (args.verdict === "NOT_POSSIBLE") record.reason = args.reason || "";
129
+ if (args.verdict === "FAIL" || args.verdict === "NOT_POSSIBLE") record.reason = reason;
113
130
  if (args.ts) record.ts = args.ts;
114
131
 
115
132
  const dir = resolve(args.dir);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "muse-crew",
3
- "version": "0.7.11",
3
+ "version": "0.7.13",
4
4
  "description": "Opinionated orchestration for Muse — workflows, identities, and tooling for autonomous software development.",
5
5
  "license": "UNLICENSED",
6
6
  "private": false,
@@ -25,5 +25,8 @@
25
25
  "workflows",
26
26
  "software-development"
27
27
  ],
28
- "author": "emojimanegg1"
28
+ "author": "emojimanegg1",
29
+ "dependencies": {
30
+ "playwright-core": "1.63.0"
31
+ }
29
32
  }
@@ -37,10 +37,14 @@ You are the dispatch trigger for Muse Crew. Run the authoritative dispatcher wor
37
37
 
38
38
  If the dispatcher returned no claims or the claims array is empty, log NO_DISPATCH and CONTINUE to Step 4.5 — do NOT exit. Verification (4.5) and evidence (6) run independently of dispatch claims. A no-claims tick must still verify parked publishes and deliver evidence. (Fixed 2026-09-14: the old "exit on NO_DISPATCH" skipped verification permanently.)
39
39
 
40
- 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, claims, and certifies; you are only the async ferry for the inspection.
40
+ 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).
41
41
  - Scan (code): `node {crewHome}/lib/crew-api.js --crew-home {crewHome} scan-verification-pending`
42
42
  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.
43
- - **READ-BACK BLOCKED (2026-09-14):** `artifact_inspect` was removed by the platform. No agent-callable replacement exists (`artifact.inspect` is malfunction diagnosis, not a read-back tool), so the inspection ferry step cannot run. For each entry in `to_verify`, log `verification-blocked: no agent-callable read-back tool <task_id> <commit>` and leave the task parked do NOT attempt an inspection call, do NOT judge content yourself, and do NOT stamp provenance. The deterministic machinery (`build-readback-request.js`, `verify-publish.js`) is preserved in `lib/` for when a read-back path exists.
43
+ - **Read-back (deterministic sensor, 2026-09-15):** no platform inspection tool is needed `lib/readback-disk.js` reads the platform's on-disk working copy of the artifact source (`~/workspace/ts-spaces/<slug>/`) and emits the machine-readable findings block the verifier parses. For each entry in `to_verify`, resolve the base via `get-provenance` (`source_commit`; the empty-tree sha `4b825dc642cb6eb9a060e54bf8d69288fbee4904` when nothing is stamped yet a first publish), then run:
44
+ `node {crewHome}/lib/readback-disk.js --repo-path "<repo_path>" --commit <commit> --base <base> --slug "<deploy_slug>" --task-id <task_id> > /tmp/readback-<task_id>.txt 2> /tmp/readback-<task_id>.err`
45
+ Use the entry's `repo_path` and `deploy_slug` verbatim. If the sensor exits 0, the result file holds the findings block — hand it to the verify step below. If it exits non-zero, do NOT save or use stdout: log `publish: verification-procedural-error <commit> <first line of the .err file>` and leave the task parked — the next tick retries. A sensor failure is procedural (the read could not be performed), never a content verdict. Do NOT judge content yourself, and do NOT stamp provenance.
46
+ - **Verify (code):** `node {crewHome}/lib/verify-publish.js --crew-home {crewHome} --task-id <task_id> --commit <commit> --base <base> --repo-path "<repo_path>" --slug "<deploy_slug>" --crew-release <crew_release> --inspection-id <task_id>-disk --result-file /tmp/readback-<task_id>.txt`
47
+ Pass `--build-agent-id <id>` from the entry's `build_agent_id` when it is present. Use the SAME `<base>` the sensor ran with. The verifier parses the findings, compares mechanically against the base..commit diff, checks supersession, and stamps only on a match. Its terminal verdicts (`publish: verified` → task re-queued; `publish: verification-failed` → stays parked) are final — log them and continue.
44
48
  - Never stamp provenance from prose. Never infer a verdict from an inspector's summary text. The verify script's machine-checked comparison is the only certification.
45
49
 
46
50
  5. **Monitor launched workflows until terminal (stay-alive — 2026-09-13):** The platform ties async workflow `agent()` authorization to the launcher's lifetime: if THIS tick ends while a workflow is still running, the workflow's next `agent()` call fails with "subagent bootstrap is no longer authorized" / "subagent reservation owner is terminal". Prevention beats recovery here, so this tick is configured with a 90-minute execution timeout (`timeout_secs: 5400` in seed/crons.json) and you MUST stay alive until every launched run reaches a terminal state. Do not exit early while a launched run is still `running` — your death is what kills it.
@@ -58,7 +62,7 @@ You are the dispatch trigger for Muse Crew. Run the authoritative dispatcher wor
58
62
  - **Task-level error (not a platform error):** The workflow's own error handling applies. Stop monitoring this run.
59
63
  - **If status is `running` or `paused`:** Continue polling.
60
64
  - **Monitor ceiling:** 90 minutes from tick start. If a run is still not terminal then (pathological — the work phase caps each `agent()` call at 60 minutes), exit; the next tick's Step 0 continues recovery through the durable mapping.
61
- - **Backstop (not the plan, the insurance):** If THIS launcher dies early for any reason (platform kill, cell recycle), the next tick's Step 0 detects the dead run through the durable platform run -> task mapping and retries it — at most ~3 minutes later. The mapping exists so a dead launcher never strands a task for an hour; the monitor exists so the launcher rarely dies mid-run in the first place.
65
+ - **Backstop (not the plan, the insurance):** If THIS launcher dies early for any reason (platform kill, cell recycle), the next tick's Step 0 detects the dead run through the durable platform run -> task mapping and retries it — at most ~15 minutes later. The mapping exists so a dead launcher never strands a task for an hour; the monitor exists so the launcher rarely dies mid-run in the first place.
62
66
  - When all launched workflows are terminal or retry-exhausted, continue to step 6 (evidence) — do not exit before delivering evidence for newly-done tasks.
63
67
 
64
68
  6. **Deliver QA evidence to chat (Eric's screenshots):** You are the crew's voice to Eric. Whenever a task completes QA, its evidence screenshots must land in this report automatically — Eric explicitly asked for them. This step is mechanical discovery, not judgment.
package/seed/crons.json CHANGED
@@ -7,7 +7,7 @@
7
7
  "mode": "task",
8
8
  "owner": "space:{dashboardSlug}",
9
9
  "schedule": {
10
- "every": "3m",
10
+ "every": "15m",
11
11
  "kind": "interval"
12
12
  },
13
13
  "timeout_secs": 5400,