tickmarkr 2.8.6 → 2.8.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/dist/adapters/grok.js +5 -9
- package/dist/adapters/prompt.js +9 -1
- package/dist/adapters/types.d.ts +1 -0
- package/dist/adapters/types.js +12 -0
- package/dist/cli/commands/compile.js +1 -1
- package/dist/cli/commands/init.d.ts +10 -0
- package/dist/cli/commands/init.js +13 -3
- package/dist/cli/commands/report.js +21 -12
- package/dist/cli/commands/status.js +14 -2
- package/dist/cli/commands/verify.js +29 -5
- package/dist/cli/commands/version.d.ts +1 -1
- package/dist/cli/commands/version.js +36 -2
- package/dist/cli/help.d.ts +1 -0
- package/dist/cli/help.js +6 -3
- package/dist/compile/collateral.d.ts +3 -1
- package/dist/compile/collateral.js +8 -16
- package/dist/compile/native.js +7 -2
- package/dist/drivers/subprocess.js +9 -4
- package/dist/gates/acceptance.d.ts +8 -0
- package/dist/gates/acceptance.js +54 -37
- package/dist/gates/baseline.js +47 -7
- package/dist/gates/run-gates.js +31 -22
- package/dist/report/bundle.d.ts +5 -2
- package/dist/report/bundle.js +11 -6
- package/dist/report/compare.d.ts +9 -0
- package/dist/report/compare.js +58 -40
- package/dist/report/operator-record.js +4 -3
- package/dist/run/daemon.d.ts +7 -3
- package/dist/run/daemon.js +157 -58
- package/dist/run/git.d.ts +5 -0
- package/dist/run/git.js +51 -1
- package/dist/run/journal.js +11 -2
- package/dist/run/repair-disposition.d.ts +2 -2
- package/dist/run/repair-disposition.js +35 -4
- package/dist/run/stall.d.ts +2 -0
- package/dist/run/stall.js +12 -4
- package/package.json +1 -1
- package/skills/tickmarkr-auto/SKILL.md +10 -8
- package/skills/tickmarkr-loop/SKILL.md +10 -8
- package/skills/tickmarkr-overseer/SKILL.md +10 -8
package/dist/gates/acceptance.js
CHANGED
|
@@ -249,18 +249,18 @@ function oracleExecutionFailure(label, code, stdout, stderr) {
|
|
|
249
249
|
const zeroRun = testsRan(output) === 0 && classification !== "regression";
|
|
250
250
|
const zeroRunEvidence = zeroRun ? "the runner reported zero tests run and no failure identity; " : "";
|
|
251
251
|
if (classification === "infra" || signalKilled || zeroRun) {
|
|
252
|
-
return {
|
|
252
|
+
return { failure: "unverified", output, result: {
|
|
253
|
+
gate: "acceptance",
|
|
254
|
+
pass: false,
|
|
255
|
+
details: `oracle failed: ${label} (exit ${code}) — ${signalEvidence || zeroRunEvidence}infrastructure blocked execution before a verdict; this oracle verified nothing${tail(output)}`,
|
|
256
|
+
meta: { cause: "oracle-execution", classification: "infra", infra: true, retryable: false },
|
|
257
|
+
} };
|
|
258
|
+
}
|
|
259
|
+
return { failure: "red", output, result: {
|
|
253
260
|
gate: "acceptance",
|
|
254
261
|
pass: false,
|
|
255
|
-
details: `oracle failed: ${label} (exit ${code})
|
|
256
|
-
|
|
257
|
-
};
|
|
258
|
-
}
|
|
259
|
-
return {
|
|
260
|
-
gate: "acceptance",
|
|
261
|
-
pass: false,
|
|
262
|
-
details: `oracle failed: ${label} (exit ${code})${tail(stderr || stdout)}`,
|
|
263
|
-
};
|
|
262
|
+
details: `oracle failed: ${label} (exit ${code})${tail(stderr || stdout)}`,
|
|
263
|
+
} };
|
|
264
264
|
}
|
|
265
265
|
// today's rule for a test oracle's run: exit 0 AND a positive printed run count (OBS-55: a zero-match run is vacuous).
|
|
266
266
|
function countedTestRun(label, filter, r) {
|
|
@@ -269,8 +269,8 @@ function countedTestRun(label, filter, r) {
|
|
|
269
269
|
const out = (r.stderr || "") + "\n" + (r.stdout || "");
|
|
270
270
|
const ran = testsRan(out);
|
|
271
271
|
if (ran === null || ran < 1) {
|
|
272
|
-
return { gate: "acceptance", pass: false,
|
|
273
|
-
|
|
272
|
+
return { failure: "untitled", output: out, result: { gate: "acceptance", pass: false,
|
|
273
|
+
details: `oracle failed: ${label} — name filter matched zero tests (filter: ${filter})${tail(out)}` } };
|
|
274
274
|
}
|
|
275
275
|
return undefined;
|
|
276
276
|
}
|
|
@@ -290,11 +290,11 @@ function countedTestRun(label, filter, r) {
|
|
|
290
290
|
*/
|
|
291
291
|
async function landedTestOracle(testCmd, title, landing, worktree) {
|
|
292
292
|
const label = `test "${title}" in ${landing}`;
|
|
293
|
-
const passed = { gate: "acceptance", pass: true, details: `✓ test: ${title} (exit 0) in ${landing}` };
|
|
294
|
-
const work = (why) => ({ gate: "acceptance", pass: false, details: `oracle failed: ${label} — ${why}` });
|
|
295
|
-
const unverified = (why) => ({ gate: "acceptance", pass: false,
|
|
296
|
-
|
|
297
|
-
|
|
293
|
+
const passed = { output: "", result: { gate: "acceptance", pass: true, details: `✓ test: ${title} (exit 0) in ${landing}` } };
|
|
294
|
+
const work = (failure, why, output = "") => ({ failure, output, result: { gate: "acceptance", pass: false, details: `oracle failed: ${label} — ${why}` } });
|
|
295
|
+
const unverified = (why, output = "") => ({ failure: "unverified", output, result: { gate: "acceptance", pass: false,
|
|
296
|
+
details: `oracle failed: ${label} — ${why}; unverified, failing closed`,
|
|
297
|
+
meta: { cause: "oracle-execution", classification: "infra", infra: true, retryable: false } } });
|
|
298
298
|
const path = resolve(worktree, landing);
|
|
299
299
|
const within = relative(worktree, path);
|
|
300
300
|
let regular = false;
|
|
@@ -303,7 +303,7 @@ async function landedTestOracle(testCmd, title, landing, worktree) {
|
|
|
303
303
|
}
|
|
304
304
|
catch { /* absent */ }
|
|
305
305
|
if (!regular)
|
|
306
|
-
return work(`suite file ${landing} is absent from the worktree or is not a regular file; the test command never started`);
|
|
306
|
+
return work("absent", `suite file ${landing} is absent from the worktree or is not a regular file; the test command never started`);
|
|
307
307
|
if (!isVitestTestCommand(testCmd, worktree)) {
|
|
308
308
|
return countedTestRun(label, title, await sh(fileFiltered(npmForwarded(testCmd), landing, title), worktree)) ?? passed;
|
|
309
309
|
}
|
|
@@ -326,17 +326,18 @@ async function landedTestOracle(testCmd, title, landing, worktree) {
|
|
|
326
326
|
const r = await shell(`${cmd} --reporter=${shq(reporterPath)} --outputFile=${shq(reportPath)}`, worktree, DEFAULT_SHELL_TIMEOUT_MS, false, { env: { ...manifestChildEnv(worktree), TICKMARKR_TEST_REPORT: reportPath, TICKMARKR_TEST_NONCE: nonce } });
|
|
327
327
|
const raw = readTestReport(reportPath);
|
|
328
328
|
const report = raw?.nonce === nonce ? raw : undefined;
|
|
329
|
+
const output = [r.stderr, r.stdout].filter(Boolean).join("\n");
|
|
329
330
|
if (r.code !== 0) {
|
|
330
331
|
// the reporter replaces the runner's own output: its failure identities are the evidence of a red.
|
|
331
332
|
const named = report ? [...Object.values(report.completed).flatMap((c) => c.failures ?? []), ...(report.certificate?.diagnostics ?? [])] : [];
|
|
332
333
|
return oracleExecutionFailure(label, r.code, r.stdout, [r.stderr, ...named].filter(Boolean).join("\n"));
|
|
333
334
|
}
|
|
334
335
|
if (!raw)
|
|
335
|
-
return unverified("the runner exited 0 but wrote no per-file report");
|
|
336
|
+
return unverified("the runner exited 0 but wrote no per-file report", output);
|
|
336
337
|
if (!report)
|
|
337
|
-
return unverified(`the per-file report carries nonce "${raw.nonce}", not this invocation's
|
|
338
|
+
return unverified(`the per-file report carries nonce "${raw.nonce}", not this invocation's`, output);
|
|
338
339
|
if (report.certificate?.exitCode !== 0)
|
|
339
|
-
return unverified("the per-file report never reached its run-end certificate with exit 0");
|
|
340
|
+
return unverified("the per-file report never reached its run-end certificate with exit 0", output);
|
|
340
341
|
const key = toManifestPath(within, worktree);
|
|
341
342
|
// a module completed more than once (one path in several projects) keeps only its LAST counters, so its record
|
|
342
343
|
// cannot show that no earlier completion ran a body: it counts as run.
|
|
@@ -346,9 +347,9 @@ async function landedTestOracle(testCmd, title, landing, worktree) {
|
|
|
346
347
|
return file !== key && (repeated.has(file) || !t || t.passed + t.failed > 0);
|
|
347
348
|
}).sort();
|
|
348
349
|
if (elsewhere.length)
|
|
349
|
-
return work(`the runner's per-file report shows a test body run outside ${landing}, in ${elsewhere.join(", ")} — only ${landing} can satisfy this criterion
|
|
350
|
+
return work("elsewhere", `the runner's per-file report shows a test body run outside ${landing}, in ${elsewhere.join(", ")} — only ${landing} can satisfy this criterion`, output);
|
|
350
351
|
if (!((report.completed[key]?.tests?.passed ?? 0) > 0))
|
|
351
|
-
return work(`the runner's per-file report shows no passing test of that title in ${landing}
|
|
352
|
+
return work("untitled", `the runner's per-file report shows no passing test of that title in ${landing}`, output);
|
|
352
353
|
return passed;
|
|
353
354
|
}
|
|
354
355
|
finally {
|
|
@@ -456,38 +457,54 @@ function citesChangedLocation(evidence, changed) {
|
|
|
456
457
|
export async function acceptanceGate(task, worktree, baseRef,
|
|
457
458
|
// OBS-1182: the judge seat's OWN effort (absent = CLI default) — never the worker's.
|
|
458
459
|
judge, via, opts = {}) {
|
|
459
|
-
//
|
|
460
|
-
// A failure returns here, before any runLlm() call: a judge can never override it.
|
|
460
|
+
// Queue row 234(b) (D-2134): measure every deterministic oracle in declaration order before judging.
|
|
461
461
|
const passedDet = [];
|
|
462
|
-
|
|
462
|
+
const failures = [];
|
|
463
|
+
const record = (index, a, run) => {
|
|
464
|
+
if (run.result.pass)
|
|
465
|
+
passedDet.push(run.result.details);
|
|
466
|
+
else
|
|
467
|
+
failures.push({ run, oracle: {
|
|
468
|
+
index, kind: a.oracle, criterion: isCommand(a) ? a.command : a.test,
|
|
469
|
+
...(isTest(a) && a.landing !== undefined ? { landing: a.landing } : {}),
|
|
470
|
+
failure: run.failure, output: run.output,
|
|
471
|
+
} });
|
|
472
|
+
};
|
|
473
|
+
for (const [index, a] of task.acceptance.entries()) {
|
|
463
474
|
if (isCommand(a)) {
|
|
464
475
|
const r = await sh(a.command, worktree);
|
|
465
476
|
if (r.code !== 0) {
|
|
466
|
-
|
|
477
|
+
record(index, a, oracleExecutionFailure(`$ ${a.command}`, r.code, r.stdout, r.stderr));
|
|
467
478
|
}
|
|
468
|
-
|
|
479
|
+
else
|
|
480
|
+
passedDet.push(`✓ $ ${a.command} (exit 0)`);
|
|
469
481
|
}
|
|
470
482
|
else if (isTest(a)) {
|
|
471
483
|
if (!opts.testCmd) {
|
|
472
|
-
|
|
473
|
-
|
|
484
|
+
record(index, a, { failure: "no-runner", output: "", result: { gate: "acceptance", pass: false,
|
|
485
|
+
details: `oracle failed: test "${a.test}" — no test command configured to run it (failing closed)` } });
|
|
486
|
+
continue;
|
|
474
487
|
}
|
|
475
488
|
// queue row 179: a declared suite file is the only file that can satisfy its criterion.
|
|
476
489
|
if (a.landing !== undefined) {
|
|
477
|
-
|
|
478
|
-
if (!landed.pass)
|
|
479
|
-
return landed;
|
|
480
|
-
passedDet.push(landed.details);
|
|
490
|
+
record(index, a, await landedTestOracle(opts.testCmd, a.test, a.landing, worktree));
|
|
481
491
|
continue;
|
|
482
492
|
}
|
|
483
493
|
// OBS-55: exit 0 alone is vacuous when the name filter matched zero tests — fail closed.
|
|
484
494
|
const red = countedTestRun(`test "${a.test}"`, a.test, await sh(testFiltered(opts.testCmd, a.test), worktree));
|
|
485
495
|
if (red)
|
|
486
|
-
|
|
487
|
-
|
|
496
|
+
record(index, a, red);
|
|
497
|
+
else
|
|
498
|
+
passedDet.push(`✓ test: ${a.test} (exit 0)`);
|
|
488
499
|
}
|
|
489
500
|
}
|
|
490
|
-
|
|
501
|
+
if (failures.length) {
|
|
502
|
+
const first = failures[0].run.result;
|
|
503
|
+
const infra = failures.find(({ run }) => run.result.meta?.infra === true)?.run.result.meta;
|
|
504
|
+
return { ...first, details: failures.map(({ run }) => run.result.details).join("\n"),
|
|
505
|
+
meta: { ...first.meta, ...infra, oracles: failures.map(({ oracle }) => oracle) } };
|
|
506
|
+
}
|
|
507
|
+
// Queue row 234(b) (D-2134): only an entirely green deterministic round reaches the judge.
|
|
491
508
|
const judgeItems = task.acceptance.filter(isJudge);
|
|
492
509
|
const detBlock = passedDet.length ? passedDet.join("\n") + "\n" : "";
|
|
493
510
|
if (!judgeItems.length) {
|
package/dist/gates/baseline.js
CHANGED
|
@@ -428,11 +428,30 @@ export function classifyFailureOutput(output) {
|
|
|
428
428
|
// and `stderr | <task id>` (digits and underscores) when the reporter no longer knows the task
|
|
429
429
|
// (dist/chunks/index.*.js, `headerText`). The stripper knew only the first form.
|
|
430
430
|
const VITEST_ECHO_BLOCK_RE = /^\s*std(?:out|err)\s+\|\s+(?:unknown test\s*$|\d+_[\d_]+\s*$|\S+\.(?:test|spec)\.[cm]?[jt]sx?(?:\s*$|\s+>\s+\S))/;
|
|
431
|
-
|
|
431
|
+
// Queue row 211 (D-2118): only Jest's complete, column-zero recovery warning is noise. A missing
|
|
432
|
+
// neighbour, changed hint, indented copy or multi-line Watchman error remains failure evidence.
|
|
433
|
+
const JEST_WATCHMAN_HEADER = "jest-haste-map: Watchman crawl failed. Retrying once with node crawler.";
|
|
434
|
+
const JEST_WATCHMAN_HINT = " Usually this happens when watchman isn't running. Create an empty `.watchmanconfig` file in your project's root folder or initialize a git or hg repository in your project.";
|
|
435
|
+
const JEST_WATCHMAN_ERROR_RE = /^ {2}Error: Watchman error: [^\r\n]+\. Make sure watchman is running for this project\. See https:\/\/facebook\.github\.io\/watchman\/docs\/troubleshooting\.$/;
|
|
436
|
+
/** Queue row 211 (D-2118): display readers retain raw Vitest echo lines while dropping this warning. */
|
|
437
|
+
const withoutJestWatchmanBlocks = (output) => {
|
|
438
|
+
const kept = [];
|
|
439
|
+
const lines = output.split("\n");
|
|
440
|
+
for (let i = 0; i < lines.length; i++) {
|
|
441
|
+
const line = lines[i];
|
|
442
|
+
if (line === JEST_WATCHMAN_HEADER && lines[i + 1] === JEST_WATCHMAN_HINT && JEST_WATCHMAN_ERROR_RE.test(lines[i + 2] ?? "")) {
|
|
443
|
+
i += 2;
|
|
444
|
+
continue;
|
|
445
|
+
}
|
|
446
|
+
kept.push(line);
|
|
447
|
+
}
|
|
448
|
+
return kept;
|
|
449
|
+
};
|
|
450
|
+
/** Queue row 211 (D-2118): omit the recovered Jest warning alongside Vitest's test-owned echo blocks. */
|
|
432
451
|
const withoutVitestEchoBlocks = (output) => {
|
|
433
452
|
const outside = [];
|
|
434
453
|
let inVitestEchoBlock = false;
|
|
435
|
-
for (const line of output
|
|
454
|
+
for (const line of withoutJestWatchmanBlocks(output)) {
|
|
436
455
|
const clean = line.replace(ANSI_RE, "");
|
|
437
456
|
const runnerLine = stripTurboPrefix(clean) ?? clean;
|
|
438
457
|
if (inVitestEchoBlock) {
|
|
@@ -625,8 +644,7 @@ function fingerprintOccurrences(output) {
|
|
|
625
644
|
}
|
|
626
645
|
// The diagnostic channel for a runner we cannot read: raw output lines, never fingerprints, never a
|
|
627
646
|
// verdict. They exist so the operator sees SOMETHING when a green command turns red unrecognizably.
|
|
628
|
-
const unrecognizedEvidence = (raw) => raw
|
|
629
|
-
.split("\n")
|
|
647
|
+
const unrecognizedEvidence = (raw) => withoutJestWatchmanBlocks(raw)
|
|
630
648
|
.map((l) => l.replace(ANSI_RE, "").trimEnd())
|
|
631
649
|
.filter((l) => VOCAB_RE.test(l))
|
|
632
650
|
.slice(0, 10)
|
|
@@ -1028,11 +1046,33 @@ export async function detectVacuousOracles(cwd, tasks) {
|
|
|
1028
1046
|
// drop a FAIL line that fingerprint-collides with baseline noise). No headline anchors → byte-identical
|
|
1029
1047
|
// fallback to today's text (non-vitest runners lose nothing). RED-pinned by tests/gates/baseline.test.ts
|
|
1030
1048
|
// "HYG-08: details headlines the failing test, not the noise".
|
|
1049
|
+
// Queue row 224(a) (D-2118): Jest identities and their first message are display-only; Console and
|
|
1050
|
+
// column-zero validation bullets are not identities. Keep the existing metadata extraction separate.
|
|
1051
|
+
const JEST_IDENTITY_RE = /^ {2}● (?!Console\s*$)\S.*$/;
|
|
1031
1052
|
function headlineDetails(raw, fresh, freshLines) {
|
|
1032
|
-
const
|
|
1053
|
+
const namedHeadlines = raw
|
|
1033
1054
|
.split("\n")
|
|
1034
1055
|
.map((l) => l.replace(ANSI_RE, ""))
|
|
1035
1056
|
.filter((l) => namesFailureEitherForm(l) || SUMMARY_FAIL_RE.test(l));
|
|
1057
|
+
const outputLines = withoutJestWatchmanBlocks(raw).map((l) => l.replace(ANSI_RE, ""));
|
|
1058
|
+
let inVitestEchoBlock = false;
|
|
1059
|
+
const headlines = outputLines.flatMap((line, index) => {
|
|
1060
|
+
const runnerLine = stripTurboPrefix(line) ?? line;
|
|
1061
|
+
const echoed = inVitestEchoBlock || VITEST_ECHO_BLOCK_RE.test(runnerLine);
|
|
1062
|
+
if (inVitestEchoBlock) {
|
|
1063
|
+
if (runnerLine.trim() === "")
|
|
1064
|
+
inVitestEchoBlock = false;
|
|
1065
|
+
}
|
|
1066
|
+
else if (echoed)
|
|
1067
|
+
inVitestEchoBlock = true;
|
|
1068
|
+
// Queue row 224(a) (D-2118): preserve raw FAIL/summary headlines; only Jest identities exclude echoes.
|
|
1069
|
+
if (namesFailureEitherForm(line) || SUMMARY_FAIL_RE.test(line))
|
|
1070
|
+
return [line];
|
|
1071
|
+
if (echoed || !JEST_IDENTITY_RE.test(line))
|
|
1072
|
+
return [];
|
|
1073
|
+
const message = withoutVitestEchoBlocks(outputLines.slice(index + 1).join("\n")).find((l) => l.trim() !== "");
|
|
1074
|
+
return message === undefined ? [line] : [line, message];
|
|
1075
|
+
});
|
|
1036
1076
|
// S22: the ORIGINAL runner lines behind v2 comparison keys are the diagnostic; the keys are secondary.
|
|
1037
1077
|
// A legacy entry's lines ARE its keys, so its details stay byte-identical.
|
|
1038
1078
|
const lines = [...new Set(freshLines)].join("\n");
|
|
@@ -1042,7 +1082,7 @@ function headlineDetails(raw, fresh, freshLines) {
|
|
|
1042
1082
|
return { details: same ? `new failures vs baseline:\n${lines}` : `new failures vs baseline:\n${lines}\n\n${keys}` };
|
|
1043
1083
|
return {
|
|
1044
1084
|
details: `failing tests:\n${headlines.join("\n")}\n\n${same ? "" : `new failure lines vs baseline:\n${lines}\n\n`}${keys}`,
|
|
1045
|
-
meta: { failingTests:
|
|
1085
|
+
...(namedHeadlines.length ? { meta: { failingTests: namedHeadlines.filter(namesFailureEitherForm) } } : {}),
|
|
1046
1086
|
};
|
|
1047
1087
|
}
|
|
1048
1088
|
export async function compareToBaseline(cwd, commands, baseline, enabled, opts = {}) {
|
|
@@ -1225,7 +1265,7 @@ export async function compareToBaseline(cwd, commands, baseline, enabled, opts =
|
|
|
1225
1265
|
if (classification === "infra") {
|
|
1226
1266
|
const evidence = failingLines.length
|
|
1227
1267
|
? failingLines.slice(0, 10).join("\n")
|
|
1228
|
-
: unrecognizedEvidence(raw) || raw.trim().split("\n").slice(0, 10).join("\n");
|
|
1268
|
+
: unrecognizedEvidence(raw) || withoutJestWatchmanBlocks(raw).join("\n").trim().split("\n").slice(0, 10).join("\n");
|
|
1229
1269
|
record({
|
|
1230
1270
|
gate: name,
|
|
1231
1271
|
pass: false,
|
package/dist/gates/run-gates.js
CHANGED
|
@@ -1147,7 +1147,21 @@ export async function runGates(task, ctx) {
|
|
|
1147
1147
|
invocations: [],
|
|
1148
1148
|
};
|
|
1149
1149
|
}
|
|
1150
|
-
|
|
1150
|
+
// Queue row 34(a) (D-2134 R5): resolve every lifetime author through both role pools.
|
|
1151
|
+
const channels = [...(ctx.judgeChannels ?? []), ...ctx.channels];
|
|
1152
|
+
const authors = ctx.carriedAuthors?.length ? ctx.carriedAuthors : [channelKey(ctx.author)];
|
|
1153
|
+
const authorVendors = carriedAuthorVendors(channels, authors);
|
|
1154
|
+
const independent = (seat) => !!seat.vendor
|
|
1155
|
+
&& !authorVendors.has(seat.vendor) && !authors.includes(channelKey(seat));
|
|
1156
|
+
const judgePool = () => (ctx.judgeChannels ?? []).filter((c) => disallowedBy(c, ctx.cfg.routing, "judge") === null);
|
|
1157
|
+
const rankJudges = (pool) => [...pool]
|
|
1158
|
+
.sort((x, y) => TIER_RANK[y.tier] - TIER_RANK[x.tier] || marginalCostRank(x) - marginalCostRank(y));
|
|
1159
|
+
const pickJudge = (pool, adapter) => rankJudges(pool.filter((c) => c.adapter !== adapter))[0] ?? rankJudges(pool)[0];
|
|
1160
|
+
const configured = channels.find((c) => channelKey(c) === channelKey(ctx.cfg.judge));
|
|
1161
|
+
const primary = authors.includes(channelKey(ctx.cfg.judge)) || (configured && authorVendors.has(configured.vendor))
|
|
1162
|
+
? pickJudge(judgePool().filter(independent), ctx.cfg.judge.adapter) ?? ctx.cfg.judge
|
|
1163
|
+
: ctx.cfg.judge;
|
|
1164
|
+
const judgeAdapter = getAdapter(primary.adapter, ctx.adapters);
|
|
1151
1165
|
const jvia = via
|
|
1152
1166
|
? { driver: via.driver, keep: via.keep, onSlot: via.onSlot, name: via.nameFor("judge", judgeAdapter.id), label: via.labelFor("judge") }
|
|
1153
1167
|
: undefined;
|
|
@@ -1181,11 +1195,11 @@ export async function runGates(task, ctx) {
|
|
|
1181
1195
|
...(unparseable ? { transcript: output } : {}),
|
|
1182
1196
|
});
|
|
1183
1197
|
}
|
|
1184
|
-
|
|
1198
|
+
const result = captured.value;
|
|
1199
|
+
const seat = channels.find((c) => channelKey(c) === result.meta?.judge);
|
|
1200
|
+
return typeof result.meta?.judge === "string" && result.meta.unparseable !== true && (!seat || !independent(seat))
|
|
1201
|
+
? { ...result, meta: { ...result.meta, judgeIndependent: false } } : result;
|
|
1185
1202
|
};
|
|
1186
|
-
const judgePool = () => (ctx.judgeChannels ?? []).filter((c) => disallowedBy(c, ctx.cfg.routing, "judge") === null);
|
|
1187
|
-
const rankJudges = (pool) => [...pool]
|
|
1188
|
-
.sort((x, y) => TIER_RANK[y.tier] - TIER_RANK[x.tier] || marginalCostRank(x) - marginalCostRank(y));
|
|
1189
1203
|
// OBS-1151 (+add.1): the fresh judgment always stands on its own reading — a prior PASS is never
|
|
1190
1204
|
// reused. Only a criterion that REVERSES the newest prior ruling on the same subject key over
|
|
1191
1205
|
// identical cited blobs needs a second, distinct judge; agreement stands (a sound FAIL included),
|
|
@@ -1210,16 +1224,18 @@ export async function runGates(task, ctx) {
|
|
|
1210
1224
|
if (!disputed.length)
|
|
1211
1225
|
return stamped;
|
|
1212
1226
|
await ctx.onGate?.({ phase: "note", gate: "acceptance", name: "judge-disagreement", payload: { primary, commit, disputed } });
|
|
1227
|
+
let notIndependent = fresh.meta?.judgeIndependent === false;
|
|
1228
|
+
const independence = () => notIndependent ? { judgeIndependent: false } : {};
|
|
1213
1229
|
const park = (why, adjudicator) => ({
|
|
1214
1230
|
gate: "acceptance", pass: false,
|
|
1215
1231
|
details: `judge disagreement on ${disputed.map((d) => d.id).join(", ")}: ${primary} reverses an earlier ruling over identical cited blobs (${[...new Set(disputed.flatMap((d) => d.paths))].join(", ")}) — ${why}; parked for an operator ruling, no worker charge`,
|
|
1216
|
-
meta: { classification: "infra", infra: true, retryable: false, cause: "judge-disagreement", judge: primary,
|
|
1232
|
+
meta: { classification: "infra", infra: true, retryable: false, cause: "judge-disagreement", judge: primary, ...independence(),
|
|
1217
1233
|
judgeDisagreement: { primary, ...(adjudicator ? { adjudicator } : {}), disputed, outcome: why } },
|
|
1218
1234
|
});
|
|
1219
1235
|
const primaryAdapter = primary.slice(0, primary.indexOf(":"));
|
|
1220
1236
|
// One DISTINCT seat: never the primary channel, a different adapter when the pool has one.
|
|
1221
1237
|
const pool = judgePool().filter((c) => channelKey(c) !== primary);
|
|
1222
|
-
const seat =
|
|
1238
|
+
const seat = pickJudge(pool.filter(independent), primaryAdapter) ?? pickJudge(pool, primaryAdapter);
|
|
1223
1239
|
if (!seat)
|
|
1224
1240
|
return park("no distinct eligible judge is available");
|
|
1225
1241
|
const adjudicator = channelKey(seat);
|
|
@@ -1230,13 +1246,14 @@ export async function runGates(task, ctx) {
|
|
|
1230
1246
|
const subjectPark = (leg, found, ran) => ({
|
|
1231
1247
|
gate: "acceptance", pass: false,
|
|
1232
1248
|
details: `the adjudication subject ${SUBJECT_LEG_TEXT[leg]}: sampled ${commit.slice(0, 12)}, found ${found.slice(0, 12)} (${leg}); parked for an operator ruling, no worker charge`,
|
|
1233
|
-
meta: { classification: "infra", infra: true, retryable: false, cause: "subject-moved", judge: primary,
|
|
1249
|
+
meta: { classification: "infra", infra: true, retryable: false, cause: "subject-moved", judge: primary, ...independence(),
|
|
1234
1250
|
adjudication: { primary, ...(ran ? { adjudicator: ran } : {}), sampled: commit, found, leg } },
|
|
1235
1251
|
});
|
|
1236
1252
|
const s0 = await openSubjectWitness(ctx.worktree, commit);
|
|
1237
1253
|
if ("leg" in s0)
|
|
1238
1254
|
return subjectPark(s0.leg, s0.leg === "unreadable" ? "unreadable" : commit);
|
|
1239
1255
|
const second = await invokeJudge(seatAdapter, seat.model, seatVia, configuredEffort(ctx.cfg, seat));
|
|
1256
|
+
notIndependent ||= second.meta?.judgeIndependent === false;
|
|
1240
1257
|
const changed = await adjudicationSubjectChange(ctx.worktree, commit, s0);
|
|
1241
1258
|
if (changed)
|
|
1242
1259
|
return subjectPark(changed.leg, changed.found, adjudicator);
|
|
@@ -1248,10 +1265,10 @@ export async function runGates(task, ctx) {
|
|
|
1248
1265
|
const agreed = disputed.every((d) => rulings.some((r) => r.id === d.id && r.met === d.met && r.paths.length > 0));
|
|
1249
1266
|
if (!agreed)
|
|
1250
1267
|
return park("the adjudicating judge split from the fresh ruling", adjudicator);
|
|
1251
|
-
return { ...stamped, meta: { ...stamped.meta, adjudication: { primary, adjudicator, criteria: disputed.map((d) => d.id), agreed: true } } };
|
|
1268
|
+
return { ...stamped, meta: { ...stamped.meta, ...independence(), adjudication: { primary, adjudicator, criteria: disputed.map((d) => d.id), agreed: true } } };
|
|
1252
1269
|
};
|
|
1253
1270
|
// OBS-1182: every judge seat launches at its OWN configured effort, never the worker's.
|
|
1254
|
-
let a = await invokeJudge(judgeAdapter,
|
|
1271
|
+
let a = await invokeJudge(judgeAdapter, primary.model, jvia, configuredEffort(ctx.cfg, primary));
|
|
1255
1272
|
// GATE-09: an unparseable judge verdict retries the JUDGE exactly once on a failover channel — never
|
|
1256
1273
|
// the worker (run-20260711-185020 P43-03 L70-72 billed a judge flake as a worker attempt). The flaked
|
|
1257
1274
|
// first verdict NEVER enters results (no false gate-result journal event, no operator notify, no stale
|
|
@@ -1268,18 +1285,10 @@ export async function runGates(task, ctx) {
|
|
|
1268
1285
|
if (!cancelled && a.meta?.unparseable === true && typeof a.meta.judge === "string") {
|
|
1269
1286
|
const flakedKey = a.meta.judge;
|
|
1270
1287
|
const flakedAdapter = flakedKey.slice(0, flakedKey.indexOf(":"));
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
// v1.87 T2: the failover seat obeys the same policy the primary judge just passed — a denied
|
|
1276
|
-
// channel is refused here too, never reached by falling through the exclusion arms below.
|
|
1277
|
-
const judgePool = (ctx.judgeChannels ?? []).filter((c) => disallowedBy(c, ctx.cfg.routing, "judge") === null);
|
|
1278
|
-
const crossAdapter = pick(judgePool.filter((c) => c.adapter !== flakedAdapter));
|
|
1279
|
-
const sameAdapter = pick(judgePool.filter((c) => c.adapter === flakedAdapter && channelKey(c) !== flakedKey));
|
|
1280
|
-
// Prefer a different adapter; if the fleet only has one adapter, retry on a different channel of
|
|
1281
|
-
// that adapter; if the fleet has only one channel, fall back to the original judge config.
|
|
1282
|
-
const retry = crossAdapter ?? sameAdapter ?? judgeSeat;
|
|
1288
|
+
// Queue row 34(a) (D-2134 R5): keep GATE-09's exclusions and fallback when independence is unavailable.
|
|
1289
|
+
const pool = judgePool().filter((c) => channelKey(c) !== flakedKey);
|
|
1290
|
+
const retry = pickJudge(pool.filter(independent), flakedAdapter)
|
|
1291
|
+
?? pickJudge(pool, flakedAdapter) ?? judgeSeat;
|
|
1283
1292
|
const retryAdapter = getAdapter(retry.adapter, ctx.adapters);
|
|
1284
1293
|
const retryJvia = via
|
|
1285
1294
|
// unconditional -r1 suffix: under keepPanes:forever a same-channel retry cannot collide with the
|
package/dist/report/bundle.d.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import type { EvidenceCitation } from "../gates/acceptance.js";
|
|
2
2
|
import type { RunEnvironment } from "../run/environment.js";
|
|
3
3
|
import type { JournalEvent } from "../run/journal.js";
|
|
4
|
-
|
|
4
|
+
import { type SessionEnvironment } from "./compare.js";
|
|
5
|
+
export declare const BUNDLE_SCHEMA_VERSION = 3;
|
|
5
6
|
export declare const gateDeclined: (data: Record<string, unknown>) => boolean;
|
|
6
7
|
export declare const KNOWN_LIMITS: readonly string[];
|
|
7
8
|
export type BundleEvidence = string | EvidenceCitation;
|
|
@@ -18,6 +19,7 @@ export interface BundleGateResult {
|
|
|
18
19
|
/** T11: true when the gate declined to run (journaled skipped:true, e.g. review below its
|
|
19
20
|
* complexity threshold) — neither a pass nor a fail; the details carry the reason. */
|
|
20
21
|
declined?: boolean;
|
|
22
|
+
held?: boolean;
|
|
21
23
|
details: string;
|
|
22
24
|
}
|
|
23
25
|
export interface BundleTask {
|
|
@@ -28,7 +30,7 @@ export interface BundleTask {
|
|
|
28
30
|
judgeCriteria: BundleJudgeCriterion[];
|
|
29
31
|
}
|
|
30
32
|
export interface BundleContentHashes {
|
|
31
|
-
/** sha256 of
|
|
33
|
+
/** Queue 35 (D-2134 R6): sha256 of JSON.stringify of the parsed source events. */
|
|
32
34
|
journal: string;
|
|
33
35
|
/** graphDefinitionHash from run-start when recorded. */
|
|
34
36
|
graphDefinitionHash?: string;
|
|
@@ -38,6 +40,7 @@ export interface ProofBundle {
|
|
|
38
40
|
schemaVersion: number;
|
|
39
41
|
runId: string;
|
|
40
42
|
environment?: RunEnvironment;
|
|
43
|
+
environments: SessionEnvironment[];
|
|
41
44
|
contentHashes: BundleContentHashes;
|
|
42
45
|
tasks: BundleTask[];
|
|
43
46
|
/** Plain-language known limits — this is not an unconditional proof of correctness. */
|
package/dist/report/bundle.js
CHANGED
|
@@ -4,11 +4,13 @@
|
|
|
4
4
|
// redactSecrets seam. No I/O and no network: the caller (report CLI) owns the write.
|
|
5
5
|
import { createHash } from "node:crypto";
|
|
6
6
|
import { redactSecrets } from "../run/redact.js";
|
|
7
|
-
import {
|
|
7
|
+
import { normalizeGateOutcome } from "../run/outcome.js";
|
|
8
|
+
import { recordedEnvironment, recordedEnvironments } from "./compare.js";
|
|
8
9
|
// Bump when the packet shape changes in a way a future reader must branch on before parsing.
|
|
9
10
|
// 2 (T11): gates gained `declined`, and `pass` narrowed from "the gate did not fail" to "the gate
|
|
10
11
|
// ran and passed" — a schema-1 reader would read a decline as a plain failure.
|
|
11
|
-
|
|
12
|
+
// Queue 35 (D-2134 R6): held gates and session environments change the packet's meaning.
|
|
13
|
+
export const BUNDLE_SCHEMA_VERSION = 3;
|
|
12
14
|
// T11: the one predicate every decline surface shares — the record, the text tickmark rate, the
|
|
13
15
|
// comparison metrics and this packet must agree on what "never ran" means. Modern runs journal
|
|
14
16
|
// `skipped: true`; runs from before that field carry the same decline only in the details prefix
|
|
@@ -19,7 +21,7 @@ export const gateDeclined = (data) => data.skipped === true || /^skipped\b/.test
|
|
|
19
21
|
export const KNOWN_LIMITS = [
|
|
20
22
|
"This packet is a portable snapshot of journaled run facts, not an independent re-verification of the work or its gates.",
|
|
21
23
|
"Judge evidence citations are copied from the journal as the judge recorded them; this packet does not re-validate citations against the judged diff.",
|
|
22
|
-
"Content hashes bind this packet to the journal
|
|
24
|
+
"Content hashes bind this packet to the JSON.stringify serialization of parsed journal events used to build it; they do not prove the underlying gates, merges, or tip verification were correct.",
|
|
23
25
|
"Secret-shaped strings are redacted by local pattern matching only; redaction is not a formal security audit.",
|
|
24
26
|
"Producing this packet never contacts a network and never uploads anything.",
|
|
25
27
|
];
|
|
@@ -135,11 +137,13 @@ export function buildProofBundle(runId, events) {
|
|
|
135
137
|
const judgeCriteria = [];
|
|
136
138
|
for (const g of gateEvents) {
|
|
137
139
|
const gate = typeof g.data.gate === "string" ? g.data.gate : "unknown";
|
|
138
|
-
//
|
|
140
|
+
// Queue 35 (D-2134 R6): a passing screen remains held until its full suite speaks.
|
|
139
141
|
const declined = gateDeclined(g.data);
|
|
140
|
-
const
|
|
142
|
+
const outcome = normalizeGateOutcome(g.data);
|
|
143
|
+
const held = outcome.kind === "held";
|
|
144
|
+
const pass = outcome.kind === "passed" && !declined;
|
|
141
145
|
const details = typeof g.data.details === "string" ? g.data.details : "";
|
|
142
|
-
gates.push({ gate, pass, ...(declined ? { declined: true } : {}), details });
|
|
146
|
+
gates.push({ gate, pass, ...(declined ? { declined: true } : {}), ...(held ? { held: true } : {}), details });
|
|
143
147
|
if (gate === "acceptance") {
|
|
144
148
|
for (const c of parseJudgeCriteria(g.data))
|
|
145
149
|
judgeCriteria.push(c);
|
|
@@ -156,6 +160,7 @@ export function buildProofBundle(runId, events) {
|
|
|
156
160
|
schemaVersion: BUNDLE_SCHEMA_VERSION,
|
|
157
161
|
runId,
|
|
158
162
|
...(environment ? { environment } : {}),
|
|
163
|
+
environments: recordedEnvironments(events),
|
|
159
164
|
contentHashes: {
|
|
160
165
|
journal: journalHash,
|
|
161
166
|
...(gHash ? { graphDefinitionHash: gHash } : {}),
|
package/dist/report/compare.d.ts
CHANGED
|
@@ -8,11 +8,18 @@ export type EnvironmentCompare = {
|
|
|
8
8
|
comparable: false;
|
|
9
9
|
reason: "mismatch";
|
|
10
10
|
recorded: string;
|
|
11
|
+
mixedSessions?: true;
|
|
11
12
|
} | {
|
|
12
13
|
comparable: false;
|
|
13
14
|
reason: "unbound";
|
|
14
15
|
};
|
|
15
16
|
export declare function hasRunStart(events: JournalEvent[]): boolean;
|
|
17
|
+
export interface SessionEnvironment {
|
|
18
|
+
event: "run-start" | "run-resume";
|
|
19
|
+
ts: string;
|
|
20
|
+
environment?: RunEnvironment;
|
|
21
|
+
}
|
|
22
|
+
export declare function recordedEnvironments(events: JournalEvent[]): SessionEnvironment[];
|
|
16
23
|
export declare function recordedEnvironment(events: JournalEvent[]): RunEnvironment | undefined;
|
|
17
24
|
export declare function environmentComparable(baselineEnv: RunEnvironment | undefined, currentEnv: RunEnvironment | undefined): EnvironmentCompare;
|
|
18
25
|
export interface RunMetrics {
|
|
@@ -21,6 +28,7 @@ export interface RunMetrics {
|
|
|
21
28
|
gateFail: number;
|
|
22
29
|
gateTotal: number;
|
|
23
30
|
costUsd: number | undefined;
|
|
31
|
+
costPartial: boolean;
|
|
24
32
|
tokensTotal: number | undefined;
|
|
25
33
|
}
|
|
26
34
|
export declare function runMetrics(events: JournalEvent[], rows?: TelemetryRow[], cost?: CostConfig): RunMetrics;
|
|
@@ -28,6 +36,7 @@ export interface RunDelta {
|
|
|
28
36
|
durationMs: number | undefined;
|
|
29
37
|
gateFail: number;
|
|
30
38
|
costUsd: number | undefined;
|
|
39
|
+
costPartial: boolean;
|
|
31
40
|
tokensTotal: number | undefined;
|
|
32
41
|
}
|
|
33
42
|
export type CompareOutcome = {
|