muse-crew 0.7.17 → 0.7.18

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.
@@ -132,13 +132,18 @@ The report is the evidence. A QA claim without its OODA report is an unverified
132
132
  ## Reading the verdict back (2026-09-15)
133
133
 
134
134
  `lib/read-ooda-verdict.js --dir <phase-dir> --expect <PASS|FAIL>` is the
135
- deterministic cross-checker the bugfix QA closeout runs after extracting the
136
- prose `VERDICT:` line. It prints one JSON line to stdout and never touches
137
- the clock or randomness:
138
-
139
- - Exit 0, `{ok:true, verdict, reason, summary, expected, actual, attempt}` —
140
- the record exists, parses, carries a `verdict` field, agrees with the
141
- prose expectation, and a FAIL carries a non-empty reason.
135
+ deterministic cross-checker the Standard and Bugfix QA closeouts run after
136
+ extracting the prose `VERDICT:` line. It prints one JSON line to stdout and
137
+ never touches the clock or randomness:
138
+
139
+ - Exit 0, `{ok:true, verdict, reason, summary, expected, actual, attempt,
140
+ visual_loop_unavailable}` — the record exists, parses, carries a `verdict`
141
+ field, agrees with the prose expectation, and a FAIL carries a non-empty
142
+ reason. `visual_loop_unavailable` reports whether the experiential browser
143
+ loop could not run: a see-act browser step exited 3 with `NOT POSSIBLE`
144
+ (unresolvable tooling), and/or `verdict.json`'s `missing_evidence` names
145
+ tool unavailability (playwright, see-act, not installed, could not
146
+ run/drive/launch, no browser).
142
147
  - Exit 2, `{ok:false, code}` — `missing` (no verdict.json), `corrupt`
143
148
  (unparseable or no verdict field), `contradiction` (record disagrees with
144
149
  the prose line), `no_reason` (FAIL with no machine-readable reason).
@@ -148,3 +153,18 @@ workflow reads. On a cross-check failure the QA phase is recorded as failed
148
153
  and retried at the same step — an unreasoned or contradictory verdict never
149
154
  routes to rework. No LLM judges report-prose consistency; the machine only
150
155
  enforces that the reason is present and the records agree.
156
+
157
+ ## A PASS without visual evidence never completes (2026-09-16)
158
+
159
+ A PASS verdict with an unavailable visual loop is not terminal. When the
160
+ cross-checker reports `visual_loop_unavailable` (the see-act loop never ran —
161
+ e.g. `playwright-core` was unresolvable from the release layout, a 2026-09-16
162
+ clean-room incident), the Standard and Bugfix QA closeouts park the task
163
+ fail-closed with `unattributable_reason=qa-visual-loop-unavailable` instead
164
+ of transitioning to done. The same park fires when the cross-check itself is
165
+ unreadable: unknown or unverifiable experiential evidence is never a PASS.
166
+
167
+ The dependency must resolve from the release layout for the loop to run at
168
+ all: `$CREW_HOME/node_modules` is a symlink into the npm install's
169
+ `node_modules`, resolved mechanically from the deploy source and repaired
170
+ on every `crew-release.sh deploy` (and re-established by `crew-init`).
@@ -136,6 +136,47 @@ _sync_workflow_docs() {
136
136
  echo "WORKFLOW-DOCS: synced $copied workflow docs to $dest"
137
137
  }
138
138
 
139
+ # ── dependency node_modules symlink ───────────────────────────────────
140
+ # lib/see-act.js and lib/render-html.js resolve playwright-core by name.
141
+ # The release layout (releases/<name>/lib/) is severed from the npm install
142
+ # that provides it: Node's upward module walk from the release lib/ dir
143
+ # never passes the install dir's node_modules, so require("playwright-core")
144
+ # threw and QA degraded to a mechanical-only PASS that still stamped done
145
+ # (clean-room defect, 2026-09-16 — a rubber stamp). The crew home therefore
146
+ # carries a top-level node_modules symlink into the install's node_modules,
147
+ # resolved mechanically from the deploy source repo:
148
+ # npm flow: <repo> is <install>/node_modules/muse-crew -> <install>/node_modules
149
+ # git flow: <repo> is a checkout (npm install run inside) -> <repo>/node_modules
150
+ # Repaired on every deploy like the lib/workflows symlinks; immutable
151
+ # releases stay untouched (the link lives at the crew-home top level, never
152
+ # inside a release dir). A missing source node_modules is a loud warning,
153
+ # never a deploy failure — the QA visual-loop guard parks instead of
154
+ # stamping when the tooling is unresolvable.
155
+ _link_dep_node_modules() {
156
+ local repo="${1:?usage: _link_dep_node_modules <repo-path> <crew-home>}"
157
+ local home="${2:?usage: _link_dep_node_modules <repo-path> <crew-home>}"
158
+ local src=""
159
+ local parent base
160
+ parent="$(dirname "$repo")"
161
+ base="$(basename "$parent")"
162
+ if [ "$base" = "node_modules" ] && [ -d "$parent" ]; then
163
+ src="$parent"
164
+ elif [ -d "$repo/node_modules" ]; then
165
+ src="$repo/node_modules"
166
+ fi
167
+ if [ -z "$src" ]; then
168
+ echo "DEP-NODE-MODULES: WARNING — no node_modules resolvable from $repo; skipping symlink (QA visual loop will park, not stamp)" >&2
169
+ return 0
170
+ fi
171
+ local link="$home/node_modules"
172
+ if [ -e "$link" ] && [ ! -L "$link" ]; then
173
+ echo "DEP-NODE-MODULES: $link exists as a real directory — leaving it (Node resolves from it directly)" >&2
174
+ return 0
175
+ fi
176
+ ln -sfn "$src" "$link"
177
+ echo "DEP-NODE-MODULES: $link -> $src"
178
+ }
179
+
139
180
  # ── deploy ────────────────────────────────────────────────────────────
140
181
  # Build, validate, and atomically activate a release from repo HEAD.
141
182
  # Single command — no cross-step lock needed.
@@ -143,6 +184,8 @@ cmd_deploy() {
143
184
  local repo="${1:?usage: deploy <repo-path> [<jarvis-home>]}"
144
185
  local home="${2:-$CREW_HOME}"
145
186
  CREW_HOME="$home"
187
+ # Absolute repo path before cd: the dependency symlink is resolved from it.
188
+ repo="$(realpath "$repo")"
146
189
 
147
190
  # Self-update: the stable script is installed once by cmd_init and would
148
191
  # otherwise go stale (2026-09-12: the stable copies predated the
@@ -233,6 +276,12 @@ cmd_deploy() {
233
276
  ln -sfn current/workflows "$CREW_HOME/workflows"
234
277
  ln -sfn current/lib "$CREW_HOME/lib"
235
278
 
279
+ # Dependency node_modules symlink (clean-room defect, 2026-09-16): keep the
280
+ # package next to its deps so lib/see-act.js and lib/render-html.js resolve
281
+ # playwright-core by name from the release lib/ dir. Repaired on every
282
+ # deploy; a missing source is a warning, never a failure.
283
+ _link_dep_node_modules "$repo" "$CREW_HOME"
284
+
236
285
  # Keep the dashboard's workflow documents in sync with the release.
237
286
  _sync_workflow_docs "$CREW_HOME"
238
287
 
@@ -12,9 +12,22 @@
12
12
  //
13
13
  // Reads <phase-dir>/verdict.json and prints exactly one JSON line to stdout.
14
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.
15
+ // Exit 0 with {ok:true, verdict, reason, summary, expected, actual, attempt,
16
+ // visual_loop_unavailable} when the record exists, parses, has a verdict
17
+ // field, the verdict equals --expect, and a FAIL carries a non-empty reason.
18
+ //
19
+ // visual_loop_unavailable (2026-09-16): a PASS verdict with missing
20
+ // experiential evidence must never be terminal — the QA closeout parks
21
+ // instead of stamping done (the clean-room rubber stamp: the see-act loop
22
+ // never ran because playwright-core was unresolvable from the release
23
+ // layout, yet the task stamped done on a mechanical-only PASS). Two
24
+ // mechanical signals, no judgment:
25
+ // (1) the OODA log in the same dir has a browser-action step
26
+ // (aria|shot|click|scroll|type) with exit 3 and NOT POSSIBLE in the
27
+ // observation — the see-act contract for "tooling unresolvable";
28
+ // (2) verdict.json's missing_evidence names tool-unavailability
29
+ // (playwright, see-act, not possible, not installed, unavailable,
30
+ // could not run/drive/launch, no browser).
18
31
  //
19
32
  // Exit 2 with {ok:false, code, error} when:
20
33
  // missing — verdict.json is absent (the agent never wrote one)
@@ -55,7 +68,8 @@ function main() {
55
68
  fail("bad_input", "unknown --expect: " + args.expect + " (PASS|FAIL)");
56
69
  }
57
70
 
58
- const verdictPath = join(resolve(args.dir), "verdict.json");
71
+ const dir = resolve(args.dir);
72
+ const verdictPath = join(dir, "verdict.json");
59
73
  if (!existsSync(verdictPath)) {
60
74
  fail("missing", "verdict.json not found: " + verdictPath);
61
75
  }
@@ -88,7 +102,54 @@ function main() {
88
102
  expected: record.expected === undefined ? "" : record.expected,
89
103
  actual: record.actual === undefined ? "" : record.actual,
90
104
  attempt: record.attempt === undefined ? "" : record.attempt,
105
+ visual_loop_unavailable: visualLoopUnavailable(dir, record),
91
106
  }) + "\n");
92
107
  }
93
108
 
109
+ // Visual-loop availability: true when the experiential browser loop could
110
+ // not run. Signal 1 — a browser-action OODA step (the see-act actions)
111
+ // exited 3 with NOT POSSIBLE in its observation (the driver's contract for
112
+ // unresolvable tooling). Signal 2 — the verdict's own missing_evidence
113
+ // names tool-unavailability. Both are string matches on machine-written
114
+ // records, never a judgment about report prose.
115
+ const BROWSER_ACTIONS = { aria: true, shot: true, click: true, scroll: true, type: true };
116
+ const TOOL_UNAVAILABLE = /(playwright|see-act|not possible|not installed|unavailable|could not (run|drive|launch)|no browser)/i;
117
+
118
+ function oodaLogUnavailable(dir) {
119
+ let text;
120
+ try {
121
+ text = readFileSync(join(dir, "ooda-log.jsonl"), "utf8");
122
+ } catch (e) {
123
+ return false;
124
+ }
125
+ const lines = text.split("\n");
126
+ for (let k = 0; k < lines.length; k++) {
127
+ const line = lines[k].trim();
128
+ if (!line) continue;
129
+ let step;
130
+ try {
131
+ step = JSON.parse(line);
132
+ } catch (e) {
133
+ continue;
134
+ }
135
+ if (!step || !BROWSER_ACTIONS[step.action]) continue;
136
+ if (step.exit === 3 && /not possible/i.test(String(step.observation || ""))) return true;
137
+ }
138
+ return false;
139
+ }
140
+
141
+ function missingEvidenceUnavailable(record) {
142
+ let me = record.missing_evidence;
143
+ if (me === undefined) me = record.missing;
144
+ if (!Array.isArray(me)) return false;
145
+ for (let k = 0; k < me.length; k++) {
146
+ if (TOOL_UNAVAILABLE.test(String(me[k]))) return true;
147
+ }
148
+ return false;
149
+ }
150
+
151
+ function visualLoopUnavailable(dir, record) {
152
+ return oodaLogUnavailable(dir) || missingEvidenceUnavailable(record);
153
+ }
154
+
94
155
  main();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "muse-crew",
3
- "version": "0.7.17",
3
+ "version": "0.7.18",
4
4
  "description": "Opinionated orchestration for Muse — workflows, identities, and tooling for autonomous software development.",
5
5
  "license": "UNLICENSED",
6
6
  "private": false,
@@ -2441,6 +2441,24 @@ while (i < STEPS.length) {
2441
2441
  };
2442
2442
  }
2443
2443
  log("QA verdict.json closeout gate passed: verdict.json agrees with prose VERDICT: " + qaVerdictExpect);
2444
+
2445
+ // QA visual-loop guard (clean-room defect 2026-09-16): Hazel's
2446
+ // verdict.json is honest about missing visual evidence, but the closeout
2447
+ // treated a PASS as terminal done even when the see-act loop never ran
2448
+ // (playwright-core was unresolvable from the release layout — the
2449
+ // dependency lived in the npm install dir, severed from the crew home).
2450
+ // A PASS verdict with missing experiential evidence must never be
2451
+ // terminal: the task parks fail-closed with
2452
+ // unattributable_reason=qa-visual-loop-unavailable instead of
2453
+ // transitioning to done. Code, not prompt text: visual_loop_unavailable
2454
+ // comes from lib/read-ooda-verdict.js (the OODA log's NOT POSSIBLE
2455
+ // browser steps and the verdict's missing_evidence tool-unavailability
2456
+ // notes), already parsed into qaVerdictGate above.
2457
+ if (verdictPassed === true && qaVerdictGate.visual_loop_unavailable === true) {
2458
+ log("QA visual loop unavailable — parking fail-closed (unattributable_reason=qa-visual-loop-unavailable), never done");
2459
+ return await parkTask("QA visual loop unavailable (unattributable_reason=qa-visual-loop-unavailable): the see-act browser loop could not run — verdict.json records missing visual evidence. A PASS without experiential evidence is never terminal. Human attention needed: repair the crew home's dependency symlink ($CREW_HOME/node_modules) or the npm install, then re-queue QA.");
2460
+ }
2461
+ log("QA visual-loop guard passed: experiential evidence present");
2444
2462
  }
2445
2463
 
2446
2464
 
@@ -29,6 +29,26 @@ if (!!dashboardSlug !== !!dashboardRepoPath) throw new Error("dashboardSlug and
29
29
 
30
30
  const orchDir = crewHome + "/.orchestration";
31
31
 
32
+ // ── Dependency node_modules resolution (clean-room defect 2026-09-16) ──
33
+ // lib/see-act.js and lib/render-html.js resolve playwright-core by name,
34
+ // but the release layout severs releases/<name>/lib/ from the npm install
35
+ // that provides node_modules — Node's upward walk never reaches it, so the
36
+ // QA see-act loop degraded to NOT POSSIBLE and a mechanical-only PASS
37
+ // stamped done (a rubber stamp). The crew home carries a top-level
38
+ // node_modules symlink into the install's node_modules, resolved here
39
+ // mechanically from crewRepoPath — no new parameters, no env vars, no
40
+ // agent judgment:
41
+ // npm flow: crewRepoPath is <install>/node_modules/muse-crew -> parent
42
+ // git flow: crewRepoPath is a checkout -> <repo>/node_modules
43
+ // crew-release.sh deploy enforces the same link on every deploy; the init
44
+ // step below is the belt-and-suspenders repeat for init.
45
+ function resolveDepNodeModules(repoPath) {
46
+ var r = String(repoPath).replace(/\/+$/, "");
47
+ var parent = r.split("/").slice(0, -1).join("/") || "/";
48
+ if (parent.split("/").pop() === "node_modules") return parent;
49
+ return r + "/node_modules";
50
+ }
51
+
32
52
  // ── Gate 0: Launchable-path validation ─────────────────────────────
33
53
  // workflow_launch only accepts workspace-contained scripts. A crewHome
34
54
  // outside the workspace would produce an instance whose workflows can
@@ -200,6 +220,48 @@ try {
200
220
  }
201
221
  log("Release: " + (releaseResult.existed ? "existing" : "bootstrapped") + " at " + releaseResult.hash);
202
222
 
223
+ // ── Dependency node_modules symlink ───────────────────────────────────
224
+ // The release bootstrap (crew-release.sh init -> deploy) already links
225
+ // $CREW_HOME/node_modules into the install's node_modules; this step is the
226
+ // explicit, mechanical belt-and-suspenders repeat. The target dir is
227
+ // computed by resolveDepNodeModules above — the agent only runs the shell
228
+ // verbatim, it judges nothing. A missing source dir is not fatal: the QA
229
+ // visual-loop guard parks instead of stamping when the tooling is
230
+ // unresolvable.
231
+ var depNodeModules = resolveDepNodeModules(crewRepoPath);
232
+ var safeDepNm = depNodeModules.split('"').join('\\"');
233
+ var safeCrewHomeNm = crewHome.split('"').join('\\"');
234
+ try {
235
+ var depLinkResult = await agent(
236
+ "Ensure the crew home's dependency node_modules symlink.\n\n" +
237
+ "Crew home: " + crewHome + "\n" +
238
+ "Dependency node_modules (computed, do not alter): " + depNodeModules + "\n\n" +
239
+ "Steps (run in shell exactly as written):\n" +
240
+ "if [ -d \"" + safeDepNm + "\" ]; then\n" +
241
+ " if [ -e \"" + safeCrewHomeNm + "/node_modules\" ] && [ ! -L \"" + safeCrewHomeNm + "/node_modules\" ]; then echo REAL_DIR_KEPT;\n" +
242
+ " else ln -sfn \"" + safeDepNm + "\" \"" + safeCrewHomeNm + "/node_modules\" && echo LINKED;\n" +
243
+ " fi\n" +
244
+ "else echo MISSING_SOURCE;\n" +
245
+ "fi\n\n" +
246
+ "Return JSON { linked: <true when the symlink now resolves into the computed dir, false otherwise>, note: \"<LINKED|REAL_DIR_KEPT|MISSING_SOURCE>\" }.",
247
+ {
248
+ key: "dep-node-modules-1",
249
+ label: "Linking dependency node_modules",
250
+ schema: {
251
+ type: "object",
252
+ properties: {
253
+ linked: { type: "boolean" },
254
+ note: { type: "string" }
255
+ },
256
+ required: ["linked", "note"]
257
+ }
258
+ }
259
+ );
260
+ log("Dependency node_modules: " + (depLinkResult.note || "unknown") + " -> " + depNodeModules);
261
+ } catch (e) {
262
+ log("Dependency node_modules link step errored (non-fatal): " + String(e.message || e));
263
+ }
264
+
203
265
  // ── Phase 2: Scaffold ─────────────────────────────────────────────────
204
266
  // Copies identities (with portraits), personas, workflow docs, and
205
267
  // feedback convention from the crew repo into .orchestration/ under crewHome.
@@ -2418,6 +2418,52 @@ while (i < STEPS.length) {
2418
2418
  }
2419
2419
  }
2420
2420
 
2421
+ // QA visual-loop guard (clean-room defect 2026-09-16): Hazel's verdict.json
2422
+ // is honest about missing visual evidence, but the closeout treated a PASS
2423
+ // as terminal done even when the see-act loop never ran (playwright-core
2424
+ // was unresolvable from the release layout — the dependency lived in the
2425
+ // npm install dir, severed from the crew home). A PASS verdict with missing
2426
+ // experiential evidence must never be terminal: the task parks fail-closed
2427
+ // with unattributable_reason=qa-visual-loop-unavailable instead of
2428
+ // transitioning to done. Code, not prompt text: the check reads the
2429
+ // machine-readable verdict via lib/read-ooda-verdict.js, which reports
2430
+ // visual_loop_unavailable from the OODA log's NOT POSSIBLE browser steps
2431
+ // and the verdict's missing_evidence tool-unavailability notes.
2432
+ if (step.name === "QA" && qaVisual && verdictPassed === true) {
2433
+ var qaLoopDir = crewHome + "/task-evidence/" + taskId + "/postchange";
2434
+ var qaLoopOut = "";
2435
+ try {
2436
+ var qaLoopCheck = await agent(
2437
+ "Check the QA visual loop's availability.\n" +
2438
+ "Run: node " + crewHome + "/current/lib/read-ooda-verdict.js --dir " + qaLoopDir + " --expect PASS\n" +
2439
+ "Return JSON { \"output\": \"<the command's full stdout, trimmed>\" } and nothing else.",
2440
+ { key: attemptKey("qa-visual-loop-check-" + taskId, totalReworkCount), label: "Checking QA visual-loop availability",
2441
+ schema: { type: "object", properties: { output: { type: "string" } }, required: ["output"] } }
2442
+ );
2443
+ qaLoopOut = (qaLoopCheck && qaLoopCheck.output ? qaLoopCheck.output : "").trim();
2444
+ } catch (e) {
2445
+ qaLoopOut = "";
2446
+ }
2447
+ // The script prints exactly one JSON line; exit info does not survive
2448
+ // the schema'd return, so the parsed object is the gate signal.
2449
+ var qaLoopGate = null;
2450
+ try {
2451
+ var qaLoopLines = qaLoopOut.split("\n");
2452
+ qaLoopGate = JSON.parse(qaLoopLines[qaLoopLines.length - 1]);
2453
+ } catch (e) {
2454
+ qaLoopGate = null;
2455
+ }
2456
+ if (!qaLoopGate || qaLoopGate.ok !== true) {
2457
+ log("QA visual-loop check unreadable — cannot confirm experiential evidence; parking fail-closed");
2458
+ return await parkTask("QA visual loop unverifiable (unattributable_reason=qa-visual-loop-unavailable): lib/read-ooda-verdict.js could not confirm the QA verdict record — a PASS without a machine-readable experiential record is never terminal. Human attention needed.");
2459
+ }
2460
+ if (qaLoopGate.visual_loop_unavailable === true) {
2461
+ log("QA visual loop unavailable — parking fail-closed (unattributable_reason=qa-visual-loop-unavailable), never done");
2462
+ return await parkTask("QA visual loop unavailable (unattributable_reason=qa-visual-loop-unavailable): the see-act browser loop could not run — verdict.json records missing visual evidence. A PASS without experiential evidence is never terminal. Human attention needed: repair the crew home's dependency symlink ($CREW_HOME/node_modules) or the npm install, then re-queue QA.");
2463
+ }
2464
+ log("QA visual-loop guard passed: experiential evidence present");
2465
+ }
2466
+
2421
2467
  // Deterministic closeout: no formatter agent. The verdict is mechanical
2422
2468
  // (extractVerdict above); the summary is the worker's report truncated.
2423
2469
  // For verdict steps passed comes from the verdict; for non-verdict steps