muse-crew 0.14.5 → 0.14.7
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/API.md +106 -12
- package/docs/decisions/AGENTS.md +5 -0
- package/docs/decisions/publish-path.md +247 -9
- package/docs/decisions/qa-reproduce.md +30 -0
- package/docs/guide.md +41 -5
- package/docs/publish-unknown-recovery.md +28 -120
- package/docs/publish-verification.md +148 -501
- package/lib/AGENTS.md +9 -13
- package/lib/advance-publish-base.js +3 -3
- package/lib/compose-evidence-caption.js +1 -2
- package/lib/compute-publish-diff.js +82 -5
- package/lib/crew-api.js +1134 -1027
- package/lib/merge-lock.sh +153 -41
- package/lib/publish-note-vocabulary.js +135 -42
- package/lib/qa-db.js +132 -0
- package/lib/schema.sql +69 -0
- package/lib/serve-artifact.js +46 -2
- package/lib/test-detached-integrate.sh +122 -0
- package/lib/test-merge-lock.sh +30 -1
- package/lib/worktree-lifecycle.sh +284 -34
- package/package.json +1 -1
- package/seed/AGENTS.md +1 -0
- package/seed/cron-body-ack-scan.md +44 -0
- package/seed/cron-body-template.md +55 -89
- package/seed/crons.json +12 -0
- package/workflows/AGENTS.md +1 -1
- package/workflows/bugfix.js +489 -224
- package/workflows/chore.js +306 -226
- package/workflows/crew-dispatch.js +57 -4
- package/workflows/crew-init.js +23 -0
- package/workflows/crew-uninstall.js +7 -4
- package/workflows/docs.js +24 -2
- package/workflows/standard.js +494 -251
- package/workflows/upgrade.js +13 -1
- package/lib/build-readback-request.js +0 -140
- package/lib/check-intent-freshness.js +0 -101
- package/lib/classify-publish-absence.js +0 -462
- package/lib/publish-content.js +0 -154
- package/lib/readback-disk.js +0 -195
- package/lib/retry-publish.js +0 -417
- package/lib/verify-publish.js +0 -416
package/workflows/bugfix.js
CHANGED
|
@@ -155,6 +155,62 @@ function extractVerdict(workerText) {
|
|
|
155
155
|
if (uniq.length !== 1) return { ok: false, count: matches.length };
|
|
156
156
|
return { ok: true, passed: last.value === "PASS" };
|
|
157
157
|
}
|
|
158
|
+
// Room #26 blocker 38 (2026-09-21): content-finding attribution. Hazel
|
|
159
|
+
// reports user-visible content observations in a machine-readable
|
|
160
|
+
// CONTENT-FINDINGS block; the workflow classifies each against the task's
|
|
161
|
+
// publish diff — mechanical set membership, never prose judgment. A finding
|
|
162
|
+
// is attributable to the task's change iff its subject file is in the
|
|
163
|
+
// publish diff. The special subject "live-data" (content in the artifact's
|
|
164
|
+
// runtime data stores — decks, cards, rows) is never in a git diff, so it
|
|
165
|
+
// always classifies environment-attributable: the crew has no live-DB write
|
|
166
|
+
// path (blocker 32: QA writes go to a fresh per-run temp DB), so live
|
|
167
|
+
// content is never the task's change. Findings are always recorded — never
|
|
168
|
+
// suppressed for looking audit-y — but environment-attributable findings
|
|
169
|
+
// never park the journey and never spawn an artifact bugfix. The
|
|
170
|
+
// platform-side half (audit sessions writing to production via the
|
|
171
|
+
// shared-concurrent path) is out of crew scope; see
|
|
172
|
+
// docs/decisions/qa-reproduce.md#content-finding-attribution.
|
|
173
|
+
function extractContentFindings(workerText) {
|
|
174
|
+
// The block is the LAST "CONTENT-FINDINGS:" line; the JSON array follows
|
|
175
|
+
// on the same line. Fail closed (ok:false) when missing or malformed —
|
|
176
|
+
// the caller blocks the phase for retry; unknown attribution must never
|
|
177
|
+
// degrade to "no findings" (the prose grounds stay preserved in the
|
|
178
|
+
// session notes).
|
|
179
|
+
var text = workerText || "";
|
|
180
|
+
var regex = /^CONTENT-FINDINGS:\s*(\[.*\])\s*$/gim;
|
|
181
|
+
var matches = [];
|
|
182
|
+
var m;
|
|
183
|
+
while ((m = regex.exec(text)) !== null) {
|
|
184
|
+
matches.push(m[1]);
|
|
185
|
+
}
|
|
186
|
+
if (matches.length === 0) return { ok: false, reason: "no CONTENT-FINDINGS block" };
|
|
187
|
+
var findings;
|
|
188
|
+
try {
|
|
189
|
+
findings = JSON.parse(matches[matches.length - 1]);
|
|
190
|
+
} catch (e) {
|
|
191
|
+
return { ok: false, reason: "CONTENT-FINDINGS block is not valid JSON" };
|
|
192
|
+
}
|
|
193
|
+
if (!Array.isArray(findings)) return { ok: false, reason: "CONTENT-FINDINGS is not an array" };
|
|
194
|
+
for (var i = 0; i < findings.length; i++) {
|
|
195
|
+
var f = findings[i];
|
|
196
|
+
if (!f || typeof f !== "object" || typeof f.subject !== "string" || typeof f.observation !== "string") {
|
|
197
|
+
return { ok: false, reason: "finding " + i + " needs string subject and observation" };
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
return { ok: true, findings: findings };
|
|
201
|
+
}
|
|
202
|
+
function classifyContentFinding(finding, diffFiles) {
|
|
203
|
+
// diffFiles: the task's publish-diff file set (repo-relative paths).
|
|
204
|
+
// Pure set membership — no prose, no judgment.
|
|
205
|
+
var subject = finding.subject;
|
|
206
|
+
if (subject === "live-data") {
|
|
207
|
+
return { attribution: "environment-attributable", reason: "live-data is never in a git publish diff — the crew has no live-DB write path (blocker 32)" };
|
|
208
|
+
}
|
|
209
|
+
if (diffFiles.indexOf(subject) !== -1) {
|
|
210
|
+
return { attribution: "task-change", reason: "subject is in the task's publish diff" };
|
|
211
|
+
}
|
|
212
|
+
return { attribution: "environment-attributable", reason: "subject is not in the task's publish diff" };
|
|
213
|
+
}
|
|
158
214
|
// See docs/decisions/workflow-core.md#verdict-reask: a report that fails extractVerdict gets bounded re-ask calls.
|
|
159
215
|
function verdictReaskKey(stepName, reworkSuffix, attempt) {
|
|
160
216
|
return "verdict-reask-" + stepName + reworkSuffix + "-a" + attempt;
|
|
@@ -237,14 +293,6 @@ var TOOL_CHECK_PREAMBLE =
|
|
|
237
293
|
"2. Write exactly one line: artifact_tools: ok - or artifact_tools: missing if the call failed or the tool does not exist.\n" +
|
|
238
294
|
"3. Run the shell command: echo tool-probe-ok - then write exactly one line: shell_transport: ok - or shell_transport: unavailable if you cannot run shell commands.\n" +
|
|
239
295
|
"Then do the assignment below.\n\n";
|
|
240
|
-
// ARTIFACT_LOAD_PREAMBLE - the one-line load instruction that precedes every
|
|
241
|
-
// special-purpose prompt calling artifact tools (rebuild trigger, provenance
|
|
242
|
-
// stamp, publish verification). The artifact tool namespace is deferred for
|
|
243
|
-
// workflow children: present but invisible until the child loads it via
|
|
244
|
-
// tool_search.load_tool_namespace (bug 3472bf36 root cause). Byte-identical
|
|
245
|
-
// across standard/bugfix/chore - pinned by tests/artifact-tools.test.js.
|
|
246
|
-
var ARTIFACT_LOAD_PREAMBLE =
|
|
247
|
-
"First call tool_search.load_tool_namespace with paths [\"artifact\"] - the artifact tool namespace is deferred and invisible until loaded.\n";
|
|
248
296
|
function buildTransportRetryTrailer(stepName, repoPath, taskId, attempt, reason) {
|
|
249
297
|
// reason: "discarded" (the runtime threw the output away - it could not be
|
|
250
298
|
// machine-read), "empty" (agent() returned without throwing but produced
|
|
@@ -422,6 +470,11 @@ async function recordPublishLedger(entry, rework) {
|
|
|
422
470
|
diff_path: entry.diff_path || null,
|
|
423
471
|
diff_sha256: entry.diff_sha256 || null,
|
|
424
472
|
base: entry.base || null,
|
|
473
|
+
// (0.14.6) Numeric publication attempt + the attempt's deterministic
|
|
474
|
+
// version. Attempt 1 is staged here by the workflow; attempts 2-3 are
|
|
475
|
+
// staged by scan-ack-pending's re-issue path with fresh versions.
|
|
476
|
+
publish_attempt: entry.publish_attempt || null,
|
|
477
|
+
version: entry.version || null,
|
|
425
478
|
});
|
|
426
479
|
var sq = function(s) { return "'" + String(s).split("'").join("'\\''") + "'"; };
|
|
427
480
|
var res = await agent(
|
|
@@ -433,9 +486,15 @@ async function recordPublishLedger(entry, rework) {
|
|
|
433
486
|
label: "Recording publish attempt in ledger",
|
|
434
487
|
schema: { type: "object", properties: { result: { type: "string" } }, required: ["result"] } }
|
|
435
488
|
);
|
|
436
|
-
|
|
489
|
+
// (2026-09-20 REVIEW) The intent entry is the machine's anchor — the
|
|
490
|
+
// caller gates on this return. LEDGER_OK in the agent's verbatim output
|
|
491
|
+
// is the only mechanical proof the line landed.
|
|
492
|
+
var ok = res && typeof res.result === "string" && res.result.indexOf("LEDGER_OK") !== -1;
|
|
493
|
+
log("Noted publish outcome '" + entry.outcome + "' for task " + taskId + " in ledger" + (ok ? "" : " (WRITE UNCONFIRMED)"));
|
|
494
|
+
return ok;
|
|
437
495
|
} catch (e) {
|
|
438
496
|
log("Publish ledger: write failed for task " + taskId + " (non-fatal, observability only): " + (e && e.message ? e.message : e));
|
|
497
|
+
return false;
|
|
439
498
|
}
|
|
440
499
|
}
|
|
441
500
|
|
|
@@ -455,13 +514,6 @@ function extractMarkerLines(workerText) {
|
|
|
455
514
|
return markers.join("\n");
|
|
456
515
|
}
|
|
457
516
|
|
|
458
|
-
// See docs/decisions/publish-path.md#already-merged-idem2: idempotency for already-merged tasks.
|
|
459
|
-
function extractAlreadyMerged(workerText) {
|
|
460
|
-
var m = /repo_diff:\s*none\s*\(already-merged:\s*([0-9a-f]{7,40})(?=[\s)]|$)/i.exec(workerText || "");
|
|
461
|
-
return m ? { sha: m[1].toLowerCase() } : { sha: null };
|
|
462
|
-
}
|
|
463
|
-
|
|
464
|
-
|
|
465
517
|
// See docs/decisions/qa-reproduce.md#worktree-confinement: the Build agent must declare its worktree.
|
|
466
518
|
function extractWorktree(workerText) {
|
|
467
519
|
var lines = (workerText || "").split("\n");
|
|
@@ -668,12 +720,24 @@ let mapGateBounceCount = 0;
|
|
|
668
720
|
// rationalized a skip against explicit instruction text — text alone did not
|
|
669
721
|
// hold, so the decision now lives in workflow code, not agent judgment.
|
|
670
722
|
let releaseDecision = null; // { release: "yes"|"no", version_bump: "patch"|"minor"|"major"|null }
|
|
671
|
-
// Already-merged
|
|
672
|
-
//
|
|
673
|
-
//
|
|
674
|
-
//
|
|
675
|
-
//
|
|
723
|
+
// Already-merged attribution (blocker 34, 2026-09-21): the sha of the
|
|
724
|
+
// task-attributed merge when classify-branch reports already-merged.
|
|
725
|
+
// Set ONLY from the classifier's mechanical output — never from agent
|
|
726
|
+
// prose. The agent-authored sha-bearing repo_diff declaration path is
|
|
727
|
+
// deleted; attribution is identity-first from git, then the task's own
|
|
728
|
+
// durable merge records.
|
|
676
729
|
let alreadyMergedSha = null;
|
|
730
|
+
// Room #26 blocker 34 (redesign, 2026-09-21): mechanical branch-state
|
|
731
|
+
// classification at Review entry. Git facts are never adjudicated by a
|
|
732
|
+
// reviewer — the workflow classifies the branch with `classify-branch`
|
|
733
|
+
// (pure git) before Cass is dispatched: "has-work",
|
|
734
|
+
// "already-merged:<sha>", or "empty-no-work". An empty branch with no
|
|
735
|
+
// verified sha and no `repo_diff: none` claim fails mechanically without
|
|
736
|
+
// dispatching Cass; reviewer prose is never parsed for git identity.
|
|
737
|
+
let branchState = null; // "has-work" | "already-merged:<sha>" | "empty-no-work"
|
|
738
|
+
let buildClaimedNoDiff = false; // Build declared plain `repo_diff: none` (no repo change — runtime-state deliverable, or believed already-merged). Same-process Build report only; there is no cross-process hydration — an empty branch on a fresh dispatch with no task-attributed merge fails mechanically.
|
|
739
|
+
let versionTouched = false; // npm: branch changed package.json's `version` (mechanical git fact, blocker-34 follow-up)
|
|
740
|
+
let branchStateErr = ""; // classifier failure detail (fail-closed grounds)
|
|
677
741
|
// Deterministic publish target — computed by the workflow (registry base +
|
|
678
742
|
// bumpVersion), never by the Publish agent.
|
|
679
743
|
let publishTarget = null; // { base, scope, target }
|
|
@@ -1212,55 +1276,126 @@ while (i < STEPS.length) {
|
|
|
1212
1276
|
"cd " + WORKTREE_HINT + "\n" +
|
|
1213
1277
|
"git add -A\n" +
|
|
1214
1278
|
"git commit -m \"fix: " + safeTitle + "\"\n\n" +
|
|
1215
|
-
"If the task's deliverable is runtime state (a cron definition, scheduler change, or dashboard/config state created outside the repo) and the repository genuinely needs no change, do NOT fabricate a commit: leave the branch with no commits ahead of the integration target and declare `repo_diff: none` in your report, naming the runtime-state deliverable. If you verified the deliverable is already on the integration target (a prior merge
|
|
1279
|
+
"If the task's deliverable is runtime state (a cron definition, scheduler change, or dashboard/config state created outside the repo) and the repository genuinely needs no change, do NOT fabricate a commit: leave the branch with no commits ahead of the integration target and declare `repo_diff: none` in your report, naming the runtime-state deliverable. If you verified the deliverable is already on the integration target (a prior merge landed it — do NOT re-implement working code), make no commit and declare `repo_diff: none` in your report. The workflow verifies the claim mechanically from git — the task's own merge records, never your declaration, are the proof of delivery. Otherwise commit your changes normally.\n\n" +
|
|
1216
1280
|
(rejectionNotes ? "This is REWORK after rejection. Address these specific issues:\n" + rejectionNotes + "\n\n" : "") +
|
|
1217
1281
|
"Report back in plain prose: what you built and the outcome." +
|
|
1218
1282
|
(PUBLISH_TYPE === "npm" ? " End your report with the release: and version_bump: lines exactly as specified above — keep them on their own lines, lowercase, unrephrased — then a line `worktree: ` followed by the exact working directory path from above (copy it verbatim \u2014 it must match character-for-character), then a final line with exactly: VERDICT: PASS if the build is complete, VERDICT: FAIL if it is not." : " End your report with a line `worktree: ` followed by the exact working directory path from above (copy it verbatim \u2014 it must match character-for-character), then exactly one line: VERDICT: PASS if the build is complete, VERDICT: FAIL if it is not.");
|
|
1219
1283
|
|
|
1220
1284
|
} else if (step.name === "Review") {
|
|
1221
|
-
//
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1285
|
+
// Room #26 blocker 34 (redesign, 2026-09-21): classify the branch
|
|
1286
|
+
// mechanically BEFORE Cass is dispatched. The classifier is pure git
|
|
1287
|
+
// (classify-branch in the lifecycle script): has-work |
|
|
1288
|
+
// already-merged:<sha> | empty-no-work. Attribution is identity-first
|
|
1289
|
+
// from git, then the task's own durable merge records — there is no
|
|
1290
|
+
// agent-authored declared-sha input and no cross-process hydration
|
|
1291
|
+
// ferry (the re-entry case it served is covered by the durable record
|
|
1292
|
+
// → classifier directly). An unparseable result fails closed as
|
|
1293
|
+
// empty-no-work — an empty branch never passes Review silently, and
|
|
1294
|
+
// the bounce to Build gives the next round a chance to classify.
|
|
1295
|
+
try {
|
|
1296
|
+
var bsResult = await agent(
|
|
1297
|
+
"Classify the task branch state. This is a mechanical git check, not a judgment call.\n" +
|
|
1298
|
+
"Run in shell and return the stdout verbatim, then the single stderr line starting with `DIAG:` verbatim:\n" +
|
|
1299
|
+
LIFECYCLE_ENV + LIFECYCLE + " classify-branch " + taskId,
|
|
1300
|
+
{ key: "classify-branch" + (totalReworkCount > 0 ? "-r" + totalReworkCount : ""), label: "Classifying branch state (mechanical)" }
|
|
1301
|
+
);
|
|
1302
|
+
var bsStr = (typeof bsResult === "string") ? bsResult : JSON.stringify(bsResult);
|
|
1303
|
+
var bsMatch = /^\s*BRANCH_STATE:\s*(has-work|already-merged:[0-9a-f]{40}|empty-no-work)\s*$/im.exec(bsStr);
|
|
1304
|
+
if (bsMatch) {
|
|
1305
|
+
branchState = bsMatch[1].toLowerCase();
|
|
1306
|
+
} else {
|
|
1307
|
+
branchStateErr = "unparseable classifier output: " + bsStr.slice(0, 120);
|
|
1235
1308
|
}
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1309
|
+
// DIAG pin (blocker 34 design v2.1): the merge-record scan is
|
|
1310
|
+
// load-bearing with no fallback — the classifier's stderr counter is
|
|
1311
|
+
// logged verbatim so silent record loss surfaces in the evidence.
|
|
1312
|
+
var bsDiagMatch = /^[ \t]*DIAG:\s*classify-branch\s+records=\d+\s+resolvable=\d+[ \t]*$/im.exec(bsStr);
|
|
1313
|
+
if (bsDiagMatch) {
|
|
1314
|
+
log(bsDiagMatch[0].trim());
|
|
1241
1315
|
} else {
|
|
1242
|
-
|
|
1243
|
-
if (hvm) {
|
|
1244
|
-
alreadyMergedSha = hvm[1].toLowerCase();
|
|
1245
|
-
log("Hydrated already-merged verification from Build session notes (fallback): " + alreadyMergedSha);
|
|
1246
|
-
}
|
|
1316
|
+
log("classify-branch: no DIAG: record-scan counter returned (scan unverifiable)");
|
|
1247
1317
|
}
|
|
1318
|
+
} catch (bsErr) {
|
|
1319
|
+
branchStateErr = "classifier call failed: " + String(bsErr && bsErr.message || bsErr).slice(0, 120);
|
|
1320
|
+
}
|
|
1321
|
+
if (!branchState) {
|
|
1322
|
+
branchState = "empty-no-work";
|
|
1323
|
+
log("Branch-state classification failed (" + branchStateErr + ") — failing closed as empty-no-work");
|
|
1324
|
+
} else if (branchState.indexOf("already-merged:") === 0) {
|
|
1325
|
+
alreadyMergedSha = branchState.slice("already-merged:".length);
|
|
1326
|
+
log("Branch state already-merged: " + alreadyMergedSha + " (mechanically verified) — Cass reviews the frozen merge diff");
|
|
1327
|
+
} else {
|
|
1328
|
+
log("Branch state: " + branchState);
|
|
1329
|
+
}
|
|
1330
|
+
// buildClaimedNoDiff was set at the Build gate from the full Build
|
|
1331
|
+
// report (same process only). Only an empty branch WITH this claim
|
|
1332
|
+
// goes to Cass for a plausibility judgment; without it, empty-no-work
|
|
1333
|
+
// fails mechanically and Cass is never dispatched.
|
|
1334
|
+
// Blocker-34 follow-up: the package.json `version` check is a mechanical
|
|
1335
|
+
// git fact, not a reviewer judgment. For npm projects the workflow checks
|
|
1336
|
+
// it here, before Cass is dispatched — a touched `version` fails Review
|
|
1337
|
+
// mechanically (versions are assigned at publish time, never in
|
|
1338
|
+
// branches). Fail closed: anything but an explicit VERSION_CLEAN counts
|
|
1339
|
+
// as touched.
|
|
1340
|
+
if (PUBLISH_TYPE === "npm") {
|
|
1341
|
+
try {
|
|
1342
|
+
var vtResult = await agent(
|
|
1343
|
+
"Check whether the task branch changed package.json's `version` field. This is a mechanical git check, not a judgment call.\n" +
|
|
1344
|
+
"Run in shell and return the stdout verbatim:\n" +
|
|
1345
|
+
LIFECYCLE_ENV + LIFECYCLE + " version-check " + taskId,
|
|
1346
|
+
{ key: "version-check" + (totalReworkCount > 0 ? "-r" + totalReworkCount : ""), label: "Checking package.json version (mechanical)" }
|
|
1347
|
+
);
|
|
1348
|
+
var vtStr = (typeof vtResult === "string") ? vtResult : JSON.stringify(vtResult);
|
|
1349
|
+
var vtMatch = /^\s*VERSION_(TOUCHED|CLEAN)\s*$/im.exec(vtStr);
|
|
1350
|
+
versionTouched = !(vtMatch && vtMatch[1].toUpperCase() === "CLEAN");
|
|
1351
|
+
} catch (vtErr) {
|
|
1352
|
+
versionTouched = true;
|
|
1353
|
+
log("Version check failed (" + String(vtErr && vtErr.message || vtErr).slice(0, 120) + ") — failing closed as touched");
|
|
1354
|
+
}
|
|
1355
|
+
log("Review: package.json `version` " + (versionTouched ? "touched by the branch — mechanical FAIL, Cass not dispatched" : "untouched — mechanical check clean"));
|
|
1356
|
+
}
|
|
1357
|
+
// The empty-branch rule Cass used to adjudicate is gone. What she
|
|
1358
|
+
// examines and what rule she applies depend on the classified state —
|
|
1359
|
+
// never on her own judgment of git identity.
|
|
1360
|
+
var reviewChangeExam, reviewBranchRule;
|
|
1361
|
+
if (branchState === "has-work") {
|
|
1362
|
+
reviewChangeExam =
|
|
1363
|
+
"Examine the code changes by running:\n" +
|
|
1364
|
+
LIFECYCLE_ENV + LIFECYCLE + " inspect " + taskId + "\n\n" +
|
|
1365
|
+
"The inspect output is authoritative: it prints the task branch's actual tip commit (TIP) and every commit ahead of the integration target (inspect prints the target name). Base your review ONLY on this output — do NOT run git log yourself to pick commits, and do NOT discuss commit hashes from any other source (they may come from stale rework rounds or a different repo).\n\n";
|
|
1366
|
+
reviewBranchRule =
|
|
1367
|
+
"MECHANICAL FACT (computed by the workflow from git — never by a reviewer): branch state = has-work. The branch has commits ahead of the integration target. Review their content; branch emptiness is settled and is not yours to judge.\n";
|
|
1368
|
+
} else if (branchState.indexOf("already-merged:") === 0) {
|
|
1369
|
+
reviewChangeExam =
|
|
1370
|
+
"The change under review is the frozen merge " + alreadyMergedSha + " — it is already on the integration target (see MECHANICAL FACT below). Examine it by running:\n" +
|
|
1371
|
+
"cd " + REPO_PATH + " && git diff " + alreadyMergedSha + "^1 " + alreadyMergedSha + "\n\n" +
|
|
1372
|
+
"That first-parent diff is the frozen, attributable change. Base your review ONLY on this diff — do NOT run git log to pick commits, and do NOT discuss commit hashes from any other source (they may come from stale rework rounds or a different repo).\n\n";
|
|
1373
|
+
reviewBranchRule =
|
|
1374
|
+
"MECHANICAL FACT (computed by the workflow from git — never by a reviewer): branch state = already-merged:" + alreadyMergedSha + ". The deliverable landed on the integration target via this task's own prior merge " + alreadyMergedSha + " (verified ancestor of the integration target). The branch is empty by design — emptiness is settled fact, not a finding.\n";
|
|
1375
|
+
} else if (buildClaimedNoDiff) {
|
|
1376
|
+
reviewChangeExam =
|
|
1377
|
+
"The branch has no commits ahead of the integration target (see MECHANICAL FACT below) — there is no diff to examine. Judge the Build report's `repo_diff: none` claim on its plausibility.\n\n";
|
|
1378
|
+
reviewBranchRule =
|
|
1379
|
+
"MECHANICAL FACT (computed by the workflow from git — never by a reviewer): the branch has no commits ahead of the integration target and no task-attributed merge on the target was verified; the Build report declares `repo_diff: none` (no repo change). Approve ONLY if the task's deliverable is plausibly runtime state (e.g. a cron definition, scheduler change, or dashboard/config state created outside the repo). Otherwise report 'no commits ahead of the integration target and no plausible runtime-state deliverable — the builder likely forgot to commit', then end your report with exactly this line: VERDICT: FAIL.\n";
|
|
1380
|
+
} else {
|
|
1381
|
+
// Mechanical FAIL — Cass is never dispatched (see the dispatch gate
|
|
1382
|
+
// below). Instructions are unused; keep the shape.
|
|
1383
|
+
reviewChangeExam = "";
|
|
1384
|
+
reviewBranchRule = "";
|
|
1248
1385
|
}
|
|
1249
1386
|
instructions = "Review independently and cold. You have NOT seen any reasoning from the builder.\nDo NOT access the task dashboard, event log, or any comments. Your review is based solely on the spec and the code.\n\n" +
|
|
1250
1387
|
(mapperSpec ? "MAPPER'S SPEC (the builder was asked to implement exactly this):\n" + mapperSpec + "\n\n" : "Read the spec (from the task description or spec files under " + crewHome + "/).\n\n") +
|
|
1251
|
-
|
|
1252
|
-
LIFECYCLE_ENV + LIFECYCLE + " inspect " + taskId + "\n\n" +
|
|
1253
|
-
"The inspect output is authoritative: it prints the task branch's actual tip commit (TIP) and every commit ahead of the integration target (inspect prints the target name). Base your review ONLY on this output — do NOT run git log yourself to pick commits, and do NOT discuss commit hashes from any other source (they may come from stale rework rounds or a different repo).\n\n" +
|
|
1388
|
+
reviewChangeExam +
|
|
1254
1389
|
"You can also read specific files in the worktree at:\n" +
|
|
1255
1390
|
WORKTREE_HINT + "/\n\n" +
|
|
1256
1391
|
"Check quality, correctness, and spec compliance.\n" +
|
|
1257
1392
|
(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" : "") +
|
|
1258
1393
|
(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" : "") +
|
|
1259
1394
|
"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" +
|
|
1260
|
-
|
|
1261
|
-
(PUBLISH_TYPE === "npm" ? "PACKAGE VERSION: this project publishes to the npm registry, and versions are assigned at publish time — never in branches
|
|
1262
|
-
"
|
|
1263
|
-
"
|
|
1395
|
+
reviewBranchRule +
|
|
1396
|
+
(PUBLISH_TYPE === "npm" ? "PACKAGE VERSION: this project publishes to the npm registry, and versions are assigned at publish time — never in branches.\n" +
|
|
1397
|
+
"MECHANICAL FACT (computed by the workflow from git — never by a reviewer): the task branch did not change package.json's `version` field. A branch that touches `version` fails Review mechanically before any reviewer is dispatched, so this is settled — do not re-check it.\n" +
|
|
1398
|
+
"The accepted Build report declares: " + releaseDecisionText() + ". " +
|
|
1264
1399
|
(releaseDecision
|
|
1265
1400
|
? "Validate this decision against the change: release must be 'yes' when the change is consumer-observable and 'no' when internal-only; the version_bump scope must fit the change (patch for fixes, minor for new behavior, major for breaking changes). If the decision is wrong or mis-scoped, report your notes, then end with exactly this line: VERDICT: FAIL."
|
|
1266
1401
|
: "The decision is missing or malformed — report 'Build report must end with release: yes|no and (when release is yes) version_bump: patch|minor|major lines', then end with exactly this line: VERDICT: FAIL.") + "\n" : "") +
|
|
@@ -1274,8 +1409,9 @@ while (i < STEPS.length) {
|
|
|
1274
1409
|
"Then run: "+ LIFECYCLE_ENV + "WORKFLOW_RUN_ID=" + lockHolder + " CREW_STAGED_BASE=<provenance.source_commit, empty when null> integrate " + taskId + " \"merge: fix: " + safeTitle + "\\\"\\n" +
|
|
1275
1410
|
"The script merges, reconciles, records, and pushes under the merge lock. Informational only: RECONCILED, NO_REMOTE, NO_REMOTE_RECONCILE. Read the output:\n" +
|
|
1276
1411
|
"- STAGED_BASE_MISMATCH: the line left the staged base (stray checkout). VERDICT: FAIL.\n" +
|
|
1277
|
-
"- MERGED_EMPTY — check FIRST (it contains the word MERGED): either a runtime-state deliverable or
|
|
1412
|
+
"- MERGED_EMPTY — check FIRST (it contains the word MERGED): either a runtime-state deliverable or a task-attributed merge the workflow verified from the task's own merge records. No lock taken, no new commit. Report 'merged empty: no repo changes'. VERDICT: PASS.\n" +
|
|
1278
1413
|
"- MERGED: the merge landed; the script pushed inline. Read the push line: PUSHED — report the hash (detached prints PUSHED: origin/main (refspec HEAD:main)), VERDICT: PASS; NO_REMOTE_PUSH — no remote configured, VERDICT: PASS; ERROR after MERGED — a retry recovers the push; report it, VERDICT: FAIL.\n" +
|
|
1414
|
+
"- STALE_MERGE: the recorded merge no longer matches the task branch (rework moved it after the merge) — the script refused to push stale state. Report the STALE_MERGE line, VERDICT: FAIL.\n" +
|
|
1279
1415
|
"- LOCK_HELD: 10-minute backoff exhausted. VERDICT: FAIL.\n" +
|
|
1280
1416
|
"- CONFLICT: the plain merge failed — aborted, the target is clean, and your task still holds the merge lock. Do NOT fail yet. Resolve it:\n" +
|
|
1281
1417
|
"RESOLUTION:\n" +
|
|
@@ -1325,8 +1461,9 @@ while (i < STEPS.length) {
|
|
|
1325
1461
|
// See docs/decisions/publish-path.md#deterministic-artifact-publish: the work agent never publishes; the parent runs the deterministic publish script.
|
|
1326
1462
|
// Blocker 22 (one-party publish, room #24): verified re-entry guard.
|
|
1327
1463
|
// The workflow parks at publish intent; the tick worker issues
|
|
1328
|
-
// artifact_edit
|
|
1329
|
-
//
|
|
1464
|
+
// artifact_edit; the ack scan stamps provenance on exact per-attempt
|
|
1465
|
+
// version acknowledgement (sole positive completion criterion; no
|
|
1466
|
+
// read-back — docs/publish-verification.md) and re-queues. The dispatcher resumes this task at the Publish step
|
|
1330
1467
|
// (the parked session's own step — the failed→retry path). If the
|
|
1331
1468
|
// stamped provenance names THIS task and its source_commit is the
|
|
1332
1469
|
// repo's current HEAD, the merge commit was already published and
|
|
@@ -1382,8 +1519,10 @@ while (i < STEPS.length) {
|
|
|
1382
1519
|
}
|
|
1383
1520
|
// See docs/decisions/publish-path.md#step1-builder-source: the builder's source tree is NOT the crew's repo; verify report before stamping.
|
|
1384
1521
|
// (2026-09-20, one-party worker-owned publish) A verified re-entry
|
|
1385
|
-
// skips the whole issuance tail: the edit already went out,
|
|
1386
|
-
//
|
|
1522
|
+
// skips the whole issuance tail: the edit already went out, the
|
|
1523
|
+
// ack scan saw the exact per-attempt version acknowledged (sole
|
|
1524
|
+
// positive completion criterion; no read-back) and it was stamped.
|
|
1525
|
+
// The empty-diff
|
|
1387
1526
|
// park must not fire on this branch.
|
|
1388
1527
|
if (!publishSkippedNoLock && !publishAlreadyVerified) {
|
|
1389
1528
|
// The trigger key of the attempt that last ran, for the publish ledger.
|
|
@@ -1485,7 +1624,7 @@ while (i < STEPS.length) {
|
|
|
1485
1624
|
var publishDiffFile = crewHome + "/.publish-diffs/" + taskId + (totalReworkCount > 0 ? "-r" + totalReworkCount : "") + ".diff";
|
|
1486
1625
|
var diffResult = await agent(
|
|
1487
1626
|
"Run exactly one command and nothing else:\n" +
|
|
1488
|
-
"node " + COMPUTE_DIFF + " --repo-path \"" + REPO_PATH + "\" --base " + publishBase + " --out \"" + publishDiffFile + "\"\n" +
|
|
1627
|
+
"node " + COMPUTE_DIFF + " --repo-path \"" + REPO_PATH + "\" --base " + publishBase + " --task-id \"" + taskId + "\" --attempt 1 --out \"" + publishDiffFile + "\"\n" +
|
|
1489
1628
|
"The script prints one small JSON object to stdout. Return the stdout verbatim as { \"output\": \"<verbatim stdout>\" } and nothing else. Do not interpret it.",
|
|
1490
1629
|
{ key: attemptKey("publish-artifact-diff-" + taskId, totalReworkCount), label: "Computing publish diff from stamped base",
|
|
1491
1630
|
schema: { type: "object", properties: { output: { type: "string" } }, required: ["output"] } }
|
|
@@ -1508,6 +1647,18 @@ while (i < STEPS.length) {
|
|
|
1508
1647
|
var mergeCommitForPublish = String(diffSummary.commit || "").trim();
|
|
1509
1648
|
var mergeCommitShortForPublish = String(mergeCommitForPublish).substring(0, 7) || "unknown";
|
|
1510
1649
|
var publishDiffSha256 = String(diffSummary.sha256 || "");
|
|
1650
|
+
// (0.14.6) The per-attempt publish version, synthesized into the
|
|
1651
|
+
// staged diff as .crew-publish-version-<task_id> (per-task receipt —
|
|
1652
|
+
// two same-project tasks can publish concurrently) by the
|
|
1653
|
+
// deterministic script.
|
|
1654
|
+
// The workflow never invents it: the script derives it from
|
|
1655
|
+
// task+commit+attempt, and the intent scan re-derives it before
|
|
1656
|
+
// claiming. A missing/malformed version parks — never publish blind.
|
|
1657
|
+
var publishVersion = String(diffSummary.version || "");
|
|
1658
|
+
// Mirrors isPublishVersion() in lib/crew-api.js: <40-hex commit>:<attempt>.
|
|
1659
|
+
if (!/^[0-9a-f]{40}:[1-9][0-9]*$/.test(publishVersion)) {
|
|
1660
|
+
return await parkTask("Publish diff for commit " + (mergeCommitForPublish || "unknown") + " carries no usable publish version — cannot stage a verifiable intent. Human attention needed.");
|
|
1661
|
+
}
|
|
1511
1662
|
if (!diffSummary.bytes) {
|
|
1512
1663
|
return await parkTask("Publish diff is empty for commit " + (mergeCommitForPublish || "unknown") + " — a merge lock was held but there is no change to carry. Human attention needed.");
|
|
1513
1664
|
}
|
|
@@ -1517,6 +1668,20 @@ while (i < STEPS.length) {
|
|
|
1517
1668
|
if (diffSummary.has_rename) {
|
|
1518
1669
|
return await parkTask("Publish diff contains a rename — the diff transport cannot carry renames. Human attention needed.");
|
|
1519
1670
|
}
|
|
1671
|
+
// Room #26 blocker 38 (2026-09-21): persist the publish-diff file
|
|
1672
|
+
// set for QA content-finding attribution. The workflow classifies
|
|
1673
|
+
// Hazel's content findings against this set mechanically at QA
|
|
1674
|
+
// closeout — the file list is the complete record of what the task
|
|
1675
|
+
// changed. The courier is pure hands: it writes the deterministic
|
|
1676
|
+
// script's own file list, never interprets it.
|
|
1677
|
+
var publishDiffFilesPath = crewHome + "/.publish-diffs/" + taskId + ".files.json";
|
|
1678
|
+
await agent(
|
|
1679
|
+
"Write the publish diff file list.\n" +
|
|
1680
|
+
"Run exactly this and return the stdout verbatim:\n" +
|
|
1681
|
+
"mkdir -p " + crewHome + "/.publish-diffs && cat > \"" + publishDiffFilesPath + "\" <<'DIFFFILES_EOF'\n" +
|
|
1682
|
+
JSON.stringify(diffSummary.files || []) + "\nDIFFFILES_EOF\n",
|
|
1683
|
+
{ key: attemptKey("publish-diff-files-" + taskId, totalReworkCount), label: "Persisting publish diff file list" }
|
|
1684
|
+
);
|
|
1520
1685
|
// Budget counts CHANGED lines (added + removed), not raw unified-diff
|
|
1521
1686
|
// output lines: context lines and file headers inflated the old
|
|
1522
1687
|
// split("\n").length count ~2x, parking a 95-line change against a
|
|
@@ -1535,131 +1700,53 @@ while (i < STEPS.length) {
|
|
|
1535
1700
|
// worker verifies the sha256 from the intent entry before issuing;
|
|
1536
1701
|
// a mismatch stops issuance. (2026-09-20, one-party worker-owned
|
|
1537
1702
|
// publish: the workflow no longer issues the edit itself.)
|
|
1538
|
-
// See docs/decisions/publish-path.md#durable-evidence-snapshot: snapshot the audit-dir listing BEFORE the trigger; fallback diffs before/after.
|
|
1539
|
-
var auditDirsBeforeTrigger = [];
|
|
1540
|
-
var auditBeforeOk = false;
|
|
1541
|
-
// Pre-trigger baselines (design §1.9): the audit-dir listing and the
|
|
1542
|
-
// manifest snapshot. The verified-path freshness check compares the
|
|
1543
|
-
// post-trigger manifest against the baseline (built_at advance +
|
|
1544
|
-
// content_sha256 change). Best-effort, never gates — a missing
|
|
1545
|
-
// manifest baseline fails the verified path closed.
|
|
1546
|
-
var preTriggerManifest = null;
|
|
1547
|
-
try {
|
|
1548
|
-
var auditBefore = await agent(
|
|
1549
|
-
"Capture pre-trigger baselines for slug \"" + PUBLISH_SLUG + "\" (best-effort snapshots, never gates).\n" +
|
|
1550
|
-
"Run: ls -1 ~/workspace/ts-spaces/" + PUBLISH_SLUG + "/audits/ 2>/dev/null; echo ---MANIFEST---; cat ~/workspace/ts-spaces/" + PUBLISH_SLUG + "/.space-build/manifest.json 2>/dev/null\n" +
|
|
1551
|
-
"Return JSON { \"dirs\": \"<newline-separated names, empty string when missing>\", \"manifest\": \"<the manifest's full text, or empty string when missing/unreadable>\" } and nothing else.",
|
|
1552
|
-
{ key: attemptKey("publish-baseline-before-" + taskId, totalReworkCount), label: "Snapshotting baselines before rebuild trigger",
|
|
1553
|
-
schema: { type: "object", properties: { dirs: { type: "string" }, manifest: { type: "string" } }, required: ["dirs", "manifest"] } }
|
|
1554
|
-
);
|
|
1555
|
-
auditDirsBeforeTrigger = String((auditBefore && auditBefore.dirs) || "").split("\n").map(function (s) { return s.trim(); }).filter(function (s) { return s.length > 0; });
|
|
1556
|
-
var manifestText = String((auditBefore && auditBefore.manifest) || "").trim();
|
|
1557
|
-
if (manifestText) {
|
|
1558
|
-
var manifestJson = JSON.parse(manifestText);
|
|
1559
|
-
preTriggerManifest = {
|
|
1560
|
-
built_at: typeof manifestJson.built_at === "string" ? manifestJson.built_at : null,
|
|
1561
|
-
content_sha256: typeof manifestJson.content_sha256 === "string" ? manifestJson.content_sha256 : null,
|
|
1562
|
-
};
|
|
1563
|
-
}
|
|
1564
|
-
// Arm only after both baselines parse.
|
|
1565
|
-
auditBeforeOk = true;
|
|
1566
|
-
log("Publish pre-trigger baselines for task " + taskId + ": " + auditDirsBeforeTrigger.length + " audit dirs, manifest " + (preTriggerManifest ? "built_at=" + preTriggerManifest.built_at : "none"));
|
|
1567
|
-
} catch (auditBeforeErr) {
|
|
1568
|
-
log("Publish pre-trigger baselines failed for task " + taskId + " (non-fatal): audit fallback DISABLED for this attempt — without a baseline, historical dirs would look new: " + (auditBeforeErr && auditBeforeErr.message ? auditBeforeErr.message : auditBeforeErr));
|
|
1569
|
-
}
|
|
1570
|
-
|
|
1571
|
-
// See docs/decisions/publish-path.md#fire-and-forget-trigger: the trigger child returns immediately; the workflow owns observation and verdict.
|
|
1572
|
-
var publishToolsOk = false;
|
|
1573
|
-
var publishToolsMissing = false;
|
|
1574
|
-
for (var toolcheckAttempt = 1; toolcheckAttempt <= 2 && !publishToolsOk; toolcheckAttempt++) {
|
|
1575
|
-
try {
|
|
1576
|
-
var toolcheckResult = await agent(
|
|
1577
|
-
"Check whether the artifact tool namespace is available.\n" +
|
|
1578
|
-
"Call tool_search.load_tool_namespace with paths [\"artifact\"].\n" +
|
|
1579
|
-
"Then emit exactly one line and nothing else: ARTIFACT_TOOLS: <ok if the load succeeded and artifact_edit and artifact_status are now functions, missing otherwise>.\n" +
|
|
1580
|
-
"Return JSON { \"signal\": \"<the exact ARTIFACT_TOOLS line>\" } and nothing else.",
|
|
1581
|
-
{ key: attemptKey("publish-artifact-toolcheck-" + taskId + (toolcheckAttempt === 1 ? "" : "-retry2"), totalReworkCount),
|
|
1582
|
-
label: "Checking artifact tool availability" + (toolcheckAttempt === 1 ? "" : " (retry)"),
|
|
1583
|
-
schema: { type: "object", properties: { signal: { type: "string" } }, required: ["signal"] } }
|
|
1584
|
-
);
|
|
1585
|
-
var toolSignal = String((toolcheckResult && toolcheckResult.signal) || "");
|
|
1586
|
-
if (/ARTIFACT_TOOLS:\s*ok/.test(toolSignal)) {
|
|
1587
|
-
publishToolsOk = true;
|
|
1588
|
-
} else if (/ARTIFACT_TOOLS:\s*missing/.test(toolSignal)) {
|
|
1589
|
-
publishToolsMissing = true;
|
|
1590
|
-
}
|
|
1591
|
-
log("Publish artifact toolcheck for task " + taskId + " (attempt " + toolcheckAttempt + " of 2): " +
|
|
1592
|
-
(publishToolsOk ? "tools ok" : publishToolsMissing ? "tools missing (explicit parsed signal)" : "inconclusive (no ARTIFACT_TOOLS signal parsed)"));
|
|
1593
|
-
} catch (toolcheckErr) {
|
|
1594
|
-
log("Publish artifact toolcheck for task " + taskId + " (attempt " + toolcheckAttempt + " of 2) threw (" + (toolcheckErr && toolcheckErr.message ? toolcheckErr.message : toolcheckErr) + ") — inconclusive: a throw proves nothing about tool availability, never counted as missing");
|
|
1595
|
-
}
|
|
1596
|
-
}
|
|
1597
|
-
if (!publishToolsOk && !publishToolsMissing) {
|
|
1598
|
-
await recordPublishLedger({
|
|
1599
|
-
commit: mergeCommitForPublish,
|
|
1600
|
-
attempt: rebuildAttemptKey,
|
|
1601
|
-
agent_id: null,
|
|
1602
|
-
applied_report: null,
|
|
1603
|
-
outcome: "unknown",
|
|
1604
|
-
detail: "artifact toolcheck inconclusive after two attempts (throws or unparseable signals — never an explicit ARTIFACT_TOOLS: missing): tool availability unproven, so the trigger was NOT issued; unknown parks fail closed with no blind retry"
|
|
1605
|
-
}, totalReworkCount);
|
|
1606
|
-
return await parkTask("Publish cannot proceed for task " + taskId + ": the artifact toolcheck was inconclusive after two attempts (no explicit ARTIFACT_TOOLS signal parsed — a throw is transport noise, not evidence). Tool availability is unproven, so no edit was issued and nothing was retried blindly. Human attention needed.");
|
|
1607
|
-
}
|
|
1608
|
-
if (!publishToolsOk) {
|
|
1609
|
-
await recordPublishLedger({
|
|
1610
|
-
commit: mergeCommitForPublish,
|
|
1611
|
-
attempt: rebuildAttemptKey,
|
|
1612
|
-
agent_id: null,
|
|
1613
|
-
applied_report: null,
|
|
1614
|
-
outcome: "rejected",
|
|
1615
|
-
detail: "artifact tool namespace explicitly missing (parsed ARTIFACT_TOOLS: missing signal, one bounded retry spent): the edit provably did not go through — no trigger issued, no blind retry"
|
|
1616
|
-
}, totalReworkCount);
|
|
1617
|
-
return await parkTask("Publish cannot proceed for task " + taskId + ": the artifact tool namespace was explicitly missing (parsed signal — the edit provably did not go through, so no trigger was issued and nothing was retried blindly). Human attention needed.");
|
|
1618
|
-
}
|
|
1619
|
-
// See docs/decisions/publish-path.md#pretrigger-baseline: a tiny schema'd baseline is captured before the trigger.
|
|
1620
|
-
var baselineAgentId = null;
|
|
1621
|
-
var baselineFailed = false;
|
|
1622
|
-
try {
|
|
1623
|
-
var publishBaseline = await agent(
|
|
1624
|
-
ARTIFACT_LOAD_PREAMBLE +
|
|
1625
|
-
"Call artifact_status with slug \"" + PUBLISH_SLUG + "\" once.\n" +
|
|
1626
|
-
"Return JSON { \"build\": <the raw \"build\" value exactly as returned, or null when there is none> } and nothing else.",
|
|
1627
|
-
{ key: attemptKey("publish-artifact-baseline-" + taskId, totalReworkCount), label: "Reading pre-trigger build state",
|
|
1628
|
-
schema: { type: "object", properties: { build: { type: ["object", "null"] } }, required: ["build"] } }
|
|
1629
|
-
);
|
|
1630
|
-
baselineAgentId = (publishBaseline && publishBaseline.build && typeof publishBaseline.build.agent_id === "string" && publishBaseline.build.agent_id) || null;
|
|
1631
|
-
log("Publish pre-trigger baseline for task " + taskId + ": " + (baselineAgentId ? "build " + baselineAgentId + " already in flight (predates the trigger — never attributed to this edit)" : "no build in flight"));
|
|
1632
|
-
} catch (baselineErr) {
|
|
1633
|
-
baselineFailed = true;
|
|
1634
|
-
log("Publish pre-trigger baseline read failed for task " + taskId + " (" + (baselineErr && baselineErr.message ? baselineErr.message : baselineErr) + ") — receipt attribution skipped; durable audit-dir evidence is the only positive signal");
|
|
1635
|
-
}
|
|
1636
1703
|
// (2026-09-20, one-party worker-owned publish) The workflow exits
|
|
1637
1704
|
// Publish at intent. Everything above — preflight, provenance base,
|
|
1705
|
+
// checksummed diff — is read-only. (2026-09-20 REVIEW: the
|
|
1706
|
+
// pre-trigger baselines, artifact toolcheck, and pre-trigger build
|
|
1707
|
+
// state agent steps were deleted — their outputs fed only the
|
|
1708
|
+
// retired content-verdict/receipt-attribution machinery, and the
|
|
1709
|
+
// toolcheck ran in a workflow child that cannot reach artifact_edit.
|
|
1710
|
+
// Issuance belongs to the session-carrying tick worker, which
|
|
1711
|
+
// handles tool-unavailability as inconclusive per the tick protocol.)
|
|
1712
|
+
// Publish at intent. Everything above — preflight, provenance base,
|
|
1638
1713
|
// checksummed diff, toolcheck, pre-trigger baselines — is read-only.
|
|
1639
1714
|
// Issuance belongs to the session-carrying tick worker
|
|
1640
|
-
// (scan-publish-
|
|
1715
|
+
// (scan-publish-intent's intent bucket), the only caller class that
|
|
1641
1716
|
// can reach artifact_edit. This entry is issuer-written BY THE
|
|
1642
1717
|
// WORKFLOW and names the intent: outcome "publish-intent", never
|
|
1643
1718
|
// "submitted" — the workflow issued nothing. The worker claims it
|
|
1644
1719
|
// via the park note below.
|
|
1645
|
-
|
|
1720
|
+
// (2026-09-20 REVIEW) The intent ledger entry is the anchor the
|
|
1721
|
+
// whole machine depends on. If the ferry drops it, the task would
|
|
1722
|
+
// park with publish-requested and no anchor — unclaimable forever.
|
|
1723
|
+
// Fail loudly instead of parking an orphan intent.
|
|
1724
|
+
var intentAnchored = await recordPublishLedger({
|
|
1646
1725
|
commit: mergeCommitForPublish,
|
|
1647
1726
|
attempt: rebuildAttemptKey,
|
|
1727
|
+
// (0.14.6) Attempt 1, staged here. The version is the attempt's
|
|
1728
|
+
// deterministic receipt string, synthesized into the staged diff;
|
|
1729
|
+
// the ack scan re-derives it from task+commit+attempt before the
|
|
1730
|
+
// tick worker may issue the edit.
|
|
1731
|
+
publish_attempt: 1,
|
|
1732
|
+
version: publishVersion,
|
|
1648
1733
|
agent_id: null,
|
|
1649
1734
|
applied_report: null,
|
|
1650
|
-
manifest_before: preTriggerManifest,
|
|
1651
1735
|
outcome: "publish-intent",
|
|
1652
1736
|
issuer: "workflow",
|
|
1653
1737
|
diff_path: publishDiffFile,
|
|
1654
1738
|
diff_sha256: publishDiffSha256,
|
|
1655
1739
|
// (2026-09-20, critic-0145 F-A4) The base the diff was computed
|
|
1656
|
-
// against: scan-publish-
|
|
1740
|
+
// against: scan-publish-intent's intent branch compares it with
|
|
1657
1741
|
// current stamped provenance and re-queues the task when it went
|
|
1658
1742
|
// stale — never issues the stale diff.
|
|
1659
1743
|
base: publishBase,
|
|
1660
|
-
detail: "publish intent: preflight
|
|
1744
|
+
detail: "publish intent: preflight and checksummed diff are complete; issuance is owned by the session-carrying tick worker. The workflow performed no artifact_edit and wrote no issuance."
|
|
1661
1745
|
}, totalReworkCount);
|
|
1662
|
-
|
|
1746
|
+
if (!intentAnchored) {
|
|
1747
|
+
throw new Error("publish-intent ledger anchor write unconfirmed for task " + taskId + " — refusing to park an unclaimable intent; the Publish phase fails loudly instead");
|
|
1748
|
+
}
|
|
1749
|
+
return await parkTask("publish: publish-requested " + mergeCommitForPublish + " attempt=1 — Publish intent recorded for task " + taskId + ": the merged change is staged as a checksummed diff (" + publishDiffFile + ", sha256 " + publishDiffSha256 + ") carrying the per-attempt publish version for the crew's publish worker to issue. The worker issues the edit directly and the acknowledgement scan owns the task from there; no action needed.");
|
|
1663
1750
|
} // end: publishSkippedNoLock — no rebuild, no stamp, nothing to ship
|
|
1664
1751
|
// STEP 2 (mechanical, always — skip path included): post-deploy
|
|
1665
1752
|
// commits builder leftovers if any, removes the worktree, and releases
|
|
@@ -1704,11 +1791,13 @@ while (i < STEPS.length) {
|
|
|
1704
1791
|
// mechanical outcome above. It must not rebuild or re-stamp: a second
|
|
1705
1792
|
// artifact_edit would trigger a duplicate build.
|
|
1706
1793
|
// (2026-09-20, one-party worker-owned publish) On a verified re-entry
|
|
1707
|
-
// the publish was issued
|
|
1708
|
-
//
|
|
1794
|
+
// the publish was issued, the ack scan observed the artifact's exact
|
|
1795
|
+
// version acknowledgement, and provenance was stamped — already
|
|
1796
|
+
// verified, nothing to issue. The work agent reports that,
|
|
1797
|
+
// VERDICT: PASS.
|
|
1709
1798
|
if (publishAlreadyVerified) {
|
|
1710
1799
|
instructions = "Publish was already completed and verified for this task — do NOT call artifact_edit, artifact_status, setprovenance, or post-deploy yourself; doing so would disturb the finalized state.\n\n" +
|
|
1711
|
-
"
|
|
1800
|
+
"Provenance stamps this task's merge commit as published (docs/publish-verification.md): the tick worker issued the edit, the ack scan observed the artifact's exact version acknowledgement — the sole positive completion criterion — and the publish was stamped. Already verified, nothing to issue and nothing to re-verify.\n\n" +
|
|
1712
1801
|
"For the change summary, run: cd " + REPO_PATH + " && git log -1 --stat\n\n" +
|
|
1713
1802
|
"Write plain prose describing what was published, then on its own line: VERDICT: PASS\n" +
|
|
1714
1803
|
"The VERDICT line must be the last line of your report.";
|
|
@@ -1721,10 +1810,10 @@ while (i < STEPS.length) {
|
|
|
1721
1810
|
instructions = "Publish the merged code to the live artifact.\n\n" +
|
|
1722
1811
|
"The publish was performed deterministically by the workflow before your step — do NOT call artifact_edit, artifact_status, setprovenance, or post-deploy yourself; doing so would trigger a duplicate build or disturb the finalized state. You perform no publish actions.\n\n" +
|
|
1723
1812
|
"For the change summary, run: cd " + REPO_PATH + " && git log -1 --stat\n\n" +
|
|
1724
|
-
"Mechanical outcome (the workflow's mechanical steps;
|
|
1813
|
+
"Mechanical outcome (the workflow's mechanical steps; the ack scan's version-acknowledgement check is still pending):\n" +
|
|
1725
1814
|
"- merge lock refreshed: yes\n" +
|
|
1726
1815
|
"- artifact rebuild triggered and completed: yes\n" +
|
|
1727
|
-
"- provenance stamped: NO — not yet, and you must NOT stamp it. The
|
|
1816
|
+
"- provenance stamped: NO — not yet, and you must NOT stamp it. The ack scan stamps it when it observes the artifact's exact on-disk acknowledgement of this attempt's version — version acknowledgement is the sole positive completion criterion (docs/publish-verification.md). A stamped-but-hollow build is exactly how canary run 8 (2026-09-11) went green on stale content.\n" +
|
|
1728
1817
|
"- post-deploy finalized: yes (worktree removed, merge lock released)\n\n" +
|
|
1729
1818
|
"POLICY: The live artifact is rebuilt only in this phase, from the repo. Never use artifact_edit to change the artifact directly — fixes go through the repo and the loop. A source fix is not done until the artifact is rebuilt from it here.\n\n" +
|
|
1730
1819
|
"The repo push already happened in Integrate — do NOT push to git in this phase.\n\n" +
|
|
@@ -1756,7 +1845,7 @@ while (i < STEPS.length) {
|
|
|
1756
1845
|
"Verify the fix against the task description in the returned data: the state the bug corrupted should now read correctly, and the task's own expected behavior should hold.\n" +
|
|
1757
1846
|
"You can also check specific data with: node " + CREW_API + " --crew-home " + crewHome + " get-events --json '{\"task_id\":\"<the task id>\"}'.\n" +
|
|
1758
1847
|
"Do NOT use artifact_inspect — it was removed by the platform (2026-09-14) and does not exist; do not substitute artifact.inspect (malfunction diagnosis, not an inspection tool).\n" +
|
|
1759
|
-
"File follow-up tasks by running in shell:\n" + crewCmd("create-task", { title: "<short title>", description: "<details>", project: "<project id>", workflow: "bugfix", filed_by: "hazel" }) + "\n(substitute the real values for the placeholders).\n" +
|
|
1848
|
+
"File follow-up tasks by running in shell (EXCEPT content findings — user-visible content/data goes in the CONTENT-FINDINGS block at the end of these instructions, never through create-task):\n" + crewCmd("create-task", { title: "<short title>", description: "<details>", project: "<project id>", workflow: "bugfix", filed_by: "hazel" }) + "\n(substitute the real values for the placeholders).\n" +
|
|
1760
1849
|
npmPublishCheck +
|
|
1761
1850
|
"Report your test results as plain prose.\n" +
|
|
1762
1851
|
"End your report with exactly one line: VERDICT: PASS if testing passes, VERDICT: FAIL if it fails.";
|
|
@@ -1784,7 +1873,7 @@ while (i < STEPS.length) {
|
|
|
1784
1873
|
"Verify the fix against the task description in the returned data: the state the bug corrupted should now read correctly, and the task's own expected behavior should hold.\n" +
|
|
1785
1874
|
"You can also check specific data with: node " + CREW_API + " --crew-home " + crewHome + " get-events --json '{\"task_id\":\"<the task id>\"}'.\n" +
|
|
1786
1875
|
npmPublishCheck +
|
|
1787
|
-
"File follow-up tasks by running in shell:\n" + crewCmd("create-task", { title: "<short title>", description: "<details>", project: "<project id>", workflow: "bugfix", filed_by: "hazel" }) + "\n(substitute the real values for the placeholders).\n\n" +
|
|
1876
|
+
"File follow-up tasks by running in shell (EXCEPT content findings — user-visible content/data goes in the CONTENT-FINDINGS block at the end of these instructions, never through create-task):\n" + crewCmd("create-task", { title: "<short title>", description: "<details>", project: "<project id>", workflow: "bugfix", filed_by: "hazel" }) + "\n(substitute the real values for the placeholders).\n\n" +
|
|
1788
1877
|
"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" +
|
|
1789
1878
|
"Report your test results as plain prose.\n" +
|
|
1790
1879
|
"End your report with exactly one line: VERDICT: PASS if testing passes, VERDICT: FAIL if it fails on the visual or the mechanical checks. Checks you could not run are evidence gaps, not silent drops: name every one in --missing — unknown is neither PASS nor 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 \"<the reported bug, fixed>\" --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). A FAIL verdict must carry a machine-readable reason: the workflow closeout cross-checks verdict.json against your prose VERDICT line, and an unreasoned or contradictory verdict fails the phase (never routes to rework).";
|
|
@@ -1811,7 +1900,7 @@ while (i < STEPS.length) {
|
|
|
1811
1900
|
"Use the returned tasks, sessions, and events to check the task's data-level effects.\n" +
|
|
1812
1901
|
"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" +
|
|
1813
1902
|
"STEP 3: File follow-up tasks for any related issues you discover.\n" +
|
|
1814
|
-
"For each issue, run in shell:\n" +
|
|
1903
|
+
"For each issue (EXCEPT content findings — user-visible content/data goes in the CONTENT-FINDINGS block at the end of these instructions, never through create-task), run in shell:\n" +
|
|
1815
1904
|
"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" +
|
|
1816
1905
|
"(replace <issue title> and <issue details> with the real values).\n\n" +
|
|
1817
1906
|
"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" +
|
|
@@ -1830,6 +1919,21 @@ while (i < STEPS.length) {
|
|
|
1830
1919
|
"2. Run: cd " + REPO_PATH + " && git log --format=%s <provenance.source_commit>..LIVE_HEAD (substitute real values). Every subject line MUST start with \"rebuild: \". If any line does not, report 'provenance mismatch: live HEAD moved past the stamped commit with non-rebuild source commits: [paste the offending subject lines]', then end your report with exactly this line: VERDICT: FAIL.\n\n" +
|
|
1831
1920
|
instructions;
|
|
1832
1921
|
}
|
|
1922
|
+
// Room #26 blocker 38 (2026-09-21): content-finding attribution
|
|
1923
|
+
// protocol. Hazel reports user-visible content/data observations in a
|
|
1924
|
+
// machine-readable CONTENT-FINDINGS block; the workflow classifies each
|
|
1925
|
+
// against the task's publish diff and files follow-ups itself — Hazel
|
|
1926
|
+
// never files bugfixes for content findings directly, and
|
|
1927
|
+
// environment-attributable content never fails her verdict alone.
|
|
1928
|
+
var contentFindingsProtocol =
|
|
1929
|
+
"CONTENT FINDINGS (machine-read — room #26 blocker 38): if you observe user-visible content or data that looks wrong, stale, or out of place (decks, cards, rows, text, files in the artifact — anything this task did not obviously produce), report it here — do NOT file a follow-up task for it yourself and do NOT fail your verdict for it alone. Emit exactly one line in this shape, immediately BEFORE your final VERDICT line (the verdict stays the last line of your report):\n" +
|
|
1930
|
+
"CONTENT-FINDINGS: [{\"subject\": \"<repo-relative file path, or the literal live-data for content in the artifact's runtime data stores>\", \"observation\": \"<what you saw, one line>\"}, ...]\n" +
|
|
1931
|
+
"Use subject \"live-data\" for anything you saw in the running artifact (UI content, database rows, uploaded files) — you are code-blind and cannot name its file. Use a repo-relative file path only when you know the content lives in a specific file (e.g. from the task description or public docs). When you saw no such content, emit the empty array: CONTENT-FINDINGS: [].\n" +
|
|
1932
|
+
"The workflow classifies each finding against the task's publish diff: a finding whose subject file is in the diff is attributable to this task's change and the workflow files a bugfix for it; anything else is recorded with its attribution (environment-attributable, or unknown when the diff is unavailable) — the finding itself never spawns an artifact bugfix. No finding is ever dropped for looking like test residue: it is classified by attribution and recorded with it. Your VERDICT judges this task's change; content you cannot attribute to it is not a failure of this task.\n";
|
|
1933
|
+
// The content-findings block is machine-read at closeout (blocker 38).
|
|
1934
|
+
// Hazel emits it immediately before the VERDICT line, so the verdict
|
|
1935
|
+
// keeps its trailing-window contract with extractVerdict.
|
|
1936
|
+
instructions += "\n" + contentFindingsProtocol;
|
|
1833
1937
|
}
|
|
1834
1938
|
|
|
1835
1939
|
// Task event history — Review and Publish are excluded. Review is cold by
|
|
@@ -1888,7 +1992,31 @@ while (i < STEPS.length) {
|
|
|
1888
1992
|
}
|
|
1889
1993
|
var workerResult = null;
|
|
1890
1994
|
var workAttempts = [];
|
|
1891
|
-
|
|
1995
|
+
// Room #26 blocker 34 (redesign): an empty branch with no verified sha and
|
|
1996
|
+
// no repo_diff: none claim fails Review mechanically — Cass is never
|
|
1997
|
+
// dispatched, so no reviewer adjudicates the emptiness. The synthetic
|
|
1998
|
+
// report below is written by the workflow and flows through the same
|
|
1999
|
+
// verdict extraction, structured verdict recording, and rejection/bounce
|
|
2000
|
+
// path as a real FAIL.
|
|
2001
|
+
// Blocker-34 follow-up: the npm `version` check joins the mechanical gate.
|
|
2002
|
+
// Either mechanical fact fails Review without dispatching Cass.
|
|
2003
|
+
var mechanicalFailReason = (versionTouched ? "version-touched" : (branchState === "empty-no-work" && !buildClaimedNoDiff ? "empty-no-work" : null));
|
|
2004
|
+
var mechanicalReviewFail = (step.name === "Review" && mechanicalFailReason !== null);
|
|
2005
|
+
if (mechanicalReviewFail) {
|
|
2006
|
+
log("Review: " + mechanicalFailReason + " — mechanical FAIL, Cass not dispatched");
|
|
2007
|
+
workerResult =
|
|
2008
|
+
"MECHANICAL REVIEW VERDICT (written by the workflow — no reviewer was dispatched).\n" +
|
|
2009
|
+
(mechanicalFailReason === "version-touched"
|
|
2010
|
+
? "Package.json `version` (checked by the workflow from git): the task branch changed the `version` field. Versions are assigned at publish time — never in branches. Remove the version change.\n"
|
|
2011
|
+
: "Branch state (classified by the workflow from git): empty-no-work — the task branch has no commits ahead of the integration target, " +
|
|
2012
|
+
"no task-attributed merge on the target was verified" + (branchStateErr ? " (branch classification itself failed: " + branchStateErr + ")" : "") + ", " +
|
|
2013
|
+
"and the accepted Build report declared no `repo_diff: none` deliverable. " +
|
|
2014
|
+
"No change was reviewed because there is no change to review. The builder likely forgot to commit.\n") +
|
|
2015
|
+
"Worktree: " + WORKTREE_HINT + "\n" +
|
|
2016
|
+
"Recovery: commit the deliverable on the task branch in the worktree above and re-run Review. If the deliverable is genuinely runtime state outside the repo, declare `repo_diff: none` in the Build report instead of leaving the branch empty without a declaration.\n" +
|
|
2017
|
+
"VERDICT: FAIL";
|
|
2018
|
+
}
|
|
2019
|
+
for (var workAttempt = 0; workAttempt <= 2 && !mechanicalReviewFail; workAttempt++) {
|
|
1892
2020
|
var workKey = workAttempt === 0 ? workKeyBase : workRetryKey(step.name, (totalReworkCount > 0 ? "-r" + totalReworkCount : ""), workAttempt);
|
|
1893
2021
|
var prevAttempt = workAttempt === 0 ? null : workAttempts[workAttempt - 1];
|
|
1894
2022
|
var retryReason = workAttempt === 0 ? null : (prevAttempt.threw ? "discarded" : (prevAttempt.outcome === "missing-artifact-tools" ? "no-tools" : (prevAttempt.outcome === "unavailable-shell-transport" ? "no-transport" : "empty")));
|
|
@@ -1989,7 +2117,18 @@ while (i < STEPS.length) {
|
|
|
1989
2117
|
session: { id: activeSessionId, task_id: taskId, identity: step.identity, step: step.name,
|
|
1990
2118
|
status: "failed", notes: "Worker report had no single unambiguous VERDICT: PASS/FAIL line (bounded re-ask exhausted)" },
|
|
1991
2119
|
event: { task_id: taskId, type: "failed",
|
|
1992
|
-
message: step.name + " verdict line missing or ambiguous, re-ask exhausted — phase failed, dispatcher will retry" }
|
|
2120
|
+
message: step.name + " verdict line missing or ambiguous, re-ask exhausted — phase failed, dispatcher will retry" },
|
|
2121
|
+
// Room #26 blocker 33: INDETERMINATE verdict record — the report
|
|
2122
|
+
// could not be read at all, but its full text is still preserved
|
|
2123
|
+
// as grounds. Review-scoped; other verdict steps keep the
|
|
2124
|
+
// existing failed-session behavior with no verdict row.
|
|
2125
|
+
verdict: (step.name === "Review" ? {
|
|
2126
|
+
step: "Review",
|
|
2127
|
+
attempt: totalReworkCount,
|
|
2128
|
+
reviewer: step.identity,
|
|
2129
|
+
verdict: "INDETERMINATE",
|
|
2130
|
+
grounds: workerText
|
|
2131
|
+
} : null)
|
|
1993
2132
|
}),
|
|
1994
2133
|
{ key: "record-block-" + step.name, label: "Recording verdict failure" }
|
|
1995
2134
|
);
|
|
@@ -2190,40 +2329,123 @@ while (i < STEPS.length) {
|
|
|
2190
2329
|
}
|
|
2191
2330
|
log("Build worktree confinement passed: " + wt.path);
|
|
2192
2331
|
|
|
2193
|
-
//
|
|
2194
|
-
|
|
2195
|
-
|
|
2196
|
-
|
|
2197
|
-
|
|
2198
|
-
|
|
2199
|
-
|
|
2200
|
-
|
|
2332
|
+
// Room #26 blocker 34 (redesign, 2026-09-21): capture the plain
|
|
2333
|
+
// `repo_diff: none` (no repo change) claim from the FULL Build report.
|
|
2334
|
+
// Each Build round sets this fresh from its own report; there is no
|
|
2335
|
+
// cross-process fallback. The agent-authored already-merged
|
|
2336
|
+
// declaration and its Build-closeout verification are deleted —
|
|
2337
|
+
// attribution is the classifier's job at Review entry.
|
|
2338
|
+
buildClaimedNoDiff = /repo_diff:\s*none/im.test(workerText || "");
|
|
2339
|
+
}
|
|
2340
|
+
|
|
2341
|
+
// Room #26 blocker 38 (2026-09-21): content-finding attribution at QA
|
|
2342
|
+
// closeout. Hazel's CONTENT-FINDINGS block is extracted deterministically
|
|
2343
|
+
// and each finding is classified against the task's publish-diff file set
|
|
2344
|
+
// (persisted at Publish). Attributable findings become bugfix tasks filed
|
|
2345
|
+
// by the workflow; environment-attributable findings are recorded as task
|
|
2346
|
+
// note events with their attribution — never suppressed, never a bugfix,
|
|
2347
|
+
// never a park. No automatic FAIL override: if the QA agent still
|
|
2348
|
+
// reports FAIL, it stands — finding attribution informs follow-up
|
|
2349
|
+
// filing only.
|
|
2350
|
+
if (step.name === "QA") {
|
|
2351
|
+
var contentFindings = extractContentFindings(workerText);
|
|
2352
|
+
if (!contentFindings.ok) {
|
|
2353
|
+
// Unknown attribution fails closed: a missing or malformed
|
|
2354
|
+
// CONTENT-FINDINGS block blocks the phase for retry — the way an
|
|
2355
|
+
// unreadable VERDICT line does (record-phase failed + halted run).
|
|
2356
|
+
// It must never degrade to "no findings" (fail-open): unknown
|
|
2357
|
+
// attribution converted to an empty set silently drops the
|
|
2358
|
+
// observations the prose grounds carry. Do NOT re-ask an LLM to
|
|
2359
|
+
// repair a machine-readable contract — enforce the contract.
|
|
2360
|
+
log("QA CONTENT-FINDINGS unreadable (" + contentFindings.reason + ") — marking failed for retry");
|
|
2361
|
+
await agent(
|
|
2362
|
+
"Record content-findings failure.\n" +
|
|
2363
|
+
"Run in shell and return the stdout verbatim:\n" + crewCmd("record-phase", {
|
|
2364
|
+
task_id: taskId,
|
|
2365
|
+
session: { id: activeSessionId, task_id: taskId, identity: step.identity, step: step.name, status: "failed",
|
|
2366
|
+
notes: "QA report had no machine-readable CONTENT-FINDINGS block (" + contentFindings.reason + "); phase failed for retry" },
|
|
2367
|
+
event: { task_id: taskId, type: "failed", message: "QA CONTENT-FINDINGS block unreadable (" + contentFindings.reason + ") — phase failed, dispatcher will retry" }
|
|
2368
|
+
}),
|
|
2369
|
+
{ key: "record-block-" + step.name, label: "Recording content-findings failure" }
|
|
2201
2370
|
);
|
|
2202
|
-
|
|
2203
|
-
|
|
2204
|
-
|
|
2371
|
+
return {
|
|
2372
|
+
__hatchWorkflowControl: "blocked",
|
|
2373
|
+
result: {
|
|
2374
|
+
blocked_reason: "QA worker report had no machine-readable CONTENT-FINDINGS block (" + contentFindings.reason + ")",
|
|
2375
|
+
message: "The " + step.identity + " agent's work may be valid — its report did not carry a CONTENT-FINDINGS block the workflow could read, and the attribution contract fails closed rather than degrading to \"no findings\". The report is preserved in the workflow log.",
|
|
2376
|
+
task_id: taskId
|
|
2377
|
+
}
|
|
2378
|
+
};
|
|
2379
|
+
}
|
|
2380
|
+
// The publish-diff file set, persisted at Publish. Unreadable →
|
|
2381
|
+
// attribution unknown: every finding records "unknown" with the
|
|
2382
|
+
// reason naming the missing diff (never environment-attributable,
|
|
2383
|
+
// never a bugfix). The QA verdict stands on its own.
|
|
2384
|
+
var qaDiffFiles = [];
|
|
2385
|
+
var qaDiffFilesOk = false;
|
|
2386
|
+
try {
|
|
2387
|
+
var qaDiffFilesOut = await agent(
|
|
2388
|
+
"Run exactly one command and return the stdout verbatim:\n" +
|
|
2389
|
+
"cat \"" + crewHome + "/.publish-diffs/" + taskId + ".files.json\" 2>/dev/null || echo DIFF_FILES_MISSING\n" +
|
|
2390
|
+
"Return JSON { \"output\": \"<the command's full stdout, trimmed>\" } and nothing else.",
|
|
2391
|
+
{ key: attemptKey("qa-diff-files-" + taskId, totalReworkCount), label: "Loading publish diff file list",
|
|
2392
|
+
schema: { type: "object", properties: { output: { type: "string" } }, required: ["output"] } }
|
|
2393
|
+
);
|
|
2394
|
+
var qaDiffFilesRaw = ((qaDiffFilesOut && qaDiffFilesOut.output) || "").trim();
|
|
2395
|
+
if (qaDiffFilesRaw && qaDiffFilesRaw !== "DIFF_FILES_MISSING") {
|
|
2396
|
+
var qaDiffParsed = JSON.parse(qaDiffFilesRaw);
|
|
2397
|
+
if (Array.isArray(qaDiffParsed)) {
|
|
2398
|
+
qaDiffFiles = qaDiffParsed;
|
|
2399
|
+
qaDiffFilesOk = true;
|
|
2400
|
+
}
|
|
2401
|
+
}
|
|
2402
|
+
} catch (e) {
|
|
2403
|
+
log("QA diff file list unreadable: " + ((e && e.message ? e.message : String(e)) || "").slice(0, 200));
|
|
2404
|
+
}
|
|
2405
|
+
if (!qaDiffFilesOk) {
|
|
2406
|
+
log("QA publish diff file list unavailable — content findings record attribution unknown");
|
|
2407
|
+
}
|
|
2408
|
+
for (var cfi = 0; cfi < contentFindings.findings.length; cfi++) {
|
|
2409
|
+
var cf = contentFindings.findings[cfi];
|
|
2410
|
+
// Unknown attribution fails closed: when the diff file set is
|
|
2411
|
+
// unavailable we cannot attribute, so the finding is recorded as
|
|
2412
|
+
// "unknown" (never environment-attributable, never a bugfix) and the
|
|
2413
|
+
// QA verdict stands — the journey does not continue on unproven
|
|
2414
|
+
// attribution.
|
|
2415
|
+
var cfc = qaDiffFilesOk ? classifyContentFinding(cf, qaDiffFiles)
|
|
2416
|
+
: { attribution: "unknown", reason: "publish diff file set unavailable — cannot attribute" };
|
|
2417
|
+
if (cfc.attribution === "task-change") {
|
|
2418
|
+
log("QA content finding attributable to task change — filing bugfix: " + cf.subject + " :: " + cf.observation.slice(0, 120));
|
|
2205
2419
|
await agent(
|
|
2206
|
-
"
|
|
2207
|
-
"Run in shell and return the stdout verbatim:\n" +
|
|
2208
|
-
|
|
2209
|
-
|
|
2210
|
-
|
|
2211
|
-
|
|
2212
|
-
|
|
2213
|
-
|
|
2420
|
+
"File the follow-up task.\n" +
|
|
2421
|
+
"Run in shell and return the stdout verbatim:\n" +
|
|
2422
|
+
"node " + CREW_API + " --crew-home " + crewHome + " create-task --json '" +
|
|
2423
|
+
JSON.stringify({ title: "QA content finding: " + cf.observation.slice(0, 120), description: "Content finding from QA on task " + taskId + " (subject: " + cf.subject + ", classified task-change: " + cfc.reason + "). Observation: " + cf.observation, project: LAUNCH_PROJECT_ID, workflow: "bugfix", filed_by: "hazel" }).split("'").join("'\\''") + "'\n" ,
|
|
2424
|
+
{ key: attemptKey("qa-content-bugfix-" + taskId + "-" + cfi, totalReworkCount), label: "Filing attributable content bugfix" }
|
|
2425
|
+
);
|
|
2426
|
+
} else {
|
|
2427
|
+
// environment-attributable and unknown findings are recorded as task
|
|
2428
|
+
// note events with their attribution — never suppressed, never a
|
|
2429
|
+
// bugfix, never a park by themselves. The QA verdict stands on its
|
|
2430
|
+
// own: we do not override a FAIL based on finding attribution, because
|
|
2431
|
+
// the verdict may rest on functional grounds the findings do not capture.
|
|
2432
|
+
log("QA content finding " + cfc.attribution + " — recording, no bugfix: " + cf.subject + " :: " + cf.observation.slice(0, 120));
|
|
2433
|
+
await agent(
|
|
2434
|
+
"Record the " + cfc.attribution + " content finding.\n" +
|
|
2435
|
+
"Run in shell and return the stdout verbatim:\n" +
|
|
2436
|
+
crewCmd("log-event", { task_id: taskId, type: "note", message: "content-finding: " + cfc.attribution + " — subject: " + cf.subject + " — " + cf.observation + " (" + cfc.reason + ")" }) + "\n" ,
|
|
2437
|
+
{ key: attemptKey("qa-content-record-" + taskId + "-" + cfi, totalReworkCount), label: "Recording " + cfc.attribution + " finding" }
|
|
2214
2438
|
);
|
|
2215
|
-
return {
|
|
2216
|
-
__hatchWorkflowControl: "blocked",
|
|
2217
|
-
result: {
|
|
2218
|
-
blocked_reason: "Build already-merged declaration failed verification",
|
|
2219
|
-
message: "The builder declared repo_diff: none (already-merged: " + am.sha + ") but " + am.sha + " is not an ancestor of the integration target. The work is not on the integration target; the phase is marked failed and the dispatcher will retry Build.",
|
|
2220
|
-
task_id: taskId
|
|
2221
|
-
}
|
|
2222
|
-
};
|
|
2223
2439
|
}
|
|
2224
|
-
alreadyMergedSha = am.sha;
|
|
2225
|
-
log("Build already-merged declaration verified: " + am.sha + " is an ancestor of the integration target");
|
|
2226
2440
|
}
|
|
2441
|
+
// No automatic FAIL override. The QA prompt instructs Hazel that her
|
|
2442
|
+
// VERDICT judges this task's change and unattributable content is not a
|
|
2443
|
+
// failure of this task; if she still reports FAIL, it stands. The
|
|
2444
|
+
// recorded finding attributions (above) give the human the evidence to
|
|
2445
|
+
// distinguish environment residue from task failure. Overriding a FAIL
|
|
2446
|
+
// merely because all listed findings are environment-attributable would
|
|
2447
|
+
// be too broad — the prose may name functional failures the findings do
|
|
2448
|
+
// not capture.
|
|
2227
2449
|
}
|
|
2228
2450
|
|
|
2229
2451
|
// Deterministic closeout: no formatter agent. The verdict is mechanical
|
|
@@ -2235,6 +2457,9 @@ while (i < STEPS.length) {
|
|
|
2235
2457
|
summary: workerText
|
|
2236
2458
|
};
|
|
2237
2459
|
let passed = stepResult.passed === true;
|
|
2460
|
+
// Blocker 38: content findings are recorded with their attribution in the
|
|
2461
|
+
// session notes above; the QA verdict stands on its own and is never
|
|
2462
|
+
// overridden by finding attribution.
|
|
2238
2463
|
|
|
2239
2464
|
// See docs/decisions/publish-path.md#integrate-verify: the agent cannot verify integrate mechanically; the workflow checks the diff.
|
|
2240
2465
|
if (step.name === "Integrate" && passed) {
|
|
@@ -2320,10 +2545,10 @@ while (i < STEPS.length) {
|
|
|
2320
2545
|
} // end: !npmPublishSkipped — a skipped publish has nothing to verify
|
|
2321
2546
|
}
|
|
2322
2547
|
|
|
2323
|
-
//
|
|
2324
|
-
// scan
|
|
2325
|
-
// the
|
|
2326
|
-
//
|
|
2548
|
+
// Version acknowledgement (docs/publish-verification.md): the tick worker
|
|
2549
|
+
// issues the edit directly and the ack scan owns the task from there —
|
|
2550
|
+
// the exact per-attempt version on the artifact's disk is the sole positive
|
|
2551
|
+
// completion criterion. A skipped or failed publish has nothing to ack.
|
|
2327
2552
|
let summary;
|
|
2328
2553
|
var workerMarkers = extractMarkerLines(workerText);
|
|
2329
2554
|
if (step.name === "Publish" && PUBLISH_TYPE === "npm" && publishTarget && (publishVerified || npmPublishSkipped)) {
|
|
@@ -2337,9 +2562,23 @@ while (i < STEPS.length) {
|
|
|
2337
2562
|
} else {
|
|
2338
2563
|
summary = (stepResult.summary || "Step completed").slice(0, 2000 - workerMarkers.length - 1) + (workerMarkers ? "\n" + workerMarkers : "");
|
|
2339
2564
|
}
|
|
2340
|
-
//
|
|
2341
|
-
|
|
2342
|
-
|
|
2565
|
+
// Room #26 blocker 34 (redesign, 2026-09-21): machine-readable review
|
|
2566
|
+
// basis, prepended to Review session notes for observability (proof
|
|
2567
|
+
// rooms read the summary to see what Review examined and why a
|
|
2568
|
+
// reviewer was or was not dispatched).
|
|
2569
|
+
if (step.name === "Review" && branchState) {
|
|
2570
|
+
var reviewBasisNote;
|
|
2571
|
+
if (mechanicalReviewFail) {
|
|
2572
|
+
reviewBasisNote = "REVIEW_BASIS: none — mechanical FAIL (" + mechanicalFailReason + "), no reviewer dispatched";
|
|
2573
|
+
} else if (branchState.indexOf("already-merged:") === 0) {
|
|
2574
|
+
var rbSha = branchState.slice("already-merged:".length);
|
|
2575
|
+
reviewBasisNote = "REVIEW_BASIS: frozen merge " + rbSha + " — reviewed via git diff " + rbSha + "^1 " + rbSha;
|
|
2576
|
+
} else if (branchState === "has-work") {
|
|
2577
|
+
reviewBasisNote = "REVIEW_BASIS: branch diff — reviewed via inspect output (task branch vs integration target)";
|
|
2578
|
+
} else {
|
|
2579
|
+
reviewBasisNote = "REVIEW_BASIS: runtime-state-none — no repo change; plausibility of Build's repo_diff: none claim";
|
|
2580
|
+
}
|
|
2581
|
+
summary = reviewBasisNote + "\n" + summary;
|
|
2343
2582
|
}
|
|
2344
2583
|
|
|
2345
2584
|
// Visual verdict evidence (2026-09-15): no parent marker is appended. Hazel
|
|
@@ -2374,18 +2613,45 @@ while (i < STEPS.length) {
|
|
|
2374
2613
|
releaseDecision = extractReleaseDecision(workerText);
|
|
2375
2614
|
}
|
|
2376
2615
|
|
|
2616
|
+
// Room #26 blocker 34 (redesign, 2026-09-21): structured review basis
|
|
2617
|
+
// for the verdict row — what the review actually examined. A mechanical
|
|
2618
|
+
// FAIL has no reviewer and no basis beyond the mechanical fact. Only
|
|
2619
|
+
// computed for Review (branchState is null on other steps).
|
|
2620
|
+
var reviewBasis = null;
|
|
2621
|
+
if (step.name === "Review" && branchState) {
|
|
2622
|
+
reviewBasis =
|
|
2623
|
+
mechanicalReviewFail ? "mechanical-fail" :
|
|
2624
|
+
branchState.indexOf("already-merged:") === 0 ? "frozen-merge:" + branchState.slice("already-merged:".length) :
|
|
2625
|
+
branchState === "has-work" ? "branch-diff" : "runtime-state-none";
|
|
2626
|
+
}
|
|
2627
|
+
|
|
2377
2628
|
await agent(
|
|
2378
2629
|
"Update the session and log the event.\n" +
|
|
2379
2630
|
"Run in shell and return the stdout verbatim:\n" + crewCmd("record-phase", {
|
|
2380
2631
|
task_id: taskId,
|
|
2381
|
-
// Room #
|
|
2382
|
-
//
|
|
2383
|
-
//
|
|
2384
|
-
//
|
|
2632
|
+
// Room #26 blocker 34 (2026-09-21): the already_merged_sha session
|
|
2633
|
+
// field is no longer written — the agent-authored declaration path
|
|
2634
|
+
// is deleted and nothing reads it. Removing the Crew API/database
|
|
2635
|
+
// column is a separate API-surface follow-up, not bundled here.
|
|
2385
2636
|
session: { id: activeSessionId, task_id: taskId, identity: step.identity, step: step.name,
|
|
2386
|
-
status: status, notes: summary
|
|
2637
|
+
status: status, notes: summary },
|
|
2387
2638
|
event: { task_id: taskId, type: status, identity: step.identity,
|
|
2388
|
-
message: step.name + " " + status + " by " + step.identity }
|
|
2639
|
+
message: step.name + " " + status + " by " + step.identity },
|
|
2640
|
+
// Room #26 blocker 33: structured, non-lossy Review verdict record.
|
|
2641
|
+
// The session note above is truncated; the verdict row carries the
|
|
2642
|
+
// full worker report as grounds, written in the same transaction.
|
|
2643
|
+
// attempt is the rework round (0 = first Review).
|
|
2644
|
+
verdict: (step.name === "Review" && verdictPassed !== null ? {
|
|
2645
|
+
step: "Review",
|
|
2646
|
+
attempt: totalReworkCount,
|
|
2647
|
+
// Room #26 blocker 34 (redesign): a mechanical FAIL has no
|
|
2648
|
+
// reviewer — the workflow wrote the verdict. Identity rows still say
|
|
2649
|
+
// the Review step ran.
|
|
2650
|
+
reviewer: mechanicalReviewFail ? "workflow" : step.identity,
|
|
2651
|
+
verdict: verdictPassed ? "PASS" : "FAIL",
|
|
2652
|
+
review_basis: reviewBasis,
|
|
2653
|
+
grounds: workerText
|
|
2654
|
+
} : null)
|
|
2389
2655
|
}),
|
|
2390
2656
|
{
|
|
2391
2657
|
key: "record-" + step.name + (totalReworkCount > 0 ? "-r" + totalReworkCount : "") + (mapGateBounceCount > 0 ? "-g" + mapGateBounceCount : ""),
|
|
@@ -2405,21 +2671,20 @@ while (i < STEPS.length) {
|
|
|
2405
2671
|
|
|
2406
2672
|
// Review/QA rejection bounces to Build
|
|
2407
2673
|
if (!passed && (step.name === "Review" || step.name === "QA")) {
|
|
2674
|
+
// Room #26 blocker 34 (redesign, 2026-09-21): the false-negative guard,
|
|
2675
|
+
// the Case B budget skip, and the already-merged corrective are all
|
|
2676
|
+
// gone. Emptiness is classified mechanically before Review, so a
|
|
2677
|
+
// rejected Review is never re-adjudicated here — the workflow owns the
|
|
2678
|
+
// git facts, the reviewer owns quality/spec compliance, and the budget
|
|
2679
|
+
// applies uniformly to every rejection. A rejected frozen merge
|
|
2680
|
+
// bounces with the reviewer's notes; a real fix commit becomes
|
|
2681
|
+
// has-work, and a do-nothing loop exhausts the normal budget.
|
|
2408
2682
|
totalReworkCount++;
|
|
2409
2683
|
if (totalReworkCount > MAX_TOTAL_REWORK) {
|
|
2410
2684
|
log("Shared rework budget exhausted for task " + taskId + " — worktree preserved at " + WORKTREE_PRESERVED_HINT + " for manual inspection");
|
|
2411
2685
|
return await parkTask("Exceeded shared rework budget (" + MAX_TOTAL_REWORK + " total rework attempts across Review and QA) after " + step.name + " rejection. Worktree preserved.");
|
|
2412
2686
|
}
|
|
2413
2687
|
rejectionNotes = summary;
|
|
2414
|
-
// Already-merged corrective (room #16 blocker 11): when Review rejected
|
|
2415
|
-
// an empty branch but the work is already on the integration target (the workflow verified
|
|
2416
|
-
// the sha), Wren must declare it — not re-implement or re-commit
|
|
2417
|
-
// already-landed work. Scoped to the empty-branch rejection; any other
|
|
2418
|
-
// rejection already carries its own specific notes.
|
|
2419
|
-
if (step.name === "Review" && alreadyMergedSha && /no commits ahead of (main|the integration target)/i.test(summary)) {
|
|
2420
|
-
rejectionNotes += "\n\nCORRECTIVE (from the workflow, not the reviewer): the deliverable is already on the integration target — the workflow mechanically verified that " + alreadyMergedSha + " is an ancestor of the integration target. Do NOT re-implement the work and do NOT create a new commit for it. In your Build report, declare exactly: repo_diff: none (already-merged: " + alreadyMergedSha + ") — then end with VERDICT: PASS.";
|
|
2421
|
-
log("Rework corrective appended for task " + taskId + ": already-merged " + alreadyMergedSha + " — Wren must declare, not rebuild");
|
|
2422
|
-
}
|
|
2423
2688
|
i = BUILD_INDEX;
|
|
2424
2689
|
log(step.name + " rejected — bouncing to Build (rework #" + totalReworkCount + " of " + MAX_TOTAL_REWORK + ")");
|
|
2425
2690
|
continue;
|