tickmarkr 2.8.2 → 2.8.4
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/claude-code.js +3 -0
- package/dist/adapters/kimi.js +31 -2
- package/dist/adapters/qwen.d.ts +1 -1
- package/dist/adapters/qwen.js +5 -3
- package/dist/adapters/types.d.ts +1 -0
- package/dist/cli/commands/approve.js +106 -1
- package/dist/gates/acceptance.js +130 -12
- package/dist/gates/llm.d.ts +1 -1
- package/dist/gates/llm.js +87 -18
- package/package.json +1 -1
|
@@ -238,6 +238,9 @@ export const claudeCode = {
|
|
|
238
238
|
// The empty -p argument selects print mode while stdin carries the prompt, keeping its nonce out
|
|
239
239
|
// of process argv. The redirect path is shell-quoted independently from the model.
|
|
240
240
|
headlessCommand: (promptFile, model, effort) => `claude -p '' --model ${shq(model)}${claudeEffortFlag(effort)} --permission-mode bypassPermissions --strict-mcp-config --mcp-config '{"mcpServers":{}}' --output-format text < ${shq(promptFile)}`,
|
|
241
|
+
// Queue row 184: `--output-format text` prints nothing until the turn ends, so a working pane reviewer reads zero seat bytes at
|
|
242
|
+
// the first-liveness beat; the beat must not re-route it (2.8.3 killed every claude pane reviewer at 30 s).
|
|
243
|
+
paneReviewBuffersOutput: true,
|
|
241
244
|
// HYG-03 / OBS-137: the residual first-entry dialog is workspace trust, not MCP config loading.
|
|
242
245
|
// Claude's only store is global last-writer-wins ~/.claude.json, so tickmarkr still does not seed it;
|
|
243
246
|
// the daemon safely answers only the exact adapter-declared dialog once per slot.
|
package/dist/adapters/kimi.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { spawnSync } from "node:child_process";
|
|
1
2
|
import { readdirSync, readFileSync, realpathSync, statSync } from "node:fs";
|
|
2
3
|
import { homedir } from "node:os";
|
|
3
4
|
import { join } from "node:path";
|
|
@@ -171,7 +172,32 @@ export function confirmKimiSeedBanner(banner, assignedModel) {
|
|
|
171
172
|
}
|
|
172
173
|
return { ok: true, status: "confirmed", ...(sessionId === undefined ? {} : { sessionId }) };
|
|
173
174
|
}
|
|
174
|
-
const
|
|
175
|
+
const KIMI_HELP_TIMEOUT_MS = 10_000;
|
|
176
|
+
function kimiHasNeverAskOption(help) {
|
|
177
|
+
let inOptions = false;
|
|
178
|
+
for (const line of help.split(/\r?\n/)) {
|
|
179
|
+
if (line === "Options:") {
|
|
180
|
+
inOptions = true;
|
|
181
|
+
continue;
|
|
182
|
+
}
|
|
183
|
+
if (inOptions && /^[^\s].*:$/.test(line))
|
|
184
|
+
break;
|
|
185
|
+
if (inOptions && /^ --auto[ \t]{2,}Start in Never Ask mode\b/.test(line))
|
|
186
|
+
return true;
|
|
187
|
+
}
|
|
188
|
+
return false;
|
|
189
|
+
}
|
|
190
|
+
const kimiTuiLaunch = (model) => {
|
|
191
|
+
let neverAsk = false;
|
|
192
|
+
try {
|
|
193
|
+
const help = spawnSync("kimi", ["--help"], {
|
|
194
|
+
encoding: "utf8", timeout: KIMI_HELP_TIMEOUT_MS, stdio: ["ignore", "pipe", "ignore"],
|
|
195
|
+
});
|
|
196
|
+
neverAsk = !help.error && help.status === 0 && kimiHasNeverAskOption(help.stdout);
|
|
197
|
+
}
|
|
198
|
+
catch { /* unreadable help keeps the existing launch form */ }
|
|
199
|
+
return `kimi ${neverAsk ? "--auto" : "-y"} -m ${shq(model)}`;
|
|
200
|
+
};
|
|
175
201
|
const KIMI_ANSI_SGR_RE = /\u001B\[[0-9;]*m/g;
|
|
176
202
|
const KIMI_EDITOR_TOP_RE = /^╭─+╮$/;
|
|
177
203
|
const KIMI_EDITOR_INPUT_RE = /^│ > .*│$/;
|
|
@@ -215,7 +241,10 @@ export const KIMI_INPUT_BOX = declareInputBox("kimi", {
|
|
|
215
241
|
fingerprint: "│ > ",
|
|
216
242
|
match: (paneText) => matchesKimiEditorBox(paneText, false),
|
|
217
243
|
emptyMatch: (paneText) => matchesKimiEditorBox(paneText, true),
|
|
218
|
-
launchCommand: (command) =>
|
|
244
|
+
launchCommand: (command) => {
|
|
245
|
+
const match = /^kimi (?:--auto|-y) -m '([^']+)'$/.exec(command);
|
|
246
|
+
return match !== null && MODEL_ID_RE.test(match[1]);
|
|
247
|
+
},
|
|
219
248
|
readinessTimeoutMs: 15_000,
|
|
220
249
|
});
|
|
221
250
|
// Shared launch-then-seed surface (T6) + banner confirm (T7/T2). One definition so the adapter
|
package/dist/adapters/qwen.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { type ClassifiedWorkerResult } from "./prompt.js";
|
|
2
2
|
import { type WorkerAdapter } from "./types.js";
|
|
3
3
|
export declare const QWEN_VERSION_IDENTITY: RegExp;
|
|
4
|
-
export declare const QWEN_HARNESS_BANNER_ROWS: readonly ["⚠ SAFE MODE — all customizations disabled (hooks, extensions, skills, MCP servers, QWEN.md). Restart without --safe-mode to resume normal operation.", "Warning: running headless with --yolo / approval-mode=yolo and no sandbox. All tool calls (shell, write, edit) auto-execute at this process's privilege level. Enable a sandbox via --sandbox / QWEN_SANDBOX, or set QWEN_CODE_SUPPRESS_YOLO_WARNING=1 to silence this notice."];
|
|
4
|
+
export declare const QWEN_HARNESS_BANNER_ROWS: readonly ["⚠ SAFE MODE — all customizations disabled (hooks, extensions, skills, MCP servers, QWEN.md). Restart without --safe-mode to resume normal operation.", "Warning: running headless with --yolo / approval-mode=yolo and no sandbox. All tool calls (shell, write, edit) auto-execute at this process's privilege level. Enable a sandbox via --sandbox / QWEN_SANDBOX, or set QWEN_CODE_SUPPRESS_YOLO_WARNING=1 to silence this notice.", "Warning: running headless with --yolo / approval-mode=yolo and no sandbox. All tool calls (shell, write, edit) auto-execute at this process's privilege level. Configure tools.executionSandbox on Linux or a supported legacy sandbox via --sandbox / QWEN_SANDBOX, or set QWEN_CODE_SUPPRESS_YOLO_WARNING=1 to silence this notice."];
|
|
5
5
|
export declare function parseQwenResult(raw: string, nonce: string): ClassifiedWorkerResult;
|
|
6
6
|
export declare const qwen: WorkerAdapter;
|
package/dist/adapters/qwen.js
CHANGED
|
@@ -3,12 +3,14 @@ import { parseWorkerResult } from "./prompt.js";
|
|
|
3
3
|
import { channelsFromConfig, shq, } from "./types.js";
|
|
4
4
|
export const QWEN_VERSION_IDENTITY = /^\d+\.\d+\.\d+/;
|
|
5
5
|
const QWEN_SKIP_UPDATE = "QWEN_CODE_SKIP_UPDATE_CHECK_ONCE=true";
|
|
6
|
-
// Verbatim stderr
|
|
7
|
-
//
|
|
8
|
-
//
|
|
6
|
+
// Verbatim stderr rows, in render order: the SAFE MODE row both recorded versions print, then the yolo
|
|
7
|
+
// row of tests/fixtures/qwen/safe-mode.stderr (qwen 0.21.15) and of safe-mode-0.25.1.stderr (qwen 0.25.1).
|
|
8
|
+
// These bytes are launch harness, not evidence that a reviewer took a turn. Keep the declaration closed
|
|
9
|
+
// and row-shaped so llm.ts can sweep partial terminal paints without a broad SAFE MODE/warning regex.
|
|
9
10
|
export const QWEN_HARNESS_BANNER_ROWS = [
|
|
10
11
|
"⚠ SAFE MODE — all customizations disabled (hooks, extensions, skills, MCP servers, QWEN.md). Restart without --safe-mode to resume normal operation.",
|
|
11
12
|
"Warning: running headless with --yolo / approval-mode=yolo and no sandbox. All tool calls (shell, write, edit) auto-execute at this process's privilege level. Enable a sandbox via --sandbox / QWEN_SANDBOX, or set QWEN_CODE_SUPPRESS_YOLO_WARNING=1 to silence this notice.",
|
|
13
|
+
"Warning: running headless with --yolo / approval-mode=yolo and no sandbox. All tool calls (shell, write, edit) auto-execute at this process's privilege level. Configure tools.executionSandbox on Linux or a supported legacy sandbox via --sandbox / QWEN_SANDBOX, or set QWEN_CODE_SUPPRESS_YOLO_WARNING=1 to silence this notice.",
|
|
12
14
|
];
|
|
13
15
|
function decodeQwenEvents(events) {
|
|
14
16
|
const text = [];
|
package/dist/adapters/types.d.ts
CHANGED
|
@@ -195,6 +195,7 @@ export interface WorkerAdapter {
|
|
|
195
195
|
channels(cfg: TickmarkrConfig): BillingChannel[];
|
|
196
196
|
headlessCommand(promptFile: string, model: string, effort?: Effort): string;
|
|
197
197
|
harnessBannerRows?: readonly string[];
|
|
198
|
+
paneReviewBuffersOutput?: boolean;
|
|
198
199
|
interactiveCommand(promptFile: string, model: string, effort?: Effort): string | null;
|
|
199
200
|
interactiveSeed?: InteractiveSeed;
|
|
200
201
|
resumeCommand?(sessionId: string, promptFile: string, model: string, effort?: Effort): string;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { graphDefinitionHash, loadGraph, saveGraph, taskDefinitionFingerprint } from "../../graph/graph.js";
|
|
2
|
+
import { filesGlob } from "../../graph/files-glob.js";
|
|
2
3
|
import { execFileSync } from "node:child_process";
|
|
3
4
|
import { userInfo } from "node:os";
|
|
4
5
|
import { allAdapters } from "../../adapters/registry.js";
|
|
@@ -242,7 +243,7 @@ export async function approve(argv, cwd = process.cwd()) {
|
|
|
242
243
|
if (amendedFiles.length === task.files.length)
|
|
243
244
|
throw new Error("--files must extend the parked files[] revision");
|
|
244
245
|
const amended = { ...graph, tasks: graph.tasks.map((t) => t.id === taskId ? { ...t, files: amendedFiles } : t) };
|
|
245
|
-
const conflicts =
|
|
246
|
+
const conflicts = scopeGrantConflicts(cwd, journal, runId, taskId, amended.tasks, events);
|
|
246
247
|
if (conflicts.length)
|
|
247
248
|
throw new Error(`refusing scope-request approval for ${taskId}: ${conflicts.join("\n")}`);
|
|
248
249
|
// C4: a live grant may not take a task past the bounds that fail every compile. The exception
|
|
@@ -394,6 +395,110 @@ function integrationBase(events, byName) {
|
|
|
394
395
|
const branch = String(events.find((e) => e.event === "run-start").data.branch);
|
|
395
396
|
return "anchor" in anchor ? { branch, base: anchor.anchor } : { branch, base: "unknown", unreadable: anchor.unreadable };
|
|
396
397
|
}
|
|
398
|
+
/**
|
|
399
|
+
* Queue 169 (D-1982/D-1983/D-1986): the in-run OBS-212 check a scope-request grant runs. Only the
|
|
400
|
+
* pairs this amendment changes are judged — the requester against each unordered sibling; a pair
|
|
401
|
+
* another amendment created was judged by that grant and is never re-judged here. A sibling is
|
|
402
|
+
* exempt only when this run already merged it into the requester's branch: its replayed status is
|
|
403
|
+
* done, its NEWEST journaled merge row names a full lowercase object id, and that commit is an
|
|
404
|
+
* ancestor of the requester's task branch (the run's integration branch plus --taskId, read exactly
|
|
405
|
+
* as the failed-task recheck reads it — a marked run's branch comes from its run-start record, never
|
|
406
|
+
* the current config). Every other overlapping pair keeps the compile-time refusal, an unreadable
|
|
407
|
+
* git answer included. The surviving errors are separabilityErrors' own rows, so the refusal text
|
|
408
|
+
* and its pair order are unchanged.
|
|
409
|
+
*/
|
|
410
|
+
function scopeGrantConflicts(cwd, journal, runId, requesterId, tasks, events) {
|
|
411
|
+
const errors = separabilityErrors(tasks);
|
|
412
|
+
if (!errors.length)
|
|
413
|
+
return errors;
|
|
414
|
+
const pairs = separabilityPairs(tasks);
|
|
415
|
+
if (pairs.length !== errors.length)
|
|
416
|
+
return errors; // a misaligned local fold refuses everything
|
|
417
|
+
const statuses = journal.replayStatuses();
|
|
418
|
+
let taskRef;
|
|
419
|
+
const requesterRef = () => (taskRef ??= `${integrationBase(events, () => integrationBranch(loadConfig(cwd), runId)).branch}--${requesterId}`);
|
|
420
|
+
const kept = [];
|
|
421
|
+
for (let i = 0; i < errors.length; i++) {
|
|
422
|
+
const { a, b } = pairs[i];
|
|
423
|
+
const sibling = a === requesterId ? b : b === requesterId ? a : undefined;
|
|
424
|
+
if (sibling === undefined)
|
|
425
|
+
continue;
|
|
426
|
+
if (mergedSiblingExempt(cwd, events, statuses.get(sibling), sibling, requesterRef))
|
|
427
|
+
continue;
|
|
428
|
+
kept.push(errors[i]);
|
|
429
|
+
}
|
|
430
|
+
return kept;
|
|
431
|
+
}
|
|
432
|
+
/** A full lowercase git object id: 40 (SHA-1) or 64 (SHA-256) hex, nothing else. */
|
|
433
|
+
const FULL_OBJECT_ID = /^[0-9a-f]{40}([0-9a-f]{24})?$/;
|
|
434
|
+
/**
|
|
435
|
+
* The one exemption: the sibling is done in THIS run's replay and its newest merge row's commit is a
|
|
436
|
+
* full object id that is an ancestor of the requester's task branch. Any unreadable git answer — a
|
|
437
|
+
* missing requester branch, a commit the repository lacks, a failed ancestry read — is no exemption.
|
|
438
|
+
*/
|
|
439
|
+
function mergedSiblingExempt(cwd, events, status, siblingId, requesterRef) {
|
|
440
|
+
if (status !== "done")
|
|
441
|
+
return false;
|
|
442
|
+
const merge = [...events].reverse().find((e) => e.event === "merge" && e.taskId === siblingId);
|
|
443
|
+
const commit = typeof merge?.data.commit === "string" ? merge.data.commit : undefined;
|
|
444
|
+
if (commit === undefined || !FULL_OBJECT_ID.test(commit))
|
|
445
|
+
return false;
|
|
446
|
+
const ref = requesterRef();
|
|
447
|
+
try {
|
|
448
|
+
execFileSync("git", ["rev-parse", "--verify", ref], { cwd, stdio: "ignore" });
|
|
449
|
+
execFileSync("git", ["merge-base", "--is-ancestor", commit, ref], { cwd, stdio: "ignore" });
|
|
450
|
+
}
|
|
451
|
+
catch {
|
|
452
|
+
return false;
|
|
453
|
+
}
|
|
454
|
+
return true;
|
|
455
|
+
}
|
|
456
|
+
/** The conflicting unordered pairs of separabilityErrors, in its exact emission order. */
|
|
457
|
+
function separabilityPairs(tasks) {
|
|
458
|
+
const byId = new Map(tasks.map((t) => [t.id, t.deps ?? []]));
|
|
459
|
+
const pairs = [];
|
|
460
|
+
for (let i = 0; i < tasks.length; i++) {
|
|
461
|
+
for (let j = i + 1; j < tasks.length; j++) {
|
|
462
|
+
const a = tasks[i], b = tasks[j];
|
|
463
|
+
if (pairReachable(a.id, byId).has(b.id) || pairReachable(b.id, byId).has(a.id))
|
|
464
|
+
continue; // ordered
|
|
465
|
+
if (pairOverlap(a.files ?? [], b.files ?? []).length > 0)
|
|
466
|
+
pairs.push({ a: a.id, b: b.id });
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
return pairs;
|
|
470
|
+
}
|
|
471
|
+
/** Tasks reachable from `id` through deps (transitive) — separabilityErrors' own fold. */
|
|
472
|
+
function pairReachable(id, byId) {
|
|
473
|
+
const seen = new Set();
|
|
474
|
+
const stack = [...(byId.get(id) ?? [])];
|
|
475
|
+
while (stack.length) {
|
|
476
|
+
const next = stack.pop();
|
|
477
|
+
if (seen.has(next))
|
|
478
|
+
continue;
|
|
479
|
+
seen.add(next);
|
|
480
|
+
stack.push(...(byId.get(next) ?? []));
|
|
481
|
+
}
|
|
482
|
+
return seen;
|
|
483
|
+
}
|
|
484
|
+
/** separabilityErrors' conservative overlap: identical patterns, or a glob matching the other's literal path. */
|
|
485
|
+
function pairOverlap(a, b) {
|
|
486
|
+
const hits = new Set();
|
|
487
|
+
const isGlob = (p) => /[*?[\]{}]/.test(p);
|
|
488
|
+
for (const pa of a) {
|
|
489
|
+
for (const pb of b) {
|
|
490
|
+
if (pa === pb) {
|
|
491
|
+
hits.add(pa);
|
|
492
|
+
continue;
|
|
493
|
+
}
|
|
494
|
+
if (isGlob(pa) && !isGlob(pb) && filesGlob(pa)(pb))
|
|
495
|
+
hits.add(`${pb} ⊂ ${pa}`);
|
|
496
|
+
else if (isGlob(pb) && !isGlob(pa) && filesGlob(pb)(pa))
|
|
497
|
+
hits.add(`${pa} ⊂ ${pb}`);
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
return [...hits].sort();
|
|
501
|
+
}
|
|
397
502
|
/** The row a decision on this task answers right now: its newest failure when failed, else its newest park. */
|
|
398
503
|
function openDecision(journal, taskId) {
|
|
399
504
|
const { events, sourceIndexes } = readJournalEvents(journal);
|
package/dist/gates/acceptance.js
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import { randomBytes } from "node:crypto";
|
|
2
|
+
import { lstatSync, mkdtempSync, readdirSync, rmSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { isAbsolute, join, relative, resolve } from "node:path";
|
|
3
5
|
import { z } from "zod";
|
|
4
6
|
import { channelKey, shq } from "../adapters/types.js";
|
|
5
7
|
import { compileNative } from "../compile/native.js";
|
|
6
8
|
import { DEFAULT_DIFF_CAP } from "../config/config.js";
|
|
7
9
|
import { renderAcceptanceItem } from "../graph/schema.js";
|
|
8
|
-
import { sh } from "../run/git.js";
|
|
10
|
+
import { DEFAULT_SHELL_TIMEOUT_MS, sh, shell } from "../run/git.js";
|
|
9
11
|
import { checkTaskDiffCaps, fetchTaskDiff, isProtectedEvidence, setAsideReceiptPath } from "./review.js";
|
|
10
12
|
import { appendAnchoredReview, COMPLETION_FAKING_CHECKLIST, extractVerdictJson, generateVerdictNonce, runLlm, SeatLaunchError, verdictNonceLine } from "./llm.js";
|
|
11
13
|
import { classifyVerdictCause } from "./verdict-cause.js";
|
|
@@ -13,6 +15,8 @@ import { reviewableLogicDiff } from "./artifact-manifest.js";
|
|
|
13
15
|
import { diffSidePath } from "./git-paths.js";
|
|
14
16
|
import { classifyFailureOutput } from "./baseline.js";
|
|
15
17
|
import { operationFactsAtHead, readOperationFacts } from "./operation-facts.js";
|
|
18
|
+
import { isVitestTestCommand, manifestChildEnv, readTestReport, singleForkRetryCommand, toManifestPath } from "./test-manifest.js";
|
|
19
|
+
import { TEST_REPORTER_SOURCE } from "./test-reporter.js";
|
|
16
20
|
// Fable F4: acceptance judge shares review's 900s timeout — 300s default killed frontier judges on cap-sized diffs.
|
|
17
21
|
const JUDGE_TIMEOUT_MS = 900_000;
|
|
18
22
|
const CitationSchema = z.object({ path: z.string(), line: z.number().int() });
|
|
@@ -98,6 +102,26 @@ export function testFiltered(testCmd, name) {
|
|
|
98
102
|
const fwd = wrapped ? "-- " : "";
|
|
99
103
|
return `${testCmd} ${fwd}-t ${shq(pattern)}`;
|
|
100
104
|
}
|
|
105
|
+
// Queue row 179: the criterion's suite file as the runner's file argument AHEAD of the title filter, appended to a
|
|
106
|
+
// base that already carries its forwarding separator (npmForwarded, under either runner). testFiltered keeps its
|
|
107
|
+
// two-argument form: a criterion with no suite file, and the eval harness.
|
|
108
|
+
const fileFiltered = (base, file, name) => `${base} ${shq(file)} -t ${shq(testFilterPattern(name))}`;
|
|
109
|
+
// The forwarding separator, decided on shell WORDS — the test gate's tokenizer (test-manifest.ts shellTokens/words,
|
|
110
|
+
// unexported) beside its unquoting — never on the raw text: behind any `NAME=value` assignments (whose value, even
|
|
111
|
+
// ` -- `, is not a word of the command), an npm manager word, bare, quoted or escaped, gets ONE `--` unless a later
|
|
112
|
+
// word already is `--` (bare, quoted or escaped). npx, pnpm, yarn and a direct runner forward their arguments as they
|
|
113
|
+
// are. Both runners' branches append through it; the Vitest branch asks the gate's parser only whether the reporter
|
|
114
|
+
// can be forwarded at all (V5), since that parser's own separator reads the raw text.
|
|
115
|
+
const SHELL_TOKEN = /(?:[^\s'"\\&|;<>]+|'[^']*'|"(?:\\.|[^"\\])*"|\\.)+|[&|;<>]+/g;
|
|
116
|
+
const unquoted = (token) => token.replace(/'([^']*)'|"((?:\\.|[^"\\])*)"|\\(.)/g, (_all, single, double, escaped) => single ?? (double !== undefined ? double.replace(/\\(["\\$`])/g, "$1") : escaped));
|
|
117
|
+
function npmForwarded(testCmd) {
|
|
118
|
+
const raw = testCmd.match(SHELL_TOKEN) ?? [];
|
|
119
|
+
let manager = 0;
|
|
120
|
+
while (/^[A-Za-z_]\w*=/.test(raw[manager] ?? ""))
|
|
121
|
+
manager++;
|
|
122
|
+
const argv = raw.slice(manager).map(unquoted);
|
|
123
|
+
return argv[0] === "npm" && !argv.includes("--") ? `${testCmd} --` : testCmd;
|
|
124
|
+
}
|
|
101
125
|
const VitestListedTestsSchema = z.array(z.object({
|
|
102
126
|
name: z.string(),
|
|
103
127
|
file: z.string(),
|
|
@@ -238,6 +262,99 @@ function oracleExecutionFailure(label, code, stdout, stderr) {
|
|
|
238
262
|
details: `oracle failed: ${label} (exit ${code})${tail(stderr || stdout)}`,
|
|
239
263
|
};
|
|
240
264
|
}
|
|
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
|
+
function countedTestRun(label, filter, r) {
|
|
267
|
+
if (r.code !== 0)
|
|
268
|
+
return oracleExecutionFailure(label, r.code, r.stdout, r.stderr);
|
|
269
|
+
const out = (r.stderr || "") + "\n" + (r.stdout || "");
|
|
270
|
+
const ran = testsRan(out);
|
|
271
|
+
if (ran === null || ran < 1) {
|
|
272
|
+
return { gate: "acceptance", pass: false,
|
|
273
|
+
details: `oracle failed: ${label} — name filter matched zero tests (filter: ${filter})${tail(out)}` };
|
|
274
|
+
}
|
|
275
|
+
return undefined;
|
|
276
|
+
}
|
|
277
|
+
/**
|
|
278
|
+
* Queue row 179 (D-2018, D-2019): a test criterion that declares its suite file L is satisfied by L alone — run
|
|
279
|
+
* 1248's oracles named one file and a same-titled test in another satisfied them. Closed table:
|
|
280
|
+
* L1 L absent or not a regular file (a directory included) in the worktree: a work verdict naming L, under either
|
|
281
|
+
* runner, and the test command never starts.
|
|
282
|
+
* Vitest: L alone, file argument then title filter, under the test gate's nonce-bound per-file reporter, because
|
|
283
|
+
* Vitest's file filter selects EVERY path containing L. PASS only when that report, of this nonce and ending in
|
|
284
|
+
* its run-end certificate with exit 0, shows a passed body in module L and none run in any other module.
|
|
285
|
+
* V2 L without the title (another file holding it): no passed body in L. V3 a deeper path ending with L holding
|
|
286
|
+
* it, whether or not L does: a body run in another module. V4 report missing, another nonce or no certificate,
|
|
287
|
+
* and V5 a script body the reporter cannot be forwarded to: fail closed, unverified.
|
|
288
|
+
* Other runner: L as its file argument before the title filter, today's run-count rule. Declared residual: such a
|
|
289
|
+
* runner gives no per-file evidence, so a test of the title in a path containing L can still satisfy it there.
|
|
290
|
+
*/
|
|
291
|
+
async function landedTestOracle(testCmd, title, landing, worktree) {
|
|
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
|
+
details: `oracle failed: ${label} — ${why}; unverified, failing closed`,
|
|
297
|
+
meta: { cause: "oracle-execution", classification: "infra", infra: true, retryable: false } });
|
|
298
|
+
const path = resolve(worktree, landing);
|
|
299
|
+
const within = relative(worktree, path);
|
|
300
|
+
let regular = false;
|
|
301
|
+
try {
|
|
302
|
+
regular = !within.startsWith("..") && !isAbsolute(within) && lstatSync(path).isFile();
|
|
303
|
+
}
|
|
304
|
+
catch { /* absent */ }
|
|
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`);
|
|
307
|
+
if (!isVitestTestCommand(testCmd, worktree)) {
|
|
308
|
+
return countedTestRun(label, title, await sh(fileFiltered(npmForwarded(testCmd), landing, title), worktree)) ?? passed;
|
|
309
|
+
}
|
|
310
|
+
// V5: the test gate's own parser, through its one pure export, throws on a compound or non-direct script body,
|
|
311
|
+
// where an appended --reporter would reach some other command. Only its refusal is read: its separator reads the
|
|
312
|
+
// raw text, so a quoted `"--"` or an assignment's ` -- ` value would misplace the file, filter and reporter.
|
|
313
|
+
try {
|
|
314
|
+
singleForkRetryCommand(testCmd, worktree, [], []);
|
|
315
|
+
}
|
|
316
|
+
catch (error) {
|
|
317
|
+
return unverified(`the per-file reporter cannot be forwarded to this test command (${error instanceof Error ? error.message : String(error)})`);
|
|
318
|
+
}
|
|
319
|
+
const cmd = fileFiltered(npmForwarded(testCmd), landing, title);
|
|
320
|
+
const dir = mkdtempSync(join(tmpdir(), "tickmarkr-oracle-"));
|
|
321
|
+
try {
|
|
322
|
+
const nonce = randomBytes(16).toString("hex");
|
|
323
|
+
const reporterPath = join(dir, `test-reporter-${nonce}.mjs`);
|
|
324
|
+
const reportPath = join(dir, `test-report-${nonce}.json`);
|
|
325
|
+
writeFileSync(reporterPath, TEST_REPORTER_SOURCE);
|
|
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
|
+
const raw = readTestReport(reportPath);
|
|
328
|
+
const report = raw?.nonce === nonce ? raw : undefined;
|
|
329
|
+
if (r.code !== 0) {
|
|
330
|
+
// the reporter replaces the runner's own output: its failure identities are the evidence of a red.
|
|
331
|
+
const named = report ? [...Object.values(report.completed).flatMap((c) => c.failures ?? []), ...(report.certificate?.diagnostics ?? [])] : [];
|
|
332
|
+
return oracleExecutionFailure(label, r.code, r.stdout, [r.stderr, ...named].filter(Boolean).join("\n"));
|
|
333
|
+
}
|
|
334
|
+
if (!raw)
|
|
335
|
+
return unverified("the runner exited 0 but wrote no per-file report");
|
|
336
|
+
if (!report)
|
|
337
|
+
return unverified(`the per-file report carries nonce "${raw.nonce}", not this invocation's`);
|
|
338
|
+
if (report.certificate?.exitCode !== 0)
|
|
339
|
+
return unverified("the per-file report never reached its run-end certificate with exit 0");
|
|
340
|
+
const key = toManifestPath(within, worktree);
|
|
341
|
+
// a module completed more than once (one path in several projects) keeps only its LAST counters, so its record
|
|
342
|
+
// cannot show that no earlier completion ran a body: it counts as run.
|
|
343
|
+
const repeated = new Set(report.duplicateCompletions ?? []);
|
|
344
|
+
const elsewhere = [...new Set([...Object.keys(report.started), ...Object.keys(report.completed)])].filter((file) => {
|
|
345
|
+
const t = report.completed[file]?.tests;
|
|
346
|
+
return file !== key && (repeated.has(file) || !t || t.passed + t.failed > 0);
|
|
347
|
+
}).sort();
|
|
348
|
+
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
|
+
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 passed;
|
|
353
|
+
}
|
|
354
|
+
finally {
|
|
355
|
+
rmSync(dir, { recursive: true, force: true });
|
|
356
|
+
}
|
|
357
|
+
}
|
|
241
358
|
// v1.70 / OBS-129: the new-file line numbers each changed HUNK spans, per path — parsed from the
|
|
242
359
|
// unified-diff so a citation is validated against real changed regions, not a substring of the whole
|
|
243
360
|
// diff text (which also matches the diff's own +++/@@ headers). A hunk's span is every new-file line it
|
|
@@ -355,17 +472,18 @@ judge, via, opts = {}) {
|
|
|
355
472
|
return { gate: "acceptance", pass: false,
|
|
356
473
|
details: `oracle failed: test "${a.test}" — no test command configured to run it (failing closed)` };
|
|
357
474
|
}
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
475
|
+
// queue row 179: a declared suite file is the only file that can satisfy its criterion.
|
|
476
|
+
if (a.landing !== undefined) {
|
|
477
|
+
const landed = await landedTestOracle(opts.testCmd, a.test, a.landing, worktree);
|
|
478
|
+
if (!landed.pass)
|
|
479
|
+
return landed;
|
|
480
|
+
passedDet.push(landed.details);
|
|
481
|
+
continue;
|
|
362
482
|
}
|
|
363
483
|
// OBS-55: exit 0 alone is vacuous when the name filter matched zero tests — fail closed.
|
|
364
|
-
const
|
|
365
|
-
if (
|
|
366
|
-
return
|
|
367
|
-
details: `oracle failed: test "${a.test}" — name filter matched zero tests (filter: ${a.test})${tail(out)}` };
|
|
368
|
-
}
|
|
484
|
+
const red = countedTestRun(`test "${a.test}"`, a.test, await sh(testFiltered(opts.testCmd, a.test), worktree));
|
|
485
|
+
if (red)
|
|
486
|
+
return red;
|
|
369
487
|
passedDet.push(`✓ test: ${a.test} (exit 0)`);
|
|
370
488
|
}
|
|
371
489
|
}
|
package/dist/gates/llm.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { type WorkerAdapter } from "../adapters/types.js";
|
|
2
2
|
import type { Effort } from "../graph/schema.js";
|
|
3
3
|
import { type ExecutorDriver, type Slot } from "../drivers/types.js";
|
|
4
|
-
import type
|
|
4
|
+
import { type SeatLaunchCause } from "../drivers/orca.js";
|
|
5
5
|
export declare const GATE_PANE_SEP = " \u00B7 ";
|
|
6
6
|
export declare const COMPLETION_FAKING_CHECKLIST = "## Completion-faking checklist\nHunt for these concrete completion-faking shortcuts before ruling on any criterion:\n- hardcoded-result: output or fixture hardcoded to satisfy the stated criterion instead of real logic\n- test-weakening: tests skipped, deleted, or assertions loosened until failing behavior looks green\n- vacuous-assertion: a test that cannot fail (asserts a constant, asserts its own setup, no assertion)\n- fixture-overfit: implementation narrowed to the exact test inputs rather than the described behavior\n- echo-not-implement: criterion text echoed in names, comments, or strings without the behavior itself\n- stub-left-behind: TODO, throw, or no-op stub where the real implementation should be\n- error-swallowing: catch or fallback that hides failures instead of handling them\n- self-mocking: the code under test mocked or faked so the test exercises the mock\n- check-bypass: lint, type, or CI checks disabled, relaxed, or excluded to get green\n- rename-as-work: code moved or renamed and presented as the requested change\n- scope-padding: unrelated edits padding the diff while the criterion's behavior is untouched\nWhen a criterion fails, the verdict MUST name which shortcut above it matches, or state that none does.";
|
|
7
7
|
/** Fable F3: per-call nonce echoed in verdict JSON and gate exit markers. */
|
package/dist/gates/llm.js
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
import { stripVTControlCharacters } from "node:util";
|
|
2
2
|
import { AsyncLocalStorage } from "node:async_hooks";
|
|
3
|
+
import { constants } from "node:buffer";
|
|
3
4
|
import { randomBytes } from "node:crypto";
|
|
4
5
|
import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
5
6
|
import { tmpdir } from "node:os";
|
|
6
7
|
import { join } from "node:path";
|
|
7
|
-
import { matchesTrustDialog } from "../adapters/types.js";
|
|
8
|
+
import { matchesTrustDialog, shq } from "../adapters/types.js";
|
|
8
9
|
import { formatOwnedName, parseOwnedName } from "../drivers/types.js";
|
|
10
|
+
import { CHECKOUT_MARK, checkoutProofLine } from "../drivers/orca.js";
|
|
9
11
|
import { bannerShell, paneDispatchCommand, PLAIN_BANNER } from "../brand.js";
|
|
10
12
|
import { sh } from "../run/git.js";
|
|
11
13
|
import { harvestCpuFlatWindowMs, normalizeStallSnapshot, WorkerTreeCpuAccountant, } from "../run/stall.js";
|
|
@@ -204,6 +206,64 @@ const START_MARKER = "TICKMARKR_START_";
|
|
|
204
206
|
const START_ROW = /^TICKMARKR_START_[\w-]+$/;
|
|
205
207
|
const IDENTITY_LINE = /^(?:review\s*·|tickmarkr(?::|$))/;
|
|
206
208
|
const IDENTITY_OPENERS = ["review ·", "tickmarkr"];
|
|
209
|
+
// Row 135(a): the orca driver's launch preamble. create returns only once it read the complete checkout
|
|
210
|
+
// frame, so every gate read holds the frame row whole — exactly checkoutProofLine's form (declared length
|
|
211
|
+
// equal to half its lowercase hex, terminated) — or, as the last row, a strict prefix of that form.
|
|
212
|
+
// Above it, as the first non-blank row, the shell may echo the typed launch line, which names that
|
|
213
|
+
// frame's own checkout as `cd <shq checkout>`. Anything else shaped like them is the seat's. Frame rows
|
|
214
|
+
// are read untrimmed: a trailing space or tab can never complete a frame or extend its prefix.
|
|
215
|
+
function frameCheckout(row) {
|
|
216
|
+
const hex = /^TICKMARKR_CHECKOUT \d+:(.*);$/.exec(row)?.[1];
|
|
217
|
+
if (hex === undefined)
|
|
218
|
+
return undefined;
|
|
219
|
+
const checkout = Buffer.from(hex, "hex").toString("utf8");
|
|
220
|
+
return checkoutProofLine(checkout) === row ? checkout : undefined;
|
|
221
|
+
}
|
|
222
|
+
function partialFrameRow(row) {
|
|
223
|
+
if (row.length > 0 && `${CHECKOUT_MARK} `.startsWith(row))
|
|
224
|
+
return true;
|
|
225
|
+
const m = /^TICKMARKR_CHECKOUT (0|[1-9]\d*)(?::([0-9a-f]*))?$/.exec(row);
|
|
226
|
+
if (m === null || !printableFrameLength(m[1]))
|
|
227
|
+
return false;
|
|
228
|
+
return m[2] === undefined || frameHexCompletable(m[2], Number(m[1]));
|
|
229
|
+
}
|
|
230
|
+
// checkoutProofLine prints `TICKMARKR_CHECKOUT <n>:`, 2n hex digits and `;` as one string, so the length it
|
|
231
|
+
// declares is a safe integer whose row fits the runtime's longest string. Any other length, unsafe or
|
|
232
|
+
// non-finite included, is never its output, nor a prefix of one.
|
|
233
|
+
function printableFrameLength(digits) {
|
|
234
|
+
const n = Number(digits);
|
|
235
|
+
return Number.isSafeInteger(n) && CHECKOUT_MARK.length + digits.length + 2 * n + 3 <= constants.MAX_STRING_LENGTH;
|
|
236
|
+
}
|
|
237
|
+
// A hex prefix checkoutProofLine could still extend to `bytes` bytes: valid UTF-8 so far, with its open
|
|
238
|
+
// sequence closable inside the declared length. A trailing half byte is tried at each of its 16 values.
|
|
239
|
+
function frameHexCompletable(hex, bytes) {
|
|
240
|
+
if (hex.length > bytes * 2)
|
|
241
|
+
return false;
|
|
242
|
+
const whole = Buffer.from(hex.slice(0, hex.length - (hex.length % 2)), "hex");
|
|
243
|
+
const candidates = hex.length % 2 === 0 ? [whole]
|
|
244
|
+
: Array.from({ length: 16 }, (_, lo) => Buffer.concat([whole, Buffer.of(parseInt(hex.at(-1), 16) * 16 + lo)]));
|
|
245
|
+
return candidates.some((c) => {
|
|
246
|
+
const owed = utf8Owed(c);
|
|
247
|
+
return owed !== undefined && owed <= bytes - c.length;
|
|
248
|
+
});
|
|
249
|
+
}
|
|
250
|
+
// Continuation bytes the last sequence still owes, or undefined when no continuation makes it UTF-8.
|
|
251
|
+
function utf8Owed(bytes) {
|
|
252
|
+
try {
|
|
253
|
+
new TextDecoder("utf-8", { fatal: true }).decode(bytes, { stream: true });
|
|
254
|
+
}
|
|
255
|
+
catch {
|
|
256
|
+
return undefined;
|
|
257
|
+
}
|
|
258
|
+
let lead = bytes.length;
|
|
259
|
+
while (lead > 0 && bytes.length - lead < 3 && (bytes[lead - 1] & 0xc0) === 0x80)
|
|
260
|
+
lead--;
|
|
261
|
+
if (lead === 0)
|
|
262
|
+
return 0;
|
|
263
|
+
const b = bytes[lead - 1];
|
|
264
|
+
const size = b >= 0xf0 ? 4 : b >= 0xe0 ? 3 : b >= 0xc0 ? 2 : 1;
|
|
265
|
+
return Math.max(0, size - (bytes.length - lead + 1));
|
|
266
|
+
}
|
|
207
267
|
// Stages of the preamble walk. Each complete harness row is accepted only at or after its stage.
|
|
208
268
|
const ECHO = 0, START = 1, BANNER = 2, IDENTITY = 3, SEAT = 4;
|
|
209
269
|
// The char offset where the seat's own text begins, or -1 when the capture ends inside the preamble.
|
|
@@ -215,6 +275,7 @@ function seatStart(output, adapterBannerRows) {
|
|
|
215
275
|
let bannerAt; // next banner row expected once the banner has begun
|
|
216
276
|
let adapterBannerAt;
|
|
217
277
|
let identitySeen = false;
|
|
278
|
+
let frameSeen = false;
|
|
218
279
|
let offset = 0;
|
|
219
280
|
for (let i = 0; i < rows.length; i++) {
|
|
220
281
|
const row = rows[i].replace(/[ \t]+$/, "");
|
|
@@ -222,8 +283,15 @@ function seatStart(output, adapterBannerRows) {
|
|
|
222
283
|
const last = i === rows.length - 1;
|
|
223
284
|
// Complete harness rows: equality against the shape the preamble allows at this stage.
|
|
224
285
|
let accepted = false;
|
|
286
|
+
const below = last ? undefined : frameCheckout(rows[i + 1]);
|
|
225
287
|
if (!identitySeen && stage < SEAT && t.length === 0)
|
|
226
288
|
accepted = true; // blank rows between preamble rows
|
|
289
|
+
else if (stage === ECHO && !frameSeen && frameCheckout(rows[i]) !== undefined) {
|
|
290
|
+
frameSeen = true;
|
|
291
|
+
accepted = true;
|
|
292
|
+
}
|
|
293
|
+
else if (below !== undefined && row.includes(`cd ${shq(below)}`) && rows.slice(0, i).every((r) => r.trim() === ""))
|
|
294
|
+
accepted = true; // orca launch echo
|
|
227
295
|
else if (stage <= ECHO && completeEchoRow(row))
|
|
228
296
|
accepted = true;
|
|
229
297
|
else if (stage <= START && START_ROW.test(row)) {
|
|
@@ -239,13 +307,10 @@ function seatStart(output, adapterBannerRows) {
|
|
|
239
307
|
bannerAt++;
|
|
240
308
|
accepted = true;
|
|
241
309
|
}
|
|
242
|
-
else if (stage <= IDENTITY && adapterBannerAt
|
|
310
|
+
else if (stage <= IDENTITY && adapterBannerRows.indexOf(row, adapterBannerAt ?? 0) >= 0) {
|
|
311
|
+
// A closed list in render order: each row at most once, in list order, skipping allowed.
|
|
243
312
|
stage = BANNER;
|
|
244
|
-
adapterBannerAt = adapterBannerRows.indexOf(row) + 1;
|
|
245
|
-
accepted = true;
|
|
246
|
-
}
|
|
247
|
-
else if (stage <= IDENTITY && adapterBannerAt !== undefined && row === adapterBannerRows[adapterBannerAt]) {
|
|
248
|
-
adapterBannerAt++;
|
|
313
|
+
adapterBannerAt = adapterBannerRows.indexOf(row, adapterBannerAt ?? 0) + 1;
|
|
249
314
|
accepted = true;
|
|
250
315
|
}
|
|
251
316
|
else if (!identitySeen && stage <= IDENTITY && IDENTITY_LINE.test(t)) {
|
|
@@ -262,21 +327,20 @@ function seatStart(output, adapterBannerRows) {
|
|
|
262
327
|
// The last row may be a partial paint: a prefix of the next harness row the preamble allows.
|
|
263
328
|
if (stage <= ECHO && partialEchoRow(row))
|
|
264
329
|
return -1;
|
|
330
|
+
if (stage === ECHO && !frameSeen && partialFrameRow(rows[i]))
|
|
331
|
+
return -1;
|
|
265
332
|
if (stage <= START && (START_MARKER.startsWith(row) || /^TICKMARKR_START_[\w-]*$/.test(row)))
|
|
266
333
|
return -1;
|
|
267
334
|
if (stage <= BANNER && (bannerAt === undefined
|
|
268
335
|
? BANNER_ROWS.some((b) => b.startsWith(row))
|
|
269
336
|
: BANNER_ROWS[bannerAt]?.startsWith(row) === true))
|
|
270
337
|
return -1;
|
|
271
|
-
if (stage <= IDENTITY && (adapterBannerAt
|
|
272
|
-
? adapterBannerRows.some((b) => b.startsWith(row))
|
|
273
|
-
: adapterBannerRows[adapterBannerAt]?.startsWith(row) === true))
|
|
338
|
+
if (stage <= IDENTITY && adapterBannerRows.slice(adapterBannerAt ?? 0).some((b) => b.startsWith(row)))
|
|
274
339
|
return -1;
|
|
275
340
|
// The identity row is painted right after the banner, so its prefix is a partial paint only there;
|
|
276
341
|
// with no banner in the capture, "review" or "tick" alone is the seat's own first row.
|
|
277
342
|
if (!identitySeen && stage <= IDENTITY
|
|
278
|
-
&& (bannerAt === BANNER_ROWS.length
|
|
279
|
-
|| (adapterBannerRows.length > 0 && adapterBannerAt === adapterBannerRows.length))
|
|
343
|
+
&& (bannerAt === BANNER_ROWS.length || adapterBannerAt !== undefined)
|
|
280
344
|
&& IDENTITY_OPENERS.some((o) => o.startsWith(t)))
|
|
281
345
|
return -1;
|
|
282
346
|
return offset;
|
|
@@ -430,15 +494,20 @@ async function runViaDriverDetailed(adapter, model, prompt, cwd, via, timeoutMs
|
|
|
430
494
|
// OBS-1177: the beat is armed for an INTERACTIVE (pane) driver only. On the subprocess driver
|
|
431
495
|
// a headless `claude -p` buffers every byte until it exits, so 0 seat bytes at 30 s is not a
|
|
432
496
|
// dead launch; that seat keeps its full ceiling, which still bounds it (fail-closed by timeout).
|
|
433
|
-
|
|
497
|
+
// Queue row 184: an adapter whose pane review command buffers its whole output until exit
|
|
498
|
+
// (claude-code's `claude -p`) has written nothing of its own until its verdict, working or
|
|
499
|
+
// dead, so the beat cannot tell the two apart and does not arm for it; its ceiling bounds it.
|
|
500
|
+
if (reviewing && via.driver.interactive && adapter.paneReviewBuffersOutput !== true && !firstLivenessObserved && now - startedAt >= REVIEW_FIRST_LIVENESS_MS) {
|
|
434
501
|
firstLivenessObserved = true;
|
|
435
502
|
// RS-2: the beat reads seat-authored bytes ALONE. CPU evidence never holds a preamble-only
|
|
436
503
|
// capture open to the ceiling; a seat that has not written one byte of its own is re-routed.
|
|
437
|
-
// OBS-944
|
|
438
|
-
//
|
|
439
|
-
// seat
|
|
440
|
-
//
|
|
441
|
-
//
|
|
504
|
+
// OBS-944: the claude-code seat's 901 s capture was byte-identical across two legs and ended
|
|
505
|
+
// at the pane-identity line — that seat never started. Until 2.8.3 the orca checkout frame
|
|
506
|
+
// counted as seat bytes and kept every claude pane seat past this beat; once it stopped
|
|
507
|
+
// counting (row 135(a)) the beat killed working claude reviewers too, hence the declared
|
|
508
|
+
// exemption above (row 184), which restores that pre-2.8.3 bound for buffering adapters only.
|
|
509
|
+
// RULING-229-06 puts the beat on the PANE path only; a headless `-p` runner
|
|
510
|
+
// (runHeadlessDetailed, no pane, no beat) buffers every byte until completion and keeps its full ceiling.
|
|
442
511
|
if (seatAuthoredBytes === 0) {
|
|
443
512
|
launchNeverStarted = true;
|
|
444
513
|
forceClose = true;
|