muse-crew 0.7.17 → 0.7.19

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`).
@@ -42,18 +42,23 @@ The rule: **the builder's applied report is never a verification signal, in
42
42
  either direction.** A matching report certifies nothing (it is derived from
43
43
  the carried diff — circular by construction, canary run 8). A mismatching or
44
44
  empty report blocks nothing (false-negative mode demonstrated by `23ca8f3f`).
45
- The workflow computes the report comparison as an observation only — it
46
- never parks on it. The parent ignores the applied report entirely when
47
- deciding whether to stamp: an `applied:[]` or mismatched report with a
48
- matching independent read-back stamps normally.
45
+ The workflow dropped the report entirely on 2026-09-16 (clean-room task
46
+ `e2a8d9f8`): the trigger's JSON closeout contract traveled over the
47
+ stochastic text channel and the runtime's JSON-candidate heuristic misfired
48
+ on its prose ("workflow agent output was not JSON"), parking a task whose
49
+ edit may have gone through. The trigger is now fire-and-forget — no schema,
50
+ no consumed return value — and there is no builder report to observe:
51
+ `applied_report` is a constant `missing-report` on the ledger line. The
52
+ parent ignores the (absent) report entirely when deciding whether to stamp.
49
53
 
50
54
  The contract is split on purpose:
51
55
 
52
- - **Workflow-owned:** carrying the merged diff to the builder, the
53
- applied-report observation (logged, never a park), the build-completion
54
- poll, post-deploy cleanup, recording the Publish session completed, and
55
- parking with `publish: verification-requested <commit>` instead of
56
- stamping. The workflow does NOT trigger the read-back inspection — an
56
+ - **Workflow-owned:** carrying the merged diff to the builder, attributing
57
+ the edit itself (fire-and-forget trigger no builder report — via
58
+ pre-trigger toolcheck, pre-trigger build-state baseline, and post-trigger
59
+ build-state diff), the build-completion poll, post-deploy cleanup,
60
+ recording the Publish session completed, and parking with `publish:
61
+ verification-requested <commit>` instead of stamping. The workflow does NOT trigger the read-back inspection — an
57
62
  async inspection triggered from inside a workflow run delivers its result
58
63
  to the root agent, never back into the run, so a workflow-side trigger is
59
64
  an orphan the verifier cannot consume. The parent triggers the one
@@ -313,24 +318,40 @@ For a task parked with `publish: verification-requested <commit>`:
313
318
  second scan sees the unexpired `publish: verification-claimed` note and
314
319
  skips. The lease expiry bounds the damage if a claimer dies.
315
320
 
316
- ## Unknown-outcome recovery (2026-09-14, Gate 1 Journey 3 attempt 7)
321
+ ## Unknown-outcome recovery (2026-09-14, Gate 1 Journey 3 attempt 7; fire-and-forget 2026-09-16)
317
322
 
318
323
  Attempt 7 parked at Publish with outcome `unknown`: the rebuild trigger's
319
324
  child failed structured closeout and the in-flight-only build-state poll
320
325
  could not see the completed build — even though the build HAD run (a fresh
321
- platform audit directory existed). Two mechanisms close this gap.
322
-
323
- **1. Workflow-side durable evidence.** Before the rebuild trigger, the
326
+ platform audit directory existed). 2026-09-16 (clean-room task `e2a8d9f8`)
327
+ showed the failure is worse than a catchable throw: the runtime's
328
+ JSON-candidate heuristic rejects the trigger call itself ("workflow agent
329
+ output was not JSON") whenever the child returns prose, whether or not the
330
+ edit went through. The trigger is therefore fire-and-forget — no schema, no
331
+ consumed return value — and the workflow always attributes the edit itself.
332
+ Two mechanisms close the gap.
333
+
334
+ **1. Pre-trigger toolcheck + baseline.** Before the trigger, a tiny schema'd
335
+ child proves the artifact tool namespace is available (one bounded retry on
336
+ explicit negative evidence — the only safe retry on the publish path:
337
+ without the tools the edit provably did not go through) and captures a
338
+ pre-trigger build-state baseline. After the trigger, the workflow diffs the
339
+ post-trigger build state against the baseline: a build whose `agent_id` is
340
+ new relative to the baseline is this edit's receipt. The baseline build's
341
+ `agent_id` is never substituted — a build already in flight at baseline
342
+ predates the trigger and is never attributed to this edit.
343
+
344
+ **2. Workflow-side durable evidence.** Before the rebuild trigger, the
324
345
  workflow snapshots the artifact's audit-directory listing
325
- (`~/workspace/ts-spaces/<slug>/audits/` — best-effort, never a gate). On a
326
- structured-output failure with no in-flight build observed, it re-lists and
327
- diffs: a timestamped directory that appeared during the trigger window is
328
- positive evidence the edit went through and the build completed. The
329
- fallback never re-issues the edit, never stamps provenance, and only routes
330
- to the parent's independent content read-back. No new directory still parks
331
- `unknown` fail-closed. The ledger distinguishes the two confirmations: `edit
332
- confirmed via durable audit evidence …` vs `edit confirmed via build-state
333
- poll …`.
346
+ (`~/workspace/ts-spaces/<slug>/audits/` — best-effort, never a gate). When
347
+ no in-flight receipt was observed, it re-lists and diffs: a timestamped
348
+ directory that appeared during the trigger window is positive evidence the
349
+ edit went through and the build completed. The fallback never re-issues the
350
+ edit, never stamps provenance, and only routes to the parent's independent
351
+ content read-back. No new directory still parks `unknown` fail-closed. The
352
+ ledger's `detail` line distinguishes the two confirmations: `… edit
353
+ confirmed via durable audit evidence (new audit dir …)` vs `… build receipt
354
+ captured by workflow-owned build-state observation (pre/post-trigger diff)`.
334
355
 
335
356
  The fallback's known limitation: audit directories are not attributed to
336
357
  tasks, so two concurrent publishes to the same artifact could cross-read.
@@ -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.19",
4
4
  "description": "Opinionated orchestration for Muse — workflows, identities, and tooling for autonomous software development.",
5
5
  "license": "UNLICENSED",
6
6
  "private": false,