muse-crew 0.8.0 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -123,6 +123,36 @@ const WORKTREE_HINT = REPO_PATH + "/.worktrees/" + taskId;
123
123
  const WORKTREE_PRESERVED_HINT = ".worktrees/" + taskId;
124
124
 
125
125
  const PUBLISH_TYPE = projectConfig.deploy_type || "";
126
+ // User-facing surface for experiential QA routing: 'artifact' (a rendered
127
+ // web UI Hazel drives with the see-act browser loop) | 'terminal' (a CLI
128
+ // Hazel drives herself, keeping attempt-scoped transcripts) | null
129
+ // (unclassified — no experiential QA). environment_type is the canonical
130
+ // UX-surface axis; deploy_type names the deployment target, but
131
+ // deploy_type === "artifact" remains a legacy artifact-surface signal so
132
+ // pre-field projects keep today's experiential QA (the migration does not
133
+ // backfill the column).
134
+ const ENV_TYPE = projectConfig.environment_type || null;
135
+ // Surface resolution: artifact wins on contradictory config (the deployed
136
+ // artifact is what users see). Unclassified surface => Capture skips, QA
137
+ // runs the plain code-blind prompt — today's behavior, unchanged.
138
+ const SURFACE_ARTIFACT = (PUBLISH_TYPE === "artifact" || ENV_TYPE === "artifact");
139
+ const SURFACE_TERMINAL = (!SURFACE_ARTIFACT && ENV_TYPE === "terminal");
140
+ const SURFACE_CLASSIFIED = SURFACE_ARTIFACT || SURFACE_TERMINAL;
141
+ // One-line surface description for the Triage prompt, so Sage judges the
142
+ // experiential flag against the project's actual user-facing surface.
143
+ const SURFACE_TRIAGE_DESC = SURFACE_ARTIFACT
144
+ ? "This project's user-facing surface is artifact: a rendered web UI."
145
+ : SURFACE_TERMINAL
146
+ ? "This project's user-facing surface is terminal: a command-line interface."
147
+ : "This project's user-facing surface is unclassified (environment_type not set): judge by what a user would directly observe.";
148
+ // UX doctrine page: the shared UX bar for this run's surface, resolved
149
+ // mechanically — every phase prompt reads UX_DOCTRINE_PATH, never a
150
+ // hardcoded filename. Canonical map: lib/ux-doctrine.js (mirrored here as a
151
+ // one-liner because the workflow runtime's relative-import support is
152
+ // unverified; tests pin the mirror). Null on unclassified surfaces: no
153
+ // shared page, and prompts say so instead of naming the wrong one.
154
+ const UX_DOCTRINE_PAGE = SURFACE_TERMINAL ? "terminal-ux.md" : (SURFACE_ARTIFACT ? "artifact-ux.md" : null);
155
+ const UX_DOCTRINE_PATH = UX_DOCTRINE_PAGE ? crewHome + "/current/docs/" + UX_DOCTRINE_PAGE : null;
126
156
  const PUBLISH_SLUG = projectConfig.deploy_slug || "";
127
157
  const PROJECT_DESC = projectConfig.description || "React + TypeScript web dashboard (client/src/, server/src/, drizzle/)";
128
158
  const RELEASE_SCRIPT = crewHome + "/crew-release.sh";
@@ -540,7 +570,7 @@ function extractMarkerLines(workerText) {
540
570
  var markers = [];
541
571
  for (var i = 0; i < lines.length; i++) {
542
572
  var line = lines[i].trim();
543
- if (/^(repo_diff:|release:|version_bump:|VERDICT:|TARGET_VERSION=|published:|experiential:|layer:|capture_targets:|worktree:)/i.test(line)) {
573
+ if (/^(repo_diff:|release:|version_bump:|VERDICT:|TARGET_VERSION=|published:|experiential:|layer:|capture_targets:|terminal_targets:|worktree:)/i.test(line)) {
544
574
  markers.push(line);
545
575
  }
546
576
  }
@@ -730,6 +760,10 @@ let isExperiential = null;
730
760
  // notes are immutable within a run, so the lookup runs at most once.
731
761
  let experientialResolved = null;
732
762
  let captureTargets = "";
763
+ // Sage's terminal_targets marker: the CLI commands/flags exercising the
764
+ // changed surface (terminal-surface tasks). Falls back to the task
765
+ // description when Sage omits it.
766
+ let terminalTargets = "";
733
767
  let mapGateBounceCount = 0;
734
768
  // Merge-time versioning: the release decision is extracted deterministically
735
769
  // from the accepted Build worker report (extractReleaseDecision) so the Publish
@@ -1034,13 +1068,13 @@ while (i < STEPS.length) {
1034
1068
  if (step.name === "Capture") {
1035
1069
  var capExp = await resolveExperiential();
1036
1070
  var bounceSuffix = (mapGateBounceCount > 0 ? "-g" + mapGateBounceCount : "");
1037
- if (capExp !== "yes" || PUBLISH_TYPE !== "artifact") {
1038
- log("Capture skipped for task " + taskId + " — " + (capExp !== "yes" ? "not experiential" : "publish target is not artifact"));
1071
+ if (capExp !== "yes" || !SURFACE_CLASSIFIED) {
1072
+ log("Capture skipped for task " + taskId + " — " + (capExp !== "yes" ? "not experiential" : "surface unclassified (environment_type=" + (ENV_TYPE || "null") + ")"));
1039
1073
  await agent(
1040
1074
  "Update the session and log the event.\n" +
1041
1075
  "Run in shell and return the stdout verbatim:\n" + crewCmd("record-phase", {
1042
1076
  task_id: taskId,
1043
- session: { id: activeSessionId, task_id: taskId, identity: step.identity, step: step.name, status: "completed", notes: "Capture skipped — not an experiential artifact task" },
1077
+ session: { id: activeSessionId, task_id: taskId, identity: step.identity, step: step.name, status: "completed", notes: "Capture skipped — not an experiential task on a classified surface" },
1044
1078
  event: { task_id: taskId, type: "completed", identity: step.identity, message: "Capture completed by " + step.identity }
1045
1079
  }),
1046
1080
  { key: "record-Capture" + bounceSuffix, label: "Recording Capture result" }
@@ -1048,6 +1082,34 @@ while (i < STEPS.length) {
1048
1082
  i++;
1049
1083
  continue;
1050
1084
  }
1085
+ // Terminal-surface Capture: Hazel runs the CLI herself against the
1086
+ // pre-change tree and archives attempt-scoped transcripts. No parent
1087
+ // protocol exists for terminal projects (there is no see-act loop to
1088
+ // drive), so the baseline is agent-driven in one shot — the Map gate
1089
+ // reads the same "baseline: captured/none" note prefixes either way.
1090
+ if (SURFACE_TERMINAL) {
1091
+ var termBaseDir = crewHome + "/task-evidence/" + taskId + "/baseline";
1092
+ log("Capture: terminal-surface baseline for task " + taskId + " — Hazel drives the CLI herself");
1093
+ await agent(
1094
+ "Run the pre-change terminal baseline yourself — there is no parent capture protocol for terminal-surface projects.\n" +
1095
+ "1. The project's repo is at " + REPO_PATH + " (pre-change state; the task branch does not exist yet). The CLI under test is the project's own command-line interface in that tree — start like a new user with --help. You are code-blind: you may RUN the CLI, never READ its source.\n" +
1096
+ "2. Terminal targets for this task: " + (terminalTargets || "not declared — derive them from --help and the task description") + ".\n" +
1097
+ "3. Run in shell: mkdir -p " + termBaseDir + "\n" +
1098
+ "4. For each target, run the command with stdout AND stderr captured to " + termBaseDir + "/<nn>-<short-slug>.txt (number them 01, 02, ...), appending the exit code as the final line. Pattern: <cmd> > " + termBaseDir + "/01-<slug>.txt 2>&1; echo \"exit=$?\" >> " + termBaseDir + "/01-<slug>.txt\n" +
1099
+ "5. READ every transcript file you wrote — an unread transcript is not evidence.\n" +
1100
+ "6. Log the baseline note — run in shell and return the stdout verbatim:\n" + crewCmd("log-event", { task_id: taskId, type: "note", identity: step.identity, message: "baseline: captured (terminal transcripts: <comma-separated filenames>)" }) + "\n" +
1101
+ " (replace <comma-separated filenames> with the real filenames). If the CLI cannot run from the pre-change tree (will not start, missing dependency), log instead: baseline: none (terminal targets not runnable: <reason>) — never fabricate a transcript.\n" +
1102
+ "7. Record the phase — run in shell and return the stdout verbatim:\n" + crewCmd("record-phase", {
1103
+ task_id: taskId,
1104
+ session: { id: activeSessionId, task_id: taskId, identity: step.identity, step: step.name, status: "completed", notes: "Terminal baseline captured by the QA agent (transcripts archived)" },
1105
+ event: { task_id: taskId, type: "completed", identity: step.identity, message: "Capture completed by " + step.identity }
1106
+ }) + "\n" +
1107
+ "Report back in plain prose: which commands you ran and what the pre-change baseline looks like.",
1108
+ { key: "terminal-baseline-" + taskId + bounceSuffix, label: "Capturing terminal baseline" }
1109
+ );
1110
+ i++;
1111
+ continue;
1112
+ }
1051
1113
  var capStatus = await baselineStatus();
1052
1114
  // Stale-decision guard: a "baseline: none (visual protocol unavailable)"
1053
1115
  // note is only durable while the protocol is unavailable. When
@@ -1125,9 +1187,9 @@ while (i < STEPS.length) {
1125
1187
  var mapBaselineRefs = "";
1126
1188
  var mapBaselineNone = false;
1127
1189
  if (step.name === "Map") {
1128
- // Must match Capture's run condition (experiential + artifact publish):
1190
+ // Must match Capture's run condition (experiential + classified surface):
1129
1191
  // when Capture skips, no baseline notes exist, so the gate must not apply.
1130
- if ((await resolveExperiential()) === "yes" && PUBLISH_TYPE === "artifact") {
1192
+ if ((await resolveExperiential()) === "yes" && SURFACE_CLASSIFIED) {
1131
1193
  var gateStatus = await baselineStatus();
1132
1194
  if (!gateStatus.baseline_found) {
1133
1195
  log("Map gate: no baseline evidence for experiential task " + taskId + " — bouncing to Capture");
@@ -1149,13 +1211,16 @@ while (i < STEPS.length) {
1149
1211
  }
1150
1212
  }
1151
1213
 
1152
- // Experiential routing: experiential artifact tasks run the Hazel
1153
- // experiential QA prompt (built in the PUBLISH_TYPE === "artifact" branch
1154
- // of the QA step) — Hazel drives the artifact herself and owns the visual
1155
- // verdict through her OODA report and write-ooda-verdict ledger.
1156
- var qaVisual = false;
1214
+ // Experiential routing: experiential tasks on a classified surface run the
1215
+ // Hazel experiential QA prompt (built in the SURFACE_ARTIFACT /
1216
+ // SURFACE_TERMINAL branches of the QA step) — Hazel drives the surface
1217
+ // herself and owns the verdict through her OODA report and the
1218
+ // write-ooda-verdict ledger. qaExperiential gates the closeout
1219
+ // experiential-loop guard (a PASS with missing experiential evidence is
1220
+ // never terminal).
1221
+ var qaExperiential = false;
1157
1222
  if (step.name === "QA") {
1158
- qaVisual = (await resolveExperiential()) === "yes" && PUBLISH_TYPE === "artifact";
1223
+ qaExperiential = (await resolveExperiential()) === "yes" && SURFACE_CLASSIFIED;
1159
1224
  }
1160
1225
 
1161
1226
  // Step-specific instructions
@@ -1163,7 +1228,7 @@ while (i < STEPS.length) {
1163
1228
  var instructions = "";
1164
1229
 
1165
1230
  if (step.name === "Triage") {
1166
- instructions = "Validate the task against the project's repo at " + REPO_PATH + " — that exact checkout, not any other copy of the project on disk. If you run git commands, cd " + REPO_PATH + " first.\nCheck clarity, note dependencies, confirm the standard workflow assignment.\nWrite a brief triage assessment as notes for the next step.\nReport back in plain prose — what you found.\nEXPERIENTIAL FLAG: does this task change anything rendered and visible in the project's user-facing artifact (pages, components, styles, layout, copy, visual states)? If yes it is experiential and gets baseline captures (plus a visual verdict where the workflow has a QA phase). End your report with exactly one line on its own, lowercase, unrephrased: experiential: yes — or experiential: no. This line is machine-read.";
1231
+ instructions = "Validate the task against the project's repo at " + REPO_PATH + " — that exact checkout, not any other copy of the project on disk. If you run git commands, cd " + REPO_PATH + " first.\nCheck clarity, note dependencies, confirm the standard workflow assignment.\nWrite a brief triage assessment as notes for the next step.\nReport back in plain prose — what you found.\nEXPERIENTIAL FLAG: does this task change anything a user can directly observe in the project's user-facing surface? " + SURFACE_TRIAGE_DESC + " For an artifact surface that means rendered and visible — pages, components, styles, layout, copy, visual states. For a terminal surface it means the CLI experience — command output, help text, flags, error messages, defaults. The shared UX bar is " + (UX_DOCTRINE_PATH ? UX_DOCTRINE_PATH + " — flag experiential when the task touches anything it covers." : "not classified for this project — flag experiential when the task touches anything user-observable in the surface described above.") + " If yes it is experiential and gets baseline evidence (plus an experiential verdict where the workflow has a QA phase). For terminal-surface tasks also declare the CLI surface to exercise: end your report with a line `terminal_targets: <comma-separated CLI commands/flags>` (machine-read; optional — falls back to the task description). End your report with exactly one line on its own, lowercase, unrephrased: experiential: yes — or experiential: no. This line is machine-read.";
1167
1232
 
1168
1233
  } else if (step.name === "Map") {
1169
1234
  var mapGatePara = "";
@@ -1171,11 +1236,16 @@ while (i < STEPS.length) {
1171
1236
  mapGatePara = "\nBASELINE GATE (experiential task): " +
1172
1237
  (mapBaselineNone
1173
1238
  ? "no baseline was capturable (baseline: none recorded) — write the spec without baseline comparison and note it."
1174
- : "pre-change baseline captures: " + mapBaselineRefs + " — consult the affected views when writing the spec.") +
1239
+ : "pre-change baseline evidence: " + mapBaselineRefs + " — consult it when writing the spec.") +
1175
1240
  " If the baseline evidence is missing with no baseline:none recorded, do not write the spec — report 'baseline evidence missing — Map gate bounce required' and stop.\n" +
1176
- "Declare capture targets for the post-change visual capture: end your report with a line `capture_targets: <comma-separated views/controls this change affects>` (optional; falls back to the task description).";
1241
+ (SURFACE_TERMINAL
1242
+ ? "Declare the CLI surface to exercise: end your report with a line `terminal_targets: <comma-separated CLI commands/flags>` (machine-read; optional — falls back to the task description)."
1243
+ : "Declare capture targets for the post-change visual capture: end your report with a line `capture_targets: <comma-separated views/controls this change affects>` (optional; falls back to the task description).");
1177
1244
  }
1178
- instructions = "Research the problem space, evaluate options, pick the shortest path.\nWrite a clear spec that a builder can execute without asking questions.\nThe builder will edit source files in a git worktree.\nProject: " + PROJECT_DESC + "\nSave the spec to exactly this file: " + SPEC_PATH + " — run mkdir -p \"" + SPEC_DIR + "\" first. Do not save the spec anywhere else; this exact path is fixed and will be checked mechanically after your step.\nReport back in plain prose — what you specified." + mapGatePara;
1245
+ instructions = "Research the problem space, evaluate options, pick the shortest path.\nWrite a clear spec that a builder can execute without asking questions.\n" +
1246
+ (SURFACE_TERMINAL ? "TERMINAL SPEC: this project's surface is a CLI. Specify the exact commands, their expected stdout/stderr, exit codes, --help text, and error messages — Hazel judges the build against this spec and the shared bar at " + UX_DOCTRINE_PATH + ".\n" : "") +
1247
+ (SURFACE_ARTIFACT ? "ARTIFACT SPEC: this project's surface is a rendered artifact. Specify the exact screens, flows, and visual states the change affects — Hazel judges the build against this spec and the shared bar at " + UX_DOCTRINE_PATH + ".\n" : "") +
1248
+ "The builder will edit source files in a git worktree.\nProject: " + PROJECT_DESC + "\nSave the spec to exactly this file: " + SPEC_PATH + " — run mkdir -p \"" + SPEC_DIR + "\" first. Do not save the spec anywhere else; this exact path is fixed and will be checked mechanically after your step.\nReport back in plain prose — what you specified." + mapGatePara;
1179
1249
 
1180
1250
  } else if (step.name === "Build") {
1181
1251
  instructions = "STEP 1: Prepare your worktree.\n" +
@@ -1190,6 +1260,8 @@ while (i < STEPS.length) {
1190
1260
  "This is the project source: " + PROJECT_DESC + "\n" +
1191
1261
  "Edit source files directly. Do NOT use artifact_edit — that happens in the Publish phase.\n" +
1192
1262
  "Do not add unrequested features. Build exactly what the spec calls for.\n" +
1263
+ (SURFACE_TERMINAL ? "TERMINAL UX: build to the shared bar at " + UX_DOCTRINE_PATH + " — --help text, error messages, and exit codes are user-facing and ship in this commit.\n" : "") +
1264
+ (SURFACE_ARTIFACT ? "ARTIFACT UX: build to the shared bar at " + UX_DOCTRINE_PATH + " — the rendered result is what the user sees; it ships in this commit.\n" : "") +
1193
1265
  "PUBLIC DOCS: If your change is public-affecting (it alters anything a user or consumer can observe: API actions, parameters, behavior, or errors), update the public docs in the same commit — API.md for API changes. Documentation and implementation ship together.\n\n" +
1194
1266
  (PUBLISH_TYPE === "npm" ? "PACKAGE VERSION: this project publishes to the npm registry. Versions are assigned at PUBLISH time — never in your branch. Do NOT touch the `version` field in package.json (or package-lock). Instead, end your report with exactly these two lines:\n" +
1195
1267
  "release: yes|no — 'yes' if this change warrants a published release (anything a consumer can observe: workflow behavior, phase lists, identities, published docs, API); 'no' if internal-only.\n" +
@@ -1234,6 +1306,8 @@ while (i < STEPS.length) {
1234
1306
  "You can also read specific files in the worktree at:\n" +
1235
1307
  WORKTREE_HINT + "/\n\n" +
1236
1308
  "Check quality, correctness, and spec compliance.\n" +
1309
+ (SURFACE_TERMINAL ? "TERMINAL UX REVIEW: judge the CLI surface against " + UX_DOCTRINE_PATH + " — help accuracy, error quality, exit codes, output clarity. Reject when the bar is not met.\n" : "") +
1310
+ (SURFACE_ARTIFACT ? "ARTIFACT UX REVIEW: judge the rendered surface against " + UX_DOCTRINE_PATH + " — alignment, spacing, hierarchy, composition, balance, finish, correctness. Reject when the bar is not met.\n" : "") +
1237
1311
  "Check that public-affecting changes have matching public doc updates (API.md or the published API contract). If the docs are missing or inaccurate, report what is stale, then end your report with exactly this line: VERDICT: FAIL.\n" +
1238
1312
  "If the branch has no commits ahead of main (inspect shows an empty commit log), approve ONLY if the Build summary declares `repo_diff: none` with (a) a plausible runtime-state deliverable (e.g. a cron created via the cron tool), or (b) an already-merged declaration `repo_diff: none (already-merged: <sha>)` AND the mechanical fact below confirms the sha verified. MECHANICAL FACT (computed by the workflow, never by the builder): already_merged sha = " + (alreadyMergedSha ? alreadyMergedSha + " (verified ancestor of main: YES)" : "none declared") + ". Otherwise report 'no commits ahead of main and no valid repo_diff: none declaration — the builder likely forgot to commit', then end your report with exactly this line: VERDICT: FAIL.\n" +
1239
1313
  (PUBLISH_TYPE === "npm" ? "PACKAGE VERSION: this project publishes to the npm registry, and versions are assigned at publish time — never in branches. Two checks:\n" +
@@ -2137,9 +2211,10 @@ while (i < STEPS.length) {
2137
2211
  "Verify the bump SCOPE: the <scope> in that line MUST equal the accepted version_bump scope \"" + releaseDecision.version_bump + "\" — if it differs, report the mismatch, then end your report with exactly this line: VERDICT: FAIL. Verify the ARITHMETIC: <base> + <scope> must equal <new-version> (patch increments the last segment only, e.g. 0.3.0 + patch → 0.3.1; minor increments the middle and resets the last to 0; major increments the first and resets the rest to 0) — if the math is wrong, report it, then end your report with exactly this line: VERDICT: FAIL.\n" +
2138
2212
  "Extract the published version from the notes line matching published: muse-crew@<version>. It MUST equal <new-version> from the TARGET_VERSION line. Then run: npm view muse-crew version. The registry version MUST equal <new-version>. If any of these checks fails, report 'npm publish verification failed: [details]', then end your report with exactly this line: VERDICT: FAIL.\n"
2139
2213
  : "";
2140
- if (PUBLISH_TYPE === "artifact") {
2214
+ if (SURFACE_ARTIFACT) {
2141
2215
  var safeDesc = taskDescription.replace(/"/g, "'").replace(/\\/g, "\\\\").slice(0, 500);
2142
- instructions = "You are code-blind QA. You NEVER read source files.\n" +
2216
+ instructions = "Read the shared UX bar FIRST: " + UX_DOCTRINE_PATH + " — it is the bar the whole crew builds to, and your verdict judges against it point by point.\n\n" +
2217
+ "You are code-blind QA. You NEVER read source files.\n" +
2143
2218
  "Public docs (API.md, README, published action schemas) are NOT source code — read them freely, exactly as a user would.\n\n" +
2144
2219
  "STEP 1: Experiential visual inspection — drive the artifact as a user would, one browser step at a time.\n" +
2145
2220
  "You have a see-act driver: " + crewHome + "/current/lib/see-act.js (a node script; one browser action per invocation; it prints one JSON line to stdout). It launches its own Chromium through a self-contained loopback proxy — the ONLY url you may give it is the local artifact server you start below. Never point it at any other URL.\n" +
@@ -2173,6 +2248,40 @@ while (i < STEPS.length) {
2173
2248
  "(replace <issue title> and <issue details> with the real values).\n\n" +
2174
2249
  "BASELINE SANITY: in the event history you fetched, the task's note events must contain a message starting with `baseline: captured` or `baseline: none`. If no message starts with either prefix, report 'baseline evidence missing at QA — the Map gate was bypassed', then end your report with exactly this line: VERDICT: FAIL.\n\n" +
2175
2250
  "Report back in plain prose — what checks you ran and their results. Checks you could not run are evidence gaps, not silent drops: name every one in --missing — unknown is neither PASS nor FAIL. End your report with exactly one line: VERDICT: PASS or VERDICT: FAIL. First ensure the OODA log exists even if you logged zero steps (touch " + crewHome + "/task-evidence/" + taskId + "/postchange/ooda-log.jsonl — an empty log is honest, an absent one is a broken report). Also write the same verdict machine-readably: node " + crewHome + "/current/lib/write-ooda-verdict.js --dir " + crewHome + "/task-evidence/" + taskId + "/postchange/ --attempt \"1\" --verdict <PASS|FAIL|NOT_POSSIBLE> --summary \"<one line>\" --expected \"<what the task required>\" --actual \"<what you observed>\" --missing '[\"honest evidence gap, if any\"]' [--reason \"<why it failed — REQUIRED and non-empty when verdict is FAIL or NOT_POSSIBLE; the script rejects a reason-less negative verdict with exit 2>\"] — this writes verdict.json (the latest verdict) and appends to verdicts.jsonl (the append-only ledger: every attempt's verdict is preserved, never overwritten).";
2251
+ } else if (SURFACE_TERMINAL) {
2252
+ // Terminal-surface experiential QA: Hazel drives the CLI herself —
2253
+ // the terminal counterpart to the see-act loop above. Same OODA
2254
+ // discipline (append-ooda-step with --action terminal and a transcript
2255
+ // per step; verdict via write-ooda-verdict), judged against the
2256
+ // shared bar resolved via UX_DOCTRINE_PATH (the terminal doctrine page here). Transcripts are delivered;
2257
+ // screenshots are never invented for terminal work.
2258
+ var termEvidence = crewHome + "/task-evidence/" + taskId + "/postchange";
2259
+ var termTargetsLine = terminalTargets || "not declared — derive from --help and the task description";
2260
+ instructions = "You are code-blind QA. You NEVER read source files.\n" +
2261
+ "Public docs (API.md, README) are NOT source code — read them freely, exactly as a user would.\n" +
2262
+ "Read the shared UX bar FIRST: " + UX_DOCTRINE_PATH + " — it is the bar the whole crew builds to, and your verdict judges against it point by point.\n\n" +
2263
+ "STEP 1: Experiential terminal inspection — drive the CLI as a user would, one command at a time.\n" +
2264
+ "a. The CLI under test lives in " + REPO_PATH + " (the merged change is on main there). Start like a new user: run --help. You may RUN the CLI; you may never READ its source.\n" +
2265
+ "b. Terminal targets for this task: " + termTargetsLine + ".\n" +
2266
+ "c. Bounded terminal loop — at most 8 commands. For each target: run it RIGHT (the happy path), then run it WRONG on purpose (bad flags, missing args, nonexistent files, empty input, contradictory flags). Error quality is half the grade: every failure must exit non-zero, say what went wrong in plain language, and tell the user the fix. A raw stack trace shown to a user is a defect — file it as one.\n" +
2267
+ "d. Evidence: capture EVERY invocation as a transcript. Run: mkdir -p " + termEvidence + "\n" +
2268
+ " For each command: <cmd> > " + termEvidence + "/<nn>-<short-slug>.txt 2>&1; echo \"exit=$?\" >> " + termEvidence + "/<nn>-<short-slug>.txt (number them 01, 02, ...). Then READ the transcript before judging it — an unread transcript is not evidence.\n" +
2269
+ "e. Log each step to the OODA log — run in shell, one command per step:\n" +
2270
+ " node " + crewHome + "/current/lib/append-ooda-step.js --log " + termEvidence + "/ooda-log.jsonl --attempt \"1\" --step <N> --action terminal --exit <code> --transcript " + termEvidence + "/<nn>-<short-slug>.txt --args '{\"cmd\":\"<the exact command>\"}' --observation \"<1-2 sentences: what the output said and what you concluded>\"\n" +
2271
+ " Steps are strictly monotonic within an attempt (1, 2, 3, ...). A rerun is a NEW attempt (\"2\", \"3\", ...) — never overwrite attempt 1. If the CLI will not run at all, log the step with --exit 3 and NOT POSSIBLE in the observation — never fabricate a transcript.\n" +
2272
+ "f. Compare against the pre-change baseline transcripts in " + crewHome + "/task-evidence/" + taskId + "/baseline/ — every finding cites its baseline and post-change transcripts by step number.\n" +
2273
+ "Then continue with the mechanical checks below. Your VERDICT covers both the experiential and the mechanical checks.\n\n" +
2274
+ "STEP 2: Verify data integrity via the crew API.\n" +
2275
+ "Run in shell and return the stdout verbatim:\n" + crewCmd("get-state", { events_limit: 1 }) + "\n" +
2276
+ "Use the returned tasks, sessions, and events to check the task's data-level effects.\n" +
2277
+ "DOCS GATE: If the change is public-affecting (it alters anything a user or consumer can observe: API actions, parameters, behavior, or errors), verify the public docs describe it. If public docs are missing or stale for a public-affecting change, report 'public docs missing/stale for [the change]', then end your report with exactly this line: VERDICT: FAIL. QA always fails when public-affecting changes lack public docs. Guide/tutorial gaps are lower priority — file a follow-up task for those instead of failing.\n\n" +
2278
+ "STEP 3: File follow-up tasks for any related issues you discover.\n" +
2279
+ "For each issue, run in shell:\n" +
2280
+ "node " + CREW_API + " --crew-home " + crewHome + " create-task --json '{\"title\": \"<issue title>\", \"description\": \"<issue details>\", \"project\": \"" + LAUNCH_PROJECT_ID + "\", \"workflow\": \"bugfix\", \"filed_by\": \"hazel\"}'\n" +
2281
+ "(replace <issue title> and <issue details> with the real values).\n\n" +
2282
+ npmPublishCheck +
2283
+ "BASELINE SANITY: in the event history you fetched, the task's note events must contain a message starting with `baseline: captured` or `baseline: none`. If no message starts with either prefix, report 'baseline evidence missing at QA — the Map gate was bypassed', then end your report with exactly this line: VERDICT: FAIL. When the baseline is terminal transcripts, confirm every terminal target you judged has a baseline transcript: a target with no pre-change transcript is an evidence gap — name it in --missing, never invent the baseline.\n\n" +
2284
+ "Report back in plain prose — what checks you ran and their results. Checks you could not run are evidence gaps, not silent drops: name every one in --missing — unknown is neither PASS nor FAIL. End your report with exactly one line: VERDICT: PASS or VERDICT: FAIL. First ensure the OODA log exists even if you logged zero steps (touch " + termEvidence + "/ooda-log.jsonl — an empty log is honest, an absent one is a broken report). Also write the same verdict machine-readably: node " + crewHome + "/current/lib/write-ooda-verdict.js --dir " + termEvidence + " --attempt \"1\" --verdict <PASS|FAIL|NOT_POSSIBLE> --summary \"<one line>\" --expected \"<what the task required>\" --actual \"<what you observed>\" --missing '[\"honest evidence gap, if any\"]' [--reason \"<why it failed — REQUIRED and non-empty when verdict is FAIL or NOT_POSSIBLE; the script rejects a reason-less negative verdict with exit 2>\"] — this writes verdict.json (the latest verdict) and appends to verdicts.jsonl (the append-only ledger: every attempt's verdict is preserved, never overwritten).";
2176
2285
  } else {
2177
2286
  instructions = "Test from a user's perspective. You are CODE-BLIND — do NOT read source code.\n" +
2178
2287
  "Public docs (API.md, README) are NOT source code — read them freely, exactly as a user would.\n" +
@@ -2184,10 +2293,10 @@ while (i < STEPS.length) {
2184
2293
  npmPublishCheck +
2185
2294
  "Report back in plain prose — what you tested and found. End your report with exactly one line: VERDICT: PASS or VERDICT: FAIL.";
2186
2295
  }
2187
- // qaVisual (experiential artifact tasks): Hazel owns the visual verdict
2188
- // through the experiential QA prompt built in the PUBLISH_TYPE ===
2189
- // "artifact" branch above (STEP 1 see-act loop + OODA report). There is
2190
- // no parent visual-verdict protocol anymore — no override here.
2296
+ // qaExperiential: Hazel owns the experiential verdict through the QA
2297
+ // prompt built in the SURFACE_ARTIFACT / SURFACE_TERMINAL branches above
2298
+ // (see-act loop or terminal loop + OODA report). There is no parent
2299
+ // visual-verdict protocol anymore — no override here.
2191
2300
  }
2192
2301
 
2193
2302
  // Task event history — Review and Publish are excluded. Review is cold by
@@ -2416,26 +2525,30 @@ while (i < STEPS.length) {
2416
2525
  }
2417
2526
  }
2418
2527
 
2419
- // QA visual-loop guard (clean-room defect 2026-09-16): Hazel's verdict.json
2420
- // is honest about missing visual evidence, but the closeout treated a PASS
2421
- // as terminal done even when the see-act loop never ran (playwright-core
2528
+ // QA experiential-loop guard (clean-room defect 2026-09-16): Hazel's verdict.json
2529
+ // is honest about missing experiential evidence, but the closeout treated a PASS
2530
+ // as terminal done even when the experiential loop never ran (playwright-core
2422
2531
  // was unresolvable from the release layout — the dependency lived in the
2423
2532
  // npm install dir, severed from the crew home). A PASS verdict with missing
2424
2533
  // experiential evidence must never be terminal: the task parks fail-closed
2425
- // with unattributable_reason=qa-visual-loop-unavailable instead of
2534
+ // with unattributable_reason=qa-visual-loop-unavailable (artifact surface)
2535
+ // or qa-terminal-loop-unavailable (terminal surface) instead of
2426
2536
  // transitioning to done. Code, not prompt text: the check reads the
2427
2537
  // machine-readable verdict via lib/read-ooda-verdict.js, which reports
2428
2538
  // visual_loop_unavailable from the OODA log's NOT POSSIBLE browser steps
2429
- // and the verdict's missing_evidence tool-unavailability notes.
2430
- if (step.name === "QA" && qaVisual && verdictPassed === true) {
2539
+ // and terminal_loop_unavailable from NOT POSSIBLE terminal steps, plus the
2540
+ // verdict's missing_evidence tool-unavailability notes.
2541
+ if (step.name === "QA" && qaExperiential && verdictPassed === true) {
2431
2542
  var qaLoopDir = crewHome + "/task-evidence/" + taskId + "/postchange";
2543
+ var qaLoopSurface = SURFACE_TERMINAL ? "terminal" : "visual";
2544
+ var qaLoopReason = SURFACE_TERMINAL ? "qa-terminal-loop-unavailable" : "qa-visual-loop-unavailable";
2432
2545
  var qaLoopOut = "";
2433
2546
  try {
2434
2547
  var qaLoopCheck = await agent(
2435
- "Check the QA visual loop's availability.\n" +
2548
+ "Check the QA " + qaLoopSurface + " loop's availability.\n" +
2436
2549
  "Run: node " + crewHome + "/current/lib/read-ooda-verdict.js --dir " + qaLoopDir + " --expect PASS\n" +
2437
2550
  "Return JSON { \"output\": \"<the command's full stdout, trimmed>\" } and nothing else.",
2438
- { key: attemptKey("qa-visual-loop-check-" + taskId, totalReworkCount), label: "Checking QA visual-loop availability",
2551
+ { key: attemptKey("qa-" + qaLoopSurface + "-loop-check-" + taskId, totalReworkCount), label: "Checking QA " + qaLoopSurface + "-loop availability",
2439
2552
  schema: { type: "object", properties: { output: { type: "string" } }, required: ["output"] } }
2440
2553
  );
2441
2554
  qaLoopOut = (qaLoopCheck && qaLoopCheck.output ? qaLoopCheck.output : "").trim();
@@ -2452,14 +2565,18 @@ while (i < STEPS.length) {
2452
2565
  qaLoopGate = null;
2453
2566
  }
2454
2567
  if (!qaLoopGate || qaLoopGate.ok !== true) {
2455
- log("QA visual-loop check unreadable — cannot confirm experiential evidence; parking fail-closed");
2456
- 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.");
2568
+ log("QA " + qaLoopSurface + "-loop check unreadable — cannot confirm experiential evidence; parking fail-closed");
2569
+ return await parkTask("QA " + qaLoopSurface + " loop unverifiable (unattributable_reason=" + qaLoopReason + "): 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.");
2457
2570
  }
2458
- if (qaLoopGate.visual_loop_unavailable === true) {
2459
- log("QA visual loop unavailable — parking fail-closed (unattributable_reason=qa-visual-loop-unavailable), never done");
2460
- 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.");
2571
+ var qaLoopUnavailable = SURFACE_TERMINAL ? qaLoopGate.terminal_loop_unavailable : qaLoopGate.visual_loop_unavailable;
2572
+ if (qaLoopUnavailable === true) {
2573
+ log("QA " + qaLoopSurface + " loop unavailableparking fail-closed (unattributable_reason=" + qaLoopReason + "), never done");
2574
+ return await parkTask("QA " + qaLoopSurface + " loop unavailable (unattributable_reason=" + qaLoopReason + "): " +
2575
+ (SURFACE_TERMINAL
2576
+ ? "the terminal loop could not run — the CLI would not execute, and verdict.json records missing terminal evidence. A PASS without experiential evidence is never terminal. Human attention needed: check the project's runtime dependencies at " + REPO_PATH + ", then re-queue QA."
2577
+ : "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."));
2461
2578
  }
2462
- log("QA visual-loop guard passed: experiential evidence present");
2579
+ log("QA " + qaLoopSurface + "-loop guard passed: experiential evidence present");
2463
2580
  }
2464
2581
 
2465
2582
  // Deterministic closeout: no formatter agent. The verdict is mechanical
@@ -2683,9 +2800,12 @@ while (i < STEPS.length) {
2683
2800
  captureTargets = ctm ? ctm[1].trim().slice(0, 300) : "";
2684
2801
  }
2685
2802
 
2686
- // Capture Sage's experiential flag (machine-read marker line).
2803
+ // Capture Sage's experiential flag (machine-read marker line) and the
2804
+ // terminal targets for terminal-surface tasks.
2687
2805
  if (step.name === "Triage" && passed) {
2688
2806
  isExperiential = extractExperiential(workerText);
2807
+ var ttm = /^terminal_targets:\s*(.+)/im.exec(workerText);
2808
+ terminalTargets = ttm ? ttm[1].trim().slice(0, 300) : "";
2689
2809
  }
2690
2810
 
2691
2811
  // Capture the accepted Build report's machine-readable release decision.