muse-crew 0.14.7 → 0.14.8
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/package.json +1 -1
- package/workflows/bugfix.js +275 -50
- package/workflows/chore.js +260 -48
- package/workflows/standard.js +275 -50
package/package.json
CHANGED
package/workflows/bugfix.js
CHANGED
|
@@ -312,6 +312,142 @@ function buildTransportRetryTrailer(stepName, repoPath, taskId, attempt, reason)
|
|
|
312
312
|
"if the " + stepName + " work is already complete, report on what was done rather than duplicating side effects. " +
|
|
313
313
|
"Then return your report as JSON in exactly the shape specified above.";
|
|
314
314
|
}
|
|
315
|
+
// parseClassifyFerry — room #26 blocker 42 (2026-09-21). Shape-constrained
|
|
316
|
+
// ferry parser for the classify-branch agent() call. The schema buys SHAPE,
|
|
317
|
+
// not provenance: the fields are LLM-authored, so the parser stays
|
|
318
|
+
// defensive. Byte-identical across standard.js, bugfix.js, chore.js
|
|
319
|
+
// (pinned by tests/already-merged.test.js). Triplication is structural:
|
|
320
|
+
// each workflow ships as a standalone launch payload (240 KiB budget,
|
|
321
|
+
// individually git-archived) — there is no shared module to hold it.
|
|
322
|
+
// The schema is the guard; the prompt is instruction, never load-bearing.
|
|
323
|
+
// Returns { state, diagField, diagText, failReason }:
|
|
324
|
+
// state: "has-work" | "already-merged:<40-hex>" | "empty-no-work" | null
|
|
325
|
+
// (null = unparseable/ambiguous/channel violation — UNKNOWN)
|
|
326
|
+
// diagField: "stderr" | "stdout" | null (which field sourced the DIAG pin)
|
|
327
|
+
// diagText: the matched DIAG line, or ""
|
|
328
|
+
// failReason: computed reason, for the retry trailer and the park note
|
|
329
|
+
function parseClassifyFerry(bsResult) {
|
|
330
|
+
var bsStdout = (bsResult && typeof bsResult.stdout === "string") ? bsResult.stdout : null;
|
|
331
|
+
var bsStderr = (bsResult && typeof bsResult.stderr === "string") ? bsResult.stderr : null;
|
|
332
|
+
if (bsStdout === null || bsStderr === null) {
|
|
333
|
+
return { state: null, diagField: null, diagText: "",
|
|
334
|
+
failReason: "classifier return fields missing or non-string (stdout and stderr must both be strings)" };
|
|
335
|
+
}
|
|
336
|
+
// State channel: stdout ONLY. A BRANCH_STATE match on stderr is never
|
|
337
|
+
// trusted — workers merge streams constantly, and trusting a channel
|
|
338
|
+
// swap as truth is the wrong shape. Search, never anchor (blocker 11):
|
|
339
|
+
// the ferry labels output ("stdout: BRANCH_STATE: has-work"). Collect
|
|
340
|
+
// ALL matches and dedupe normalized values: exactly one distinct value
|
|
341
|
+
// is truth; zero or two-or-more is UNKNOWN — alternation order never
|
|
342
|
+
// adjudicates.
|
|
343
|
+
var bsRe = /\bBRANCH_STATE:\s*(has-work|already-merged:[0-9a-f]{40}|empty-no-work)(?![\w-])/gi;
|
|
344
|
+
var bsSeen = {};
|
|
345
|
+
var bsM;
|
|
346
|
+
while ((bsM = bsRe.exec(bsStdout)) !== null) {
|
|
347
|
+
bsSeen[bsM[1].toLowerCase()] = true;
|
|
348
|
+
}
|
|
349
|
+
var bsStates = Object.keys(bsSeen);
|
|
350
|
+
if (bsStates.length !== 1) {
|
|
351
|
+
return { state: null, diagField: null, diagText: "",
|
|
352
|
+
failReason: bsStates.length === 0
|
|
353
|
+
? "no BRANCH_STATE match in the stdout field"
|
|
354
|
+
: "ambiguous BRANCH_STATE values in stdout (" + bsStates.length + " distinct)" };
|
|
355
|
+
}
|
|
356
|
+
var bsState = bsStates[0];
|
|
357
|
+
// DIAG pin: search both fields, stderr first. Keep the DIAG: prefix — a
|
|
358
|
+
// prose mention without it must not satisfy the pin.
|
|
359
|
+
var bsDiagRe = /DIAG:\s*classify-branch\s+records=\d+\s+resolvable=\d+/;
|
|
360
|
+
var bsDiagField = bsDiagRe.test(bsStderr) ? "stderr" : (bsDiagRe.test(bsStdout) ? "stdout" : null);
|
|
361
|
+
var bsDiagText = "";
|
|
362
|
+
if (bsDiagField !== null) {
|
|
363
|
+
bsDiagText = bsDiagRe.exec(bsDiagField === "stderr" ? bsStderr : bsStdout)[0];
|
|
364
|
+
}
|
|
365
|
+
if (bsState.indexOf("already-merged:") === 0 && bsDiagField === null) {
|
|
366
|
+
// already-merged's sha flows into Cass's `git diff <sha>^1 <sha>` — a
|
|
367
|
+
// hallucinated-but-well-formed sha is a false-PASS vector. The script
|
|
368
|
+
// emits exactly one DIAG on every path that emits a BRANCH_STATE, so a
|
|
369
|
+
// missing pin means stderr wasn't faithfully ferried: UNKNOWN.
|
|
370
|
+
// Residual risk, named (blocker-42 fib 4): a worker could fabricate
|
|
371
|
+
// BOTH fields wholesale — the pin is a tripwire, not proof of
|
|
372
|
+
// execution. The workflow has no exec channel for a deterministic
|
|
373
|
+
// cross-check; if the platform ever offers one, verify the sha
|
|
374
|
+
// against the task's durable merge records directly.
|
|
375
|
+
return { state: null, diagField: null, diagText: "",
|
|
376
|
+
failReason: "already-merged without the DIAG pin — stderr not faithfully ferried" };
|
|
377
|
+
}
|
|
378
|
+
return { state: bsState, diagField: bsDiagField, diagText: bsDiagText, failReason: "" };
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
// classifyRetryTrailer — room #26 blocker 42 (2026-09-21). Corrective (not
|
|
382
|
+
// flat) trailer for the in-place classify retries. The failure mode was
|
|
383
|
+
// systematic labeling, not a coin flip: the trailer carries the COMPUTED
|
|
384
|
+
// parse-failure reason, the failed return truncated as a negative example,
|
|
385
|
+
// and the explicit field mapping — feedback from the actual failure,
|
|
386
|
+
// following the buildTransportRetryTrailer idiom. Bound: 2 retries
|
|
387
|
+
// (unmeasured house convention, matches the shared rework bound of 2 —
|
|
388
|
+
// against room retry data). Byte-identical across the three workflows
|
|
389
|
+
// (pinned by tests/already-merged.test.js).
|
|
390
|
+
function classifyRetryTrailer(reason, failedReturn) {
|
|
391
|
+
// Pass-2 simplification: the reason string already carries the truth
|
|
392
|
+
// (throw vs parse failure), so one neutral sentence replaces the
|
|
393
|
+
// throw/parse branch — the stringly-typed prefix contract between the
|
|
394
|
+
// call site and this function is deleted, not moved. Prompt accuracy,
|
|
395
|
+
// not prompt hardening.
|
|
396
|
+
return "\n\nCLASSIFY RETRY: the previous classifier attempt failed (" + reason + "). " +
|
|
397
|
+
"Return ONLY the two named fields: { \"stdout\": \"<the command's exact stdout>\", \"stderr\": \"<the command's exact stderr>\" }. " +
|
|
398
|
+
"Put each stream in its named field — do not add labels like \"stdout:\" in front of the content. " +
|
|
399
|
+
"Previous attempt detail (do not repeat this shape): " + String(failedReturn).slice(0, 200);
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
// parseVersionFerry — review pass 2, blocker-42 class (2026-09-21). The npm
|
|
403
|
+
// version-check ferry had the full pre-42 shape: schema-less agent(),
|
|
404
|
+
// line-anchored parser, and an unparseable fallback asserting a POSITIVE
|
|
405
|
+
// touched claim that spent the shared rework budget. Same treatment as
|
|
406
|
+
// parseClassifyFerry: shape-constrained {stdout} schema, search-and-dedupe
|
|
407
|
+
// parse, UNKNOWN on unparseable with in-place retries, honest park.
|
|
408
|
+
// Byte-identical across standard.js, bugfix.js, chore.js
|
|
409
|
+
// (pinned by tests/already-merged.test.js).
|
|
410
|
+
// Returns { versionState, failReason }:
|
|
411
|
+
// versionState: "touched" | "clean" | null (null = UNKNOWN)
|
|
412
|
+
// failReason: computed reason, for the retry trailer and the park note
|
|
413
|
+
function parseVersionFerry(vtResult) {
|
|
414
|
+
var vtStdout = (vtResult && typeof vtResult.stdout === "string") ? vtResult.stdout : null;
|
|
415
|
+
if (vtStdout === null) {
|
|
416
|
+
return { versionState: null,
|
|
417
|
+
failReason: "version-check return field missing or non-string (stdout must be a string)" };
|
|
418
|
+
}
|
|
419
|
+
// Search, never anchor (blocker 11): the ferry labels output
|
|
420
|
+
// ("stdout: VERSION_CLEAN"). Collect ALL matches and dedupe normalized
|
|
421
|
+
// values: exactly one distinct value is truth; zero or two-or-more is
|
|
422
|
+
// UNKNOWN — alternation order never adjudicates.
|
|
423
|
+
var vtRe = /\bVERSION_(TOUCHED|CLEAN)(?![\w-])/gi;
|
|
424
|
+
var vtSeen = {};
|
|
425
|
+
var vtM;
|
|
426
|
+
while ((vtM = vtRe.exec(vtStdout)) !== null) {
|
|
427
|
+
vtSeen[vtM[1].toUpperCase()] = true;
|
|
428
|
+
}
|
|
429
|
+
var vtStates = Object.keys(vtSeen);
|
|
430
|
+
if (vtStates.length !== 1) {
|
|
431
|
+
return { versionState: null,
|
|
432
|
+
failReason: vtStates.length === 0
|
|
433
|
+
? "no VERSION_(TOUCHED|CLEAN) match in the stdout field"
|
|
434
|
+
: "ambiguous VERSION values in stdout (" + vtStates.length + " distinct)" };
|
|
435
|
+
}
|
|
436
|
+
return { versionState: vtStates[0] === "CLEAN" ? "clean" : "touched", failReason: "" };
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
// versionRetryTrailer — review pass 2, blocker-42 class (2026-09-21).
|
|
440
|
+
// Neutral corrective trailer for the in-place version-check retries: the
|
|
441
|
+
// computed parse-failure reason plus the failed return as a negative
|
|
442
|
+
// example. Byte-identical across the three workflows
|
|
443
|
+
// (pinned by tests/already-merged.test.js).
|
|
444
|
+
function versionRetryTrailer(reason, failedReturn) {
|
|
445
|
+
return "\n\nVERSION-CHECK RETRY: the previous version-check attempt failed (" + reason + "). " +
|
|
446
|
+
"Return ONLY the named field: { \"stdout\": \"<the command's exact stdout>\" }. " +
|
|
447
|
+
"Put the command's stdout in the named field — do not add labels like \"stdout:\" in front of the content. " +
|
|
448
|
+
"Previous attempt detail (do not repeat this shape): " + String(failedReturn).slice(0, 200);
|
|
449
|
+
}
|
|
450
|
+
|
|
315
451
|
// parseToolSignals - bug 3472bf36. The work agent's TOOL CHECK emits two
|
|
316
452
|
// exact signal lines: artifact_tools: ok|missing and
|
|
317
453
|
// shell_transport: ok|unavailable. The workflow reads ONLY these lines.
|
|
@@ -737,7 +873,7 @@ let alreadyMergedSha = null;
|
|
|
737
873
|
let branchState = null; // "has-work" | "already-merged:<sha>" | "empty-no-work"
|
|
738
874
|
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
875
|
let versionTouched = false; // npm: branch changed package.json's `version` (mechanical git fact, blocker-34 follow-up)
|
|
740
|
-
let branchStateErr = ""; // classifier failure detail (
|
|
876
|
+
let branchStateErr = ""; // classifier failure detail (retry trailer + park note grounds)
|
|
741
877
|
// Deterministic publish target — computed by the workflow (registry base +
|
|
742
878
|
// bumpVersion), never by the Publish agent.
|
|
743
879
|
let publishTarget = null; // { base, scope, target }
|
|
@@ -1287,43 +1423,87 @@ while (i < STEPS.length) {
|
|
|
1287
1423
|
// (classify-branch in the lifecycle script): has-work |
|
|
1288
1424
|
// already-merged:<sha> | empty-no-work. Attribution is identity-first
|
|
1289
1425
|
// from git, then the task's own durable merge records — there is no
|
|
1290
|
-
// agent-authored declared-sha input
|
|
1291
|
-
//
|
|
1292
|
-
//
|
|
1293
|
-
//
|
|
1294
|
-
//
|
|
1295
|
-
|
|
1296
|
-
|
|
1426
|
+
// agent-authored declared-sha input.
|
|
1427
|
+
// Blocker 42 (2026-09-21): the classifier output crosses an agent()
|
|
1428
|
+
// ferry — the old comment's "no cross-process hydration ferry" was a
|
|
1429
|
+
// fib. Machine facts travel in structured fields ({stdout, stderr}
|
|
1430
|
+
// schema) now, never in prose to be "returned verbatim". The schema
|
|
1431
|
+
// is the guard; the prompt is instruction, never load-bearing.
|
|
1432
|
+
// Unparseable is UNKNOWN, never a positive empty-no-work claim (room
|
|
1433
|
+
// #27: a labeled ferry — "stdout: BRANCH_STATE: has-work" — defeated
|
|
1434
|
+
// the ^-anchored parsers, and the empty-no-work fallback burned the
|
|
1435
|
+
// whole shared rework budget on a branch with real work). UNKNOWN
|
|
1436
|
+
// retries the classify call IN PLACE (up to 2, fresh -u1/-u2 keys — a
|
|
1437
|
+
// full Build round cannot fix an instrument failure, and the only
|
|
1438
|
+
// in-process bounce path spends the shared budget unconditionally).
|
|
1439
|
+
// Instrumentation failures NEVER touch totalReworkCount: an instrument
|
|
1440
|
+
// failure is not a build-quality failure. Exhaustion parks honestly
|
|
1441
|
+
// as unclassifiable — "unknown keeps the task alive (bounded)"; the
|
|
1442
|
+
// park on exhaustion is the fail-closed part. The UNKNOWN path never
|
|
1443
|
+
// asserts empty-no-work: no log line or park note presents it as the
|
|
1444
|
+
// classification. The string can still appear inside quoted evidence
|
|
1445
|
+
// (the verbatim failed return travels in the trailer and the note).
|
|
1446
|
+
// INVARIANT: branchState leaves this block holding a known value, or
|
|
1447
|
+
// the step has parked — unknown never reaches any consumer below.
|
|
1448
|
+
// Re-classify on EVERY Review entry: rework rewinds this step
|
|
1449
|
+
// in-process, and the branch changed between rounds. The loop's
|
|
1450
|
+
// null-gate governs only the in-place instrument retries within
|
|
1451
|
+
// this pass — without the reset, a second Review pass would trust
|
|
1452
|
+
// the first pass's stale classification and a real fix commit
|
|
1453
|
+
// would never "become has-work".
|
|
1454
|
+
branchState = null;
|
|
1455
|
+
branchStateErr = "";
|
|
1456
|
+
alreadyMergedSha = null;
|
|
1457
|
+
var classifyFailedReturn = "";
|
|
1458
|
+
for (var classifyAttempt = 0; classifyAttempt <= 2 && branchState === null; classifyAttempt++) {
|
|
1459
|
+
var classifyPrompt =
|
|
1297
1460
|
"Classify the task branch state. This is a mechanical git check, not a judgment call.\n" +
|
|
1298
|
-
"Run in shell
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1461
|
+
"Run in shell: " + LIFECYCLE_ENV + LIFECYCLE + " classify-branch " + taskId + "\n" +
|
|
1462
|
+
"Return the two streams as the two named fields and nothing else: { \"stdout\": \"<the command's exact stdout>\", \"stderr\": \"<the command's exact stderr>\" }." +
|
|
1463
|
+
(classifyAttempt === 0 ? "" : classifyRetryTrailer(branchStateErr, classifyFailedReturn));
|
|
1464
|
+
try {
|
|
1465
|
+
var bsResult = await agent(
|
|
1466
|
+
classifyPrompt,
|
|
1467
|
+
{ key: "classify-branch" + (classifyAttempt === 0 ? "" : "-u" + classifyAttempt) + (totalReworkCount > 0 ? "-r" + totalReworkCount : ""),
|
|
1468
|
+
label: "Classifying branch state (mechanical)" + (classifyAttempt === 0 ? "" : " (instrument retry " + classifyAttempt + " of 2)"),
|
|
1469
|
+
schema: { type: "object", properties: { stdout: { type: "string" }, stderr: { type: "string" } }, required: ["stdout", "stderr"] } }
|
|
1470
|
+
);
|
|
1471
|
+
classifyFailedReturn = (bsResult && typeof bsResult === "object" ? JSON.stringify(bsResult) : String(bsResult)).slice(0, 500);
|
|
1472
|
+
var classifyParsed = parseClassifyFerry(bsResult);
|
|
1473
|
+
if (classifyParsed.state !== null) {
|
|
1474
|
+
branchState = classifyParsed.state;
|
|
1475
|
+
branchStateErr = "";
|
|
1476
|
+
// DIAG pin: advisory for has-work/empty-no-work (blocker-34 v2.1
|
|
1477
|
+
// semantics — logged with its source field so silent record loss
|
|
1478
|
+
// surfaces in the evidence); already-merged is gated on the pin
|
|
1479
|
+
// inside parseClassifyFerry.
|
|
1480
|
+
if (classifyParsed.diagField !== null) {
|
|
1481
|
+
log("classify-branch DIAG pin (" + classifyParsed.diagField + " field): " + classifyParsed.diagText);
|
|
1482
|
+
} else {
|
|
1483
|
+
log("classify-branch: no DIAG: record-scan counter returned (scan unverifiable)");
|
|
1484
|
+
}
|
|
1485
|
+
} else {
|
|
1486
|
+
branchStateErr = classifyParsed.failReason;
|
|
1487
|
+
if (classifyAttempt < 2) {
|
|
1488
|
+
log("classify-branch unparseable (" + branchStateErr + ") — retrying in place (attempt " + (classifyAttempt + 1) + " of 2)");
|
|
1489
|
+
}
|
|
1490
|
+
}
|
|
1491
|
+
} catch (bsErr) {
|
|
1492
|
+
classifyFailedReturn = String(bsErr && bsErr.message || bsErr).slice(0, 500);
|
|
1493
|
+
branchStateErr = "classifier call failed: " + String(bsErr && bsErr.message || bsErr).slice(0, 120);
|
|
1494
|
+
if (classifyAttempt < 2) {
|
|
1495
|
+
log("classify-branch instrument failure (" + branchStateErr + ") — retrying in place (attempt " + (classifyAttempt + 1) + " of 2)");
|
|
1496
|
+
}
|
|
1317
1497
|
}
|
|
1318
|
-
} catch (bsErr) {
|
|
1319
|
-
branchStateErr = "classifier call failed: " + String(bsErr && bsErr.message || bsErr).slice(0, 120);
|
|
1320
1498
|
}
|
|
1321
|
-
if (
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1499
|
+
if (branchState === null) {
|
|
1500
|
+
// Exhaustion: park honestly. An instrument failure is not a
|
|
1501
|
+
// measurement of emptiness — never "empty-no-work".
|
|
1502
|
+
return await parkTask("Branch state unclassifiable after 2 instrument retries (" + branchStateErr + "; last failure: " + classifyFailedReturn + ") — the classifier instrument failed, not the branch. Human attention needed: inspect the task branch directly to determine its state — no trustworthy branch classification was produced.");
|
|
1503
|
+
}
|
|
1504
|
+
if (branchState.indexOf("already-merged:") === 0) {
|
|
1325
1505
|
alreadyMergedSha = branchState.slice("already-merged:".length);
|
|
1326
|
-
log("Branch state already-merged: " + alreadyMergedSha + " (
|
|
1506
|
+
log("Branch state already-merged: " + alreadyMergedSha + " (DIAG pin present — tripwire satisfied) — Cass reviews the frozen merge diff");
|
|
1327
1507
|
} else {
|
|
1328
1508
|
log("Branch state: " + branchState);
|
|
1329
1509
|
}
|
|
@@ -1335,23 +1515,55 @@ while (i < STEPS.length) {
|
|
|
1335
1515
|
// git fact, not a reviewer judgment. For npm projects the workflow checks
|
|
1336
1516
|
// it here, before Cass is dispatched — a touched `version` fails Review
|
|
1337
1517
|
// mechanically (versions are assigned at publish time, never in
|
|
1338
|
-
// branches).
|
|
1339
|
-
//
|
|
1518
|
+
// branches). Review pass 2 (blocker-42 class): this ferry had the full
|
|
1519
|
+
// pre-42 shape — schema-less agent(), line-anchored parser, and an
|
|
1520
|
+
// unparseable fallback asserting a POSITIVE touched claim that spent the
|
|
1521
|
+
// shared rework budget. Same treatment as the classifier:
|
|
1522
|
+
// shape-constrained {stdout} schema, search-and-dedupe parse, UNKNOWN
|
|
1523
|
+
// with in-place retries, honest park. An instrument failure is not a
|
|
1524
|
+
// measurement of touched — never assert touched from unparseable output.
|
|
1340
1525
|
if (PUBLISH_TYPE === "npm") {
|
|
1341
|
-
|
|
1342
|
-
|
|
1526
|
+
var versionState = null; // "touched" | "clean" | null (null = UNKNOWN)
|
|
1527
|
+
var versionStateErr = "";
|
|
1528
|
+
var versionFailedReturn = "";
|
|
1529
|
+
for (var versionAttempt = 0; versionAttempt <= 2 && versionState === null; versionAttempt++) {
|
|
1530
|
+
var versionPrompt =
|
|
1343
1531
|
"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
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1532
|
+
"Run in shell: " + LIFECYCLE_ENV + LIFECYCLE + " version-check " + taskId + "\n" +
|
|
1533
|
+
"Return the command's exact stdout as the single named field and nothing else: { \"stdout\": \"<the command's exact stdout>\" }." +
|
|
1534
|
+
(versionAttempt === 0 ? "" : versionRetryTrailer(versionStateErr, versionFailedReturn));
|
|
1535
|
+
try {
|
|
1536
|
+
var vtResult = await agent(
|
|
1537
|
+
versionPrompt,
|
|
1538
|
+
{ key: "version-check" + (versionAttempt === 0 ? "" : "-u" + versionAttempt) + (totalReworkCount > 0 ? "-r" + totalReworkCount : ""),
|
|
1539
|
+
label: "Checking package.json version (mechanical)" + (versionAttempt === 0 ? "" : " (instrument retry " + versionAttempt + " of 2)"),
|
|
1540
|
+
schema: { type: "object", properties: { stdout: { type: "string" } }, required: ["stdout"] } }
|
|
1541
|
+
);
|
|
1542
|
+
versionFailedReturn = (vtResult && typeof vtResult === "object" ? JSON.stringify(vtResult) : String(vtResult)).slice(0, 500);
|
|
1543
|
+
var vtParsed = parseVersionFerry(vtResult);
|
|
1544
|
+
if (vtParsed.versionState !== null) {
|
|
1545
|
+
versionState = vtParsed.versionState;
|
|
1546
|
+
versionStateErr = "";
|
|
1547
|
+
} else {
|
|
1548
|
+
versionStateErr = vtParsed.failReason;
|
|
1549
|
+
if (versionAttempt < 2) {
|
|
1550
|
+
log("version-check unparseable (" + versionStateErr + ") — retrying in place (attempt " + (versionAttempt + 1) + " of 2)");
|
|
1551
|
+
}
|
|
1552
|
+
}
|
|
1553
|
+
} catch (vtErr) {
|
|
1554
|
+
versionFailedReturn = String(vtErr && vtErr.message || vtErr).slice(0, 500);
|
|
1555
|
+
versionStateErr = "version-check call failed: " + String(vtErr && vtErr.message || vtErr).slice(0, 120);
|
|
1556
|
+
if (versionAttempt < 2) {
|
|
1557
|
+
log("version-check instrument failure (" + versionStateErr + ") — retrying in place (attempt " + (versionAttempt + 1) + " of 2)");
|
|
1558
|
+
}
|
|
1559
|
+
}
|
|
1354
1560
|
}
|
|
1561
|
+
if (versionState === null) {
|
|
1562
|
+
// Exhaustion: park honestly. An instrument failure is not a
|
|
1563
|
+
// measurement of touched — never assert touched.
|
|
1564
|
+
return await parkTask("package.json `version` state unclassifiable after 2 instrument retries (" + versionStateErr + "; last failure: " + versionFailedReturn + ") — the version-check instrument failed, not the branch. Human attention needed: inspect the task branch directly to determine its state — no trustworthy version classification was produced.");
|
|
1565
|
+
}
|
|
1566
|
+
versionTouched = (versionState === "touched");
|
|
1355
1567
|
log("Review: package.json `version` " + (versionTouched ? "touched by the branch — mechanical FAIL, Cass not dispatched" : "untouched — mechanical check clean"));
|
|
1356
1568
|
}
|
|
1357
1569
|
// The empty-branch rule Cass used to adjudicate is gone. What she
|
|
@@ -1674,13 +1886,26 @@ while (i < STEPS.length) {
|
|
|
1674
1886
|
// closeout — the file list is the complete record of what the task
|
|
1675
1887
|
// changed. The courier is pure hands: it writes the deterministic
|
|
1676
1888
|
// script's own file list, never interprets it.
|
|
1889
|
+
// Blocker 40 (2026-09-21): this courier carries the same
|
|
1890
|
+
// {output: string} schema as the sibling diff-computation call.
|
|
1891
|
+
// The command is stdout-silent; schema-less, the runtime's
|
|
1892
|
+
// non-empty-result contract throws on the empty string and the
|
|
1893
|
+
// workflow parks fail-closed for a side effect that landed. Empty
|
|
1894
|
+
// stdout is schema-valid, so the contract no longer fires. "The
|
|
1895
|
+
// schema bypasses the contract" is INFERRED, not observed —
|
|
1896
|
+
// falsification: if a schema-carrying courier still parks on
|
|
1897
|
+
// silent stdout, the platform contract is deeper than the schema
|
|
1898
|
+
// and this is wrong; room evidence decides. (The file on disk is
|
|
1899
|
+
// not read as proof until QA closeout — the QA loader fails
|
|
1900
|
+
// closed to "unknown" attribution when it is absent.)
|
|
1677
1901
|
var publishDiffFilesPath = crewHome + "/.publish-diffs/" + taskId + ".files.json";
|
|
1678
1902
|
await agent(
|
|
1679
1903
|
"Write the publish diff file list.\n" +
|
|
1680
|
-
"Run exactly this and return the stdout verbatim:\n" +
|
|
1904
|
+
"Run exactly this and return the stdout verbatim as { \"output\": \"<verbatim stdout>\" } and nothing else. Do not interpret it:\n" +
|
|
1681
1905
|
"mkdir -p " + crewHome + "/.publish-diffs && cat > \"" + publishDiffFilesPath + "\" <<'DIFFFILES_EOF'\n" +
|
|
1682
1906
|
JSON.stringify(diffSummary.files || []) + "\nDIFFFILES_EOF\n",
|
|
1683
|
-
{ key: attemptKey("publish-diff-files-" + taskId, totalReworkCount), label: "Persisting publish diff file list"
|
|
1907
|
+
{ key: attemptKey("publish-diff-files-" + taskId, totalReworkCount), label: "Persisting publish diff file list",
|
|
1908
|
+
schema: { type: "object", properties: { output: { type: "string" } }, required: ["output"] } }
|
|
1684
1909
|
);
|
|
1685
1910
|
// Budget counts CHANGED lines (added + removed), not raw unified-diff
|
|
1686
1911
|
// output lines: context lines and file headers inflated the old
|
package/workflows/chore.js
CHANGED
|
@@ -322,6 +322,142 @@ function buildTransportRetryTrailer(stepName, repoPath, taskId, attempt, reason)
|
|
|
322
322
|
"if the " + stepName + " work is already complete, report on what was done rather than duplicating side effects. " +
|
|
323
323
|
"Then return your report as JSON in exactly the shape specified above.";
|
|
324
324
|
}
|
|
325
|
+
// parseClassifyFerry — room #26 blocker 42 (2026-09-21). Shape-constrained
|
|
326
|
+
// ferry parser for the classify-branch agent() call. The schema buys SHAPE,
|
|
327
|
+
// not provenance: the fields are LLM-authored, so the parser stays
|
|
328
|
+
// defensive. Byte-identical across standard.js, bugfix.js, chore.js
|
|
329
|
+
// (pinned by tests/already-merged.test.js). Triplication is structural:
|
|
330
|
+
// each workflow ships as a standalone launch payload (240 KiB budget,
|
|
331
|
+
// individually git-archived) — there is no shared module to hold it.
|
|
332
|
+
// The schema is the guard; the prompt is instruction, never load-bearing.
|
|
333
|
+
// Returns { state, diagField, diagText, failReason }:
|
|
334
|
+
// state: "has-work" | "already-merged:<40-hex>" | "empty-no-work" | null
|
|
335
|
+
// (null = unparseable/ambiguous/channel violation — UNKNOWN)
|
|
336
|
+
// diagField: "stderr" | "stdout" | null (which field sourced the DIAG pin)
|
|
337
|
+
// diagText: the matched DIAG line, or ""
|
|
338
|
+
// failReason: computed reason, for the retry trailer and the park note
|
|
339
|
+
function parseClassifyFerry(bsResult) {
|
|
340
|
+
var bsStdout = (bsResult && typeof bsResult.stdout === "string") ? bsResult.stdout : null;
|
|
341
|
+
var bsStderr = (bsResult && typeof bsResult.stderr === "string") ? bsResult.stderr : null;
|
|
342
|
+
if (bsStdout === null || bsStderr === null) {
|
|
343
|
+
return { state: null, diagField: null, diagText: "",
|
|
344
|
+
failReason: "classifier return fields missing or non-string (stdout and stderr must both be strings)" };
|
|
345
|
+
}
|
|
346
|
+
// State channel: stdout ONLY. A BRANCH_STATE match on stderr is never
|
|
347
|
+
// trusted — workers merge streams constantly, and trusting a channel
|
|
348
|
+
// swap as truth is the wrong shape. Search, never anchor (blocker 11):
|
|
349
|
+
// the ferry labels output ("stdout: BRANCH_STATE: has-work"). Collect
|
|
350
|
+
// ALL matches and dedupe normalized values: exactly one distinct value
|
|
351
|
+
// is truth; zero or two-or-more is UNKNOWN — alternation order never
|
|
352
|
+
// adjudicates.
|
|
353
|
+
var bsRe = /\bBRANCH_STATE:\s*(has-work|already-merged:[0-9a-f]{40}|empty-no-work)(?![\w-])/gi;
|
|
354
|
+
var bsSeen = {};
|
|
355
|
+
var bsM;
|
|
356
|
+
while ((bsM = bsRe.exec(bsStdout)) !== null) {
|
|
357
|
+
bsSeen[bsM[1].toLowerCase()] = true;
|
|
358
|
+
}
|
|
359
|
+
var bsStates = Object.keys(bsSeen);
|
|
360
|
+
if (bsStates.length !== 1) {
|
|
361
|
+
return { state: null, diagField: null, diagText: "",
|
|
362
|
+
failReason: bsStates.length === 0
|
|
363
|
+
? "no BRANCH_STATE match in the stdout field"
|
|
364
|
+
: "ambiguous BRANCH_STATE values in stdout (" + bsStates.length + " distinct)" };
|
|
365
|
+
}
|
|
366
|
+
var bsState = bsStates[0];
|
|
367
|
+
// DIAG pin: search both fields, stderr first. Keep the DIAG: prefix — a
|
|
368
|
+
// prose mention without it must not satisfy the pin.
|
|
369
|
+
var bsDiagRe = /DIAG:\s*classify-branch\s+records=\d+\s+resolvable=\d+/;
|
|
370
|
+
var bsDiagField = bsDiagRe.test(bsStderr) ? "stderr" : (bsDiagRe.test(bsStdout) ? "stdout" : null);
|
|
371
|
+
var bsDiagText = "";
|
|
372
|
+
if (bsDiagField !== null) {
|
|
373
|
+
bsDiagText = bsDiagRe.exec(bsDiagField === "stderr" ? bsStderr : bsStdout)[0];
|
|
374
|
+
}
|
|
375
|
+
if (bsState.indexOf("already-merged:") === 0 && bsDiagField === null) {
|
|
376
|
+
// already-merged's sha flows into Cass's `git diff <sha>^1 <sha>` — a
|
|
377
|
+
// hallucinated-but-well-formed sha is a false-PASS vector. The script
|
|
378
|
+
// emits exactly one DIAG on every path that emits a BRANCH_STATE, so a
|
|
379
|
+
// missing pin means stderr wasn't faithfully ferried: UNKNOWN.
|
|
380
|
+
// Residual risk, named (blocker-42 fib 4): a worker could fabricate
|
|
381
|
+
// BOTH fields wholesale — the pin is a tripwire, not proof of
|
|
382
|
+
// execution. The workflow has no exec channel for a deterministic
|
|
383
|
+
// cross-check; if the platform ever offers one, verify the sha
|
|
384
|
+
// against the task's durable merge records directly.
|
|
385
|
+
return { state: null, diagField: null, diagText: "",
|
|
386
|
+
failReason: "already-merged without the DIAG pin — stderr not faithfully ferried" };
|
|
387
|
+
}
|
|
388
|
+
return { state: bsState, diagField: bsDiagField, diagText: bsDiagText, failReason: "" };
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
// classifyRetryTrailer — room #26 blocker 42 (2026-09-21). Corrective (not
|
|
392
|
+
// flat) trailer for the in-place classify retries. The failure mode was
|
|
393
|
+
// systematic labeling, not a coin flip: the trailer carries the COMPUTED
|
|
394
|
+
// parse-failure reason, the failed return truncated as a negative example,
|
|
395
|
+
// and the explicit field mapping — feedback from the actual failure,
|
|
396
|
+
// following the buildTransportRetryTrailer idiom. Bound: 2 retries
|
|
397
|
+
// (unmeasured house convention, matches the shared rework bound of 2 —
|
|
398
|
+
// against room retry data). Byte-identical across the three workflows
|
|
399
|
+
// (pinned by tests/already-merged.test.js).
|
|
400
|
+
function classifyRetryTrailer(reason, failedReturn) {
|
|
401
|
+
// Pass-2 simplification: the reason string already carries the truth
|
|
402
|
+
// (throw vs parse failure), so one neutral sentence replaces the
|
|
403
|
+
// throw/parse branch — the stringly-typed prefix contract between the
|
|
404
|
+
// call site and this function is deleted, not moved. Prompt accuracy,
|
|
405
|
+
// not prompt hardening.
|
|
406
|
+
return "\n\nCLASSIFY RETRY: the previous classifier attempt failed (" + reason + "). " +
|
|
407
|
+
"Return ONLY the two named fields: { \"stdout\": \"<the command's exact stdout>\", \"stderr\": \"<the command's exact stderr>\" }. " +
|
|
408
|
+
"Put each stream in its named field — do not add labels like \"stdout:\" in front of the content. " +
|
|
409
|
+
"Previous attempt detail (do not repeat this shape): " + String(failedReturn).slice(0, 200);
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
// parseVersionFerry — review pass 2, blocker-42 class (2026-09-21). The npm
|
|
413
|
+
// version-check ferry had the full pre-42 shape: schema-less agent(),
|
|
414
|
+
// line-anchored parser, and an unparseable fallback asserting a POSITIVE
|
|
415
|
+
// touched claim that spent the shared rework budget. Same treatment as
|
|
416
|
+
// parseClassifyFerry: shape-constrained {stdout} schema, search-and-dedupe
|
|
417
|
+
// parse, UNKNOWN on unparseable with in-place retries, honest park.
|
|
418
|
+
// Byte-identical across standard.js, bugfix.js, chore.js
|
|
419
|
+
// (pinned by tests/already-merged.test.js).
|
|
420
|
+
// Returns { versionState, failReason }:
|
|
421
|
+
// versionState: "touched" | "clean" | null (null = UNKNOWN)
|
|
422
|
+
// failReason: computed reason, for the retry trailer and the park note
|
|
423
|
+
function parseVersionFerry(vtResult) {
|
|
424
|
+
var vtStdout = (vtResult && typeof vtResult.stdout === "string") ? vtResult.stdout : null;
|
|
425
|
+
if (vtStdout === null) {
|
|
426
|
+
return { versionState: null,
|
|
427
|
+
failReason: "version-check return field missing or non-string (stdout must be a string)" };
|
|
428
|
+
}
|
|
429
|
+
// Search, never anchor (blocker 11): the ferry labels output
|
|
430
|
+
// ("stdout: VERSION_CLEAN"). Collect ALL matches and dedupe normalized
|
|
431
|
+
// values: exactly one distinct value is truth; zero or two-or-more is
|
|
432
|
+
// UNKNOWN — alternation order never adjudicates.
|
|
433
|
+
var vtRe = /\bVERSION_(TOUCHED|CLEAN)(?![\w-])/gi;
|
|
434
|
+
var vtSeen = {};
|
|
435
|
+
var vtM;
|
|
436
|
+
while ((vtM = vtRe.exec(vtStdout)) !== null) {
|
|
437
|
+
vtSeen[vtM[1].toUpperCase()] = true;
|
|
438
|
+
}
|
|
439
|
+
var vtStates = Object.keys(vtSeen);
|
|
440
|
+
if (vtStates.length !== 1) {
|
|
441
|
+
return { versionState: null,
|
|
442
|
+
failReason: vtStates.length === 0
|
|
443
|
+
? "no VERSION_(TOUCHED|CLEAN) match in the stdout field"
|
|
444
|
+
: "ambiguous VERSION values in stdout (" + vtStates.length + " distinct)" };
|
|
445
|
+
}
|
|
446
|
+
return { versionState: vtStates[0] === "CLEAN" ? "clean" : "touched", failReason: "" };
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
// versionRetryTrailer — review pass 2, blocker-42 class (2026-09-21).
|
|
450
|
+
// Neutral corrective trailer for the in-place version-check retries: the
|
|
451
|
+
// computed parse-failure reason plus the failed return as a negative
|
|
452
|
+
// example. Byte-identical across the three workflows
|
|
453
|
+
// (pinned by tests/already-merged.test.js).
|
|
454
|
+
function versionRetryTrailer(reason, failedReturn) {
|
|
455
|
+
return "\n\nVERSION-CHECK RETRY: the previous version-check attempt failed (" + reason + "). " +
|
|
456
|
+
"Return ONLY the named field: { \"stdout\": \"<the command's exact stdout>\" }. " +
|
|
457
|
+
"Put the command's stdout in the named field — do not add labels like \"stdout:\" in front of the content. " +
|
|
458
|
+
"Previous attempt detail (do not repeat this shape): " + String(failedReturn).slice(0, 200);
|
|
459
|
+
}
|
|
460
|
+
|
|
325
461
|
// parseToolSignals - bug 3472bf36. The work agent's TOOL CHECK emits two
|
|
326
462
|
// exact signal lines: artifact_tools: ok|missing and
|
|
327
463
|
// shell_transport: ok|unavailable. The workflow reads ONLY these lines.
|
|
@@ -694,7 +830,7 @@ let alreadyMergedSha = null;
|
|
|
694
830
|
let branchState = null; // "has-work" | "already-merged:<sha>" | "empty-no-work"
|
|
695
831
|
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.
|
|
696
832
|
let versionTouched = false; // npm: branch changed package.json's `version` (mechanical git fact, blocker-34 follow-up)
|
|
697
|
-
let branchStateErr = ""; // classifier failure detail (
|
|
833
|
+
let branchStateErr = ""; // classifier failure detail (retry trailer + park note grounds)
|
|
698
834
|
// Deterministic publish target — computed by the workflow (registry base +
|
|
699
835
|
// bumpVersion), never by the Publish agent.
|
|
700
836
|
let publishTarget = null; // { base, scope, target }
|
|
@@ -1150,43 +1286,87 @@ while (i < STEPS.length) {
|
|
|
1150
1286
|
// (classify-branch in the lifecycle script): has-work |
|
|
1151
1287
|
// already-merged:<sha> | empty-no-work. Attribution is identity-first
|
|
1152
1288
|
// from git, then the task's own durable merge records — there is no
|
|
1153
|
-
// agent-authored declared-sha input
|
|
1154
|
-
//
|
|
1155
|
-
//
|
|
1156
|
-
//
|
|
1157
|
-
//
|
|
1158
|
-
|
|
1159
|
-
|
|
1289
|
+
// agent-authored declared-sha input.
|
|
1290
|
+
// Blocker 42 (2026-09-21): the classifier output crosses an agent()
|
|
1291
|
+
// ferry — the old comment's "no cross-process hydration ferry" was a
|
|
1292
|
+
// fib. Machine facts travel in structured fields ({stdout, stderr}
|
|
1293
|
+
// schema) now, never in prose to be "returned verbatim". The schema
|
|
1294
|
+
// is the guard; the prompt is instruction, never load-bearing.
|
|
1295
|
+
// Unparseable is UNKNOWN, never a positive empty-no-work claim (room
|
|
1296
|
+
// #27: a labeled ferry — "stdout: BRANCH_STATE: has-work" — defeated
|
|
1297
|
+
// the ^-anchored parsers, and the empty-no-work fallback burned the
|
|
1298
|
+
// whole shared rework budget on a branch with real work). UNKNOWN
|
|
1299
|
+
// retries the classify call IN PLACE (up to 2, fresh -u1/-u2 keys — a
|
|
1300
|
+
// full Build round cannot fix an instrument failure, and the only
|
|
1301
|
+
// in-process bounce path spends the shared budget unconditionally).
|
|
1302
|
+
// Instrumentation failures NEVER touch reworkCount: an instrument
|
|
1303
|
+
// failure is not a build-quality failure. Exhaustion parks honestly
|
|
1304
|
+
// as unclassifiable — "unknown keeps the task alive (bounded)"; the
|
|
1305
|
+
// park on exhaustion is the fail-closed part. The UNKNOWN path never
|
|
1306
|
+
// asserts empty-no-work: no log line or park note presents it as the
|
|
1307
|
+
// classification. The string can still appear inside quoted evidence
|
|
1308
|
+
// (the verbatim failed return travels in the trailer and the note).
|
|
1309
|
+
// INVARIANT: branchState leaves this block holding a known value, or
|
|
1310
|
+
// the step has parked — unknown never reaches any consumer below.
|
|
1311
|
+
// Re-classify on EVERY Review entry: rework rewinds this step
|
|
1312
|
+
// in-process, and the branch changed between rounds. The loop's
|
|
1313
|
+
// null-gate governs only the in-place instrument retries within
|
|
1314
|
+
// this pass — without the reset, a second Review pass would trust
|
|
1315
|
+
// the first pass's stale classification and a real fix commit
|
|
1316
|
+
// would never "become has-work".
|
|
1317
|
+
branchState = null;
|
|
1318
|
+
branchStateErr = "";
|
|
1319
|
+
alreadyMergedSha = null;
|
|
1320
|
+
var classifyFailedReturn = "";
|
|
1321
|
+
for (var classifyAttempt = 0; classifyAttempt <= 2 && branchState === null; classifyAttempt++) {
|
|
1322
|
+
var classifyPrompt =
|
|
1160
1323
|
"Classify the task branch state. This is a mechanical git check, not a judgment call.\n" +
|
|
1161
|
-
"Run in shell
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1324
|
+
"Run in shell: " + LIFECYCLE_ENV + LIFECYCLE + " classify-branch " + taskId + "\n" +
|
|
1325
|
+
"Return the two streams as the two named fields and nothing else: { \"stdout\": \"<the command's exact stdout>\", \"stderr\": \"<the command's exact stderr>\" }." +
|
|
1326
|
+
(classifyAttempt === 0 ? "" : classifyRetryTrailer(branchStateErr, classifyFailedReturn));
|
|
1327
|
+
try {
|
|
1328
|
+
var bsResult = await agent(
|
|
1329
|
+
classifyPrompt,
|
|
1330
|
+
{ key: "classify-branch" + (classifyAttempt === 0 ? "" : "-u" + classifyAttempt) + (reworkCount > 0 ? "-r" + reworkCount : ""),
|
|
1331
|
+
label: "Classifying branch state (mechanical)" + (classifyAttempt === 0 ? "" : " (instrument retry " + classifyAttempt + " of 2)"),
|
|
1332
|
+
schema: { type: "object", properties: { stdout: { type: "string" }, stderr: { type: "string" } }, required: ["stdout", "stderr"] } }
|
|
1333
|
+
);
|
|
1334
|
+
classifyFailedReturn = (bsResult && typeof bsResult === "object" ? JSON.stringify(bsResult) : String(bsResult)).slice(0, 500);
|
|
1335
|
+
var classifyParsed = parseClassifyFerry(bsResult);
|
|
1336
|
+
if (classifyParsed.state !== null) {
|
|
1337
|
+
branchState = classifyParsed.state;
|
|
1338
|
+
branchStateErr = "";
|
|
1339
|
+
// DIAG pin: advisory for has-work/empty-no-work (blocker-34 v2.1
|
|
1340
|
+
// semantics — logged with its source field so silent record loss
|
|
1341
|
+
// surfaces in the evidence); already-merged is gated on the pin
|
|
1342
|
+
// inside parseClassifyFerry.
|
|
1343
|
+
if (classifyParsed.diagField !== null) {
|
|
1344
|
+
log("classify-branch DIAG pin (" + classifyParsed.diagField + " field): " + classifyParsed.diagText);
|
|
1345
|
+
} else {
|
|
1346
|
+
log("classify-branch: no DIAG: record-scan counter returned (scan unverifiable)");
|
|
1347
|
+
}
|
|
1348
|
+
} else {
|
|
1349
|
+
branchStateErr = classifyParsed.failReason;
|
|
1350
|
+
if (classifyAttempt < 2) {
|
|
1351
|
+
log("classify-branch unparseable (" + branchStateErr + ") — retrying in place (attempt " + (classifyAttempt + 1) + " of 2)");
|
|
1352
|
+
}
|
|
1353
|
+
}
|
|
1354
|
+
} catch (bsErr) {
|
|
1355
|
+
classifyFailedReturn = String(bsErr && bsErr.message || bsErr).slice(0, 500);
|
|
1356
|
+
branchStateErr = "classifier call failed: " + String(bsErr && bsErr.message || bsErr).slice(0, 120);
|
|
1357
|
+
if (classifyAttempt < 2) {
|
|
1358
|
+
log("classify-branch instrument failure (" + branchStateErr + ") — retrying in place (attempt " + (classifyAttempt + 1) + " of 2)");
|
|
1359
|
+
}
|
|
1180
1360
|
}
|
|
1181
|
-
} catch (bsErr) {
|
|
1182
|
-
branchStateErr = "classifier call failed: " + String(bsErr && bsErr.message || bsErr).slice(0, 120);
|
|
1183
1361
|
}
|
|
1184
|
-
if (
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1362
|
+
if (branchState === null) {
|
|
1363
|
+
// Exhaustion: park honestly. An instrument failure is not a
|
|
1364
|
+
// measurement of emptiness — never "empty-no-work".
|
|
1365
|
+
return await parkTask("Branch state unclassifiable after 2 instrument retries (" + branchStateErr + "; last failure: " + classifyFailedReturn + ") — the classifier instrument failed, not the branch. Human attention needed: inspect the task branch directly to determine its state — no trustworthy branch classification was produced.");
|
|
1366
|
+
}
|
|
1367
|
+
if (branchState.indexOf("already-merged:") === 0) {
|
|
1188
1368
|
alreadyMergedSha = branchState.slice("already-merged:".length);
|
|
1189
|
-
log("Branch state already-merged: " + alreadyMergedSha + " (
|
|
1369
|
+
log("Branch state already-merged: " + alreadyMergedSha + " (DIAG pin present — tripwire satisfied) — Cass reviews the frozen merge diff");
|
|
1190
1370
|
} else {
|
|
1191
1371
|
log("Branch state: " + branchState);
|
|
1192
1372
|
}
|
|
@@ -1198,23 +1378,55 @@ while (i < STEPS.length) {
|
|
|
1198
1378
|
// git fact, not a reviewer judgment. For npm projects the workflow checks
|
|
1199
1379
|
// it here, before Cass is dispatched — a touched `version` fails Review
|
|
1200
1380
|
// mechanically (versions are assigned at publish time, never in
|
|
1201
|
-
// branches).
|
|
1202
|
-
//
|
|
1381
|
+
// branches). Review pass 2 (blocker-42 class): this ferry had the full
|
|
1382
|
+
// pre-42 shape — schema-less agent(), line-anchored parser, and an
|
|
1383
|
+
// unparseable fallback asserting a POSITIVE touched claim that spent the
|
|
1384
|
+
// shared rework budget. Same treatment as the classifier:
|
|
1385
|
+
// shape-constrained {stdout} schema, search-and-dedupe parse, UNKNOWN
|
|
1386
|
+
// with in-place retries, honest park. An instrument failure is not a
|
|
1387
|
+
// measurement of touched — never assert touched from unparseable output.
|
|
1203
1388
|
if (PUBLISH_TYPE === "npm") {
|
|
1204
|
-
|
|
1205
|
-
|
|
1389
|
+
var versionState = null; // "touched" | "clean" | null (null = UNKNOWN)
|
|
1390
|
+
var versionStateErr = "";
|
|
1391
|
+
var versionFailedReturn = "";
|
|
1392
|
+
for (var versionAttempt = 0; versionAttempt <= 2 && versionState === null; versionAttempt++) {
|
|
1393
|
+
var versionPrompt =
|
|
1206
1394
|
"Check whether the task branch changed package.json's `version` field. This is a mechanical git check, not a judgment call.\n" +
|
|
1207
|
-
"Run in shell
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1395
|
+
"Run in shell: " + LIFECYCLE_ENV + LIFECYCLE + " version-check " + taskId + "\n" +
|
|
1396
|
+
"Return the command's exact stdout as the single named field and nothing else: { \"stdout\": \"<the command's exact stdout>\" }." +
|
|
1397
|
+
(versionAttempt === 0 ? "" : versionRetryTrailer(versionStateErr, versionFailedReturn));
|
|
1398
|
+
try {
|
|
1399
|
+
var vtResult = await agent(
|
|
1400
|
+
versionPrompt,
|
|
1401
|
+
{ key: "version-check" + (versionAttempt === 0 ? "" : "-u" + versionAttempt) + (reworkCount > 0 ? "-r" + reworkCount : ""),
|
|
1402
|
+
label: "Checking package.json version (mechanical)" + (versionAttempt === 0 ? "" : " (instrument retry " + versionAttempt + " of 2)"),
|
|
1403
|
+
schema: { type: "object", properties: { stdout: { type: "string" } }, required: ["stdout"] } }
|
|
1404
|
+
);
|
|
1405
|
+
versionFailedReturn = (vtResult && typeof vtResult === "object" ? JSON.stringify(vtResult) : String(vtResult)).slice(0, 500);
|
|
1406
|
+
var vtParsed = parseVersionFerry(vtResult);
|
|
1407
|
+
if (vtParsed.versionState !== null) {
|
|
1408
|
+
versionState = vtParsed.versionState;
|
|
1409
|
+
versionStateErr = "";
|
|
1410
|
+
} else {
|
|
1411
|
+
versionStateErr = vtParsed.failReason;
|
|
1412
|
+
if (versionAttempt < 2) {
|
|
1413
|
+
log("version-check unparseable (" + versionStateErr + ") — retrying in place (attempt " + (versionAttempt + 1) + " of 2)");
|
|
1414
|
+
}
|
|
1415
|
+
}
|
|
1416
|
+
} catch (vtErr) {
|
|
1417
|
+
versionFailedReturn = String(vtErr && vtErr.message || vtErr).slice(0, 500);
|
|
1418
|
+
versionStateErr = "version-check call failed: " + String(vtErr && vtErr.message || vtErr).slice(0, 120);
|
|
1419
|
+
if (versionAttempt < 2) {
|
|
1420
|
+
log("version-check instrument failure (" + versionStateErr + ") — retrying in place (attempt " + (versionAttempt + 1) + " of 2)");
|
|
1421
|
+
}
|
|
1422
|
+
}
|
|
1423
|
+
}
|
|
1424
|
+
if (versionState === null) {
|
|
1425
|
+
// Exhaustion: park honestly. An instrument failure is not a
|
|
1426
|
+
// measurement of touched — never assert touched.
|
|
1427
|
+
return await parkTask("package.json `version` state unclassifiable after 2 instrument retries (" + versionStateErr + "; last failure: " + versionFailedReturn + ") — the version-check instrument failed, not the branch. Human attention needed: inspect the task branch directly to determine its state — no trustworthy version classification was produced.");
|
|
1217
1428
|
}
|
|
1429
|
+
versionTouched = (versionState === "touched");
|
|
1218
1430
|
log("Review: package.json `version` " + (versionTouched ? "touched by the branch — mechanical FAIL, Cass not dispatched" : "untouched — mechanical check clean"));
|
|
1219
1431
|
}
|
|
1220
1432
|
// The empty-branch rule Cass used to adjudicate is gone. What she
|
package/workflows/standard.js
CHANGED
|
@@ -320,6 +320,142 @@ function buildTransportRetryTrailer(stepName, repoPath, taskId, attempt, reason)
|
|
|
320
320
|
"if the " + stepName + " work is already complete, report on what was done rather than duplicating side effects. " +
|
|
321
321
|
"Then return your report as JSON in exactly the shape specified above.";
|
|
322
322
|
}
|
|
323
|
+
// parseClassifyFerry — room #26 blocker 42 (2026-09-21). Shape-constrained
|
|
324
|
+
// ferry parser for the classify-branch agent() call. The schema buys SHAPE,
|
|
325
|
+
// not provenance: the fields are LLM-authored, so the parser stays
|
|
326
|
+
// defensive. Byte-identical across standard.js, bugfix.js, chore.js
|
|
327
|
+
// (pinned by tests/already-merged.test.js). Triplication is structural:
|
|
328
|
+
// each workflow ships as a standalone launch payload (240 KiB budget,
|
|
329
|
+
// individually git-archived) — there is no shared module to hold it.
|
|
330
|
+
// The schema is the guard; the prompt is instruction, never load-bearing.
|
|
331
|
+
// Returns { state, diagField, diagText, failReason }:
|
|
332
|
+
// state: "has-work" | "already-merged:<40-hex>" | "empty-no-work" | null
|
|
333
|
+
// (null = unparseable/ambiguous/channel violation — UNKNOWN)
|
|
334
|
+
// diagField: "stderr" | "stdout" | null (which field sourced the DIAG pin)
|
|
335
|
+
// diagText: the matched DIAG line, or ""
|
|
336
|
+
// failReason: computed reason, for the retry trailer and the park note
|
|
337
|
+
function parseClassifyFerry(bsResult) {
|
|
338
|
+
var bsStdout = (bsResult && typeof bsResult.stdout === "string") ? bsResult.stdout : null;
|
|
339
|
+
var bsStderr = (bsResult && typeof bsResult.stderr === "string") ? bsResult.stderr : null;
|
|
340
|
+
if (bsStdout === null || bsStderr === null) {
|
|
341
|
+
return { state: null, diagField: null, diagText: "",
|
|
342
|
+
failReason: "classifier return fields missing or non-string (stdout and stderr must both be strings)" };
|
|
343
|
+
}
|
|
344
|
+
// State channel: stdout ONLY. A BRANCH_STATE match on stderr is never
|
|
345
|
+
// trusted — workers merge streams constantly, and trusting a channel
|
|
346
|
+
// swap as truth is the wrong shape. Search, never anchor (blocker 11):
|
|
347
|
+
// the ferry labels output ("stdout: BRANCH_STATE: has-work"). Collect
|
|
348
|
+
// ALL matches and dedupe normalized values: exactly one distinct value
|
|
349
|
+
// is truth; zero or two-or-more is UNKNOWN — alternation order never
|
|
350
|
+
// adjudicates.
|
|
351
|
+
var bsRe = /\bBRANCH_STATE:\s*(has-work|already-merged:[0-9a-f]{40}|empty-no-work)(?![\w-])/gi;
|
|
352
|
+
var bsSeen = {};
|
|
353
|
+
var bsM;
|
|
354
|
+
while ((bsM = bsRe.exec(bsStdout)) !== null) {
|
|
355
|
+
bsSeen[bsM[1].toLowerCase()] = true;
|
|
356
|
+
}
|
|
357
|
+
var bsStates = Object.keys(bsSeen);
|
|
358
|
+
if (bsStates.length !== 1) {
|
|
359
|
+
return { state: null, diagField: null, diagText: "",
|
|
360
|
+
failReason: bsStates.length === 0
|
|
361
|
+
? "no BRANCH_STATE match in the stdout field"
|
|
362
|
+
: "ambiguous BRANCH_STATE values in stdout (" + bsStates.length + " distinct)" };
|
|
363
|
+
}
|
|
364
|
+
var bsState = bsStates[0];
|
|
365
|
+
// DIAG pin: search both fields, stderr first. Keep the DIAG: prefix — a
|
|
366
|
+
// prose mention without it must not satisfy the pin.
|
|
367
|
+
var bsDiagRe = /DIAG:\s*classify-branch\s+records=\d+\s+resolvable=\d+/;
|
|
368
|
+
var bsDiagField = bsDiagRe.test(bsStderr) ? "stderr" : (bsDiagRe.test(bsStdout) ? "stdout" : null);
|
|
369
|
+
var bsDiagText = "";
|
|
370
|
+
if (bsDiagField !== null) {
|
|
371
|
+
bsDiagText = bsDiagRe.exec(bsDiagField === "stderr" ? bsStderr : bsStdout)[0];
|
|
372
|
+
}
|
|
373
|
+
if (bsState.indexOf("already-merged:") === 0 && bsDiagField === null) {
|
|
374
|
+
// already-merged's sha flows into Cass's `git diff <sha>^1 <sha>` — a
|
|
375
|
+
// hallucinated-but-well-formed sha is a false-PASS vector. The script
|
|
376
|
+
// emits exactly one DIAG on every path that emits a BRANCH_STATE, so a
|
|
377
|
+
// missing pin means stderr wasn't faithfully ferried: UNKNOWN.
|
|
378
|
+
// Residual risk, named (blocker-42 fib 4): a worker could fabricate
|
|
379
|
+
// BOTH fields wholesale — the pin is a tripwire, not proof of
|
|
380
|
+
// execution. The workflow has no exec channel for a deterministic
|
|
381
|
+
// cross-check; if the platform ever offers one, verify the sha
|
|
382
|
+
// against the task's durable merge records directly.
|
|
383
|
+
return { state: null, diagField: null, diagText: "",
|
|
384
|
+
failReason: "already-merged without the DIAG pin — stderr not faithfully ferried" };
|
|
385
|
+
}
|
|
386
|
+
return { state: bsState, diagField: bsDiagField, diagText: bsDiagText, failReason: "" };
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
// classifyRetryTrailer — room #26 blocker 42 (2026-09-21). Corrective (not
|
|
390
|
+
// flat) trailer for the in-place classify retries. The failure mode was
|
|
391
|
+
// systematic labeling, not a coin flip: the trailer carries the COMPUTED
|
|
392
|
+
// parse-failure reason, the failed return truncated as a negative example,
|
|
393
|
+
// and the explicit field mapping — feedback from the actual failure,
|
|
394
|
+
// following the buildTransportRetryTrailer idiom. Bound: 2 retries
|
|
395
|
+
// (unmeasured house convention, matches the shared rework bound of 2 —
|
|
396
|
+
// against room retry data). Byte-identical across the three workflows
|
|
397
|
+
// (pinned by tests/already-merged.test.js).
|
|
398
|
+
function classifyRetryTrailer(reason, failedReturn) {
|
|
399
|
+
// Pass-2 simplification: the reason string already carries the truth
|
|
400
|
+
// (throw vs parse failure), so one neutral sentence replaces the
|
|
401
|
+
// throw/parse branch — the stringly-typed prefix contract between the
|
|
402
|
+
// call site and this function is deleted, not moved. Prompt accuracy,
|
|
403
|
+
// not prompt hardening.
|
|
404
|
+
return "\n\nCLASSIFY RETRY: the previous classifier attempt failed (" + reason + "). " +
|
|
405
|
+
"Return ONLY the two named fields: { \"stdout\": \"<the command's exact stdout>\", \"stderr\": \"<the command's exact stderr>\" }. " +
|
|
406
|
+
"Put each stream in its named field — do not add labels like \"stdout:\" in front of the content. " +
|
|
407
|
+
"Previous attempt detail (do not repeat this shape): " + String(failedReturn).slice(0, 200);
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
// parseVersionFerry — review pass 2, blocker-42 class (2026-09-21). The npm
|
|
411
|
+
// version-check ferry had the full pre-42 shape: schema-less agent(),
|
|
412
|
+
// line-anchored parser, and an unparseable fallback asserting a POSITIVE
|
|
413
|
+
// touched claim that spent the shared rework budget. Same treatment as
|
|
414
|
+
// parseClassifyFerry: shape-constrained {stdout} schema, search-and-dedupe
|
|
415
|
+
// parse, UNKNOWN on unparseable with in-place retries, honest park.
|
|
416
|
+
// Byte-identical across standard.js, bugfix.js, chore.js
|
|
417
|
+
// (pinned by tests/already-merged.test.js).
|
|
418
|
+
// Returns { versionState, failReason }:
|
|
419
|
+
// versionState: "touched" | "clean" | null (null = UNKNOWN)
|
|
420
|
+
// failReason: computed reason, for the retry trailer and the park note
|
|
421
|
+
function parseVersionFerry(vtResult) {
|
|
422
|
+
var vtStdout = (vtResult && typeof vtResult.stdout === "string") ? vtResult.stdout : null;
|
|
423
|
+
if (vtStdout === null) {
|
|
424
|
+
return { versionState: null,
|
|
425
|
+
failReason: "version-check return field missing or non-string (stdout must be a string)" };
|
|
426
|
+
}
|
|
427
|
+
// Search, never anchor (blocker 11): the ferry labels output
|
|
428
|
+
// ("stdout: VERSION_CLEAN"). Collect ALL matches and dedupe normalized
|
|
429
|
+
// values: exactly one distinct value is truth; zero or two-or-more is
|
|
430
|
+
// UNKNOWN — alternation order never adjudicates.
|
|
431
|
+
var vtRe = /\bVERSION_(TOUCHED|CLEAN)(?![\w-])/gi;
|
|
432
|
+
var vtSeen = {};
|
|
433
|
+
var vtM;
|
|
434
|
+
while ((vtM = vtRe.exec(vtStdout)) !== null) {
|
|
435
|
+
vtSeen[vtM[1].toUpperCase()] = true;
|
|
436
|
+
}
|
|
437
|
+
var vtStates = Object.keys(vtSeen);
|
|
438
|
+
if (vtStates.length !== 1) {
|
|
439
|
+
return { versionState: null,
|
|
440
|
+
failReason: vtStates.length === 0
|
|
441
|
+
? "no VERSION_(TOUCHED|CLEAN) match in the stdout field"
|
|
442
|
+
: "ambiguous VERSION values in stdout (" + vtStates.length + " distinct)" };
|
|
443
|
+
}
|
|
444
|
+
return { versionState: vtStates[0] === "CLEAN" ? "clean" : "touched", failReason: "" };
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
// versionRetryTrailer — review pass 2, blocker-42 class (2026-09-21).
|
|
448
|
+
// Neutral corrective trailer for the in-place version-check retries: the
|
|
449
|
+
// computed parse-failure reason plus the failed return as a negative
|
|
450
|
+
// example. Byte-identical across the three workflows
|
|
451
|
+
// (pinned by tests/already-merged.test.js).
|
|
452
|
+
function versionRetryTrailer(reason, failedReturn) {
|
|
453
|
+
return "\n\nVERSION-CHECK RETRY: the previous version-check attempt failed (" + reason + "). " +
|
|
454
|
+
"Return ONLY the named field: { \"stdout\": \"<the command's exact stdout>\" }. " +
|
|
455
|
+
"Put the command's stdout in the named field — do not add labels like \"stdout:\" in front of the content. " +
|
|
456
|
+
"Previous attempt detail (do not repeat this shape): " + String(failedReturn).slice(0, 200);
|
|
457
|
+
}
|
|
458
|
+
|
|
323
459
|
// parseToolSignals - bug 3472bf36. The work agent's TOOL CHECK emits two
|
|
324
460
|
// exact signal lines: artifact_tools: ok|missing and
|
|
325
461
|
// shell_transport: ok|unavailable. The workflow reads ONLY these lines.
|
|
@@ -696,7 +832,7 @@ let alreadyMergedSha = null;
|
|
|
696
832
|
let branchState = null; // "has-work" | "already-merged:<sha>" | "empty-no-work"
|
|
697
833
|
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.
|
|
698
834
|
let versionTouched = false; // npm: branch changed package.json's `version` (mechanical git fact, blocker-34 follow-up)
|
|
699
|
-
let branchStateErr = ""; // classifier failure detail (
|
|
835
|
+
let branchStateErr = ""; // classifier failure detail (retry trailer + park note grounds)
|
|
700
836
|
// Deterministic publish target — computed by the workflow (registry base +
|
|
701
837
|
// bumpVersion), never by the Publish agent.
|
|
702
838
|
let publishTarget = null; // { base, scope, target }
|
|
@@ -1176,43 +1312,87 @@ while (i < STEPS.length) {
|
|
|
1176
1312
|
// (classify-branch in the lifecycle script): has-work |
|
|
1177
1313
|
// already-merged:<sha> | empty-no-work. Attribution is identity-first
|
|
1178
1314
|
// from git, then the task's own durable merge records — there is no
|
|
1179
|
-
// agent-authored declared-sha input
|
|
1180
|
-
//
|
|
1181
|
-
//
|
|
1182
|
-
//
|
|
1183
|
-
//
|
|
1184
|
-
|
|
1185
|
-
|
|
1315
|
+
// agent-authored declared-sha input.
|
|
1316
|
+
// Blocker 42 (2026-09-21): the classifier output crosses an agent()
|
|
1317
|
+
// ferry — the old comment's "no cross-process hydration ferry" was a
|
|
1318
|
+
// fib. Machine facts travel in structured fields ({stdout, stderr}
|
|
1319
|
+
// schema) now, never in prose to be "returned verbatim". The schema
|
|
1320
|
+
// is the guard; the prompt is instruction, never load-bearing.
|
|
1321
|
+
// Unparseable is UNKNOWN, never a positive empty-no-work claim (room
|
|
1322
|
+
// #27: a labeled ferry — "stdout: BRANCH_STATE: has-work" — defeated
|
|
1323
|
+
// the ^-anchored parsers, and the empty-no-work fallback burned the
|
|
1324
|
+
// whole shared rework budget on a branch with real work). UNKNOWN
|
|
1325
|
+
// retries the classify call IN PLACE (up to 2, fresh -u1/-u2 keys — a
|
|
1326
|
+
// full Build round cannot fix an instrument failure, and the only
|
|
1327
|
+
// in-process bounce path spends the shared budget unconditionally).
|
|
1328
|
+
// Instrumentation failures NEVER touch totalReworkCount: an instrument
|
|
1329
|
+
// failure is not a build-quality failure. Exhaustion parks honestly
|
|
1330
|
+
// as unclassifiable — "unknown keeps the task alive (bounded)"; the
|
|
1331
|
+
// park on exhaustion is the fail-closed part. The UNKNOWN path never
|
|
1332
|
+
// asserts empty-no-work: no log line or park note presents it as the
|
|
1333
|
+
// classification. The string can still appear inside quoted evidence
|
|
1334
|
+
// (the verbatim failed return travels in the trailer and the note).
|
|
1335
|
+
// INVARIANT: branchState leaves this block holding a known value, or
|
|
1336
|
+
// the step has parked — unknown never reaches any consumer below.
|
|
1337
|
+
// Re-classify on EVERY Review entry: rework rewinds this step
|
|
1338
|
+
// in-process, and the branch changed between rounds. The loop's
|
|
1339
|
+
// null-gate governs only the in-place instrument retries within
|
|
1340
|
+
// this pass — without the reset, a second Review pass would trust
|
|
1341
|
+
// the first pass's stale classification and a real fix commit
|
|
1342
|
+
// would never "become has-work".
|
|
1343
|
+
branchState = null;
|
|
1344
|
+
branchStateErr = "";
|
|
1345
|
+
alreadyMergedSha = null;
|
|
1346
|
+
var classifyFailedReturn = "";
|
|
1347
|
+
for (var classifyAttempt = 0; classifyAttempt <= 2 && branchState === null; classifyAttempt++) {
|
|
1348
|
+
var classifyPrompt =
|
|
1186
1349
|
"Classify the task branch state. This is a mechanical git check, not a judgment call.\n" +
|
|
1187
|
-
"Run in shell
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1350
|
+
"Run in shell: " + LIFECYCLE_ENV + LIFECYCLE + " classify-branch " + taskId + "\n" +
|
|
1351
|
+
"Return the two streams as the two named fields and nothing else: { \"stdout\": \"<the command's exact stdout>\", \"stderr\": \"<the command's exact stderr>\" }." +
|
|
1352
|
+
(classifyAttempt === 0 ? "" : classifyRetryTrailer(branchStateErr, classifyFailedReturn));
|
|
1353
|
+
try {
|
|
1354
|
+
var bsResult = await agent(
|
|
1355
|
+
classifyPrompt,
|
|
1356
|
+
{ key: "classify-branch" + (classifyAttempt === 0 ? "" : "-u" + classifyAttempt) + (totalReworkCount > 0 ? "-r" + totalReworkCount : ""),
|
|
1357
|
+
label: "Classifying branch state (mechanical)" + (classifyAttempt === 0 ? "" : " (instrument retry " + classifyAttempt + " of 2)"),
|
|
1358
|
+
schema: { type: "object", properties: { stdout: { type: "string" }, stderr: { type: "string" } }, required: ["stdout", "stderr"] } }
|
|
1359
|
+
);
|
|
1360
|
+
classifyFailedReturn = (bsResult && typeof bsResult === "object" ? JSON.stringify(bsResult) : String(bsResult)).slice(0, 500);
|
|
1361
|
+
var classifyParsed = parseClassifyFerry(bsResult);
|
|
1362
|
+
if (classifyParsed.state !== null) {
|
|
1363
|
+
branchState = classifyParsed.state;
|
|
1364
|
+
branchStateErr = "";
|
|
1365
|
+
// DIAG pin: advisory for has-work/empty-no-work (blocker-34 v2.1
|
|
1366
|
+
// semantics — logged with its source field so silent record loss
|
|
1367
|
+
// surfaces in the evidence); already-merged is gated on the pin
|
|
1368
|
+
// inside parseClassifyFerry.
|
|
1369
|
+
if (classifyParsed.diagField !== null) {
|
|
1370
|
+
log("classify-branch DIAG pin (" + classifyParsed.diagField + " field): " + classifyParsed.diagText);
|
|
1371
|
+
} else {
|
|
1372
|
+
log("classify-branch: no DIAG: record-scan counter returned (scan unverifiable)");
|
|
1373
|
+
}
|
|
1374
|
+
} else {
|
|
1375
|
+
branchStateErr = classifyParsed.failReason;
|
|
1376
|
+
if (classifyAttempt < 2) {
|
|
1377
|
+
log("classify-branch unparseable (" + branchStateErr + ") — retrying in place (attempt " + (classifyAttempt + 1) + " of 2)");
|
|
1378
|
+
}
|
|
1379
|
+
}
|
|
1380
|
+
} catch (bsErr) {
|
|
1381
|
+
classifyFailedReturn = String(bsErr && bsErr.message || bsErr).slice(0, 500);
|
|
1382
|
+
branchStateErr = "classifier call failed: " + String(bsErr && bsErr.message || bsErr).slice(0, 120);
|
|
1383
|
+
if (classifyAttempt < 2) {
|
|
1384
|
+
log("classify-branch instrument failure (" + branchStateErr + ") — retrying in place (attempt " + (classifyAttempt + 1) + " of 2)");
|
|
1385
|
+
}
|
|
1206
1386
|
}
|
|
1207
|
-
} catch (bsErr) {
|
|
1208
|
-
branchStateErr = "classifier call failed: " + String(bsErr && bsErr.message || bsErr).slice(0, 120);
|
|
1209
1387
|
}
|
|
1210
|
-
if (
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1388
|
+
if (branchState === null) {
|
|
1389
|
+
// Exhaustion: park honestly. An instrument failure is not a
|
|
1390
|
+
// measurement of emptiness — never "empty-no-work".
|
|
1391
|
+
return await parkTask("Branch state unclassifiable after 2 instrument retries (" + branchStateErr + "; last failure: " + classifyFailedReturn + ") — the classifier instrument failed, not the branch. Human attention needed: inspect the task branch directly to determine its state — no trustworthy branch classification was produced.");
|
|
1392
|
+
}
|
|
1393
|
+
if (branchState.indexOf("already-merged:") === 0) {
|
|
1214
1394
|
alreadyMergedSha = branchState.slice("already-merged:".length);
|
|
1215
|
-
log("Branch state already-merged: " + alreadyMergedSha + " (
|
|
1395
|
+
log("Branch state already-merged: " + alreadyMergedSha + " (DIAG pin present — tripwire satisfied) — Cass reviews the frozen merge diff");
|
|
1216
1396
|
} else {
|
|
1217
1397
|
log("Branch state: " + branchState);
|
|
1218
1398
|
}
|
|
@@ -1224,23 +1404,55 @@ while (i < STEPS.length) {
|
|
|
1224
1404
|
// git fact, not a reviewer judgment. For npm projects the workflow checks
|
|
1225
1405
|
// it here, before Cass is dispatched — a touched `version` fails Review
|
|
1226
1406
|
// mechanically (versions are assigned at publish time, never in
|
|
1227
|
-
// branches).
|
|
1228
|
-
//
|
|
1407
|
+
// branches). Review pass 2 (blocker-42 class): this ferry had the full
|
|
1408
|
+
// pre-42 shape — schema-less agent(), line-anchored parser, and an
|
|
1409
|
+
// unparseable fallback asserting a POSITIVE touched claim that spent the
|
|
1410
|
+
// shared rework budget. Same treatment as the classifier:
|
|
1411
|
+
// shape-constrained {stdout} schema, search-and-dedupe parse, UNKNOWN
|
|
1412
|
+
// with in-place retries, honest park. An instrument failure is not a
|
|
1413
|
+
// measurement of touched — never assert touched from unparseable output.
|
|
1229
1414
|
if (PUBLISH_TYPE === "npm") {
|
|
1230
|
-
|
|
1231
|
-
|
|
1415
|
+
var versionState = null; // "touched" | "clean" | null (null = UNKNOWN)
|
|
1416
|
+
var versionStateErr = "";
|
|
1417
|
+
var versionFailedReturn = "";
|
|
1418
|
+
for (var versionAttempt = 0; versionAttempt <= 2 && versionState === null; versionAttempt++) {
|
|
1419
|
+
var versionPrompt =
|
|
1232
1420
|
"Check whether the task branch changed package.json's `version` field. This is a mechanical git check, not a judgment call.\n" +
|
|
1233
|
-
"Run in shell
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1421
|
+
"Run in shell: " + LIFECYCLE_ENV + LIFECYCLE + " version-check " + taskId + "\n" +
|
|
1422
|
+
"Return the command's exact stdout as the single named field and nothing else: { \"stdout\": \"<the command's exact stdout>\" }." +
|
|
1423
|
+
(versionAttempt === 0 ? "" : versionRetryTrailer(versionStateErr, versionFailedReturn));
|
|
1424
|
+
try {
|
|
1425
|
+
var vtResult = await agent(
|
|
1426
|
+
versionPrompt,
|
|
1427
|
+
{ key: "version-check" + (versionAttempt === 0 ? "" : "-u" + versionAttempt) + (totalReworkCount > 0 ? "-r" + totalReworkCount : ""),
|
|
1428
|
+
label: "Checking package.json version (mechanical)" + (versionAttempt === 0 ? "" : " (instrument retry " + versionAttempt + " of 2)"),
|
|
1429
|
+
schema: { type: "object", properties: { stdout: { type: "string" } }, required: ["stdout"] } }
|
|
1430
|
+
);
|
|
1431
|
+
versionFailedReturn = (vtResult && typeof vtResult === "object" ? JSON.stringify(vtResult) : String(vtResult)).slice(0, 500);
|
|
1432
|
+
var vtParsed = parseVersionFerry(vtResult);
|
|
1433
|
+
if (vtParsed.versionState !== null) {
|
|
1434
|
+
versionState = vtParsed.versionState;
|
|
1435
|
+
versionStateErr = "";
|
|
1436
|
+
} else {
|
|
1437
|
+
versionStateErr = vtParsed.failReason;
|
|
1438
|
+
if (versionAttempt < 2) {
|
|
1439
|
+
log("version-check unparseable (" + versionStateErr + ") — retrying in place (attempt " + (versionAttempt + 1) + " of 2)");
|
|
1440
|
+
}
|
|
1441
|
+
}
|
|
1442
|
+
} catch (vtErr) {
|
|
1443
|
+
versionFailedReturn = String(vtErr && vtErr.message || vtErr).slice(0, 500);
|
|
1444
|
+
versionStateErr = "version-check call failed: " + String(vtErr && vtErr.message || vtErr).slice(0, 120);
|
|
1445
|
+
if (versionAttempt < 2) {
|
|
1446
|
+
log("version-check instrument failure (" + versionStateErr + ") — retrying in place (attempt " + (versionAttempt + 1) + " of 2)");
|
|
1447
|
+
}
|
|
1448
|
+
}
|
|
1243
1449
|
}
|
|
1450
|
+
if (versionState === null) {
|
|
1451
|
+
// Exhaustion: park honestly. An instrument failure is not a
|
|
1452
|
+
// measurement of touched — never assert touched.
|
|
1453
|
+
return await parkTask("package.json `version` state unclassifiable after 2 instrument retries (" + versionStateErr + "; last failure: " + versionFailedReturn + ") — the version-check instrument failed, not the branch. Human attention needed: inspect the task branch directly to determine its state — no trustworthy version classification was produced.");
|
|
1454
|
+
}
|
|
1455
|
+
versionTouched = (versionState === "touched");
|
|
1244
1456
|
log("Review: package.json `version` " + (versionTouched ? "touched by the branch — mechanical FAIL, Cass not dispatched" : "untouched — mechanical check clean"));
|
|
1245
1457
|
}
|
|
1246
1458
|
// The empty-branch rule Cass used to adjudicate is gone. What she
|
|
@@ -1563,13 +1775,26 @@ while (i < STEPS.length) {
|
|
|
1563
1775
|
// closeout — the file list is the complete record of what the task
|
|
1564
1776
|
// changed. The courier is pure hands: it writes the deterministic
|
|
1565
1777
|
// script's own file list, never interprets it.
|
|
1778
|
+
// Blocker 40 (2026-09-21): this courier carries the same
|
|
1779
|
+
// {output: string} schema as the sibling diff-computation call.
|
|
1780
|
+
// The command is stdout-silent; schema-less, the runtime's
|
|
1781
|
+
// non-empty-result contract throws on the empty string and the
|
|
1782
|
+
// workflow parks fail-closed for a side effect that landed. Empty
|
|
1783
|
+
// stdout is schema-valid, so the contract no longer fires. "The
|
|
1784
|
+
// schema bypasses the contract" is INFERRED, not observed —
|
|
1785
|
+
// falsification: if a schema-carrying courier still parks on
|
|
1786
|
+
// silent stdout, the platform contract is deeper than the schema
|
|
1787
|
+
// and this is wrong; room evidence decides. (The file on disk is
|
|
1788
|
+
// not read as proof until QA closeout — the QA loader fails
|
|
1789
|
+
// closed to "unknown" attribution when it is absent.)
|
|
1566
1790
|
var publishDiffFilesPath = crewHome + "/.publish-diffs/" + taskId + ".files.json";
|
|
1567
1791
|
await agent(
|
|
1568
1792
|
"Write the publish diff file list.\n" +
|
|
1569
|
-
"Run exactly this and return the stdout verbatim:\n" +
|
|
1793
|
+
"Run exactly this and return the stdout verbatim as { \"output\": \"<verbatim stdout>\" } and nothing else. Do not interpret it:\n" +
|
|
1570
1794
|
"mkdir -p " + crewHome + "/.publish-diffs && cat > \"" + publishDiffFilesPath + "\" <<'DIFFFILES_EOF'\n" +
|
|
1571
1795
|
JSON.stringify(diffSummary.files || []) + "\nDIFFFILES_EOF\n",
|
|
1572
|
-
{ key: attemptKey("publish-diff-files-" + taskId, totalReworkCount), label: "Persisting publish diff file list"
|
|
1796
|
+
{ key: attemptKey("publish-diff-files-" + taskId, totalReworkCount), label: "Persisting publish diff file list",
|
|
1797
|
+
schema: { type: "object", properties: { output: { type: "string" } }, required: ["output"] } }
|
|
1573
1798
|
);
|
|
1574
1799
|
// Budget counts CHANGED lines (added + removed), not raw unified-diff
|
|
1575
1800
|
// output lines: context lines and file headers inflated the old
|