tickmarkr 1.72.0 → 1.74.0
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/cli/commands/approve.js +21 -3
- package/dist/cli/commands/resume.js +10 -2
- package/dist/cli/commands/status.d.ts +2 -0
- package/dist/cli/commands/status.js +260 -41
- package/dist/cli/commands/ui.d.ts +6 -2
- package/dist/cli/commands/ui.js +2 -4
- package/dist/config/config.d.ts +4 -0
- package/dist/config/config.js +6 -1
- package/dist/drivers/herdr.d.ts +5 -0
- package/dist/drivers/herdr.js +100 -10
- package/dist/drivers/subprocess.d.ts +3 -3
- package/dist/drivers/subprocess.js +10 -5
- package/dist/gates/acceptance.js +19 -1
- package/dist/gates/llm.d.ts +4 -0
- package/dist/gates/llm.js +16 -3
- package/dist/gates/run-gates.d.ts +1 -1
- package/dist/gates/run-gates.js +25 -3
- package/dist/route/preference.d.ts +2 -1
- package/dist/route/preference.js +8 -2
- package/dist/run/daemon.d.ts +1 -0
- package/dist/run/daemon.js +139 -4
- package/dist/run/git.d.ts +2 -0
- package/dist/run/git.js +55 -0
- package/dist/run/journal.d.ts +21 -1
- package/dist/run/journal.js +83 -5
- package/dist/tui/ink/components.d.ts +18 -0
- package/dist/tui/ink/components.js +30 -2
- package/dist/tui/ink/studio-app.d.ts +59 -0
- package/dist/tui/ink/studio-app.js +320 -0
- package/package.json +1 -1
- package/dist/tui/app.d.ts +0 -72
- package/dist/tui/app.js +0 -334
- package/dist/tui/engine.d.ts +0 -52
- package/dist/tui/engine.js +0 -92
- package/dist/tui/frame.d.ts +0 -22
- package/dist/tui/frame.js +0 -56
- package/dist/tui/input.d.ts +0 -41
- package/dist/tui/input.js +0 -100
- package/dist/tui/views/consult-dossier.d.ts +0 -49
- package/dist/tui/views/consult-dossier.js +0 -169
- package/dist/tui/views/diff-modal.d.ts +0 -9
- package/dist/tui/views/diff-modal.js +0 -19
- package/dist/tui/views/fleet-view.d.ts +0 -91
- package/dist/tui/views/fleet-view.js +0 -337
- package/dist/tui/views/preview-view.d.ts +0 -7
- package/dist/tui/views/preview-view.js +0 -80
- package/dist/tui/views/profile-view.d.ts +0 -15
- package/dist/tui/views/profile-view.js +0 -93
- package/dist/tui/views/routing-view.d.ts +0 -48
- package/dist/tui/views/routing-view.js +0 -234
- package/dist/tui/views/runs-view.d.ts +0 -70
- package/dist/tui/views/runs-view.js +0 -387
package/dist/drivers/herdr.js
CHANGED
|
@@ -10,6 +10,8 @@ export const TRAILER_WIDTH_MARGIN = 2; // cols below (floor + margin) refuse a r
|
|
|
10
10
|
export const DELIVERY_ATTEMPTS = 3;
|
|
11
11
|
const DELIVERY_VERIFY_TIMEOUT_MS = 2000; // per attempt — a paste that hasn't rendered in 2s is retyped
|
|
12
12
|
const DELIVERY_READ_LINES = 80;
|
|
13
|
+
const DELIVERY_SETTLE_READ_ATTEMPTS = 6;
|
|
14
|
+
const DELIVERY_SETTLE_POLL_MS = 100;
|
|
13
15
|
/** First-generation join direction from measured trailer-safe floor (43-MEASUREMENT.md). */
|
|
14
16
|
export function workerSplitDirection(paneCols, safeFloor = TRAILER_SAFE_FLOOR_COLS, margin = TRAILER_WIDTH_MARGIN) {
|
|
15
17
|
if (paneCols == null || paneCols <= 0)
|
|
@@ -25,8 +27,11 @@ export class HerdrDriver {
|
|
|
25
27
|
// grouped slot()/close() mutate shared group state across awaits — serialize them so two
|
|
26
28
|
// concurrent first members can never both create the group tab (mergeSerial idiom, daemon.ts)
|
|
27
29
|
groupSerial = Promise.resolve();
|
|
28
|
-
// OBS-
|
|
30
|
+
// OBS-120: canonical dispatch slots hold this chain from allocation through verified binding and
|
|
31
|
+
// delivery. Legacy/manual slots still serialize each delivery on the same chain (OBS-119).
|
|
29
32
|
deliverySerial = Promise.resolve();
|
|
33
|
+
dispatchLeases = new WeakMap();
|
|
34
|
+
deliveredPanes = new WeakMap();
|
|
30
35
|
// VIS-10: the run's workspace id, captured once at construction (the daemon inherits it from the
|
|
31
36
|
// operator's env before the driver is built). Required at slot() time, never in the constructor —
|
|
32
37
|
// pickDriver and its unit test construct HerdrDriver without env, so slot() is the trust gate.
|
|
@@ -47,6 +52,41 @@ export class HerdrDriver {
|
|
|
47
52
|
this.deliverySerial = p.catch(() => undefined);
|
|
48
53
|
return p;
|
|
49
54
|
}
|
|
55
|
+
// OBS-120: ExecutorDriver exposes slot() and run() separately, so a canonical tickmarkr slot
|
|
56
|
+
// carries a lease across that API seam. The next dispatch cannot allocate until this slot's first
|
|
57
|
+
// run either delivers or fails. Allocation errors release immediately; no failed dispatch poisons
|
|
58
|
+
// the queue. The daemon calls run() directly after preparing the command for every owned slot.
|
|
59
|
+
reserveDispatch(allocate) {
|
|
60
|
+
let release;
|
|
61
|
+
const held = new Promise((resolve) => { release = resolve; });
|
|
62
|
+
const start = this.deliverySerial.then(() => undefined, () => undefined);
|
|
63
|
+
this.deliverySerial = start.then(() => held);
|
|
64
|
+
return start.then(async () => {
|
|
65
|
+
let slot;
|
|
66
|
+
try {
|
|
67
|
+
slot = await allocate();
|
|
68
|
+
this.dispatchLeases.set(slot, { release });
|
|
69
|
+
return slot;
|
|
70
|
+
}
|
|
71
|
+
catch (error) {
|
|
72
|
+
if (slot) {
|
|
73
|
+
try {
|
|
74
|
+
await this.close(slot);
|
|
75
|
+
}
|
|
76
|
+
catch { /* allocation already failed closed; cleanup is best-effort */ }
|
|
77
|
+
}
|
|
78
|
+
release();
|
|
79
|
+
throw error;
|
|
80
|
+
}
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
async verifyPaneIdentityBinding(slot) {
|
|
84
|
+
const paneId = await this.namedPaneId(slot.name);
|
|
85
|
+
if (paneId !== slot.id) {
|
|
86
|
+
throw new Error(`herdr pane identity binding verification failed for ${slot.name}: allocated ${slot.id}, resolved ${paneId ?? "none"} — refusing delivery`);
|
|
87
|
+
}
|
|
88
|
+
return paneId;
|
|
89
|
+
}
|
|
50
90
|
// ponytail: narrow panes hard-wrap the input line — collapse whitespace before comparing.
|
|
51
91
|
deliveryMatches(transcript, cmd) {
|
|
52
92
|
const norm = (s) => s.replace(/\s+/g, "");
|
|
@@ -78,8 +118,12 @@ export class HerdrDriver {
|
|
|
78
118
|
return null;
|
|
79
119
|
}
|
|
80
120
|
}
|
|
81
|
-
//
|
|
121
|
+
// Before delivery, resolve fresh via the durable name because pane ids can compact. After delivery,
|
|
122
|
+
// pin the verified target so early liveness cannot drift to a label rebound onto another pane.
|
|
82
123
|
async paneId(slot) {
|
|
124
|
+
const delivered = this.deliveredPanes.get(slot);
|
|
125
|
+
if (delivered)
|
|
126
|
+
return delivered;
|
|
83
127
|
return await this.namedPaneId(slot.name) ?? slot.id;
|
|
84
128
|
}
|
|
85
129
|
// VIS-09 P43-03: runtime width for the layout gate (43-MEASUREMENT.md licensing condition 2).
|
|
@@ -104,12 +148,18 @@ export class HerdrDriver {
|
|
|
104
148
|
// reconcile.ts and this driver's own renameGroupTab/glyphFor decode role/taskId/attempt from
|
|
105
149
|
// those shapes without a call-site migration; T2 retires this branch by always passing `owned`.
|
|
106
150
|
const resolved = opts?.owned ? formatOwnedName(opts.owned) : name;
|
|
151
|
+
const allocate = opts?.group
|
|
152
|
+
? () => this.serial(() => this.groupSlot(cwd, resolved, opts.group))
|
|
153
|
+
: () => this.tabSlot(cwd, resolved, opts?.label);
|
|
154
|
+
// Production dispatch names are canonical even when the gate call site supplies the already-
|
|
155
|
+
// formatted name rather than SlotOpts.owned. Hold one lease across slot() → run(); legacy/manual
|
|
156
|
+
// slots retain their existing allocation-only semantics for compatibility.
|
|
157
|
+
if (parseOwnedName(resolved))
|
|
158
|
+
return this.reserveDispatch(allocate);
|
|
107
159
|
// group wins if both are set (a group tab is already stage-labeled; passing both is a caller bug).
|
|
108
160
|
// label (without group) → dedicated labeled tab via tabSlot's third param: no groups-map entry, no
|
|
109
161
|
// refcount, no groupSerial, no degrade latch — dedicated tabs have no shared state to guard (SUP-01).
|
|
110
|
-
|
|
111
|
-
return this.serial(() => this.groupSlot(cwd, resolved, opts.group));
|
|
112
|
-
return this.tabSlot(cwd, resolved, opts?.label); // label undefined → defaults to name (today's behavior)
|
|
162
|
+
return allocate(); // label undefined → defaults to name (today's behavior)
|
|
113
163
|
}
|
|
114
164
|
// today's per-slot tab path, plus the VIS-04 orphan reap
|
|
115
165
|
// label defaults to the slot name; group tabs pass the STAGE name instead — a first-member label
|
|
@@ -320,15 +370,40 @@ export class HerdrDriver {
|
|
|
320
370
|
// cleared (C-u), and retyped, bounded; persistent corruption fails closed WITH the captured
|
|
321
371
|
// transcript — the dispatch-time pincer the ledger asks for, not post-hoc `git:` archaeology.
|
|
322
372
|
async run(slot, cmd) {
|
|
323
|
-
|
|
373
|
+
const lease = this.dispatchLeases.get(slot);
|
|
374
|
+
if (lease) {
|
|
375
|
+
this.dispatchLeases.delete(slot);
|
|
376
|
+
try {
|
|
377
|
+
const paneId = await this.verifyPaneIdentityBinding(slot);
|
|
378
|
+
await this.deliver(slot, cmd, paneId);
|
|
379
|
+
this.deliveredPanes.set(slot, paneId);
|
|
380
|
+
}
|
|
381
|
+
finally {
|
|
382
|
+
lease.release();
|
|
383
|
+
}
|
|
384
|
+
return;
|
|
385
|
+
}
|
|
386
|
+
return this.deliveryQueue(async () => {
|
|
387
|
+
const paneId = await this.paneId(slot);
|
|
388
|
+
await this.deliver(slot, cmd, paneId);
|
|
389
|
+
this.deliveredPanes.set(slot, paneId);
|
|
390
|
+
});
|
|
324
391
|
}
|
|
325
|
-
async deliver(slot, cmd) {
|
|
326
|
-
const pane = await this.paneId(slot);
|
|
392
|
+
async deliver(slot, cmd, pane) {
|
|
327
393
|
let transcript = "";
|
|
328
394
|
for (let attempt = 0; attempt < DELIVERY_ATTEMPTS; attempt++) {
|
|
329
395
|
if (attempt > 0) {
|
|
330
|
-
//
|
|
331
|
-
//
|
|
396
|
+
// A TUI may still be painting while the failed delivery is captured (OBS-135). Judge the
|
|
397
|
+
// line only after two consecutive pane reads agree; an already-stable frame returns on the
|
|
398
|
+
// first fresh read without a timer. A changing pane is bounded and preserves OBS-85's
|
|
399
|
+
// fail-closed error instead of guessing from an adapter fingerprint.
|
|
400
|
+
const settled = await this.settleDeliveryLine(pane, slot.cwd, transcript);
|
|
401
|
+
transcript = settled.transcript;
|
|
402
|
+
if (!settled.ok) {
|
|
403
|
+
throw new Error(`herdr delivery clear failed — refusing to retype onto a corrupted line (OBS-85); pane transcript:\n${transcript}`);
|
|
404
|
+
}
|
|
405
|
+
// Clear the corrupted input line before retyping; a failed clear must NOT be retyped onto —
|
|
406
|
+
// corrupt-prefix + clean-retype would concatenate and false-verify by containment.
|
|
332
407
|
const cleared = await this.herdr(`pane send-keys ${shq(pane)} C-u`, slot.cwd);
|
|
333
408
|
if (cleared.code !== 0)
|
|
334
409
|
throw new Error(`herdr delivery clear failed — refusing to retype onto a corrupted line (OBS-85); pane transcript:\n${transcript}`);
|
|
@@ -348,6 +423,21 @@ export class HerdrDriver {
|
|
|
348
423
|
}
|
|
349
424
|
throw new Error(`herdr delivery corrupted after ${DELIVERY_ATTEMPTS} attempts — enter never pressed (OBS-85); pane transcript:\n${transcript}`);
|
|
350
425
|
}
|
|
426
|
+
async settleDeliveryLine(pane, cwd, initialTranscript) {
|
|
427
|
+
let transcript = initialTranscript;
|
|
428
|
+
for (let readAttempt = 0; readAttempt < DELIVERY_SETTLE_READ_ATTEMPTS; readAttempt++) {
|
|
429
|
+
const read = await this.herdr(`pane read ${shq(pane)} --source recent-unwrapped --lines ${DELIVERY_READ_LINES}`, cwd);
|
|
430
|
+
if (read.code !== 0)
|
|
431
|
+
return { ok: false, transcript: read.stdout || transcript };
|
|
432
|
+
if (read.stdout === transcript)
|
|
433
|
+
return { ok: true, transcript };
|
|
434
|
+
transcript = read.stdout;
|
|
435
|
+
if (readAttempt < DELIVERY_SETTLE_READ_ATTEMPTS - 1) {
|
|
436
|
+
await new Promise((resolve) => setTimeout(resolve, DELIVERY_SETTLE_POLL_MS));
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
return { ok: false, transcript };
|
|
440
|
+
}
|
|
351
441
|
async deliveryReadMatches(pane, cmd, cwd) {
|
|
352
442
|
const read = await this.herdr(`pane read ${shq(pane)} --source recent-unwrapped --lines ${DELIVERY_READ_LINES}`, cwd);
|
|
353
443
|
return read.code === 0 && this.deliveryMatches(read.stdout, cmd);
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import type { ExecutorDriver, NotifyOpts, Slot } from "./types.js";
|
|
2
2
|
export declare const MAX_BUF: number;
|
|
3
3
|
export declare const HERDR_CONTROL_VARS: readonly ["HERDR_ENV", "HERDR_SOCKET_PATH"];
|
|
4
|
-
/** Copy of env with herdr control-plane vars stripped.
|
|
4
|
+
/** Copy of worker env with the fork cap applied and herdr control-plane vars stripped. */
|
|
5
5
|
export declare function sealHerdrEnv(env?: NodeJS.ProcessEnv): NodeJS.ProcessEnv;
|
|
6
|
-
/**
|
|
7
|
-
export declare function herdrSealShellPrefix(): string;
|
|
6
|
+
/** Pane/login-shell form of the same worker env seal (herdr seed + daemon setup). */
|
|
7
|
+
export declare function herdrSealShellPrefix(env?: NodeJS.ProcessEnv): string;
|
|
8
8
|
export declare class SubprocessDriver implements ExecutorDriver {
|
|
9
9
|
id: string;
|
|
10
10
|
interactive: boolean;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { spawn } from "node:child_process";
|
|
2
|
-
import {
|
|
2
|
+
import { shq } from "../adapters/types.js";
|
|
3
|
+
import { createWorktree, DEFAULT_FORK_CAP, FORK_CAP_ENV } from "../run/git.js";
|
|
3
4
|
// HARD-03: cap retained worker output so a chatty worker can't grow tickmarkr's heap unbounded.
|
|
4
5
|
// 2MB is safe against BOTH consumers: the largest read() call site asks for 1000 lines
|
|
5
6
|
// (daemon.ts:217/247), and every marker (TICKMARKR_RESULT, TICKMARKR_EXIT_<nonce>) is emitted at the
|
|
@@ -12,16 +13,20 @@ export const MAX_BUF = 2 * 1024 * 1024;
|
|
|
12
13
|
// (and its own herdr driver CLI calls) keep the live session. Socket path is the wire; HERDR_ENV
|
|
13
14
|
// is the "I am inside herdr" gate every agent skill checks before mutating panes.
|
|
14
15
|
export const HERDR_CONTROL_VARS = ["HERDR_ENV", "HERDR_SOCKET_PATH"];
|
|
15
|
-
/** Copy of env with herdr control-plane vars stripped.
|
|
16
|
+
/** Copy of worker env with the fork cap applied and herdr control-plane vars stripped. */
|
|
16
17
|
export function sealHerdrEnv(env = process.env) {
|
|
17
18
|
const out = { ...env };
|
|
19
|
+
if (!(FORK_CAP_ENV in out))
|
|
20
|
+
out[FORK_CAP_ENV] = DEFAULT_FORK_CAP;
|
|
18
21
|
for (const k of HERDR_CONTROL_VARS)
|
|
19
22
|
delete out[k];
|
|
20
23
|
return out;
|
|
21
24
|
}
|
|
22
|
-
/**
|
|
23
|
-
export function herdrSealShellPrefix() {
|
|
24
|
-
|
|
25
|
+
/** Pane/login-shell form of the same worker env seal (herdr seed + daemon setup). */
|
|
26
|
+
export function herdrSealShellPrefix(env = process.env) {
|
|
27
|
+
const forkCap = sealHerdrEnv(env)[FORK_CAP_ENV] ?? DEFAULT_FORK_CAP;
|
|
28
|
+
return `export ${FORK_CAP_ENV}=${shq(forkCap)}; ` +
|
|
29
|
+
HERDR_CONTROL_VARS.map((k) => `unset ${k}`).join("; ") + "; ";
|
|
25
30
|
}
|
|
26
31
|
export class SubprocessDriver {
|
|
27
32
|
id = "subprocess";
|
package/dist/gates/acceptance.js
CHANGED
|
@@ -172,6 +172,22 @@ function compressLines(lines) {
|
|
|
172
172
|
}
|
|
173
173
|
return parts.join(", ");
|
|
174
174
|
}
|
|
175
|
+
// OBS-134: whole-file deletions are already fully described by their path. Sending every removed line
|
|
176
|
+
// spends the cap and judge context on content that cannot exist after the change. Added and modified
|
|
177
|
+
// sections pass through byte-for-byte, so their anti-flooding budget is unchanged.
|
|
178
|
+
function judgeRelevantDiff(diff) {
|
|
179
|
+
return diff.split(/(?=^diff --git )/m).map((section) => {
|
|
180
|
+
if (!/^deleted file mode /m.test(section))
|
|
181
|
+
return section;
|
|
182
|
+
const oldPath = /^--- (.+)$/m.exec(section)?.[1]
|
|
183
|
+
?? /^Binary files (.+) and \/dev\/null differ$/m.exec(section)?.[1];
|
|
184
|
+
if (!oldPath || oldPath === "/dev/null")
|
|
185
|
+
return section;
|
|
186
|
+
const unquoted = oldPath.startsWith('"') && oldPath.endsWith('"') ? oldPath.slice(1, -1) : oldPath;
|
|
187
|
+
const path = unquoted.replace(/^a\//, "");
|
|
188
|
+
return `deleted file: ${path}\n`;
|
|
189
|
+
}).join("");
|
|
190
|
+
}
|
|
175
191
|
// A citation is valid evidence iff the cited line falls inside a changed hunk of the cited file (OBS-129:
|
|
176
192
|
// changed-hunk span, not exact-added-line). A legacy free-text quote (string) keeps v1.64's substring
|
|
177
193
|
// check — non-empty and present somewhere in the diff.
|
|
@@ -225,7 +241,9 @@ export async function acceptanceGate(task, worktree, baseRef, judge, via, opts =
|
|
|
225
241
|
const warn = onlyJudge
|
|
226
242
|
? "WARNING: only judge oracles — no deterministic command/test oracle guards this task (deterministic preferred, spec §2).\n"
|
|
227
243
|
: "";
|
|
228
|
-
const
|
|
244
|
+
const fetched = await fetchTaskDiff(worktree, baseRef);
|
|
245
|
+
const diff = judgeRelevantDiff(fetched.full);
|
|
246
|
+
const forCap = judgeRelevantDiff(fetched.forCap);
|
|
229
247
|
const diffCap = opts.diffCap ?? DEFAULT_DIFF_CAP;
|
|
230
248
|
const capFail = checkDiffCap("acceptance", forCap.length, diffCap, warn + detBlock);
|
|
231
249
|
if (capFail)
|
package/dist/gates/llm.d.ts
CHANGED
|
@@ -26,6 +26,10 @@ export interface GateVia {
|
|
|
26
26
|
nameFor: (role: "judge" | "review", adapter: string) => string;
|
|
27
27
|
labelFor: (role: "judge" | "review") => string;
|
|
28
28
|
}
|
|
29
|
+
export declare function captureLlmOutput<T>(run: () => Promise<T>): Promise<{
|
|
30
|
+
value: T;
|
|
31
|
+
outputs: string[];
|
|
32
|
+
}>;
|
|
29
33
|
export declare function runHeadless(adapter: WorkerAdapter, model: string, prompt: string, cwd: string, timeoutMs?: number): Promise<string>;
|
|
30
34
|
export declare function runViaDriver(adapter: WorkerAdapter, model: string, prompt: string, cwd: string, via: LlmVia, timeoutMs?: number): Promise<string>;
|
|
31
35
|
export declare function runLlm(adapter: WorkerAdapter, model: string, prompt: string, cwd: string, via?: LlmVia, timeoutMs?: number): Promise<string>;
|
package/dist/gates/llm.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { AsyncLocalStorage } from "node:async_hooks";
|
|
1
2
|
import { randomBytes } from "node:crypto";
|
|
2
3
|
import { mkdtempSync, writeFileSync } from "node:fs";
|
|
3
4
|
import { tmpdir } from "node:os";
|
|
@@ -82,6 +83,16 @@ export function rolePaneNameFromPrompt(prompt, fallback) {
|
|
|
82
83
|
return gatePaneName("review", id, retry ? "-r1" : "");
|
|
83
84
|
return fallback;
|
|
84
85
|
}
|
|
86
|
+
const llmOutputCapture = new AsyncLocalStorage();
|
|
87
|
+
// OBS-132: acceptance.ts owns verdict parsing and is deliberately byte-untouched. This async-scoped
|
|
88
|
+
// recorder lets run-gates observe the exact output that acceptance parsed without changing runLlm's
|
|
89
|
+
// return value or leaking concurrent tasks into one another. Callers retain output only when the
|
|
90
|
+
// resulting verdict is a production failure; healthy output is discarded in memory.
|
|
91
|
+
export async function captureLlmOutput(run) {
|
|
92
|
+
const outputs = [];
|
|
93
|
+
const value = await llmOutputCapture.run(outputs, run);
|
|
94
|
+
return { value, outputs };
|
|
95
|
+
}
|
|
85
96
|
export async function runHeadless(adapter, model, prompt, cwd, timeoutMs = 300000) {
|
|
86
97
|
const pf = join(mkdtempSync(join(tmpdir(), "tickmarkr-llm-")), "prompt.md");
|
|
87
98
|
writeFileSync(pf, prompt);
|
|
@@ -119,10 +130,12 @@ export async function runViaDriver(adapter, model, prompt, cwd, via, timeoutMs =
|
|
|
119
130
|
out = augmentFakeVerdictOutput(adapter, out, nonce, prompt);
|
|
120
131
|
return out;
|
|
121
132
|
}
|
|
122
|
-
export function runLlm(adapter, model, prompt, cwd, via, timeoutMs = 300000) {
|
|
123
|
-
|
|
133
|
+
export async function runLlm(adapter, model, prompt, cwd, via, timeoutMs = 300000) {
|
|
134
|
+
const out = await (via
|
|
124
135
|
? runViaDriver(adapter, model, prompt, cwd, via, timeoutMs)
|
|
125
|
-
: runHeadless(adapter, model, prompt, cwd, timeoutMs);
|
|
136
|
+
: runHeadless(adapter, model, prompt, cwd, timeoutMs));
|
|
137
|
+
llmOutputCapture.getStore()?.push(out);
|
|
138
|
+
return out;
|
|
126
139
|
}
|
|
127
140
|
export function extractJson(raw) {
|
|
128
141
|
const fenced = [...raw.matchAll(/```json\s*\n([\s\S]*?)```/g)].at(-1);
|
|
@@ -2,7 +2,7 @@ import { type Assignment, type BillingChannel, type WorkerAdapter, type WorkerRe
|
|
|
2
2
|
import { type TickmarkrConfig } from "../config/config.js";
|
|
3
3
|
import { type GateName, type Task } from "../graph/schema.js";
|
|
4
4
|
import { type Baseline } from "./baseline.js";
|
|
5
|
-
import type
|
|
5
|
+
import { type GateVia } from "./llm.js";
|
|
6
6
|
import type { GateResult } from "./types.js";
|
|
7
7
|
export type GateEvent = {
|
|
8
8
|
phase: "start";
|
package/dist/gates/run-gates.js
CHANGED
|
@@ -5,9 +5,11 @@ import { GATE_NAMES } from "../graph/schema.js";
|
|
|
5
5
|
import { acceptanceGate } from "./acceptance.js";
|
|
6
6
|
import { compareToBaseline } from "./baseline.js";
|
|
7
7
|
import { evidenceGate } from "./evidence.js";
|
|
8
|
+
import { captureLlmOutput } from "./llm.js";
|
|
8
9
|
import { marginalCostRank } from "../route/router.js";
|
|
9
10
|
import { reviewGate } from "./review.js";
|
|
10
11
|
import { scopeGate } from "./scope.js";
|
|
12
|
+
import { withJudgeInvocationEvidence } from "../run/journal.js";
|
|
11
13
|
export async function runGates(task, ctx) {
|
|
12
14
|
const results = [];
|
|
13
15
|
let commits = [];
|
|
@@ -63,7 +65,27 @@ export async function runGates(task, ctx) {
|
|
|
63
65
|
: undefined;
|
|
64
66
|
// v1.19 (T2): testCmd threads the detected test runner to the gate so named-test oracles run
|
|
65
67
|
// deterministically (filtered via -t) before any LLM judge dispatch.
|
|
66
|
-
|
|
68
|
+
const invocations = [];
|
|
69
|
+
const invokeJudge = async (adapter, model, via) => {
|
|
70
|
+
const started = Date.now();
|
|
71
|
+
const captured = await captureLlmOutput(() => acceptanceGate(task, ctx.worktree, ctx.baseRef, { adapter, model }, via, { testCmd: ctx.commands.test, diffCap: ctx.cfg.gates.diffCap }));
|
|
72
|
+
const channel = channelKey({ adapter: adapter.id, model });
|
|
73
|
+
const unparseable = captured.value.meta?.unparseable === true;
|
|
74
|
+
// acceptanceGate has exactly one runLlm call. Keep the map shape so a future deterministic early
|
|
75
|
+
// return (zero outputs) stays telemetry-free instead of manufacturing a judge invocation.
|
|
76
|
+
for (const output of captured.outputs) {
|
|
77
|
+
invocations.push({
|
|
78
|
+
taskId: task.id,
|
|
79
|
+
channel,
|
|
80
|
+
outcome: unparseable ? "failed" : "done",
|
|
81
|
+
judgeOutcome: unparseable ? "unparseable" : "parseable",
|
|
82
|
+
durationMs: Date.now() - started,
|
|
83
|
+
...(unparseable ? { transcript: output } : {}),
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
return captured.value;
|
|
87
|
+
};
|
|
88
|
+
let a = await invokeJudge(judgeAdapter, ctx.cfg.judge.model, jvia);
|
|
67
89
|
// GATE-09: an unparseable judge verdict retries the JUDGE exactly once on a failover channel — never
|
|
68
90
|
// the worker (run-20260711-185020 P43-03 L70-72 billed a judge flake as a worker attempt). The flaked
|
|
69
91
|
// first verdict NEVER enters results (no false gate-result journal event, no operator notify, no stale
|
|
@@ -96,10 +118,10 @@ export async function runGates(task, ctx) {
|
|
|
96
118
|
? { driver: ctx.via.driver, keep: ctx.via.keep, onSlot: ctx.via.onSlot, name: ctx.via.nameFor("judge", retryAdapter.id) + "-r1", label: ctx.via.labelFor("judge") }
|
|
97
119
|
: undefined;
|
|
98
120
|
// the retry IS a second acceptanceGate call: one code path, one parser, zero new parse leniency.
|
|
99
|
-
a = await
|
|
121
|
+
a = await invokeJudge(retryAdapter, retry.model, retryJvia);
|
|
100
122
|
a = { ...a, meta: { ...a.meta, judgeRetry: { flaked: flakedKey, retried: channelKey({ adapter: retry.adapter, model: retry.model }) } } };
|
|
101
123
|
}
|
|
102
|
-
await record(a);
|
|
124
|
+
await withJudgeInvocationEvidence(invocations, () => record(a));
|
|
103
125
|
if (failed())
|
|
104
126
|
return { results, commits };
|
|
105
127
|
}
|
|
@@ -4,6 +4,7 @@ export interface Disallowed {
|
|
|
4
4
|
by: "deny" | "allow";
|
|
5
5
|
entry: string;
|
|
6
6
|
}
|
|
7
|
+
export type PreferenceRole = "worker" | "judge" | "review" | "consult";
|
|
7
8
|
export declare function excludedChannels(cfg: TickmarkrConfig, adapters: {
|
|
8
9
|
id: string;
|
|
9
10
|
}[] | string[], health: Record<string, AuthHealth>): {
|
|
@@ -24,7 +25,7 @@ export declare function preferRanks(c: {
|
|
|
24
25
|
export declare function disallowedBy(c: {
|
|
25
26
|
adapter: string;
|
|
26
27
|
model: string;
|
|
27
|
-
}, routing: TickmarkrConfig["routing"]): Disallowed | null;
|
|
28
|
+
}, routing: TickmarkrConfig["routing"], role?: PreferenceRole): Disallowed | null;
|
|
28
29
|
export interface DenyPreferCollision {
|
|
29
30
|
kind: "prefer" | "pin";
|
|
30
31
|
shape: string;
|
package/dist/route/preference.js
CHANGED
|
@@ -36,10 +36,16 @@ export function preferRanks(c, cfg) {
|
|
|
36
36
|
return out;
|
|
37
37
|
}
|
|
38
38
|
// entries in either list accept adapter id, model id, or adapter:model (grammar A1)
|
|
39
|
-
export function disallowedBy(c, routing) {
|
|
39
|
+
export function disallowedBy(c, routing, role = "worker") {
|
|
40
40
|
const { allow, deny } = routing;
|
|
41
41
|
const matches = (e) => e === c.adapter || e === c.model || e === channelKey(c);
|
|
42
|
-
const
|
|
42
|
+
const workerDeny = role === "worker" ? deny?.workers : undefined;
|
|
43
|
+
const d = [
|
|
44
|
+
...(deny?.adapters ?? []),
|
|
45
|
+
...(deny?.models ?? []),
|
|
46
|
+
...(workerDeny?.adapters ?? []),
|
|
47
|
+
...(workerDeny?.models ?? []),
|
|
48
|
+
].find(matches);
|
|
43
49
|
if (d)
|
|
44
50
|
return { by: "deny", entry: d };
|
|
45
51
|
if (allow) {
|
package/dist/run/daemon.d.ts
CHANGED
package/dist/run/daemon.js
CHANGED
|
@@ -14,11 +14,12 @@ import { formatOwnedName } from "../drivers/types.js";
|
|
|
14
14
|
import { captureBaseline, detectGateCommands, detectVacuousOracles } from "../gates/baseline.js";
|
|
15
15
|
import { runGates } from "../gates/run-gates.js";
|
|
16
16
|
import { addEvidence, attributeBlocked, blockedTasks, getTask, graphDefinitionHash, loadGraph, pendingTasks, readyTasks, saveGraph, setStatus } from "../graph/graph.js";
|
|
17
|
+
import { GATE_NAMES } from "../graph/schema.js";
|
|
17
18
|
import { augmentRetryBrief, consult, renderRetryGuidance } from "./consult.js";
|
|
18
19
|
import { runEnvironment } from "./environment.js";
|
|
19
|
-
import { cleanupRunWorktrees, gitHead, linkNodeModules, sh, shGit, WORKTREE_LAYOUT_CONTRACT, worktreePath } from "./git.js";
|
|
20
|
+
import { cleanupRunWorktrees, gitHead, linkNodeModules, npmDependencyInstallCommand, npmDependencyManifestChanged, sh, shGit, WORKTREE_LAYOUT_CONTRACT, worktreePath } from "./git.js";
|
|
20
21
|
import { runInteractiveSeed } from "./interactive-seed.js";
|
|
21
|
-
import { classifyWorkerResultCause, engagementComparable, Journal, loadRoutingProfile, newRunId } from "./journal.js";
|
|
22
|
+
import { classifyWorkerResultCause, engagementComparable, Journal, loadRoutingProfile, newRunId, phaseForGate, recordedTaskFailureKind } from "./journal.js";
|
|
22
23
|
import { acquireRunLock, releaseRunLock } from "./lock.js";
|
|
23
24
|
import { ensureIntegration, integrationBranch, integrationHead, mergeTask, verifyIntegrationTip } from "./merge.js";
|
|
24
25
|
import { nextChannel, route } from "../route/router.js";
|
|
@@ -229,6 +230,7 @@ export async function runDaemon(repoRoot, opts = {}) {
|
|
|
229
230
|
// GATE-08 lesson at the humanGate guard: condition on the data, not the code path). Dead-code
|
|
230
231
|
// equivalence to the router.ts:194 profile⇒undefined pattern: no map entry ⇒ today's literal.
|
|
231
232
|
const resume = opts.resume ? journal.replayResumeState() : new Map();
|
|
233
|
+
const satisfiedGates = opts.resume ? journal.replaySatisfiedGates() : new Map();
|
|
232
234
|
const replayedExclusions = opts.resume ? journal.replayExcludedChannels() : new Set();
|
|
233
235
|
if (opts.resume) {
|
|
234
236
|
// v1.53 T5: a superseded run is dead — resuming it beside its successor is the exact
|
|
@@ -259,7 +261,13 @@ export async function runDaemon(repoRoot, opts = {}) {
|
|
|
259
261
|
});
|
|
260
262
|
}
|
|
261
263
|
baseline = JSON.parse(readFileSync(join(journal.dir, "baseline.json"), "utf8"));
|
|
264
|
+
const replayEvents = journal.read();
|
|
262
265
|
for (const [id, st] of journal.replayStatuses()) {
|
|
266
|
+
if (opts.retryFailed && st === "failed" && recordedTaskFailureKind(replayEvents, id) === "dispatch") {
|
|
267
|
+
graph = setStatus(graph, id, "pending");
|
|
268
|
+
resume.delete(id);
|
|
269
|
+
continue;
|
|
270
|
+
}
|
|
263
271
|
// operator release: a graph.json edit back to "pending" beats a replayed human/failed park (locked decision 12)
|
|
264
272
|
if ((st === "human" || st === "failed") && getTask(graph, id).status === "pending")
|
|
265
273
|
continue;
|
|
@@ -268,6 +276,7 @@ export async function runDaemon(repoRoot, opts = {}) {
|
|
|
268
276
|
journal.append("run-resume", undefined, {
|
|
269
277
|
pid: process.pid, // v1.13 (VIS-11): record the live daemon pid for status liveness
|
|
270
278
|
...(replayedExclusions.size > 0 ? { excludedChannels: [...replayedExclusions].sort() } : {}),
|
|
279
|
+
...(opts.retryFailed ? { retryFailed: true } : {}),
|
|
271
280
|
});
|
|
272
281
|
}
|
|
273
282
|
else {
|
|
@@ -366,7 +375,10 @@ export async function runDaemon(repoRoot, opts = {}) {
|
|
|
366
375
|
// merges are serialized — two concurrent `git merge`s in one worktree would corrupt each other
|
|
367
376
|
let mergeChain = Promise.resolve();
|
|
368
377
|
const mergeSerial = (taskBranch, t, gated) => {
|
|
369
|
-
const next = mergeChain.then(() =>
|
|
378
|
+
const next = mergeChain.then(() => {
|
|
379
|
+
journal.phaseStart(t.id, "merge");
|
|
380
|
+
return mergeTask(intWt, taskBranch, `tickmarkr: merge ${t.id} ${t.title}`, gated);
|
|
381
|
+
});
|
|
370
382
|
mergeChain = next.catch(() => undefined);
|
|
371
383
|
return next;
|
|
372
384
|
};
|
|
@@ -526,6 +538,114 @@ export async function runDaemon(repoRoot, opts = {}) {
|
|
|
526
538
|
await park(t, `consult verdict: ${v.action} — ${v.notes}`, trigger, assignment, attempts, startMs, gateFails, consults, tokens, metered, retryMode); // decompose|human
|
|
527
539
|
return false;
|
|
528
540
|
};
|
|
541
|
+
// OBS-130: an operator-approved gate-fail resumes from the persisted task branch, after the exact
|
|
542
|
+
// satisfied gate. No worker dispatch and no earlier gate/judge invocation occur on this path.
|
|
543
|
+
// replaySatisfiedGates() admits only an explicit task-approved ledger event, scoped by task+gate.
|
|
544
|
+
const satisfiedGate = satisfiedGates.get(t.id);
|
|
545
|
+
if (satisfiedGate) {
|
|
546
|
+
const taskBase = await integrationHead(intWt);
|
|
547
|
+
const taskBranch = `${branch}--${t.id}`;
|
|
548
|
+
const priorWt = worktreePath(repoRoot, taskBranch);
|
|
549
|
+
if (!existsSync(priorWt)) {
|
|
550
|
+
throw new Error(`approved gate ${satisfiedGate} cannot resume: task worktree is missing`);
|
|
551
|
+
}
|
|
552
|
+
const commitsToCarry = await commitsAheadOf(taskBase, priorWt);
|
|
553
|
+
const wt = await driver.worktree(repoRoot, taskBranch, taskBase);
|
|
554
|
+
const carriedCommits = await cherryPickCommits(wt, commitsToCarry);
|
|
555
|
+
journal.append("worktree-recreation", t.id, { attempted: commitsToCarry, carried: carriedCommits });
|
|
556
|
+
if (!linkNodeModules(repoRoot, wt, { force: true })) {
|
|
557
|
+
await park(t, "environmental: node_modules link could not be re-asserted before gates (OBS-47)", "setup", assignment, rs?.attempts ?? 0, startMs, gateFails, consults, tokens, metered, retryMode);
|
|
558
|
+
return;
|
|
559
|
+
}
|
|
560
|
+
if (await npmDependencyManifestChanged(wt, taskBase)) {
|
|
561
|
+
const installCommand = npmDependencyInstallCommand(wt);
|
|
562
|
+
const installed = await sh(installCommand, repoRoot, 10 * 60_000);
|
|
563
|
+
if (installed.code !== 0) {
|
|
564
|
+
throw new Error(`dependency install failed (exit ${installed.code}): ${installed.stderr || installed.stdout}`);
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
const workerEvent = [...journal.read()].reverse()
|
|
568
|
+
.find((e) => e.event === "worker-result" && e.taskId === t.id);
|
|
569
|
+
if (!workerEvent)
|
|
570
|
+
throw new Error(`approved gate ${satisfiedGate} cannot resume: worker result is missing`);
|
|
571
|
+
const priorResult = {
|
|
572
|
+
ok: workerEvent.data.ok === true,
|
|
573
|
+
summary: typeof workerEvent.data.summary === "string" ? workerEvent.data.summary : "",
|
|
574
|
+
deviations: Array.isArray(workerEvent.data.deviations)
|
|
575
|
+
? workerEvent.data.deviations.filter((d) => typeof d === "string")
|
|
576
|
+
: [],
|
|
577
|
+
raw: "",
|
|
578
|
+
};
|
|
579
|
+
const gateAuthor = rs?.lastAssignment ?? assignment;
|
|
580
|
+
const satisfiedIndex = GATE_NAMES.indexOf(satisfiedGate);
|
|
581
|
+
const remainingGates = t.gates.filter((gate) => GATE_NAMES.indexOf(gate) > satisfiedIndex);
|
|
582
|
+
const resumedTask = { ...t, gates: remainingGates };
|
|
583
|
+
gateLoop: while (true) {
|
|
584
|
+
const gated = await gitHead(wt);
|
|
585
|
+
journal.phaseStart(t.id, "gates");
|
|
586
|
+
const { results } = await runGates(resumedTask, {
|
|
587
|
+
worktree: wt, baseRef: taskBase, result: priorResult, author: gateAuthor,
|
|
588
|
+
commands, baseline, channels, adapters, cfg,
|
|
589
|
+
via: cfg.visibility.llm === "pane"
|
|
590
|
+
? {
|
|
591
|
+
driver: trackedDriver,
|
|
592
|
+
keep: keepLlm,
|
|
593
|
+
onSlot: keepLlm ? (s) => keptSlots.push(s) : undefined,
|
|
594
|
+
nameFor: (role) => formatOwnedName({ role, taskId: t.id, attempt: 0, runId }),
|
|
595
|
+
labelFor: (role) => `${role.toUpperCase()} ${t.id}`,
|
|
596
|
+
}
|
|
597
|
+
: undefined,
|
|
598
|
+
excludeReviewers: badReviewers,
|
|
599
|
+
onGate: async (e) => {
|
|
600
|
+
if (e.phase === "start") {
|
|
601
|
+
journal.phaseStart(t.id, phaseForGate(e.gate), { gate: e.gate, index: e.index, total: e.total });
|
|
602
|
+
return;
|
|
603
|
+
}
|
|
604
|
+
const g = e.result;
|
|
605
|
+
journal.append("gate-result", t.id, {
|
|
606
|
+
gate: g.gate, pass: g.pass, details: g.details,
|
|
607
|
+
...(g.meta?.skipped === true ? { skipped: true } : {}),
|
|
608
|
+
});
|
|
609
|
+
if (g.gate === "review" && !g.pass && /unparseable/.test(g.details)
|
|
610
|
+
&& typeof g.meta?.reviewer === "string") {
|
|
611
|
+
badReviewers.push(g.meta.reviewer);
|
|
612
|
+
}
|
|
613
|
+
},
|
|
614
|
+
});
|
|
615
|
+
const approvedCommits = await commitsAheadOf(taskBase, wt);
|
|
616
|
+
graph = addEvidence(graph, t.id, { commits: approvedCommits, gateResults: results });
|
|
617
|
+
saveGraph(repoRoot, graph);
|
|
618
|
+
if (!results.every((g) => g.pass)) {
|
|
619
|
+
gateFails++;
|
|
620
|
+
await park(t, "post-approval gate failed", "gate-fail", gateAuthor, rs?.attempts ?? 0, startMs, gateFails, consults, tokens, metered, retryMode);
|
|
621
|
+
return;
|
|
622
|
+
}
|
|
623
|
+
const m = await mergeSerial(taskBranch, t, gated);
|
|
624
|
+
if (m.tipMoved) {
|
|
625
|
+
journal.append("tip-moved", t.id, m.tipMoved);
|
|
626
|
+
if (tipMoves++ === 0)
|
|
627
|
+
continue gateLoop;
|
|
628
|
+
await park(t, "task branch tip moved twice after gating", "tip-moved", gateAuthor, rs?.attempts ?? 0, startMs, gateFails, consults, tokens, metered, retryMode);
|
|
629
|
+
return;
|
|
630
|
+
}
|
|
631
|
+
if (!m.ok) {
|
|
632
|
+
journal.append("merge-conflict", t.id, { conflict: m.conflict });
|
|
633
|
+
await park(t, `merge conflict after approved gate: ${m.conflict ?? "unknown conflict"}`, "merge-conflict", gateAuthor, rs?.attempts ?? 0, startMs, gateFails, consults, tokens, metered, retryMode);
|
|
634
|
+
return;
|
|
635
|
+
}
|
|
636
|
+
graph = setStatus(graph, t.id, "done");
|
|
637
|
+
saveGraph(repoRoot, graph);
|
|
638
|
+
journal.append("task-done", t.id, { attempts: rs?.attempts ?? 0, assignment: gateAuthor });
|
|
639
|
+
journal.append("merge", t.id, { branch: taskBranch, commit: await integrationHead(intWt) });
|
|
640
|
+
journal.telemetry({
|
|
641
|
+
taskId: t.id, shape: t.shape, adapter: gateAuthor.adapter, model: gateAuthor.model,
|
|
642
|
+
channel: gateAuthor.channel, attempts: rs?.attempts ?? 0, outcome: "done",
|
|
643
|
+
durationMs: Date.now() - startMs, firstAttemptOk: false, gateFails, consults, retryMode,
|
|
644
|
+
});
|
|
645
|
+
await reconcile({ spareLiveLlm: true });
|
|
646
|
+
return;
|
|
647
|
+
}
|
|
648
|
+
}
|
|
529
649
|
// Phase 46 (RES-01): start at the replayed attempt count; a replayed count ≥ MAX_ATTEMPTS parks via
|
|
530
650
|
// the existing attempt-cap check below with zero new code. Fresh path: rs is undefined ⇒ 0.
|
|
531
651
|
// v1.24 OBS-18: after task-approved{release:attempt-cap}, replay zeros attempts so this loop
|
|
@@ -594,6 +714,7 @@ export async function runDaemon(repoRoot, opts = {}) {
|
|
|
594
714
|
graph = setStatus(graph, t.id, "running");
|
|
595
715
|
saveGraph(repoRoot, graph);
|
|
596
716
|
journal.append("task-dispatch", t.id, { assignment, attempt, provenance: dispatchProvenance(r.provenance), retryMode });
|
|
717
|
+
journal.phaseStart(t.id, "worker", { attempt, assignment });
|
|
597
718
|
const taskBase = await integrationHead(intWt); // deps are merged → visible to this task
|
|
598
719
|
const taskBranch = `${branch}--${t.id}`; // "--": a ref can't nest under the existing integration branch (locked decision 10)
|
|
599
720
|
const priorWt = worktreePath(repoRoot, taskBranch);
|
|
@@ -1044,9 +1165,22 @@ export async function runDaemon(repoRoot, opts = {}) {
|
|
|
1044
1165
|
await park(t, "environmental: node_modules link could not be re-asserted before gates (OBS-47)", "setup", assignment, attempt + 1, startMs, gateFails, consults, tokens, metered, retryMode);
|
|
1045
1166
|
return;
|
|
1046
1167
|
}
|
|
1168
|
+
// OBS-126: workers cannot write the provisioned node_modules target outside their sandbox.
|
|
1169
|
+
// Once the link is known-good, the daemon installs only attempts whose npm manifest differs
|
|
1170
|
+
// from the integration-tip baseline. Keep lock/package manifests untouched: the worker's
|
|
1171
|
+
// committed files are the deliverable, while this step only provisions the gate-visible tree.
|
|
1172
|
+
if (await npmDependencyManifestChanged(wt, taskBase)) {
|
|
1173
|
+
const installCommand = npmDependencyInstallCommand(wt);
|
|
1174
|
+
const installed = await sh(installCommand, repoRoot, 10 * 60_000);
|
|
1175
|
+
if (installed.code !== 0) {
|
|
1176
|
+
throw new Error(`dependency install failed (exit ${installed.code}): ${installed.stderr || installed.stdout}`);
|
|
1177
|
+
}
|
|
1178
|
+
}
|
|
1047
1179
|
const onGate = async (e) => {
|
|
1048
|
-
if (e.phase === "start")
|
|
1180
|
+
if (e.phase === "start") {
|
|
1181
|
+
journal.phaseStart(t.id, phaseForGate(e.gate), { gate: e.gate, index: e.index, total: e.total });
|
|
1049
1182
|
return;
|
|
1183
|
+
}
|
|
1050
1184
|
const g = e.result;
|
|
1051
1185
|
// GATE-09 (ROADMAP SC-4): journal every judge retry as an attributable event — which gate flaked,
|
|
1052
1186
|
// which channel flaked, which channel retried — so `tickmarkr journal`/report can distinguish "judge
|
|
@@ -1075,6 +1209,7 @@ export async function runDaemon(repoRoot, opts = {}) {
|
|
|
1075
1209
|
let commits = [];
|
|
1076
1210
|
gateLoop: while (true) {
|
|
1077
1211
|
const gated = await gitHead(wt);
|
|
1212
|
+
journal.phaseStart(t.id, "gates");
|
|
1078
1213
|
({ results, commits } = await runGates(t, {
|
|
1079
1214
|
worktree: wt, baseRef: taskBase, result, author: assignment,
|
|
1080
1215
|
commands, baseline, channels, adapters, cfg,
|
package/dist/run/git.d.ts
CHANGED
|
@@ -12,6 +12,8 @@ export declare function sh(cmd: string, cwd: string, timeoutMs?: number): Promis
|
|
|
12
12
|
export declare function shGit(cmd: string, cwd: string, timeoutMs?: number): Promise<ShResult>;
|
|
13
13
|
export declare function shOk(cmd: string, cwd: string): Promise<string>;
|
|
14
14
|
export declare function shGitOk(cmd: string, cwd: string): Promise<string>;
|
|
15
|
+
export declare function npmDependencyManifestChanged(cwd: string, baselineRef: string): Promise<boolean>;
|
|
16
|
+
export declare function npmDependencyInstallCommand(cwd: string): string;
|
|
15
17
|
export declare function gitHead(cwd: string): Promise<string>;
|
|
16
18
|
export declare const sanitizeBranch: (branch: string) => string;
|
|
17
19
|
export declare const WORKTREES_DIR = "worktrees.noindex";
|