muse-crew 0.7.16 → 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.
- package/docs/ooda-report.md +27 -7
- package/lib/crew-release.sh +49 -0
- package/lib/read-ooda-verdict.js +65 -4
- package/package.json +1 -1
- package/workflows/bugfix.js +32 -4
- package/workflows/chore.js +14 -4
- package/workflows/crew-init.js +62 -0
- package/workflows/standard.js +60 -4
package/docs/ooda-report.md
CHANGED
|
@@ -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
|
|
136
|
-
prose `VERDICT:` line. It prints one JSON line to stdout and
|
|
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`
|
|
141
|
-
prose expectation, and a FAIL carries a non-empty
|
|
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`).
|
package/lib/crew-release.sh
CHANGED
|
@@ -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
|
|
package/lib/read-ooda-verdict.js
CHANGED
|
@@ -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
|
|
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
|
|
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
package/workflows/bugfix.js
CHANGED
|
@@ -1894,6 +1894,13 @@ while (i < STEPS.length) {
|
|
|
1894
1894
|
// agent calls that threw instead of returning a verdict —
|
|
1895
1895
|
// recorded for the post-poll diagnosis, never terminal alone.
|
|
1896
1896
|
var chunkFailures = [];
|
|
1897
|
+
// Poll status-read error count (2026-09-16): a failed
|
|
1898
|
+
// artifact_status read (rate limiting / TOO_MANY_REQUESTS /
|
|
1899
|
+
// 429, network error, or any non-build response) is
|
|
1900
|
+
// inconclusive — it is NOT evidence of absence and must never
|
|
1901
|
+
// be folded into the no-build-observed signal. Counted for
|
|
1902
|
+
// the post-poll diagnosis only; never terminal alone.
|
|
1903
|
+
var pollStatusErrors = 0;
|
|
1897
1904
|
for (var chunk = 1; chunk <= 3; chunk++) {
|
|
1898
1905
|
if (chunk > 1) {
|
|
1899
1906
|
var refreshPoll = await agent(
|
|
@@ -1917,14 +1924,15 @@ while (i < STEPS.length) {
|
|
|
1917
1924
|
try {
|
|
1918
1925
|
buildPoll = await agent(
|
|
1919
1926
|
"First call tool_search.load_tool_namespace with paths [\"artifact\"]. Then poll artifact_status for slug \"" + PUBLISH_SLUG + "\" \u2014 for OUR build only, the one whose agent_id is \"" + rebuildAgentId + "\" (the receipt captured when the edit was accepted; the agent_id is the artifact system's in-flight build correlation ID, stable across polls while the build runs). Check every 30 seconds, up to 7 checks (3.5 minutes max). On each check, read the raw build object:\n" +
|
|
1927
|
+
"If an artifact_status call itself FAILS (rate limiting / TOO_MANY_REQUESTS / 429, network error, or any response that is not a build object): that check is inconclusive \u2014 do NOT record it as \"no build\". Stop polling immediately and return with status_error: true. A failed read is not evidence of absence and must never be folded into the no-build-observed signal.\n" +
|
|
1920
1928
|
"On every check, record whether you have positively OBSERVED our build: a running build whose agent_id equals \"" + rebuildAgentId + "\", or a completed-build record whose agent_id equals \"" + rebuildAgentId + "\" (if the tool surfaces one \u2014 match it mechanically, never assume).\n" +
|
|
1921
1929
|
"- If no build is running (build is null) and you have NOT observed our build: our build's completion is UNPROVEN. Absence of a running build is not evidence our build ran. Do NOT report done.\n" +
|
|
1922
1930
|
"- If no build is running (build is null) and you previously observed our build running: our build finished. Stop and report done.\n" +
|
|
1923
1931
|
"- If the running build's agent_id equals \"" + rebuildAgentId + "\": still ours \u2014 keep waiting.\n" +
|
|
1924
1932
|
"- If the running build's agent_id is present but DIFFERENT: that is a stranger's build. Do NOT attribute its completion to our attempt and do NOT wait on it \u2014 keep checking within budget; if the budget expires without observing our build, report done=false. Record it in saw_stranger regardless of what else you observe.\n" +
|
|
1925
|
-
"Return JSON { \"build_done\": <true ONLY when you positively observed our build and it is no longer running, false otherwise>, \"saw_our_build\": <true if you observed our build at any check, false if never>, \"saw_stranger\": true if at ANY check a running build had an agent_id different from ours (\"" + rebuildAgentId + "\"), false otherwise, \"status\": \"<final status or timeout note>\", \"observed_agent_id\": \"<the agent_id seen on the last check, or null when no build was running>\" } and nothing else.",
|
|
1933
|
+
"Return JSON { \"build_done\": <true ONLY when you positively observed our build and it is no longer running, false otherwise>, \"saw_our_build\": <true if you observed our build at any check, false if never>, \"saw_stranger\": true if at ANY check a running build had an agent_id different from ours (\"" + rebuildAgentId + "\"), false otherwise, \"status\": \"<final status or timeout note>\", \"observed_agent_id\": \"<the agent_id seen on the last check, or null when no build was running>\", \"status_error\": <true ONLY when a status read failed as described above, false or omitted otherwise> } and nothing else.",
|
|
1926
1934
|
{ key: pollKey, label: "Waiting for artifact build to complete (chunk " + chunk + " of 3)",
|
|
1927
|
-
schema: { type: "object", properties: { build_done: { type: "boolean" }, saw_our_build: { type: "boolean" }, saw_stranger: { type: "boolean" }, status: { type: "string" }, observed_agent_id: { type: ["string", "null"] } }, required: ["build_done"] },
|
|
1935
|
+
schema: { type: "object", properties: { build_done: { type: "boolean" }, saw_our_build: { type: "boolean" }, saw_stranger: { type: "boolean" }, status: { type: "string" }, observed_agent_id: { type: ["string", "null"] }, status_error: { type: "boolean" } }, required: ["build_done"] },
|
|
1928
1936
|
timeoutMs: 270000 }
|
|
1929
1937
|
);
|
|
1930
1938
|
} catch (chunkErr) {
|
|
@@ -1942,6 +1950,7 @@ while (i < STEPS.length) {
|
|
|
1942
1950
|
pollSawOurBuild = pollSawOurBuild || (buildPoll.saw_our_build === true);
|
|
1943
1951
|
pollSawStranger = pollSawStranger || (buildPoll.saw_stranger === true);
|
|
1944
1952
|
lastObservedAgentId = buildPoll.observed_agent_id || null;
|
|
1953
|
+
if (buildPoll.status_error === true) { pollStatusErrors++; log("Artifact build poll chunk " + chunk + " of 3 hit a status-read error \u2014 recorded as inconclusive, continuing to the next chunk; a failed read is not evidence of absence."); }
|
|
1945
1954
|
if (buildPoll.build_done) { break; }
|
|
1946
1955
|
}
|
|
1947
1956
|
}
|
|
@@ -2064,9 +2073,10 @@ while (i < STEPS.length) {
|
|
|
2064
2073
|
}, totalReworkCount);
|
|
2065
2074
|
} else {
|
|
2066
2075
|
var unattributableReason = strangerObserved ? "stranger-build-observed-during-poll"
|
|
2076
|
+
: ((pollStatusErrors > 0 && !pollSawOurBuild) ? "status-read-errors-during-poll"
|
|
2067
2077
|
: (pollEndState === "build-still-running-at-poll-end" ? "build-still-running-at-poll-end"
|
|
2068
|
-
: (newAuditDirsAfterPoll.length === 0 ? "no-new-audit-dir-in-window" : "audit-report-unreadable-or-missing"));
|
|
2069
|
-
publishFailure = "Artifact build completion unproven (fail-closed, no provenance stamped): unattributable_reason=" + unattributableReason + "; poll_end_state=" + pollEndState + "; " + "saw_our_build=" + pollSawOurBuild + "; new_audit_dirs=" + newAuditDirsAfterPoll.length + "; poll_chunks_failed=" + chunkFailures.length + ". Attribution is by window, not by build identity. The publish may or may not have landed. Fail-closed.";
|
|
2078
|
+
: (newAuditDirsAfterPoll.length === 0 ? "no-new-audit-dir-in-window" : "audit-report-unreadable-or-missing")));
|
|
2079
|
+
publishFailure = "Artifact build completion unproven (fail-closed, no provenance stamped): unattributable_reason=" + unattributableReason + "; poll_end_state=" + pollEndState + "; " + "saw_our_build=" + pollSawOurBuild + "; new_audit_dirs=" + newAuditDirsAfterPoll.length + "; poll_chunks_failed=" + chunkFailures.length + "; poll_status_errors=" + pollStatusErrors + ". Attribution is by window, not by build identity. The publish may or may not have landed. Fail-closed.";
|
|
2070
2080
|
await recordPublishLedger({
|
|
2071
2081
|
commit: mergeCommitForPublish,
|
|
2072
2082
|
attempt: rebuildAttemptKey,
|
|
@@ -2431,6 +2441,24 @@ while (i < STEPS.length) {
|
|
|
2431
2441
|
};
|
|
2432
2442
|
}
|
|
2433
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");
|
|
2434
2462
|
}
|
|
2435
2463
|
|
|
2436
2464
|
|
package/workflows/chore.js
CHANGED
|
@@ -1907,6 +1907,13 @@ while (i < STEPS.length) {
|
|
|
1907
1907
|
// agent calls that threw instead of returning a verdict —
|
|
1908
1908
|
// recorded for the post-poll diagnosis, never terminal alone.
|
|
1909
1909
|
var chunkFailures = [];
|
|
1910
|
+
// Poll status-read error count (2026-09-16): a failed
|
|
1911
|
+
// artifact_status read (rate limiting / TOO_MANY_REQUESTS /
|
|
1912
|
+
// 429, network error, or any non-build response) is
|
|
1913
|
+
// inconclusive — it is NOT evidence of absence and must never
|
|
1914
|
+
// be folded into the no-build-observed signal. Counted for
|
|
1915
|
+
// the post-poll diagnosis only; never terminal alone.
|
|
1916
|
+
var pollStatusErrors = 0;
|
|
1910
1917
|
for (var chunk = 1; chunk <= 3; chunk++) {
|
|
1911
1918
|
if (chunk > 1) {
|
|
1912
1919
|
var refreshPoll = await agent(
|
|
@@ -1930,14 +1937,15 @@ while (i < STEPS.length) {
|
|
|
1930
1937
|
try {
|
|
1931
1938
|
buildPoll = await agent(
|
|
1932
1939
|
"First call tool_search.load_tool_namespace with paths [\"artifact\"]. Then poll artifact_status for slug \"" + PUBLISH_SLUG + "\" \u2014 for OUR build only, the one whose agent_id is \"" + rebuildAgentId + "\" (the receipt captured when the edit was accepted; the agent_id is the artifact system's in-flight build correlation ID, stable across polls while the build runs). Check every 30 seconds, up to 7 checks (3.5 minutes max). On each check, read the raw build object:\n" +
|
|
1940
|
+
"If an artifact_status call itself FAILS (rate limiting / TOO_MANY_REQUESTS / 429, network error, or any response that is not a build object): that check is inconclusive \u2014 do NOT record it as \"no build\". Stop polling immediately and return with status_error: true. A failed read is not evidence of absence and must never be folded into the no-build-observed signal.\n" +
|
|
1933
1941
|
"On every check, record whether you have positively OBSERVED our build: a running build whose agent_id equals \"" + rebuildAgentId + "\", or a completed-build record whose agent_id equals \"" + rebuildAgentId + "\" (if the tool surfaces one \u2014 match it mechanically, never assume).\n" +
|
|
1934
1942
|
"- If no build is running (build is null) and you have NOT observed our build: our build's completion is UNPROVEN. Absence of a running build is not evidence our build ran. Do NOT report done.\n" +
|
|
1935
1943
|
"- If no build is running (build is null) and you previously observed our build running: our build finished. Stop and report done.\n" +
|
|
1936
1944
|
"- If the running build's agent_id equals \"" + rebuildAgentId + "\": still ours \u2014 keep waiting.\n" +
|
|
1937
1945
|
"- If the running build's agent_id is present but DIFFERENT: that is a stranger's build. Do NOT attribute its completion to our attempt and do NOT wait on it \u2014 keep checking within budget; if the budget expires without observing our build, report done=false. Record it in saw_stranger regardless of what else you observe.\n" +
|
|
1938
|
-
"Return JSON { \"build_done\": <true ONLY when you positively observed our build and it is no longer running, false otherwise>, \"saw_our_build\": <true if you observed our build at any check, false if never>, \"saw_stranger\": true if at ANY check a running build had an agent_id different from ours (\"" + rebuildAgentId + "\"), false otherwise, \"status\": \"<final status or timeout note>\", \"observed_agent_id\": \"<the agent_id seen on the last check, or null when no build was running>\" } and nothing else.",
|
|
1946
|
+
"Return JSON { \"build_done\": <true ONLY when you positively observed our build and it is no longer running, false otherwise>, \"saw_our_build\": <true if you observed our build at any check, false if never>, \"saw_stranger\": true if at ANY check a running build had an agent_id different from ours (\"" + rebuildAgentId + "\"), false otherwise, \"status\": \"<final status or timeout note>\", \"observed_agent_id\": \"<the agent_id seen on the last check, or null when no build was running>\", \"status_error\": <true ONLY when a status read failed as described above, false or omitted otherwise> } and nothing else.",
|
|
1939
1947
|
{ key: pollKey, label: "Waiting for artifact build to complete (chunk " + chunk + " of 3)",
|
|
1940
|
-
schema: { type: "object", properties: { build_done: { type: "boolean" }, saw_our_build: { type: "boolean" }, saw_stranger: { type: "boolean" }, status: { type: "string" }, observed_agent_id: { type: ["string", "null"] } }, required: ["build_done"] },
|
|
1948
|
+
schema: { type: "object", properties: { build_done: { type: "boolean" }, saw_our_build: { type: "boolean" }, saw_stranger: { type: "boolean" }, status: { type: "string" }, observed_agent_id: { type: ["string", "null"] }, status_error: { type: "boolean" } }, required: ["build_done"] },
|
|
1941
1949
|
timeoutMs: 270000 }
|
|
1942
1950
|
);
|
|
1943
1951
|
} catch (chunkErr) {
|
|
@@ -1955,6 +1963,7 @@ while (i < STEPS.length) {
|
|
|
1955
1963
|
pollSawOurBuild = pollSawOurBuild || (buildPoll.saw_our_build === true);
|
|
1956
1964
|
pollSawStranger = pollSawStranger || (buildPoll.saw_stranger === true);
|
|
1957
1965
|
lastObservedAgentId = buildPoll.observed_agent_id || null;
|
|
1966
|
+
if (buildPoll.status_error === true) { pollStatusErrors++; log("Artifact build poll chunk " + chunk + " of 3 hit a status-read error \u2014 recorded as inconclusive, continuing to the next chunk; a failed read is not evidence of absence."); }
|
|
1958
1967
|
if (buildPoll.build_done) { break; }
|
|
1959
1968
|
}
|
|
1960
1969
|
}
|
|
@@ -2075,9 +2084,10 @@ while (i < STEPS.length) {
|
|
|
2075
2084
|
}, reworkCount);
|
|
2076
2085
|
} else {
|
|
2077
2086
|
var unattributableReason = strangerObserved ? "stranger-build-observed-during-poll"
|
|
2087
|
+
: ((pollStatusErrors > 0 && !pollSawOurBuild) ? "status-read-errors-during-poll"
|
|
2078
2088
|
: (pollEndState === "build-still-running-at-poll-end" ? "build-still-running-at-poll-end"
|
|
2079
|
-
: (newAuditDirsAfterPoll.length === 0 ? "no-new-audit-dir-in-window" : "audit-report-unreadable-or-missing"));
|
|
2080
|
-
publishFailure = "Artifact build completion unproven (fail-closed, no provenance stamped): unattributable_reason=" + unattributableReason + "; poll_end_state=" + pollEndState + "; " + "saw_our_build=" + pollSawOurBuild + "; new_audit_dirs=" + newAuditDirsAfterPoll.length + "; poll_chunks_failed=" + chunkFailures.length + ". Attribution is by window, not by build identity. The publish may or may not have landed. Fail-closed.";
|
|
2089
|
+
: (newAuditDirsAfterPoll.length === 0 ? "no-new-audit-dir-in-window" : "audit-report-unreadable-or-missing")));
|
|
2090
|
+
publishFailure = "Artifact build completion unproven (fail-closed, no provenance stamped): unattributable_reason=" + unattributableReason + "; poll_end_state=" + pollEndState + "; " + "saw_our_build=" + pollSawOurBuild + "; new_audit_dirs=" + newAuditDirsAfterPoll.length + "; poll_chunks_failed=" + chunkFailures.length + "; poll_status_errors=" + pollStatusErrors + ". Attribution is by window, not by build identity. The publish may or may not have landed. Fail-closed.";
|
|
2081
2091
|
await recordPublishLedger({
|
|
2082
2092
|
commit: mergeCommitForPublish,
|
|
2083
2093
|
attempt: rebuildAttemptKey,
|
package/workflows/crew-init.js
CHANGED
|
@@ -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.
|
package/workflows/standard.js
CHANGED
|
@@ -1859,6 +1859,13 @@ while (i < STEPS.length) {
|
|
|
1859
1859
|
// agent calls that threw instead of returning a verdict —
|
|
1860
1860
|
// recorded for the post-poll diagnosis, never terminal alone.
|
|
1861
1861
|
var chunkFailures = [];
|
|
1862
|
+
// Poll status-read error count (2026-09-16): a failed
|
|
1863
|
+
// artifact_status read (rate limiting / TOO_MANY_REQUESTS /
|
|
1864
|
+
// 429, network error, or any non-build response) is
|
|
1865
|
+
// inconclusive — it is NOT evidence of absence and must never
|
|
1866
|
+
// be folded into the no-build-observed signal. Counted for
|
|
1867
|
+
// the post-poll diagnosis only; never terminal alone.
|
|
1868
|
+
var pollStatusErrors = 0;
|
|
1862
1869
|
for (var chunk = 1; chunk <= 3; chunk++) {
|
|
1863
1870
|
if (chunk > 1) {
|
|
1864
1871
|
var refreshPoll = await agent(
|
|
@@ -1882,14 +1889,15 @@ while (i < STEPS.length) {
|
|
|
1882
1889
|
try {
|
|
1883
1890
|
buildPoll = await agent(
|
|
1884
1891
|
"First call tool_search.load_tool_namespace with paths [\"artifact\"]. Then poll artifact_status for slug \"" + PUBLISH_SLUG + "\" \u2014 for OUR build only, the one whose agent_id is \"" + rebuildAgentId + "\" (the receipt captured when the edit was accepted; the agent_id is the artifact system's in-flight build correlation ID, stable across polls while the build runs). Check every 30 seconds, up to 7 checks (3.5 minutes max). On each check, read the raw build object:\n" +
|
|
1892
|
+
"If an artifact_status call itself FAILS (rate limiting / TOO_MANY_REQUESTS / 429, network error, or any response that is not a build object): that check is inconclusive \u2014 do NOT record it as \"no build\". Stop polling immediately and return with status_error: true. A failed read is not evidence of absence and must never be folded into the no-build-observed signal.\n" +
|
|
1885
1893
|
"On every check, record whether you have positively OBSERVED our build: a running build whose agent_id equals \"" + rebuildAgentId + "\", or a completed-build record whose agent_id equals \"" + rebuildAgentId + "\" (if the tool surfaces one \u2014 match it mechanically, never assume).\n" +
|
|
1886
1894
|
"- If no build is running (build is null) and you have NOT observed our build: our build's completion is UNPROVEN. Absence of a running build is not evidence our build ran. Do NOT report done.\n" +
|
|
1887
1895
|
"- If no build is running (build is null) and you previously observed our build running: our build finished. Stop and report done.\n" +
|
|
1888
1896
|
"- If the running build's agent_id equals \"" + rebuildAgentId + "\": still ours \u2014 keep waiting.\n" +
|
|
1889
1897
|
"- If the running build's agent_id is present but DIFFERENT: that is a stranger's build. Do NOT attribute its completion to our attempt and do NOT wait on it \u2014 keep checking within budget; if the budget expires without observing our build, report done=false. Record it in saw_stranger regardless of what else you observe.\n" +
|
|
1890
|
-
"Return JSON { \"build_done\": <true ONLY when you positively observed our build and it is no longer running, false otherwise>, \"saw_our_build\": <true if you observed our build at any check, false if never>, \"saw_stranger\": true if at ANY check a running build had an agent_id different from ours (\"" + rebuildAgentId + "\"), false otherwise, \"status\": \"<final status or timeout note>\", \"observed_agent_id\": \"<the agent_id seen on the last check, or null when no build was running>\" } and nothing else.",
|
|
1898
|
+
"Return JSON { \"build_done\": <true ONLY when you positively observed our build and it is no longer running, false otherwise>, \"saw_our_build\": <true if you observed our build at any check, false if never>, \"saw_stranger\": true if at ANY check a running build had an agent_id different from ours (\"" + rebuildAgentId + "\"), false otherwise, \"status\": \"<final status or timeout note>\", \"observed_agent_id\": \"<the agent_id seen on the last check, or null when no build was running>\", \"status_error\": <true ONLY when a status read failed as described above, false or omitted otherwise> } and nothing else.",
|
|
1891
1899
|
{ key: pollKey, label: "Waiting for artifact build to complete (chunk " + chunk + " of 3)",
|
|
1892
|
-
schema: { type: "object", properties: { build_done: { type: "boolean" }, saw_our_build: { type: "boolean" }, saw_stranger: { type: "boolean" }, status: { type: "string" }, observed_agent_id: { type: ["string", "null"] } }, required: ["build_done"] },
|
|
1900
|
+
schema: { type: "object", properties: { build_done: { type: "boolean" }, saw_our_build: { type: "boolean" }, saw_stranger: { type: "boolean" }, status: { type: "string" }, observed_agent_id: { type: ["string", "null"] }, status_error: { type: "boolean" } }, required: ["build_done"] },
|
|
1893
1901
|
timeoutMs: 270000 }
|
|
1894
1902
|
);
|
|
1895
1903
|
} catch (chunkErr) {
|
|
@@ -1907,6 +1915,7 @@ while (i < STEPS.length) {
|
|
|
1907
1915
|
pollSawOurBuild = pollSawOurBuild || (buildPoll.saw_our_build === true);
|
|
1908
1916
|
pollSawStranger = pollSawStranger || (buildPoll.saw_stranger === true);
|
|
1909
1917
|
lastObservedAgentId = buildPoll.observed_agent_id || null;
|
|
1918
|
+
if (buildPoll.status_error === true) { pollStatusErrors++; log("Artifact build poll chunk " + chunk + " of 3 hit a status-read error \u2014 recorded as inconclusive, continuing to the next chunk; a failed read is not evidence of absence."); }
|
|
1910
1919
|
if (buildPoll.build_done) { break; }
|
|
1911
1920
|
}
|
|
1912
1921
|
}
|
|
@@ -2029,9 +2038,10 @@ while (i < STEPS.length) {
|
|
|
2029
2038
|
}, totalReworkCount);
|
|
2030
2039
|
} else {
|
|
2031
2040
|
var unattributableReason = strangerObserved ? "stranger-build-observed-during-poll"
|
|
2041
|
+
: ((pollStatusErrors > 0 && !pollSawOurBuild) ? "status-read-errors-during-poll"
|
|
2032
2042
|
: (pollEndState === "build-still-running-at-poll-end" ? "build-still-running-at-poll-end"
|
|
2033
|
-
: (newAuditDirsAfterPoll.length === 0 ? "no-new-audit-dir-in-window" : "audit-report-unreadable-or-missing"));
|
|
2034
|
-
publishFailure = "Artifact build completion unproven (fail-closed, no provenance stamped): unattributable_reason=" + unattributableReason + "; poll_end_state=" + pollEndState + "; " + "saw_our_build=" + pollSawOurBuild + "; new_audit_dirs=" + newAuditDirsAfterPoll.length + "; poll_chunks_failed=" + chunkFailures.length + ". Attribution is by window, not by build identity. The publish may or may not have landed. Fail-closed.";
|
|
2043
|
+
: (newAuditDirsAfterPoll.length === 0 ? "no-new-audit-dir-in-window" : "audit-report-unreadable-or-missing")));
|
|
2044
|
+
publishFailure = "Artifact build completion unproven (fail-closed, no provenance stamped): unattributable_reason=" + unattributableReason + "; poll_end_state=" + pollEndState + "; " + "saw_our_build=" + pollSawOurBuild + "; new_audit_dirs=" + newAuditDirsAfterPoll.length + "; poll_chunks_failed=" + chunkFailures.length + "; poll_status_errors=" + pollStatusErrors + ". Attribution is by window, not by build identity. The publish may or may not have landed. Fail-closed.";
|
|
2035
2045
|
await recordPublishLedger({
|
|
2036
2046
|
commit: mergeCommitForPublish,
|
|
2037
2047
|
attempt: rebuildAttemptKey,
|
|
@@ -2408,6 +2418,52 @@ while (i < STEPS.length) {
|
|
|
2408
2418
|
}
|
|
2409
2419
|
}
|
|
2410
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
|
+
|
|
2411
2467
|
// Deterministic closeout: no formatter agent. The verdict is mechanical
|
|
2412
2468
|
// (extractVerdict above); the summary is the worker's report truncated.
|
|
2413
2469
|
// For verdict steps passed comes from the verdict; for non-verdict steps
|