tickmarkr 2.5.4 → 2.5.5
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/registry.js +6 -1
- package/dist/adapters/types.d.ts +3 -0
- package/dist/cli/commands/approve.d.ts +1 -0
- package/dist/cli/commands/approve.js +54 -6
- package/dist/cli/commands/doctor.js +44 -29
- package/dist/cli/commands/fleet.js +195 -33
- package/dist/cli/commands/init.js +196 -6
- package/dist/cli/commands/resume.js +4 -2
- package/dist/cli/commands/run.js +11 -2
- package/dist/cli/commands/verify.js +1 -1
- package/dist/cli/help.d.ts +2 -0
- package/dist/cli/help.js +3 -1
- package/dist/config/config.d.ts +14 -8
- package/dist/config/config.js +29 -26
- package/dist/config/fleet-overlay.d.ts +3 -9
- package/dist/config/fleet-overlay.js +68 -11
- package/dist/config/fleet-why.d.ts +7 -0
- package/dist/config/fleet-why.js +5 -0
- package/dist/drivers/index.d.ts +15 -1
- package/dist/drivers/index.js +38 -10
- package/dist/drivers/orca.d.ts +99 -10
- package/dist/drivers/orca.js +586 -97
- package/dist/gates/baseline.d.ts +3 -0
- package/dist/gates/baseline.js +2 -1
- package/dist/gates/cache.d.ts +100 -0
- package/dist/gates/cache.js +389 -0
- package/dist/gates/run-gates.d.ts +3 -0
- package/dist/gates/run-gates.js +117 -10
- package/dist/gates/test-manifest.d.ts +99 -0
- package/dist/gates/test-manifest.js +389 -0
- package/dist/gates/test-reporter.d.ts +4 -0
- package/dist/gates/test-reporter.js +49 -0
- package/dist/route/preference.d.ts +22 -1
- package/dist/route/preference.js +123 -25
- package/dist/route/router.js +31 -6
- package/dist/run/daemon.d.ts +13 -0
- package/dist/run/daemon.js +160 -70
- package/dist/run/git.d.ts +10 -1
- package/dist/run/git.js +43 -9
- package/dist/run/journal.d.ts +14 -2
- package/dist/run/journal.js +78 -10
- package/dist/run/lease.d.ts +14 -0
- package/dist/run/lease.js +87 -0
- package/dist/run/merge.d.ts +2 -0
- package/dist/run/merge.js +91 -3
- package/dist/run/operator-state.d.ts +11 -0
- package/dist/run/operator-state.js +17 -3
- package/dist/tui/cockpit/board.d.ts +96 -0
- package/dist/tui/cockpit/board.js +346 -0
- package/dist/tui/cockpit/decision-actions.js +2 -0
- package/dist/tui/cockpit/layout.d.ts +5 -1
- package/dist/tui/cockpit/layout.js +8 -3
- package/dist/tui/cockpit/live-runtime.js +71 -21
- package/dist/tui/cockpit/run-view.d.ts +7 -5
- package/dist/tui/cockpit/run-view.js +12 -11
- package/dist/tui/ink/fleet-app.d.ts +41 -27
- package/dist/tui/ink/fleet-app.js +204 -31
- package/package.json +1 -1
- package/skills/tickmarkr-overseer/SKILL.md +173 -113
package/dist/route/router.js
CHANGED
|
@@ -103,6 +103,32 @@ function withoutExcluded(channels, exclude) {
|
|
|
103
103
|
// OBS-57: in-run demotion after consecutive no-trailer windows — route around poisoned channels for the rest of the run.
|
|
104
104
|
return channels.filter((c) => !exclude.has(channelKey(c)));
|
|
105
105
|
}
|
|
106
|
+
// Pools spread providers before tier, independently within each marginal-cost run. Stable
|
|
107
|
+
// cost sorting preserves declaration order when forming groups; gateways share the served
|
|
108
|
+
// model's provider, so adding a second transport does not buy that provider another turn.
|
|
109
|
+
function rankAnyPool(live, task) {
|
|
110
|
+
const sorted = [...live].sort((a, b) => marginalCostRank(a) - marginalCostRank(b));
|
|
111
|
+
const out = [];
|
|
112
|
+
for (let i = 0; i < sorted.length;) {
|
|
113
|
+
let j = i + 1;
|
|
114
|
+
while (j < sorted.length && marginalCostRank(sorted[j]) === marginalCostRank(sorted[i]))
|
|
115
|
+
j++;
|
|
116
|
+
const groups = new Map();
|
|
117
|
+
for (const c of sorted.slice(i, j)) {
|
|
118
|
+
const provider = routingModelProvider(c.model, c.vendor);
|
|
119
|
+
const group = groups.get(provider) ?? [];
|
|
120
|
+
group.push(c);
|
|
121
|
+
groups.set(provider, group);
|
|
122
|
+
}
|
|
123
|
+
const ordered = [...groups.values()];
|
|
124
|
+
const k = spreadOffset(task) % ordered.length;
|
|
125
|
+
for (const group of [...ordered.slice(k), ...ordered.slice(0, k)]) {
|
|
126
|
+
out.push(...group.sort((a, b) => TIER_RANK[a.tier] - TIER_RANK[b.tier]));
|
|
127
|
+
}
|
|
128
|
+
i = j;
|
|
129
|
+
}
|
|
130
|
+
return out;
|
|
131
|
+
}
|
|
106
132
|
const maybeSlaLint = (lints, task, profile, slaMinutes, c) => {
|
|
107
133
|
if (slaMinutes === undefined || !profile)
|
|
108
134
|
return;
|
|
@@ -206,21 +232,20 @@ export function route(task, cfg, channels, profile, preferCtx, exclude, exploreC
|
|
|
206
232
|
// live preserves pool declaration order — ordered mode and any-mode stable-sort ties depend on it
|
|
207
233
|
const live = pool.channels
|
|
208
234
|
.map((id) => channels.find((c) => channelKey(c) === id))
|
|
209
|
-
.filter((c) => c !== undefined);
|
|
235
|
+
.filter((c) => c !== undefined && (!taskFloor || TIER_RANK[c.tier] >= TIER_RANK[taskFloor]));
|
|
210
236
|
if (!live.length) {
|
|
211
|
-
throw new RoutingError(`${task.id}: routing.map.${task.shape}.pool has no live channel — declared: ${pool.channels.join(", ")}; doctor found: ${channels.map(channelKey).join(", ") || "(nothing)"}`);
|
|
237
|
+
throw new RoutingError(`${task.id}: routing.map.${task.shape}.pool has no live channel${taskFloor ? ` at task floor ${taskFloor}` : ""} — declared: ${pool.channels.join(", ")}; doctor found: ${channels.map(channelKey).join(", ") || "(nothing)"}`);
|
|
212
238
|
}
|
|
213
|
-
//
|
|
214
|
-
// upgrade path is feeding the pool-filtered set through the learned path below.
|
|
239
|
+
// No learned/explore inside pools: any spreads deterministically; ordered keeps declaration order.
|
|
215
240
|
const chosen = pool.mode === "ordered"
|
|
216
241
|
? live[0]
|
|
217
|
-
:
|
|
242
|
+
: rankAnyPool(live, task)[0];
|
|
218
243
|
lintMapPinFloor(chosen.tier, "pool");
|
|
219
244
|
maybeSlaLint(lints, task, profile, slaMinutes, chosen);
|
|
220
245
|
return { assignment: toAssignment(chosen), ladder: ladderFor(task, entry), lints, provenance: `${degraded}pool ${pool.mode} ${channelKey(chosen)} (config routing.map)` };
|
|
221
246
|
}
|
|
222
247
|
const baseTier = floor ?? "cheap";
|
|
223
|
-
//
|
|
248
|
+
// A task floor is hard on pool and floor/auto paths; the map-pin branch above stays supreme.
|
|
224
249
|
const minTier = taskFloor && TIER_RANK[taskFloor] > TIER_RANK[baseTier] ? taskFloor : baseTier;
|
|
225
250
|
if (prefActive)
|
|
226
251
|
for (const p of prefer ?? [])
|
package/dist/run/daemon.d.ts
CHANGED
|
@@ -126,6 +126,19 @@ export declare const WORKER_STARTUP_WINDOW_BYTES: number;
|
|
|
126
126
|
/** Test seam — exercises both sides of the startup boundary without a production-length fixture. */
|
|
127
127
|
export declare function setWorkerStartupWindowMsForTests(ms: number): void;
|
|
128
128
|
export declare function resetWorkerStartupWindowMsForTests(): void;
|
|
129
|
+
export interface StartupFailureEvidence {
|
|
130
|
+
matchedBytes: string;
|
|
131
|
+
offset: number;
|
|
132
|
+
row: string;
|
|
133
|
+
rowNumber: number;
|
|
134
|
+
}
|
|
135
|
+
/** One seat, one startup prefix. Once execution or its input box is seen, later panes are ineligible. */
|
|
136
|
+
export declare class StartupFailureDetector {
|
|
137
|
+
private readonly adapter;
|
|
138
|
+
private closed;
|
|
139
|
+
constructor(adapter: Pick<WorkerAdapter, "inputBox" | "harnessBannerRows">);
|
|
140
|
+
sample(output: string, launchedAt: number | undefined): StartupFailureEvidence | undefined;
|
|
141
|
+
}
|
|
129
142
|
/** Test seam — shrink the quota-banner silence gate without minute-long sleeps. */
|
|
130
143
|
export declare function setQuotaBannerSilentMsForTests(ms: number): void;
|
|
131
144
|
export declare function resetQuotaBannerSilentMsForTests(): void;
|
package/dist/run/daemon.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { commandLeaseEnvironment, CommandLeases, currentCommandLeaseToken, isRunnerCommand, runWithCommandLease, withCommandLease } from "./lease.js";
|
|
1
2
|
import { execFileSync, spawn } from "node:child_process";
|
|
2
3
|
import { createHash, randomBytes } from "node:crypto";
|
|
3
4
|
import { shq } from "../adapters/types.js";
|
|
@@ -9,7 +10,7 @@ import { fileURLToPath } from "node:url";
|
|
|
9
10
|
import { stringify } from "yaml";
|
|
10
11
|
import { classifyDeadChannel, NO_TRAILER_SUMMARY, trailerPattern, UNPARSEABLE_TRAILER_SUMMARY, writePrompt } from "../adapters/prompt.js";
|
|
11
12
|
import { allAdapters, getAdapter, probeAll, readDoctor, rolePools } from "../adapters/registry.js";
|
|
12
|
-
import { SettledTrailerTracker, addUsage, channelKey, matchesTrustDialog, QUOTA_RE } from "../adapters/types.js";
|
|
13
|
+
import { SettledTrailerTracker, addUsage, channelKey, matchesInputBox, matchesTrustDialog, QUOTA_RE } from "../adapters/types.js";
|
|
13
14
|
import { bannerShell, paneDispatchCommand } from "../brand.js";
|
|
14
15
|
import { collateralHits } from "../compile/collateral.js";
|
|
15
16
|
import { DEFAULT_DIFF_CAP, globalConfigDir, loadConfigWithMode, readOverlayFile, repoOverlayPath, } from "../config/config.js";
|
|
@@ -27,7 +28,7 @@ import { augmentRetryBrief, consult, renderRetryGuidance } from "./consult.js";
|
|
|
27
28
|
import { runEnvironment } from "./environment.js";
|
|
28
29
|
import { cleanupRunWorktrees, deriveForkCap, FORK_CAP_ENV, gitHead, linkNodeModules, npmDependencyInstallCommand, npmDependencyManifestChanged, preserveWorktree, resolvedCapacity, runWithForkBudget, runWithVerificationBudget, sameCapacity, sh, shGit, SUITE_PARENT_ENV, WORKTREE_LAYOUT_CONTRACT, worktreePath } from "./git.js";
|
|
29
30
|
import { runInteractiveSeed } from "./interactive-seed.js";
|
|
30
|
-
import { activeRetryBan, classifyTaskFailure, classifyWorkerResultCause, deferredReviewFindings, engagementComparable, formatPriorFindingEvidence, GATE_FINGERPRINT_CAP, GATE_SATISFIED_RELEASE, identicalGateFailures, isDeferredFinding, journaledFailureBrief, Journal, loadRoutingProfile, newRunId, normalizeGateFailure, outstandingConsultGuidance, outstandingReviewFindings, pendingRechecks, pendingRepairFindings, phaseForGate, readPriorRunEvidence, recordedTaskFailureKind, RECHECK_RELEASE, renderStructuredReviewFinding, repairReachSinceApproval, repairsSinceApproval, reviewRoundsSinceApproval, runHasEnded, structuredFindings, upheldFeedbackByTask } from "./journal.js";
|
|
31
|
+
import { applyScopeAmendments, activeRetryBan, classifyTaskFailure, classifyWorkerResultCause, deferredReviewFindings, engagementComparable, formatPriorFindingEvidence, GATE_FINGERPRINT_CAP, GATE_SATISFIED_RELEASE, identicalGateFailures, isDeferredFinding, journaledFailureBrief, Journal, loadRoutingProfile, newRunId, normalizeGateFailure, outstandingConsultGuidance, outstandingReviewFindings, pendingRechecks, pendingRepairFindings, phaseForGate, readPriorRunEvidence, recordedTaskFailureKind, RECHECK_RELEASE, renderStructuredReviewFinding, repairReachSinceApproval, repairsSinceApproval, reviewRoundsSinceApproval, runHasEnded, structuredFindings, upheldFeedbackByTask } from "./journal.js";
|
|
31
32
|
import { isDiffCapPark, pickReviewer } from "../gates/review.js";
|
|
32
33
|
import { acquireApprovalSerialization, acquireRunLock, isPidLive, releaseRunLock } from "./lock.js";
|
|
33
34
|
import { ensureIntegration, integrationBranch, integrationHead, mergeTask, verifyIntegrationTip } from "./merge.js";
|
|
@@ -379,14 +380,67 @@ export function setWorkerStartupWindowMsForTests(ms) {
|
|
|
379
380
|
export function resetWorkerStartupWindowMsForTests() {
|
|
380
381
|
workerStartupWindowMs = WORKER_STARTUP_WINDOW_MS;
|
|
381
382
|
}
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
383
|
+
/** One seat, one startup prefix. Once execution or its input box is seen, later panes are ineligible. */
|
|
384
|
+
export class StartupFailureDetector {
|
|
385
|
+
adapter;
|
|
386
|
+
closed = false;
|
|
387
|
+
constructor(adapter) {
|
|
388
|
+
this.adapter = adapter;
|
|
389
|
+
}
|
|
390
|
+
sample(output, launchedAt) {
|
|
391
|
+
if (this.closed || launchedAt === undefined || Date.now() - launchedAt > workerStartupWindowMs)
|
|
392
|
+
return;
|
|
393
|
+
// read() returns a tail, not a guaranteed transcript origin. A saturated read cannot
|
|
394
|
+
// establish startup ownership: the first tool frame may already have scrolled out.
|
|
395
|
+
if (Buffer.byteLength(output) >= WORKER_STARTUP_WINDOW_BYTES || output.split("\n").length >= 500) {
|
|
396
|
+
this.closed = true;
|
|
397
|
+
return;
|
|
398
|
+
}
|
|
399
|
+
const bounded = output;
|
|
400
|
+
const rows = bounded.split("\n");
|
|
401
|
+
let inputStart = rows.length;
|
|
402
|
+
const input = this.adapter.inputBox;
|
|
403
|
+
if (input && matchesInputBox(bounded, input)) {
|
|
404
|
+
// Locate the beginning, including multi-row composers whose footer proves the matcher.
|
|
405
|
+
let end = rows.length;
|
|
406
|
+
let low = 1;
|
|
407
|
+
while (low < end) {
|
|
408
|
+
const mid = Math.floor((low + end) / 2);
|
|
409
|
+
if (matchesInputBox(rows.slice(0, mid).join("\n"), input))
|
|
410
|
+
end = mid;
|
|
411
|
+
else
|
|
412
|
+
low = mid + 1;
|
|
413
|
+
}
|
|
414
|
+
inputStart = 0;
|
|
415
|
+
let high = end;
|
|
416
|
+
while (inputStart + 1 < high) {
|
|
417
|
+
const mid = Math.floor((inputStart + high) / 2);
|
|
418
|
+
if (matchesInputBox(rows.slice(mid, end).join("\n"), input))
|
|
419
|
+
inputStart = mid;
|
|
420
|
+
else
|
|
421
|
+
high = mid;
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
let offset = 0;
|
|
425
|
+
for (const [index, row] of rows.entries()) {
|
|
426
|
+
const cleanRow = row.replace(/\u001b\[[0-?]*[ -/]*[@-~]/g, "");
|
|
427
|
+
// Structured tool frames and the terminal's tool headings close the prefix BEFORE their body.
|
|
428
|
+
const toolFrame = /"(?:type|role)"\s*:\s*"(?:tool[^" ]*|function_call[^" ]*|command_execution)"|^\s*(?:[•⏺]\s*(?:Ran|Explored|Searched|Called|Bash|Read|Write|Edit|Shell|Exec|Tool)\b|⎋ |(?:tool[-_ ](?:call|output|result)|function_call)\b)/i.test(cleanRow);
|
|
429
|
+
if (toolFrame || index >= inputStart) {
|
|
430
|
+
this.closed = true;
|
|
431
|
+
return;
|
|
432
|
+
}
|
|
433
|
+
if (!this.adapter.harnessBannerRows?.includes(cleanRow)) {
|
|
434
|
+
const match = WORKER_STARTUP_FAILURE_RE.exec(row);
|
|
435
|
+
if (match)
|
|
436
|
+
return {
|
|
437
|
+
matchedBytes: match[0], offset: offset + Buffer.byteLength(row.slice(0, match.index)),
|
|
438
|
+
row, rowNumber: index + 1,
|
|
439
|
+
};
|
|
440
|
+
}
|
|
441
|
+
offset += Buffer.byteLength(row) + 1;
|
|
442
|
+
}
|
|
443
|
+
}
|
|
390
444
|
}
|
|
391
445
|
// OBS-901/906: one daemon-owned writer for every execution surface. Drivers expose one retained
|
|
392
446
|
// stream through read(); the daemon persists that exact surface before any close/reconcile can make
|
|
@@ -570,17 +624,54 @@ async function cancellableTipBattery(intWt, commands, runDir, baseline, signal)
|
|
|
570
624
|
});
|
|
571
625
|
const { verifyIntegrationTip } = await import(${JSON.stringify(new URL(`./merge.${extension}`, import.meta.url).href)});
|
|
572
626
|
const { runWithVerificationBudget } = await import(${JSON.stringify(new URL(`./git.${extension}`, import.meta.url).href)});
|
|
627
|
+
const { runWithCommandLease } = await import(${JSON.stringify(new URL(`./lease.${extension}`, import.meta.url).href)});
|
|
628
|
+
let sequence = 0;
|
|
629
|
+
const grants = new Map();
|
|
630
|
+
const commandCapacities = new Map();
|
|
631
|
+
process.on('message', (message) => {
|
|
632
|
+
if (message.type === 'lease-grant') { grants.get(message.id)?.(message); grants.delete(message.id); }
|
|
633
|
+
});
|
|
634
|
+
const lease = async (command, execute) => {
|
|
635
|
+
const id = ++sequence;
|
|
636
|
+
const grant = new Promise(resolve => grants.set(id, resolve));
|
|
637
|
+
process.send({ type: 'lease-acquire', id, command });
|
|
638
|
+
const { capacity, token } = await grant;
|
|
639
|
+
commandCapacities.set(command, capacity);
|
|
640
|
+
try { return await runWithVerificationBudget(capacity, () => execute(token)); }
|
|
641
|
+
finally { process.send({ type: 'lease-release', id }); }
|
|
642
|
+
};
|
|
573
643
|
let input = ''; for await (const chunk of process.stdin) input += chunk;
|
|
574
644
|
const { intWt, commands, runDir, baseline, capacity } = JSON.parse(input);
|
|
575
645
|
try {
|
|
576
|
-
const result = await runWithVerificationBudget(capacity, () => verifyIntegrationTip(intWt, commands, runDir, baseline));
|
|
577
|
-
if (!cancelled) process.stdout.write(JSON.stringify(result));
|
|
646
|
+
const result = await runWithVerificationBudget(capacity, () => runWithCommandLease(lease, () => verifyIntegrationTip(intWt, commands, runDir, baseline)));
|
|
647
|
+
if (!cancelled) process.stdout.write(JSON.stringify(result.map(row => ({ ...row, capacity: [...commandCapacities].reverse().find(([command]) => command === row.cmd || command.startsWith(row.cmd + " "))?.[1] ?? capacity }))));
|
|
578
648
|
} catch (error) { if (!cancelled) throw error; }
|
|
649
|
+
finally { process.disconnect(); }
|
|
579
650
|
`;
|
|
580
651
|
const child = spawn(process.execPath, [
|
|
581
652
|
...(extension === "ts" ? ["--import", createRequire(import.meta.url).resolve("tsx")] : []),
|
|
582
653
|
"--input-type=module", "-e", script,
|
|
583
|
-
], { stdio: ["pipe", "pipe", "pipe"], detached: true });
|
|
654
|
+
], { env: commandLeaseEnvironment(process.env), stdio: ["pipe", "pipe", "pipe", "ipc"], detached: true });
|
|
655
|
+
const releases = new Map();
|
|
656
|
+
const leases = new Set();
|
|
657
|
+
let closed = false;
|
|
658
|
+
child.on("message", (message) => {
|
|
659
|
+
if (message.type === "lease-release") {
|
|
660
|
+
releases.get(message.id)?.();
|
|
661
|
+
releases.delete(message.id);
|
|
662
|
+
}
|
|
663
|
+
if (message.type !== "lease-acquire")
|
|
664
|
+
return;
|
|
665
|
+
const held = withCommandLease(message.command, async () => {
|
|
666
|
+
if (closed || signal.aborted)
|
|
667
|
+
return;
|
|
668
|
+
const released = new Promise((resolve) => releases.set(message.id, resolve));
|
|
669
|
+
child.send({ type: "lease-grant", id: message.id, capacity: resolvedCapacity(), token: currentCommandLeaseToken() });
|
|
670
|
+
await released;
|
|
671
|
+
});
|
|
672
|
+
leases.add(held);
|
|
673
|
+
void held.catch(() => { }).finally(() => leases.delete(held));
|
|
674
|
+
});
|
|
584
675
|
let output = "";
|
|
585
676
|
let errors = "";
|
|
586
677
|
child.stdout.on("data", (chunk) => { output += chunk; });
|
|
@@ -589,6 +680,10 @@ async function cancellableTipBattery(intWt, commands, runDir, baseline, signal)
|
|
|
589
680
|
const finished = new Promise((resolve, reject) => {
|
|
590
681
|
child.once("error", reject);
|
|
591
682
|
child.once("close", (code) => {
|
|
683
|
+
closed = true;
|
|
684
|
+
for (const release of releases.values())
|
|
685
|
+
release();
|
|
686
|
+
releases.clear();
|
|
592
687
|
if (signal.aborted && (code === 143 || code === null))
|
|
593
688
|
reject(signal.reason);
|
|
594
689
|
else if (code !== 0)
|
|
@@ -608,6 +703,7 @@ async function cancellableTipBattery(intWt, commands, runDir, baseline, signal)
|
|
|
608
703
|
}
|
|
609
704
|
finally {
|
|
610
705
|
signal.removeEventListener("abort", cancel);
|
|
706
|
+
await Promise.allSettled(leases);
|
|
611
707
|
}
|
|
612
708
|
}
|
|
613
709
|
/**
|
|
@@ -662,9 +758,10 @@ export async function verifyIntegrationTipCached(intWt, commands, journal, opts
|
|
|
662
758
|
for (const r of await (opts.signal
|
|
663
759
|
? cancellableTipBattery(intWt, commands, journal.dir, opts.baseline, opts.signal)
|
|
664
760
|
: verifyIntegrationTip(intWt, commands, journal.dir, opts.baseline))) {
|
|
761
|
+
const measuredCapacity = r.capacity ?? capacity;
|
|
665
762
|
if (r.pass) {
|
|
666
763
|
// Q121s: a forgiven pass journals its fingerprints — honest about what was carried, never a silent green.
|
|
667
|
-
journal.append("tip-verify", undefined, { gate: r.gate, cmd: r.cmd, pass: true, exitCode: r.exitCode, details: r.details, ...(r.forgiven ? { forgiven: true, fingerprints: r.fingerprints } : {}), tip, cmdHash, capacity });
|
|
764
|
+
journal.append("tip-verify", undefined, { gate: r.gate, cmd: r.cmd, pass: true, exitCode: r.exitCode, details: r.details, ...(r.forgiven ? { forgiven: true, fingerprints: r.fingerprints } : {}), tip, cmdHash, capacity: measuredCapacity });
|
|
668
765
|
}
|
|
669
766
|
else {
|
|
670
767
|
journal.append("tip-verify-failed", undefined, {
|
|
@@ -677,7 +774,7 @@ export async function verifyIntegrationTipCached(intWt, commands, journal, opts
|
|
|
677
774
|
lastMergedTask: opts.lastMergedTask,
|
|
678
775
|
tip,
|
|
679
776
|
cmdHash,
|
|
680
|
-
capacity,
|
|
777
|
+
capacity: measuredCapacity,
|
|
681
778
|
});
|
|
682
779
|
tipFailed = true;
|
|
683
780
|
}
|
|
@@ -734,7 +831,6 @@ async function observeWorkerProcessTree(marker, cwd) {
|
|
|
734
831
|
}
|
|
735
832
|
return tree.size === 0 ? "empty" : "running";
|
|
736
833
|
}
|
|
737
|
-
const SUITE_COMMAND_RE = /(?:^|[\s/])(vitest(?:\.mjs)?|jest|mocha)(?:[\s/]|$)|\bnpm(?:\s+run)?\s+test\b/i;
|
|
738
834
|
function processCwd(pid) {
|
|
739
835
|
try {
|
|
740
836
|
return realpathSync(readlinkSync(`/proc/${pid}/cwd`));
|
|
@@ -802,7 +898,7 @@ export function countLiveSuites(snapshot, repoRoot, daemonPid = process.pid, cwd
|
|
|
802
898
|
// runner ("…as a vitest test whose…"), so a finished interactive worker counted as a live suite and
|
|
803
899
|
// held a task's gates for 9 min 46 s. A runner is named in a command's HEAD — `node <bin>`,
|
|
804
900
|
// `npm test`, `npx vitest`, `sh -c npm test` — never 140 KB into it: read the first four tokens only.
|
|
805
|
-
const candidates = rows.filter((row) => !ancestors.has(row.pid) &&
|
|
901
|
+
const candidates = rows.filter((row) => !ancestors.has(row.pid) && isRunnerCommand(row.command));
|
|
806
902
|
const attributable = new Set(candidates.filter((row) => {
|
|
807
903
|
if (descendants.has(row.pid))
|
|
808
904
|
return true;
|
|
@@ -1347,7 +1443,7 @@ export async function runDaemon(repoRoot, opts = {}) {
|
|
|
1347
1443
|
const rm = resolveRunMode(repoRoot, { flag: opts.mode, spec: graph.mode, globalDir: opts.globalDir });
|
|
1348
1444
|
const cfg = rm.cfg;
|
|
1349
1445
|
// Resolve both budgets once: worker fan-out divides by dispatch concurrency; verification
|
|
1350
|
-
// divides by one
|
|
1446
|
+
// divides by one runner command holding a lease. Explicit operator caps still win.
|
|
1351
1447
|
const concurrency = opts.concurrency ?? cfg.concurrency;
|
|
1352
1448
|
return await runWithForkBudget(concurrency, async () => {
|
|
1353
1449
|
const conservativeCapacity = resolvedCapacity();
|
|
@@ -1604,52 +1700,30 @@ export async function runDaemon(repoRoot, opts = {}) {
|
|
|
1604
1700
|
journal.append("watch-board-reopen-failed", undefined, { pane: loss.pane, attempt, error: reopened.error, ...(boardless ? { boardless: true } : {}) });
|
|
1605
1701
|
console.error(`tickmarkr: board not reopened (attempt ${attempt}): ${reopened.error}`);
|
|
1606
1702
|
};
|
|
1607
|
-
//
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
const startedAt = Date.now();
|
|
1627
|
-
for (;;) {
|
|
1628
|
-
signal?.throwIfAborted();
|
|
1629
|
-
const count = await liveSuiteCount(repoRoot);
|
|
1630
|
-
if (count === 0)
|
|
1631
|
-
break;
|
|
1632
|
-
// OBS-889: a census that never reaches zero held T5's gates with no row and no end. Proceed at
|
|
1633
|
-
// the ceiling, flagged: the verdict that follows was produced beside whatever is still counted.
|
|
1634
|
-
if (Date.now() - startedAt >= suiteWaitCeilingMs) {
|
|
1635
|
-
journal.append("suite-wait-ceiling", taskId, { count, waitedMs: Date.now() - startedAt });
|
|
1636
|
-
journal.append("suite-budget", taskId, {
|
|
1637
|
-
count, occupancyCap: occupancyCapacity.forkCap, conservativeCap: conservativeCapacity.forkCap,
|
|
1638
|
-
});
|
|
1639
|
-
return await runWithVerificationBudget(conservativeCapacity, run);
|
|
1640
|
-
}
|
|
1641
|
-
if (count !== lastCount)
|
|
1642
|
-
journal.append("suite-wait", taskId, { count });
|
|
1643
|
-
lastCount = count;
|
|
1644
|
-
await new Promise((wake) => setTimeout(wake, SUITE_POLL_MS));
|
|
1703
|
+
// Context carries attribution through gates and remote inference; only shell commands acquire.
|
|
1704
|
+
const commandLeases = new CommandLeases();
|
|
1705
|
+
const withCommandContext = (taskId, run, signal) => runWithCommandLease((_command, execute) => commandLeases.run(async () => {
|
|
1706
|
+
let lastCount = -1;
|
|
1707
|
+
const startedAt = Date.now();
|
|
1708
|
+
for (;;) {
|
|
1709
|
+
signal?.throwIfAborted();
|
|
1710
|
+
const count = await liveSuiteCount(repoRoot);
|
|
1711
|
+
if (count === 0)
|
|
1712
|
+
break;
|
|
1713
|
+
if (count !== lastCount)
|
|
1714
|
+
journal.append("suite-wait", taskId, { count });
|
|
1715
|
+
lastCount = count;
|
|
1716
|
+
if (Date.now() - startedAt >= suiteWaitCeilingMs) {
|
|
1717
|
+
journal.append("suite-wait-ceiling", taskId, { count, waitedMs: Date.now() - startedAt });
|
|
1718
|
+
journal.append("suite-budget", taskId, {
|
|
1719
|
+
count, occupancyCap: occupancyCapacity.forkCap, conservativeCap: conservativeCapacity.forkCap,
|
|
1720
|
+
});
|
|
1721
|
+
return await runWithVerificationBudget(conservativeCapacity, execute);
|
|
1645
1722
|
}
|
|
1646
|
-
|
|
1647
|
-
}
|
|
1648
|
-
finally {
|
|
1649
|
-
suitePending--;
|
|
1650
|
-
release();
|
|
1723
|
+
await new Promise((wake) => setTimeout(wake, SUITE_POLL_MS));
|
|
1651
1724
|
}
|
|
1652
|
-
|
|
1725
|
+
return await execute();
|
|
1726
|
+
}, (count) => journal.append("suite-wait", taskId, { count }), SUITE_POLL_MS, signal), run);
|
|
1653
1727
|
let baseRef;
|
|
1654
1728
|
let baseline;
|
|
1655
1729
|
let baselinePending = false;
|
|
@@ -1687,6 +1761,7 @@ export async function runDaemon(repoRoot, opts = {}) {
|
|
|
1687
1761
|
// The SAME comparator status uses (engagementComparable); one decision, two consumers. Fail closed:
|
|
1688
1762
|
// no resume path silently accepts a mismatched or unbound journal. --graph-changed is the operator's
|
|
1689
1763
|
// audited release for the stop-amend-resume workflow, journaling a graph-rehash naming both hashes.
|
|
1764
|
+
graph = applyScopeAmendments(graph, journal, true);
|
|
1690
1765
|
const loadedHash = graphDefinitionHash(graph);
|
|
1691
1766
|
const cmp = engagementComparable(journal.read(), loadedHash);
|
|
1692
1767
|
if (!cmp.comparable) {
|
|
@@ -1732,7 +1807,7 @@ export async function runDaemon(repoRoot, opts = {}) {
|
|
|
1732
1807
|
baselinePending = true;
|
|
1733
1808
|
// Workers can run beside capture, but no gate may observe an absent or partial baseline.
|
|
1734
1809
|
// Keep publication and warnings inside the same barrier as the suite's final verdict.
|
|
1735
|
-
baselineCapture =
|
|
1810
|
+
baselineCapture = withCommandContext(undefined, async () => {
|
|
1736
1811
|
const captured = await captureBaseline(repoRoot, commands);
|
|
1737
1812
|
writeFileSync(join(journal.dir, "baseline.json"), JSON.stringify(captured, null, 2));
|
|
1738
1813
|
baseline = captured;
|
|
@@ -1919,15 +1994,20 @@ export async function runDaemon(repoRoot, opts = {}) {
|
|
|
1919
1994
|
// worker made. Do not replace the scope gate's stronger attribution with that inference.
|
|
1920
1995
|
const paths = reds.filter((g) => g.gate !== "scope" || !g.meta?.collateral).flatMap((g) => namedPaths(g.details));
|
|
1921
1996
|
if (refusal.length || (paths.length > 0 && paths.every((path) => !allowed(path)))) {
|
|
1922
|
-
|
|
1997
|
+
const classification = {
|
|
1923
1998
|
gate: refusal.length ? "worker" : reds[0]?.gate,
|
|
1924
1999
|
paths: [...new Set(refusal.length ? refusal : paths)],
|
|
1925
2000
|
repair: `files[] repair hint: ${[...new Set(refusal.length ? refusal : paths)].join(", ")}`,
|
|
1926
2001
|
attempt: attempts + 1, chargeable: false, source: "diagnostic",
|
|
1927
|
-
}
|
|
2002
|
+
};
|
|
2003
|
+
if (refusal.length)
|
|
2004
|
+
journal.append("scope-request", t.id, classification);
|
|
2005
|
+
else
|
|
2006
|
+
journal.append("scope-authoring", t.id, classification);
|
|
1928
2007
|
// OBS-547 (Leg-2 T3 material): unchargeable ⇒ metered 0, exactly as the predicted path below —
|
|
1929
2008
|
// passing the physical count would write `meteredAttempts: 1` beside `attempts: 0`.
|
|
1930
|
-
|
|
2009
|
+
const approveCommand = `tickmarkr approve ${runId} ${t.id} --files ${[...new Set(refusal)].map((path) => /^[\w./-]+$/.test(path) ? path : shq(path)).join(",")}`;
|
|
2010
|
+
await park(t, `${refusal.length ? "scope request" : "authoring defect"} — files[] repair hint: ${[...new Set(refusal.length ? refusal : paths)].join(", ")}; current files[]: ${t.files.join(", ")}`, refusal.length ? "scope-request" : "authoring", assignment, attempts, startMs, gateFails, consults, tokens, 0, retryMode, refusal.length ? { paths: [...new Set(refusal)], approveCommand, graphDefinitionHash: graphDefinitionHash(graph) } : {});
|
|
1931
2011
|
return true;
|
|
1932
2012
|
}
|
|
1933
2013
|
}
|
|
@@ -1967,6 +2047,7 @@ export async function runDaemon(repoRoot, opts = {}) {
|
|
|
1967
2047
|
approvalSweepCursor = events.length;
|
|
1968
2048
|
if (approvals.length === 0)
|
|
1969
2049
|
return;
|
|
2050
|
+
graph = applyScopeAmendments(graph, journal);
|
|
1970
2051
|
resume = journal.replayResumeState();
|
|
1971
2052
|
satisfiedGates = journal.replaySatisfiedGates();
|
|
1972
2053
|
replayedGateResults = resumeLifecycleOpen
|
|
@@ -2626,7 +2707,7 @@ export async function runDaemon(repoRoot, opts = {}) {
|
|
|
2626
2707
|
await trackedDriver.project?.(t.id, "in-review");
|
|
2627
2708
|
await waitForBaseline(t.id);
|
|
2628
2709
|
journal.phaseStart(t.id, "gates");
|
|
2629
|
-
const { results } = await
|
|
2710
|
+
const { results } = await withCommandContext(t.id, () => runReviewRecovery(resumedTask, {
|
|
2630
2711
|
carriedFindings: outstandingReviewFindings(journal.read(), t.id),
|
|
2631
2712
|
worktree: wt, baseRef: taskBase, result: priorResult, author: gateAuthor,
|
|
2632
2713
|
commands, baseline, channels: pools.review, judgeChannels: pools.judge, adapters, cfg, artifactDir: journal.dir,
|
|
@@ -2892,7 +2973,8 @@ export async function runDaemon(repoRoot, opts = {}) {
|
|
|
2892
2973
|
feedback = feedback ? `${feedback}\n\n${consultBrief}` : consultBrief;
|
|
2893
2974
|
}
|
|
2894
2975
|
}
|
|
2895
|
-
|
|
2976
|
+
const scopeApproval = [...journaledSoFar].reverse().find((e) => e.taskId === t.id && (e.event === "task-approved" || e.event === "task-dispatch"));
|
|
2977
|
+
retryMode = scopeApproval?.data.release === "scope-request" ? "fresh" : repairFindings
|
|
2896
2978
|
? "repair"
|
|
2897
2979
|
: priorSession
|
|
2898
2980
|
&& priorSession.channel === channelKey(assignment)
|
|
@@ -2919,6 +3001,7 @@ export async function runDaemon(repoRoot, opts = {}) {
|
|
|
2919
3001
|
// ledger cannot tell a carried dispatch from an amnesiac one — the exact question a run that
|
|
2920
3002
|
// spends two frontier attempts re-deriving a known defect has to be able to answer afterwards.
|
|
2921
3003
|
journal.append("task-dispatch", t.id, {
|
|
3004
|
+
...(scopeApproval?.data.release === "scope-request" ? { files: t.files, graphDefinitionHash: graphDefinitionHash(graph) } : {}),
|
|
2922
3005
|
assignment, attempt, provenance: dispatchProvenance([
|
|
2923
3006
|
channelKey(assignment) === channelKey(r.assignment) ? r.provenance
|
|
2924
3007
|
: t.routingHints?.pin ? `pin ${t.routingHints.pin.via}:${t.routingHints.pin.model} not re-tried` : "ladder assignment",
|
|
@@ -3243,6 +3326,12 @@ export async function runDaemon(repoRoot, opts = {}) {
|
|
|
3243
3326
|
let processExited = false;
|
|
3244
3327
|
let earlyLaunchDead = false;
|
|
3245
3328
|
let startupFailure = false;
|
|
3329
|
+
let startupEvidence;
|
|
3330
|
+
const startupDetector = new StartupFailureDetector(adapter);
|
|
3331
|
+
const startupFailureInWindow = (text, launchedAt) => {
|
|
3332
|
+
startupEvidence ??= startupDetector.sample(text, launchedAt);
|
|
3333
|
+
return startupEvidence !== undefined;
|
|
3334
|
+
};
|
|
3246
3335
|
let deadWorkerPark;
|
|
3247
3336
|
let settleParsed;
|
|
3248
3337
|
const trailerFrames = interactive && adapter.busyFrameMarkers
|
|
@@ -4039,6 +4128,7 @@ export async function runDaemon(repoRoot, opts = {}) {
|
|
|
4039
4128
|
if (!workerFinished) {
|
|
4040
4129
|
journal.append("worker-reaped-before-harvest", t.id, {
|
|
4041
4130
|
slot: slot.name, attempt, cause: workerCause ?? "stall-timeout",
|
|
4131
|
+
...(startupEvidence ? { evidence: startupEvidence } : {}),
|
|
4042
4132
|
});
|
|
4043
4133
|
}
|
|
4044
4134
|
}
|
|
@@ -4374,7 +4464,7 @@ export async function runDaemon(repoRoot, opts = {}) {
|
|
|
4374
4464
|
}
|
|
4375
4465
|
}
|
|
4376
4466
|
else {
|
|
4377
|
-
({ results, commits } = await
|
|
4467
|
+
({ results, commits } = await withCommandContext(t.id, () => runReviewRecovery(t, {
|
|
4378
4468
|
carriedFindings: outstandingFindings,
|
|
4379
4469
|
worktree: wt, baseRef: taskBase, result, author: assignment,
|
|
4380
4470
|
commands, baseline, channels: pools.review, judgeChannels: pools.judge, adapters, cfg, artifactDir: journal.dir,
|
|
@@ -4767,7 +4857,7 @@ export async function runDaemon(repoRoot, opts = {}) {
|
|
|
4767
4857
|
poll = setTimeout(pollApprovals, APPROVAL_POLL_MS);
|
|
4768
4858
|
let tipFailed;
|
|
4769
4859
|
try {
|
|
4770
|
-
const verification =
|
|
4860
|
+
const verification = withCommandContext(undefined, () => verifyIntegrationTipCached(intWt, commands, journal, { lastMergedTask, baseline, signal: controller.signal }), controller.signal);
|
|
4771
4861
|
activeTipVerify = { controller, settled: verification.then(() => { }, () => { }) };
|
|
4772
4862
|
tipFailed = await verification;
|
|
4773
4863
|
}
|
package/dist/run/git.d.ts
CHANGED
|
@@ -4,7 +4,7 @@ export { ROUTING_ENV_SEAMS };
|
|
|
4
4
|
export declare const FORK_CAP_ENV = "VITEST_MAX_FORKS";
|
|
5
5
|
export declare const SUITE_PARENT_ENV = "TICKMARKR_SUITE_PARENT";
|
|
6
6
|
export declare const DEFAULT_FORK_CAP = "6";
|
|
7
|
-
/** Verification
|
|
7
|
+
/** Verification uses command leases; workers keep their concurrency budget. */
|
|
8
8
|
export declare const runWithVerificationBudget: <T>(capacity: RunCapacity, fn: () => Promise<T>) => Promise<T>;
|
|
9
9
|
/**
|
|
10
10
|
* OBS-618: how many PROCESSES one vitest fork can hold at its peak — the fork, a daemon it spawns,
|
|
@@ -89,6 +89,7 @@ export declare const resolvedCapacity: () => RunCapacity;
|
|
|
89
89
|
export declare const DEFAULT_SHELL_TIMEOUT_MS = 600000;
|
|
90
90
|
export interface ShResult {
|
|
91
91
|
code: number;
|
|
92
|
+
signalExit?: boolean;
|
|
92
93
|
stdout: string;
|
|
93
94
|
stderr: string;
|
|
94
95
|
timedOut?: boolean;
|
|
@@ -104,6 +105,14 @@ export declare const setSpawnForTests: (fn: typeof spawn) => void;
|
|
|
104
105
|
export declare const resetSpawnForTests: () => void;
|
|
105
106
|
export declare const SPAWN_ATTEMPT_LIMIT = 4;
|
|
106
107
|
export declare const SPAWN_RETRY_BACKOFF_MS = 50;
|
|
108
|
+
export interface ShellOptions {
|
|
109
|
+
env?: NodeJS.ProcessEnv;
|
|
110
|
+
signal?: AbortSignal;
|
|
111
|
+
onSpawn?: (pid: number | undefined) => void;
|
|
112
|
+
onTimeout?: () => void;
|
|
113
|
+
}
|
|
114
|
+
/** Shared command seam, including invocation-bound manifested runners. */
|
|
115
|
+
export declare function shell(cmd: string, cwd: string, timeoutMs: number, login?: boolean, options?: ShellOptions): Promise<ShResult>;
|
|
107
116
|
export declare function sh(cmd: string, cwd: string, timeoutMs?: number): Promise<ShResult>;
|
|
108
117
|
export declare function shGit(cmd: string, cwd: string, timeoutMs?: number): Promise<ShResult>;
|
|
109
118
|
export declare function shOk(cmd: string, cwd: string): Promise<string>;
|