tickmarkr 2.6.1 → 2.6.3
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/README.md +16 -3
- package/dist/adapters/catalog-remote.js +89 -47
- package/dist/adapters/claude-code.js +9 -6
- package/dist/adapters/codex.js +7 -4
- package/dist/adapters/prompt.d.ts +1 -0
- package/dist/adapters/prompt.js +14 -6
- package/dist/adapters/registry.js +3 -3
- package/dist/adapters/types.d.ts +12 -4
- package/dist/adapters/types.js +6 -0
- package/dist/cli/commands/approve.d.ts +11 -4
- package/dist/cli/commands/approve.js +82 -27
- package/dist/cli/commands/compile.js +13 -3
- package/dist/cli/commands/doctor.d.ts +8 -2
- package/dist/cli/commands/doctor.js +11 -3
- package/dist/cli/commands/fleet.js +87 -11
- package/dist/cli/commands/plan.js +13 -8
- package/dist/cli/commands/report.d.ts +2 -1
- package/dist/cli/commands/report.js +74 -8
- package/dist/cli/commands/resume.js +4 -2
- package/dist/cli/commands/status.js +43 -20
- package/dist/cli/help.d.ts +2 -0
- package/dist/cli/help.js +9 -2
- package/dist/compile/native.js +7 -0
- package/dist/config/config.d.ts +35 -2
- package/dist/config/config.js +86 -10
- package/dist/config/fleet-overlay.d.ts +13 -2
- package/dist/config/fleet-overlay.js +60 -0
- package/dist/drivers/herdr.d.ts +12 -0
- package/dist/drivers/herdr.js +51 -0
- package/dist/drivers/orca.d.ts +35 -2
- package/dist/drivers/orca.js +222 -67
- package/dist/drivers/types.d.ts +2 -0
- package/dist/drivers/types.js +2 -2
- package/dist/eval/canary.d.ts +2 -1
- package/dist/eval/canary.js +2 -2
- package/dist/eval/dispatch.js +1 -0
- package/dist/gates/acceptance.d.ts +9 -1
- package/dist/gates/acceptance.js +31 -4
- package/dist/gates/baseline.d.ts +32 -2
- package/dist/gates/baseline.js +111 -24
- package/dist/gates/cache.d.ts +8 -0
- package/dist/gates/cache.js +12 -2
- package/dist/gates/llm.d.ts +11 -4
- package/dist/gates/llm.js +40 -21
- package/dist/gates/review.d.ts +14 -1
- package/dist/gates/review.js +160 -34
- package/dist/gates/run-gates.d.ts +56 -4
- package/dist/gates/run-gates.js +358 -58
- package/dist/gates/test-manifest.d.ts +45 -1
- package/dist/gates/test-manifest.js +78 -12
- package/dist/graph/schema.d.ts +2 -0
- package/dist/graph/schema.js +2 -0
- package/dist/plan/scope.js +2 -2
- package/dist/route/preference.d.ts +20 -2
- package/dist/route/preference.js +48 -13
- package/dist/route/router.d.ts +12 -1
- package/dist/route/router.js +56 -24
- package/dist/run/consult.d.ts +15 -1
- package/dist/run/consult.js +18 -7
- package/dist/run/daemon.d.ts +38 -2
- package/dist/run/daemon.js +895 -192
- package/dist/run/git.d.ts +8 -0
- package/dist/run/git.js +14 -0
- package/dist/run/interactive-seed.d.ts +4 -0
- package/dist/run/interactive-seed.js +35 -9
- package/dist/run/journal.d.ts +152 -3
- package/dist/run/journal.js +551 -50
- package/dist/run/lease.d.ts +13 -0
- package/dist/run/lease.js +45 -0
- package/dist/run/merge.d.ts +3 -1
- package/dist/run/merge.js +3 -2
- package/dist/run/operator-summary.d.ts +3 -0
- package/dist/run/operator-summary.js +3 -1
- package/dist/run/protocol.d.ts +46 -1
- package/dist/run/protocol.js +14 -2
- package/dist/run/receipt-resolver.d.ts +22 -0
- package/dist/run/receipt-resolver.js +40 -1
- package/dist/run/repair-selection.d.ts +11 -1
- package/dist/run/repair-selection.js +17 -9
- package/dist/run/supervision.d.ts +7 -1
- package/dist/run/supervision.js +5 -2
- package/dist/run/wall-budget.d.ts +48 -0
- package/dist/run/wall-budget.js +280 -0
- package/dist/tui/cockpit/board.js +3 -3
- package/dist/tui/cockpit/decision-actions.d.ts +8 -5
- package/dist/tui/cockpit/decision-actions.js +55 -32
- package/dist/tui/cockpit/derive.js +13 -2
- package/dist/tui/cockpit/live-runtime.d.ts +10 -0
- package/dist/tui/cockpit/live-runtime.js +50 -3
- package/dist/tui/cockpit/run-cockpit.d.ts +3 -0
- package/dist/tui/cockpit/run-cockpit.js +27 -2
- package/dist/tui/cockpit/run-view.d.ts +9 -2
- package/dist/tui/cockpit/run-view.js +66 -9
- package/dist/tui/cockpit/setup-cockpit.d.ts +6 -0
- package/dist/tui/cockpit/setup-cockpit.js +10 -3
- package/dist/tui/ink/fleet-app.d.ts +15 -3
- package/dist/tui/ink/fleet-app.js +91 -22
- package/package.json +3 -1
- package/schema/config.schema.json +825 -0
- package/skills/tickmarkr-loop/SKILL.md +15 -3
- package/skills/tickmarkr-overseer/SKILL.md +42 -0
- package/skills/tickmarkr-overseer/scripts/classify-vitest-log.sh +91 -0
- package/skills/tickmarkr-overseer/scripts/context-statusline.sh +81 -0
- package/skills/tickmarkr-overseer/scripts/grade-ci.sh +36 -34
- package/skills/tickmarkr-overseer/scripts/watch-journal.sh +6 -4
|
@@ -80,11 +80,49 @@ export declare function verifyManifestReport(opts: {
|
|
|
80
80
|
report: TestReport | undefined;
|
|
81
81
|
killedFile?: string;
|
|
82
82
|
hangBudgetMs?: number;
|
|
83
|
+
/** Active elapsed time (detected suspend subtracted) versus raw wall service at the kill. */
|
|
84
|
+
hangActiveMs?: number;
|
|
85
|
+
hangWallMs?: number;
|
|
83
86
|
}): ManifestVerdict;
|
|
84
87
|
/** How much longer than its baseline measurement one file may legitimately run before it is a hang. */
|
|
85
88
|
export declare const FILE_HANG_SLACK = 3;
|
|
86
89
|
export declare const DEFAULT_FILE_HANG_BUDGET_MS = 60000;
|
|
87
90
|
export declare function fileHangBudgetMs(file: string, baselineDurations?: readonly BaselineFileDuration[] | null, ceilingMs?: number, longestFile?: BaselineFileDuration | null): number;
|
|
91
|
+
/**
|
|
92
|
+
* OBS-953 (+add): a per-file hang budget counted in raw wall time charged a lid-close to the file that
|
|
93
|
+
* was running. Each poll compares how far the wall clock and the monotonic clock advanced since the
|
|
94
|
+
* last one. Wall advancing more than monotonic by over CLOCK_JUMP_SLACK_MS is a DETECTED discontinuity
|
|
95
|
+
* (`host-suspend`): its offset is subtracted from the active elapsed time of every file started before
|
|
96
|
+
* it ended. A poll that arrives overdue while both clocks advanced together is recorded as `unknown`
|
|
97
|
+
* and subtracts nothing — an ambiguous gap never excuses an active hang. Whether the host really slept
|
|
98
|
+
* is not provable from these clocks (a manually set clock jumps the same way); only the offset is.
|
|
99
|
+
*/
|
|
100
|
+
export declare const CLOCK_JUMP_SLACK_MS = 1000;
|
|
101
|
+
export interface HostInterruption {
|
|
102
|
+
kind: "host-suspend" | "unknown";
|
|
103
|
+
/** Wall time of the previous poll and of the poll that observed the gap. */
|
|
104
|
+
from: number;
|
|
105
|
+
to: number;
|
|
106
|
+
wallMs: number;
|
|
107
|
+
monoMs: number;
|
|
108
|
+
/** The wall-over-monotonic offset of a detected suspend; 0 for an unknown gap. */
|
|
109
|
+
subtractedMs: number;
|
|
110
|
+
}
|
|
111
|
+
export interface HangClocks {
|
|
112
|
+
wall: () => number;
|
|
113
|
+
mono: () => number;
|
|
114
|
+
}
|
|
115
|
+
export declare const setHangClocksForTests: (clocks: HangClocks) => void;
|
|
116
|
+
export declare const resetHangClocksForTests: () => void;
|
|
117
|
+
/** A poll gap worth recording, or undefined for an on-time poll. */
|
|
118
|
+
export declare function classifyPollGap(prev: {
|
|
119
|
+
wall: number;
|
|
120
|
+
mono: number;
|
|
121
|
+
}, now: {
|
|
122
|
+
wall: number;
|
|
123
|
+
mono: number;
|
|
124
|
+
}, pollMs: number): HostInterruption | undefined;
|
|
125
|
+
export declare const runWithInterruptionSink: <T>(sink: (interruption: HostInterruption) => void, run: () => Promise<T>) => Promise<T>;
|
|
88
126
|
export interface ManifestRunResult {
|
|
89
127
|
evidenceReceipt: GateEvidenceReceipt;
|
|
90
128
|
evidenceReceipts: GateEvidenceReceipt[];
|
|
@@ -94,6 +132,10 @@ export interface ManifestRunResult {
|
|
|
94
132
|
report: TestReport | undefined;
|
|
95
133
|
killedFile?: string;
|
|
96
134
|
hangBudgetMs?: number;
|
|
135
|
+
/** A hang's active elapsed time (suspend subtracted) and its raw wall service, kept apart. */
|
|
136
|
+
hangActiveMs?: number;
|
|
137
|
+
hangWallMs?: number;
|
|
138
|
+
interruptions: HostInterruption[];
|
|
97
139
|
/** The child's own pid (its process GROUP id too, since it is spawned detached) — for a caller
|
|
98
140
|
* that wants to prove the group is really gone after a hang kill (`process.kill(-pid, 0)` throws). */
|
|
99
141
|
pid?: number;
|
|
@@ -151,7 +193,9 @@ export declare function manifestFileCount(cmd: string, cwd: string): Promise<num
|
|
|
151
193
|
* selection (OBS-1166: a selected screen's retry rediscovered the whole selection and refused). So the
|
|
152
194
|
* retry is built from the UN-narrowed base command, its own `--` rule, the stranded files as the only
|
|
153
195
|
* positional filters, and an `--exclude` of every completed file; the caller then requires discovery
|
|
154
|
-
* to prove the exact retry set before launch.
|
|
196
|
+
* to prove the exact retry set before launch. OBS-1180: Vitest matches an absolute filter against the
|
|
197
|
+
* module path it resolved through every symlink, so the filters are rooted at the canonical realpath of
|
|
198
|
+
* `cwd` — a symlinked worktree root otherwise filters to an empty discovery. */
|
|
155
199
|
export declare function singleForkRetryCommand(base: string, cwd: string, stranded: readonly string[], completed: readonly string[]): string;
|
|
156
200
|
/** One configured runner execution, and its own collection under the same arguments and environment.
|
|
157
201
|
* The installed runner is trusted (R28 add.1 option B); the nonce catches stale artifacts, not forgery. */
|
|
@@ -1,7 +1,9 @@
|
|
|
1
|
+
import { AsyncLocalStorage } from "node:async_hooks";
|
|
1
2
|
import { createHash, randomBytes } from "node:crypto";
|
|
2
3
|
import { existsSync, mkdtempSync, readFileSync, realpathSync, rmSync, writeFileSync } from "node:fs";
|
|
3
4
|
import { tmpdir } from "node:os";
|
|
4
5
|
import { isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
6
|
+
import { performance } from "node:perf_hooks";
|
|
5
7
|
import { TEST_REPORTER_SOURCE } from "./test-reporter.js";
|
|
6
8
|
import { shq } from "../adapters/types.js";
|
|
7
9
|
import { beginGateEvidence, redactGateOutput } from "./baseline.js";
|
|
@@ -175,13 +177,16 @@ export function readTestReport(path) {
|
|
|
175
177
|
* module killed for a per-file hang never reaches its report.
|
|
176
178
|
*/
|
|
177
179
|
export function verifyManifestReport(opts) {
|
|
178
|
-
const { manifest, nonce, exitCode, report, killedFile, hangBudgetMs } = opts;
|
|
180
|
+
const { manifest, nonce, exitCode, report, killedFile, hangBudgetMs, hangActiveMs, hangWallMs } = opts;
|
|
179
181
|
if (killedFile !== undefined) {
|
|
182
|
+
const service = hangActiveMs !== undefined && hangWallMs !== undefined
|
|
183
|
+
? ` (${Math.round(hangActiveMs)}ms active of ${Math.round(hangWallMs)}ms wall)` : "";
|
|
180
184
|
return {
|
|
181
185
|
kind: "infra",
|
|
182
186
|
pass: false,
|
|
183
|
-
details: `infra hang: "${killedFile}" started and did not complete within its ${hangBudgetMs}ms budget — killed, its process group is gone`,
|
|
184
|
-
meta: { classification: "infra", infra: true, kind: "hang", file: killedFile, hangBudgetMs
|
|
187
|
+
details: `infra hang: "${killedFile}" started and did not complete within its ${hangBudgetMs}ms budget${service} — killed, its process group is gone`,
|
|
188
|
+
meta: { classification: "infra", infra: true, kind: "hang", file: killedFile, hangBudgetMs,
|
|
189
|
+
...(hangActiveMs !== undefined ? { activeMs: hangActiveMs, wallMs: hangWallMs } : {}) },
|
|
185
190
|
};
|
|
186
191
|
}
|
|
187
192
|
if (!report) {
|
|
@@ -334,6 +339,36 @@ export function fileHangBudgetMs(file, baselineDurations, ceilingMs = DEFAULT_FI
|
|
|
334
339
|
const known = timings.find((d) => d.file === file)?.durationMs;
|
|
335
340
|
return Math.min(ceiling, known === undefined ? FILE_HANG_SLACK * longest : Math.max(FILE_HANG_SLACK * known, longest));
|
|
336
341
|
}
|
|
342
|
+
/**
|
|
343
|
+
* OBS-953 (+add): a per-file hang budget counted in raw wall time charged a lid-close to the file that
|
|
344
|
+
* was running. Each poll compares how far the wall clock and the monotonic clock advanced since the
|
|
345
|
+
* last one. Wall advancing more than monotonic by over CLOCK_JUMP_SLACK_MS is a DETECTED discontinuity
|
|
346
|
+
* (`host-suspend`): its offset is subtracted from the active elapsed time of every file started before
|
|
347
|
+
* it ended. A poll that arrives overdue while both clocks advanced together is recorded as `unknown`
|
|
348
|
+
* and subtracts nothing — an ambiguous gap never excuses an active hang. Whether the host really slept
|
|
349
|
+
* is not provable from these clocks (a manually set clock jumps the same way); only the offset is.
|
|
350
|
+
*/
|
|
351
|
+
export const CLOCK_JUMP_SLACK_MS = 1_000;
|
|
352
|
+
const systemClocks = { wall: () => Date.now(), mono: () => performance.now() };
|
|
353
|
+
let hangClocks = systemClocks;
|
|
354
|
+
export const setHangClocksForTests = (clocks) => { hangClocks = clocks; };
|
|
355
|
+
export const resetHangClocksForTests = () => { hangClocks = systemClocks; };
|
|
356
|
+
/** A poll gap worth recording, or undefined for an on-time poll. */
|
|
357
|
+
export function classifyPollGap(prev, now, pollMs) {
|
|
358
|
+
const wallMs = now.wall - prev.wall;
|
|
359
|
+
const monoMs = now.mono - prev.mono;
|
|
360
|
+
const gap = { from: prev.wall, to: now.wall, wallMs, monoMs };
|
|
361
|
+
if (wallMs - monoMs > CLOCK_JUMP_SLACK_MS)
|
|
362
|
+
return { kind: "host-suspend", ...gap, subtractedMs: wallMs - monoMs };
|
|
363
|
+
if (monoMs > pollMs + CLOCK_JUMP_SLACK_MS)
|
|
364
|
+
return { kind: "unknown", ...gap, subtractedMs: 0 };
|
|
365
|
+
return undefined;
|
|
366
|
+
}
|
|
367
|
+
/** The detected suspend a file started at `startedAt` sat through. */
|
|
368
|
+
const suspendedSince = (interruptions, startedAt) => interruptions.reduce((sum, i) => sum + Math.min(i.subtractedMs, Math.max(0, i.to - Math.max(i.from, startedAt))), 0);
|
|
369
|
+
// The daemon journals each interruption where it ran (task gate, tip verify); no sink, no journal row.
|
|
370
|
+
const interruptionSink = new AsyncLocalStorage();
|
|
371
|
+
export const runWithInterruptionSink = (sink, run) => interruptionSink.run(sink, run);
|
|
337
372
|
/** Supervise the configured command and poll the runner's atomic lifecycle snapshots. Every
|
|
338
373
|
* timeout kills the detached process group, including descendants holding the output pipes. */
|
|
339
374
|
export function runManifestedTest(cmd, cwd, opts) {
|
|
@@ -345,20 +380,35 @@ export function runManifestedTest(cmd, cwd, opts) {
|
|
|
345
380
|
let pid;
|
|
346
381
|
let killedFile;
|
|
347
382
|
let hangBudgetMs;
|
|
383
|
+
let hangActiveMs;
|
|
384
|
+
let hangWallMs;
|
|
348
385
|
let poll;
|
|
386
|
+
const interruptions = [];
|
|
387
|
+
const sink = interruptionSink.getStore();
|
|
388
|
+
let last = { wall: hangClocks.wall(), mono: hangClocks.mono() };
|
|
349
389
|
const checkHang = (atCeiling = false) => {
|
|
390
|
+
const at = { wall: hangClocks.wall(), mono: hangClocks.mono() };
|
|
391
|
+
const gap = classifyPollGap(last, at, pollMs);
|
|
392
|
+
last = at;
|
|
393
|
+
if (gap) {
|
|
394
|
+
interruptions.push(gap);
|
|
395
|
+
sink?.(gap);
|
|
396
|
+
}
|
|
350
397
|
const report = readTestReport(opts.reportPath);
|
|
351
398
|
if (!report || report.nonce !== opts.nonce)
|
|
352
399
|
return;
|
|
353
|
-
const now = Date.now();
|
|
354
400
|
for (const file of opts.manifest) {
|
|
355
401
|
const startedAt = report.started[file];
|
|
356
402
|
if (startedAt === undefined || file in report.completed)
|
|
357
403
|
continue;
|
|
358
404
|
const budget = fileHangBudgetMs(file, opts.baselineDurations, overallCeilingMs, opts.longestFile);
|
|
359
|
-
|
|
405
|
+
const wallMs = at.wall - startedAt;
|
|
406
|
+
const activeMs = wallMs - suspendedSince(interruptions, startedAt);
|
|
407
|
+
if (activeMs >= budget || (atCeiling && budget === overallCeilingMs)) {
|
|
360
408
|
killedFile = file;
|
|
361
409
|
hangBudgetMs = budget;
|
|
410
|
+
hangActiveMs = activeMs;
|
|
411
|
+
hangWallMs = wallMs;
|
|
362
412
|
if (!atCeiling)
|
|
363
413
|
controller.abort();
|
|
364
414
|
return;
|
|
@@ -372,6 +422,7 @@ export function runManifestedTest(cmd, cwd, opts) {
|
|
|
372
422
|
pid = childPid;
|
|
373
423
|
// Neither the per-file clocks nor the command ceiling includes lease queue time.
|
|
374
424
|
clearInterval(poll);
|
|
425
|
+
last = { wall: hangClocks.wall(), mono: hangClocks.mono() };
|
|
375
426
|
poll = setInterval(checkHang, pollMs);
|
|
376
427
|
},
|
|
377
428
|
}).then((result) => {
|
|
@@ -380,7 +431,7 @@ export function runManifestedTest(cmd, cwd, opts) {
|
|
|
380
431
|
evidenceReceipt, evidenceReceipts: [...evidence.history, evidenceReceipt],
|
|
381
432
|
exitCode: result.signalExit ? undefined : result.code,
|
|
382
433
|
stdout: result.stdout, stderr: result.stderr,
|
|
383
|
-
report: readTestReport(opts.reportPath), killedFile, hangBudgetMs, pid,
|
|
434
|
+
report: readTestReport(opts.reportPath), killedFile, hangBudgetMs, hangActiveMs, hangWallMs, interruptions, pid,
|
|
384
435
|
};
|
|
385
436
|
}).catch((error) => {
|
|
386
437
|
if (error instanceof Error) {
|
|
@@ -492,10 +543,17 @@ function strandedSingleForkFiles(files, nonce, run) {
|
|
|
492
543
|
* selection (OBS-1166: a selected screen's retry rediscovered the whole selection and refused). So the
|
|
493
544
|
* retry is built from the UN-narrowed base command, its own `--` rule, the stranded files as the only
|
|
494
545
|
* positional filters, and an `--exclude` of every completed file; the caller then requires discovery
|
|
495
|
-
* to prove the exact retry set before launch.
|
|
546
|
+
* to prove the exact retry set before launch. OBS-1180: Vitest matches an absolute filter against the
|
|
547
|
+
* module path it resolved through every symlink, so the filters are rooted at the canonical realpath of
|
|
548
|
+
* `cwd` — a symlinked worktree root otherwise filters to an empty discovery. */
|
|
496
549
|
export function singleForkRetryCommand(base, cwd, stranded, completed) {
|
|
550
|
+
let root = cwd;
|
|
551
|
+
try {
|
|
552
|
+
root = realpathSync(cwd);
|
|
553
|
+
}
|
|
554
|
+
catch { /* unreadable cwd — the exact rediscovery below fails closed */ }
|
|
497
555
|
const excluded = completed.map(f => `--exclude=${shq(f.replace(/[\\*?[\]{}()!+@]/g, "\\$&"))}`).join(" ");
|
|
498
|
-
return `${base}${runnerInvocation(base, cwd).separator} ${stranded.map(f => shq(join(
|
|
556
|
+
return `${base}${runnerInvocation(base, cwd).separator} ${stranded.map(f => shq(join(root, f))).join(" ")} ${excluded}`;
|
|
499
557
|
}
|
|
500
558
|
/** One configured runner execution, and its own collection under the same arguments and environment.
|
|
501
559
|
* The installed runner is trusted (R28 add.1 option B); the nonce catches stale artifacts, not forgery. */
|
|
@@ -532,8 +590,10 @@ export async function evaluateManifestedTest(cmd, cwd, opts) {
|
|
|
532
590
|
pollMs: 20,
|
|
533
591
|
});
|
|
534
592
|
let verdict = verifyManifestReport({ manifest: files, nonce, exitCode: invoked.exitCode,
|
|
535
|
-
report: invoked.report, killedFile: invoked.killedFile, hangBudgetMs: invoked.hangBudgetMs
|
|
593
|
+
report: invoked.report, killedFile: invoked.killedFile, hangBudgetMs: invoked.hangBudgetMs,
|
|
594
|
+
hangActiveMs: invoked.hangActiveMs, hangWallMs: invoked.hangWallMs });
|
|
536
595
|
evidenceReceipts.push(...invoked.evidenceReceipts);
|
|
596
|
+
const interruptions = [...invoked.interruptions];
|
|
537
597
|
const stranded = strandedSingleForkFiles(files, nonce, invoked);
|
|
538
598
|
if (stranded) {
|
|
539
599
|
const first = invoked.report;
|
|
@@ -556,8 +616,10 @@ export async function evaluateManifestedTest(cmd, cwd, opts) {
|
|
|
556
616
|
longestFile: opts.longestFile, overallCeilingMs: opts.overallCeilingMs ?? DEFAULT_FILE_HANG_BUDGET_MS, pollMs: 20,
|
|
557
617
|
});
|
|
558
618
|
evidenceReceipts.push(...invoked.evidenceReceipts);
|
|
619
|
+
interruptions.push(...invoked.interruptions);
|
|
559
620
|
verdict = verifyManifestReport({ manifest: stranded, nonce, exitCode: invoked.exitCode,
|
|
560
|
-
report: invoked.report, killedFile: invoked.killedFile, hangBudgetMs: invoked.hangBudgetMs
|
|
621
|
+
report: invoked.report, killedFile: invoked.killedFile, hangBudgetMs: invoked.hangBudgetMs,
|
|
622
|
+
hangActiveMs: invoked.hangActiveMs, hangWallMs: invoked.hangWallMs });
|
|
561
623
|
// An all-skipped retry may contribute lifecycle accounting, but only the combined manifest
|
|
562
624
|
// can establish executed success. Never rewrite either invocation's persisted certificate.
|
|
563
625
|
if (verdict.pass || verdict.meta.noExecutedModules) {
|
|
@@ -589,11 +651,15 @@ export async function evaluateManifestedTest(cmd, cwd, opts) {
|
|
|
589
651
|
+ [...runnerErrors,
|
|
590
652
|
stdoutTail.toString(), stderrTail.toString()].filter(Boolean).map(text => `\n${text}`).join("")
|
|
591
653
|
: "";
|
|
654
|
+
// The interruption is reported beside the verdict, never folded into it or into raw wall service.
|
|
655
|
+
const interrupted = interruptions.length
|
|
656
|
+
? `\nhost interruptions: ${interruptions.map(i => `${i.kind} ${Math.round(i.wallMs)}ms wall / ${Math.round(i.monoMs)}ms monotonic, ${Math.round(i.subtractedMs)}ms subtracted`).join("; ")}`
|
|
657
|
+
: "";
|
|
592
658
|
return { evidenceReceipt, evidenceReceipts, pass: verdict.pass, kind: verdict.kind,
|
|
593
|
-
details: verdict.details + diagnostics,
|
|
659
|
+
details: verdict.details + interrupted + diagnostics,
|
|
594
660
|
classification: verdict.meta.classification,
|
|
595
661
|
meta: { ...verdict.meta, nonce, manifest: files, manifestPath, listingCommand: invocation.listing, verification,
|
|
596
|
-
...(recovery ? { recovery, retryable: false } : {}),
|
|
662
|
+
...(recovery ? { recovery, retryable: false } : {}), ...(interruptions.length ? { interruptions } : {}),
|
|
597
663
|
spawnedCommand, processExit: invoked.exitCode, pid: invoked.pid, stdoutPath, stderrPath },
|
|
598
664
|
exitCode: invoked.exitCode ?? -1, reportPath };
|
|
599
665
|
}
|
package/dist/graph/schema.d.ts
CHANGED
|
@@ -4,12 +4,14 @@ export declare const GRAPH_ROUTING_MODES: readonly ["partner-led", "risk-based",
|
|
|
4
4
|
export declare const STATUSES: readonly ["pending", "running", "gated", "failed", "done", "human"];
|
|
5
5
|
export declare const GATE_NAMES: readonly ["build", "test", "lint", "evidence", "scope", "acceptance", "review"];
|
|
6
6
|
export declare const TIERS: readonly ["cheap", "mid", "frontier"];
|
|
7
|
+
export declare const EFFORTS: readonly ["low", "medium", "high"];
|
|
7
8
|
export declare const SPEC_SOURCES: readonly ["speckit", "gsd", "prd", "native"];
|
|
8
9
|
export declare const ORACLES: readonly ["command", "test", "judge"];
|
|
9
10
|
export type Shape = (typeof SHAPES)[number];
|
|
10
11
|
export type TaskStatus = (typeof STATUSES)[number];
|
|
11
12
|
export type GateName = (typeof GATE_NAMES)[number];
|
|
12
13
|
export type Oracle = (typeof ORACLES)[number];
|
|
14
|
+
export type Effort = (typeof EFFORTS)[number];
|
|
13
15
|
export type SpecSource = (typeof SPEC_SOURCES)[number];
|
|
14
16
|
export declare const AcceptanceItemSchema: z.ZodUnion<readonly [z.ZodString, z.ZodObject<{
|
|
15
17
|
oracle: z.ZodLiteral<"command">;
|
package/dist/graph/schema.js
CHANGED
|
@@ -7,6 +7,8 @@ export const STATUSES = ["pending", "running", "gated", "failed", "done", "human
|
|
|
7
7
|
export const GATE_NAMES = ["build", "test", "lint", "evidence", "scope", "acceptance", "review"];
|
|
8
8
|
const MANDATORY_GATES = ["build", "test", "lint", "evidence", "scope"];
|
|
9
9
|
export const TIERS = ["cheap", "mid", "frontier"];
|
|
10
|
+
// OBS-1182: launch effort — channel metadata beside tier, never part of model identity or channelKey.
|
|
11
|
+
export const EFFORTS = ["low", "medium", "high"];
|
|
10
12
|
export const SPEC_SOURCES = ["speckit", "gsd", "prd", "native"];
|
|
11
13
|
// v1.19 acceptance oracles: command (exit code), test (named test), judge (LLM, free-text rubric).
|
|
12
14
|
// A plain string is the read-old/write-new compat form — semantically a judge oracle (spec §2).
|
package/dist/plan/scope.js
CHANGED
|
@@ -169,7 +169,7 @@ function bindCandidate(candidate, channels) {
|
|
|
169
169
|
`(doctor found: ${channels.map((ch) => `${ch.adapter}:${ch.model}`).join(", ") || "(nothing)"}) — ` +
|
|
170
170
|
"re-run scope --preview and confirm again");
|
|
171
171
|
}
|
|
172
|
-
return { adapter: c.adapter, model: c.model, channel: c.channel, tier: c.tier };
|
|
172
|
+
return { adapter: c.adapter, model: c.model, channel: c.channel, tier: c.tier, ...(c.effort ? { effort: c.effort } : {}) };
|
|
173
173
|
}
|
|
174
174
|
// Adapter-level probes establish the current installation/auth state, but shipped adapters do not
|
|
175
175
|
// return per-model verdicts from probe(). Preserve cached verdicts only where that fresh snapshot is
|
|
@@ -202,7 +202,7 @@ export async function scopeIntent(intentFile, repoRoot, options) {
|
|
|
202
202
|
driver, name: `scope-${name}-${attempts}-${adapter.id}`, label: "SCOPE",
|
|
203
203
|
keep: options.cfg.visibility.keepPanes === "forever",
|
|
204
204
|
} : undefined;
|
|
205
|
-
const draft = extractDraft(await runLlm(adapter, assignment.model, prompt, repoRoot, via));
|
|
205
|
+
const draft = extractDraft(await runLlm(adapter, assignment.model, prompt, repoRoot, via, undefined, assignment.effort));
|
|
206
206
|
let tasks;
|
|
207
207
|
try {
|
|
208
208
|
tasks = validateDraft(draft);
|
|
@@ -11,6 +11,16 @@ export declare const routingModelProvider: typeof modelProvider;
|
|
|
11
11
|
export declare const modelRouteIdentity: (model: string, fallback?: string) => string;
|
|
12
12
|
export declare const channelRouteIdentity: (key: string, fallback?: string) => string;
|
|
13
13
|
export declare function routingEntrySeatLines(cfg: TickmarkrConfig): string[];
|
|
14
|
+
export declare const observedIdentity: (health: Record<string, AuthHealth> | null | undefined, adapter: string, model: string) => string | undefined;
|
|
15
|
+
export declare const observedSeat: (health: Record<string, AuthHealth> | null | undefined, adapter: string, model: string) => {
|
|
16
|
+
adapter: string;
|
|
17
|
+
model: string;
|
|
18
|
+
identity: string;
|
|
19
|
+
} | {
|
|
20
|
+
adapter: string;
|
|
21
|
+
model: string;
|
|
22
|
+
identity?: undefined;
|
|
23
|
+
};
|
|
14
24
|
export declare function excludedChannels(cfg: TickmarkrConfig, adapters: {
|
|
15
25
|
id: string;
|
|
16
26
|
}[] | string[], health: Record<string, AuthHealth>): {
|
|
@@ -59,6 +69,14 @@ export interface DenyPreferCollision {
|
|
|
59
69
|
detail: string;
|
|
60
70
|
disallowed: Disallowed;
|
|
61
71
|
}
|
|
62
|
-
export declare function preferEntryDenied(p: string, cfg: TickmarkrConfig): Disallowed | null;
|
|
63
|
-
export declare function denyPreferCollisions(cfg: TickmarkrConfig, shapes?: Iterable<string>): DenyPreferCollision[];
|
|
72
|
+
export declare function preferEntryDenied(p: string, cfg: TickmarkrConfig, health?: Record<string, AuthHealth> | null): Disallowed | null;
|
|
73
|
+
export declare function denyPreferCollisions(cfg: TickmarkrConfig, shapes?: Iterable<string>, health?: Record<string, AuthHealth> | null): DenyPreferCollision[];
|
|
74
|
+
export interface DeadPoolEntry {
|
|
75
|
+
shape: string;
|
|
76
|
+
entry: string;
|
|
77
|
+
disallowed: Disallowed;
|
|
78
|
+
admitted: string[];
|
|
79
|
+
}
|
|
80
|
+
export declare function deadPoolEntries(cfg: TickmarkrConfig, shapes?: Iterable<string>, health?: Record<string, AuthHealth> | null): DeadPoolEntry[];
|
|
81
|
+
export declare function deadPoolEntryLine({ shape, entry, disallowed, admitted }: DeadPoolEntry): string;
|
|
64
82
|
export declare function denyPreferCollisionLine({ kind, shape, detail, disallowed }: DenyPreferCollision): string;
|
package/dist/route/preference.js
CHANGED
|
@@ -47,6 +47,13 @@ export function routingEntrySeatLines(cfg) {
|
|
|
47
47
|
add("routing.deny.workers.models", cfg.routing.deny?.workers?.models, ["worker"]);
|
|
48
48
|
return lines;
|
|
49
49
|
}
|
|
50
|
+
// OBS-1143: the probed identity doctor cached for adapter:model — the one discoverChannels routes
|
|
51
|
+
// with. Absent ⇒ unknown, and the deny matcher stays conservative (alias-family match).
|
|
52
|
+
export const observedIdentity = (health, adapter, model) => health?.[adapter]?.modelAuth?.[model]?.identity ?? health?.[adapter]?.modelIdentities?.[model];
|
|
53
|
+
export const observedSeat = (health, adapter, model) => {
|
|
54
|
+
const identity = observedIdentity(health, adapter, model);
|
|
55
|
+
return identity ? { adapter, model, identity } : { adapter, model };
|
|
56
|
+
};
|
|
50
57
|
const adapterIds = (adapters) => typeof adapters[0] === "string" ? adapters : adapters.map((a) => a.id);
|
|
51
58
|
export function excludedChannels(cfg, adapters, health) {
|
|
52
59
|
const { allow, deny } = cfg.routing;
|
|
@@ -57,9 +64,7 @@ export function excludedChannels(cfg, adapters, health) {
|
|
|
57
64
|
if (!health[id]?.installed || !health[id]?.authed)
|
|
58
65
|
continue;
|
|
59
66
|
for (const c of channelsFromConfig(id, cfg)) {
|
|
60
|
-
const
|
|
61
|
-
const chan = identity ? { ...c, identity } : c;
|
|
62
|
-
const d = disallowedBy(chan, cfg.routing);
|
|
67
|
+
const d = disallowedBy(observedSeat(health, c.adapter, c.model), cfg.routing);
|
|
63
68
|
if (d)
|
|
64
69
|
out.push({ key: channelKey(c), d });
|
|
65
70
|
}
|
|
@@ -175,13 +180,20 @@ const disallowedFromPreferError = (msg) => {
|
|
|
175
180
|
const m = msg.match(/prefer entry .+ is disallowed by routing\.(deny|allow) \(([^)]+)\)/);
|
|
176
181
|
return m ? { by: m[1], entry: m[2] } : null;
|
|
177
182
|
};
|
|
178
|
-
|
|
183
|
+
// OBS-1143: `health` is doctor's CACHED verdict (never a probe); its identities ride the channels
|
|
184
|
+
// route() reads them from, so the preflight answers exactly as the router would.
|
|
185
|
+
export function preferEntryDenied(p, cfg, health) {
|
|
179
186
|
if (!cfg.routing.allow && !cfg.routing.deny)
|
|
180
187
|
return null;
|
|
181
188
|
const probe = structuredClone(cfg);
|
|
182
189
|
probe.routing.map = { ...probe.routing.map, [PREFLIGHT_SHAPE]: { prefer: [p] } };
|
|
190
|
+
const observed = Object.keys(cfg.tiers).flatMap((id) => channelsFromConfig(id, cfg))
|
|
191
|
+
.flatMap((c) => {
|
|
192
|
+
const identity = observedIdentity(health, c.adapter, c.model);
|
|
193
|
+
return identity ? [{ ...c, identity }] : [];
|
|
194
|
+
});
|
|
183
195
|
try {
|
|
184
|
-
route(preflightTask, probe,
|
|
196
|
+
route(preflightTask, probe, observed);
|
|
185
197
|
return null;
|
|
186
198
|
}
|
|
187
199
|
catch (e) {
|
|
@@ -194,7 +206,9 @@ export function preferEntryDenied(p, cfg) {
|
|
|
194
206
|
// route — resume hands it the loaded graph's shape set, so a collision on a shape no resumed task
|
|
195
207
|
// uses can no longer refuse the only crash-recovery path. Omitted ⇒ the whole routing map: doctor
|
|
196
208
|
// audits the config itself, which has no graph to be scoped by.
|
|
197
|
-
|
|
209
|
+
// OBS-1143: `health` (doctor's cached verdict) supplies each alias's observed identity; omitted or
|
|
210
|
+
// unrecorded ⇒ unknown, which stays conservative.
|
|
211
|
+
export function denyPreferCollisions(cfg, shapes, health) {
|
|
198
212
|
if (!cfg.routing.allow && !cfg.routing.deny)
|
|
199
213
|
return [];
|
|
200
214
|
const inGraph = shapes === undefined ? undefined : new Set(shapes);
|
|
@@ -203,7 +217,7 @@ export function denyPreferCollisions(cfg, shapes) {
|
|
|
203
217
|
if (inGraph && !inGraph.has(shape))
|
|
204
218
|
continue;
|
|
205
219
|
if (entry.pin) {
|
|
206
|
-
const d = disallowedBy(
|
|
220
|
+
const d = disallowedBy(observedSeat(health, entry.pin.via, entry.pin.model), cfg.routing);
|
|
207
221
|
if (d) {
|
|
208
222
|
out.push({
|
|
209
223
|
kind: "pin",
|
|
@@ -214,20 +228,17 @@ export function denyPreferCollisions(cfg, shapes) {
|
|
|
214
228
|
}
|
|
215
229
|
}
|
|
216
230
|
const prefer = entry.prefer ?? [];
|
|
217
|
-
if (prefer.length && prefer.every((p) => preferEntryDenied(p, cfg) !== null)) {
|
|
231
|
+
if (prefer.length && prefer.every((p) => preferEntryDenied(p, cfg, health) !== null)) {
|
|
218
232
|
out.push({
|
|
219
233
|
kind: "prefer",
|
|
220
234
|
shape,
|
|
221
235
|
detail: prefer.join(" > "),
|
|
222
|
-
disallowed: preferEntryDenied(prefer[0], cfg),
|
|
236
|
+
disallowed: preferEntryDenied(prefer[0], cfg, health),
|
|
223
237
|
});
|
|
224
238
|
}
|
|
225
239
|
const pool = entry.pool;
|
|
226
240
|
if (pool) {
|
|
227
|
-
const denied = pool.channels.map((p) =>
|
|
228
|
-
const i = p.indexOf(":");
|
|
229
|
-
return disallowedBy({ adapter: p.slice(0, i), model: p.slice(i + 1) }, cfg.routing);
|
|
230
|
-
});
|
|
241
|
+
const denied = pool.channels.map((p) => poolEntryDisallowed(p, cfg, health));
|
|
231
242
|
// only a FULLY dead pool collides — a partial deny still leaves live members to route
|
|
232
243
|
if (denied.every((d) => d !== null)) {
|
|
233
244
|
out.push({ kind: "pool", shape, detail: pool.channels.join(" > "), disallowed: denied[0] });
|
|
@@ -236,6 +247,30 @@ export function denyPreferCollisions(cfg, shapes) {
|
|
|
236
247
|
}
|
|
237
248
|
return out;
|
|
238
249
|
}
|
|
250
|
+
const poolEntryDisallowed = (p, cfg, health) => {
|
|
251
|
+
const i = p.indexOf(":");
|
|
252
|
+
return disallowedBy(observedSeat(health, p.slice(0, i), p.slice(i + 1)), cfg.routing);
|
|
253
|
+
};
|
|
254
|
+
// OBS-1144: every disallowed entry a pool carries, beside the admitted remainder the router routes
|
|
255
|
+
// instead — the router skips the entry; an empty remainder is the exhausted pool it refuses.
|
|
256
|
+
export function deadPoolEntries(cfg, shapes, health) {
|
|
257
|
+
if (!cfg.routing.allow && !cfg.routing.deny)
|
|
258
|
+
return [];
|
|
259
|
+
const inGraph = shapes === undefined ? undefined : new Set(shapes);
|
|
260
|
+
return Object.entries(cfg.routing.map).flatMap(([shape, entry]) => {
|
|
261
|
+
if (!entry.pool || (inGraph && !inGraph.has(shape)))
|
|
262
|
+
return [];
|
|
263
|
+
const judged = entry.pool.channels.map((p) => ({ p, d: poolEntryDisallowed(p, cfg, health) }));
|
|
264
|
+
const admitted = judged.filter(({ d }) => d === null).map(({ p }) => p);
|
|
265
|
+
return judged.flatMap(({ p, d }) => (d ? [{ shape, entry: p, disallowed: d, admitted }] : []));
|
|
266
|
+
});
|
|
267
|
+
}
|
|
268
|
+
export function deadPoolEntryLine({ shape, entry, disallowed, admitted }) {
|
|
269
|
+
const verdict = admitted.length
|
|
270
|
+
? `skipped — routes the admitted remainder ${admitted.join(" > ")}`
|
|
271
|
+
: "the pool is exhausted — no admitted entry remains, so the shape is unroutable";
|
|
272
|
+
return `routing.map.${shape}.pool entry ${entry} is disallowed by routing.${disallowed.by} (${disallowed.entry}) — ${verdict}`;
|
|
273
|
+
}
|
|
239
274
|
export function denyPreferCollisionLine({ kind, shape, detail, disallowed }) {
|
|
240
275
|
const verb = kind === "pin" ? "is disallowed" : "fully disallowed";
|
|
241
276
|
return `deny∩prefer: routing.map.${shape}.${kind} ${detail} ${verb} by routing.${disallowed.by} (${disallowed.entry}) — remove the ${disallowed.by} entry or adjust ${kind}`;
|
package/dist/route/router.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { type Assignment, type BillingChannel } from "../adapters/types.js";
|
|
2
|
-
import { type TickmarkrConfig } from "../config/config.js";
|
|
2
|
+
import { type TickmarkrConfig, type Tier } from "../config/config.js";
|
|
3
3
|
import type { Task } from "../graph/schema.js";
|
|
4
4
|
import { type RoutingProfile } from "./profile.js";
|
|
5
5
|
export type LadderStep = "retry" | "escalate" | "consult" | "human";
|
|
@@ -44,6 +44,17 @@ export declare function rankPreferredChannels<C extends ChannelSeat>(channels: r
|
|
|
44
44
|
}): C[];
|
|
45
45
|
/** Review preserves its established tier-then-cost order inside a preference band. */
|
|
46
46
|
export declare const reviewPreferenceTieBreak: (a: BillingChannel, b: BillingChannel) => number;
|
|
47
|
+
export declare function resolvedFloor(task: Task, cfg: TickmarkrConfig): {
|
|
48
|
+
tier: Tier;
|
|
49
|
+
source: "task" | "config";
|
|
50
|
+
} | undefined;
|
|
51
|
+
export declare const taskPool: (task: Task, cfg: TickmarkrConfig) => {
|
|
52
|
+
mode: "any" | "ordered";
|
|
53
|
+
channels: string[];
|
|
54
|
+
} | undefined;
|
|
55
|
+
/** Park-reason suffix naming the pool an exhausted re-dispatch stayed inside; empty when unpooled. */
|
|
56
|
+
export declare const poolExhaustion: (task: Task, cfg: TickmarkrConfig) => string;
|
|
57
|
+
export declare const inTaskPool: (task: Task, cfg: TickmarkrConfig, key: string) => boolean;
|
|
47
58
|
export declare function route(task: Task, cfg: TickmarkrConfig, channels: BillingChannel[], profile?: RoutingProfile, preferCtx?: RoutingPreferContext, exclude?: ReadonlySet<string>, exploreCtx?: ExploreContext): Route;
|
|
48
59
|
export declare function nextChannel(current: Assignment, task: Task, cfg: TickmarkrConfig, channels: BillingChannel[], tried: string[], profile?: RoutingProfile, exclude?: ReadonlySet<string>): Assignment | null;
|
|
49
60
|
export type ClimbPick = Assignment & {
|