tickmarkr 2.5.4 → 2.5.6
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 +3 -1
- 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 +118 -9
- package/dist/cli/commands/doctor.d.ts +10 -0
- package/dist/cli/commands/doctor.js +44 -29
- package/dist/cli/commands/fleet.js +199 -33
- package/dist/cli/commands/init.js +196 -6
- package/dist/cli/commands/plan.js +20 -4
- package/dist/cli/commands/resume.js +4 -2
- package/dist/cli/commands/run.js +11 -2
- package/dist/cli/commands/verify.js +102 -81
- package/dist/cli/help.d.ts +2 -0
- package/dist/cli/help.js +3 -1
- package/dist/config/config.d.ts +25 -8
- package/dist/config/config.js +41 -29
- package/dist/config/fleet-overlay.d.ts +3 -9
- package/dist/config/fleet-overlay.js +114 -19
- 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 +39 -10
- package/dist/drivers/orca.d.ts +119 -10
- package/dist/drivers/orca.js +781 -110
- package/dist/gates/baseline.d.ts +17 -3
- package/dist/gates/baseline.js +63 -15
- package/dist/gates/cache.d.ts +101 -0
- package/dist/gates/cache.js +401 -0
- package/dist/gates/llm.d.ts +3 -0
- package/dist/gates/llm.js +11 -0
- package/dist/gates/review.d.ts +28 -3
- package/dist/gates/review.js +106 -14
- package/dist/gates/run-gates.d.ts +9 -1
- package/dist/gates/run-gates.js +407 -102
- package/dist/gates/test-manifest.d.ts +128 -0
- package/dist/gates/test-manifest.js +463 -0
- package/dist/gates/test-reporter.d.ts +4 -0
- package/dist/gates/test-reporter.js +57 -0
- package/dist/graph/graph.d.ts +2 -0
- package/dist/graph/graph.js +45 -1
- 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/consult.js +5 -4
- package/dist/run/daemon.d.ts +15 -0
- package/dist/run/daemon.js +638 -167
- package/dist/run/execution-budget.d.ts +25 -0
- package/dist/run/execution-budget.js +142 -0
- package/dist/run/git.d.ts +50 -1
- package/dist/run/git.js +131 -12
- package/dist/run/journal.d.ts +16 -2
- package/dist/run/journal.js +115 -25
- package/dist/run/lease.d.ts +58 -0
- package/dist/run/lease.js +310 -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/run/recovery.d.ts +8 -0
- package/dist/run/recovery.js +25 -0
- package/dist/run/repair-selection.d.ts +12 -0
- package/dist/run/repair-selection.js +56 -0
- package/dist/run/stall.d.ts +6 -1
- package/dist/run/stall.js +60 -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 +49 -27
- package/dist/tui/ink/fleet-app.js +229 -38
- package/package.json +2 -2
- package/skills/tickmarkr-overseer/SKILL.md +173 -113
- package/skills/tickmarkr-overseer/scripts/grade-ci.sh +29 -2
|
@@ -0,0 +1,401 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { execSync } from "node:child_process";
|
|
3
|
+
import { existsSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, realpathSync, rmSync, unlinkSync, writeFileSync } from "node:fs";
|
|
4
|
+
import { dirname, join, resolve } from "node:path";
|
|
5
|
+
import { tmpdir } from "node:os";
|
|
6
|
+
import { describeCapacity, resolvedCapacity, shGit, verificationProtocol } from "../run/git.js";
|
|
7
|
+
import { shq } from "../adapters/types.js";
|
|
8
|
+
export const DEFAULT_VERDICT_CACHE_BOUND = 128;
|
|
9
|
+
let testCacheBound;
|
|
10
|
+
export function setVerdictCacheBoundForTests(bound) {
|
|
11
|
+
testCacheBound = bound;
|
|
12
|
+
}
|
|
13
|
+
export function resetVerdictCacheBoundForTests() {
|
|
14
|
+
testCacheBound = undefined;
|
|
15
|
+
globalStoreSequence = 0;
|
|
16
|
+
}
|
|
17
|
+
export const LOCKFILES = [
|
|
18
|
+
"package-lock.json",
|
|
19
|
+
"npm-shrinkwrap.json",
|
|
20
|
+
"pnpm-lock.yaml",
|
|
21
|
+
"yarn.lock",
|
|
22
|
+
"bun.lock",
|
|
23
|
+
"bun.lockb",
|
|
24
|
+
];
|
|
25
|
+
export function lockfileHash(worktree) {
|
|
26
|
+
const hash = createHash("sha256");
|
|
27
|
+
let found = false;
|
|
28
|
+
for (const file of LOCKFILES) {
|
|
29
|
+
const p = join(worktree, file);
|
|
30
|
+
if (existsSync(p)) {
|
|
31
|
+
found = true;
|
|
32
|
+
try {
|
|
33
|
+
hash.update(file).update("\0").update(readFileSync(p)).update("\0");
|
|
34
|
+
}
|
|
35
|
+
catch {
|
|
36
|
+
hash.update(file).update("\0unreadable\0");
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
return found ? hash.digest("hex").slice(0, 16) : "no-lockfile";
|
|
41
|
+
}
|
|
42
|
+
export function canonicalJson(obj) {
|
|
43
|
+
if (obj === null || typeof obj !== "object")
|
|
44
|
+
return JSON.stringify(obj);
|
|
45
|
+
if (Array.isArray(obj))
|
|
46
|
+
return "[" + obj.map(canonicalJson).join(",") + "]";
|
|
47
|
+
const keys = Object.keys(obj).sort();
|
|
48
|
+
return "{" + keys.map((k) => JSON.stringify(k) + ":" + canonicalJson(obj[k])).join(",") + "}";
|
|
49
|
+
}
|
|
50
|
+
export function baselineIdentity(baseline) {
|
|
51
|
+
if (!baseline)
|
|
52
|
+
return "none";
|
|
53
|
+
// Measurements control execution budgets, not which failures the baseline forgives.
|
|
54
|
+
const commands = Object.fromEntries(Object.entries(baseline.commands).map(([gate, entry]) => {
|
|
55
|
+
const { durationMs: _duration, fileDurationSumMs: _sum, impliedParallelism: _parallelism, longestFile: _longest, fileDurations: _files, ceilingMs: _ceiling, ...evidence } = entry;
|
|
56
|
+
return [gate, evidence];
|
|
57
|
+
}));
|
|
58
|
+
return createHash("sha256").update(canonicalJson(commands)).digest("hex").slice(0, 16);
|
|
59
|
+
}
|
|
60
|
+
// The tree the command actually ran against: HEAD plus every tracked change and ordinary untracked
|
|
61
|
+
// path (a fixture that flips an uncommitted package.json changes the verdict). Built through a
|
|
62
|
+
// disposable index, as preserveWorktree does, so the checkout's real index is never touched.
|
|
63
|
+
export async function getWorktreeTree(worktree) {
|
|
64
|
+
const scratch = mkdtempSync(join(tmpdir(), "tickmarkr-verdict-index-"));
|
|
65
|
+
const withIndex = (cmd) => `GIT_INDEX_FILE=${shq(join(scratch, "index"))} ${cmd}`;
|
|
66
|
+
try {
|
|
67
|
+
const read = await shGit(withIndex("git read-tree HEAD"), worktree);
|
|
68
|
+
if (read.code !== 0)
|
|
69
|
+
return "";
|
|
70
|
+
const add = await shGit(withIndex("git add -A -- ."), worktree);
|
|
71
|
+
if (add.code !== 0)
|
|
72
|
+
return "";
|
|
73
|
+
const tree = await shGit(withIndex("git write-tree"), worktree);
|
|
74
|
+
return tree.code === 0 ? tree.stdout.trim() : "";
|
|
75
|
+
}
|
|
76
|
+
finally {
|
|
77
|
+
rmSync(scratch, { recursive: true, force: true });
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
export function environmentFingerprint(env) {
|
|
81
|
+
const nodeRuntime = env.nodeRuntime ?? process.version;
|
|
82
|
+
const lockfile = env.lockfile ?? (env.worktree ? lockfileHash(env.worktree) : "no-lockfile");
|
|
83
|
+
const cap = env.capacity ?? resolvedCapacity();
|
|
84
|
+
const capacity = { forkCap: cap.forkCap, cores: cap.cores };
|
|
85
|
+
const selectedSet = env.selectedSet ? [...env.selectedSet].sort() : undefined;
|
|
86
|
+
const verification = env.verification ?? verificationProtocol(process.env, env.worktree ?? process.cwd());
|
|
87
|
+
// R41: the protocol and the EFFECTIVE lifecycle are IN the hashed payload, so every entry written
|
|
88
|
+
// before this stamp — green or red — keys differently and is never answered; no store surgery is
|
|
89
|
+
// needed. `source` is provenance (kept in parts, printed on the row) and never enters the key: an
|
|
90
|
+
// explicit `false` and an npmrc `false` are the same policy for the child that ran.
|
|
91
|
+
const payload = canonicalJson({
|
|
92
|
+
nodeRuntime,
|
|
93
|
+
lockfile,
|
|
94
|
+
capacity,
|
|
95
|
+
selectedSet: selectedSet ?? null,
|
|
96
|
+
scope: env.scope ?? "battery",
|
|
97
|
+
verification: { protocol: verification.protocol, lifecycle: verification.lifecycle },
|
|
98
|
+
});
|
|
99
|
+
const fingerprint = createHash("sha256").update(payload).digest("hex").slice(0, 16);
|
|
100
|
+
return {
|
|
101
|
+
fingerprint,
|
|
102
|
+
parts: { nodeRuntime, lockfile, capacity, selectedSet, verification },
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
export async function computeVerificationIdentity(params) {
|
|
106
|
+
const tree = params.tree ?? (await getWorktreeTree(params.worktree));
|
|
107
|
+
if (!tree)
|
|
108
|
+
return undefined;
|
|
109
|
+
const baseline = baselineIdentity(params.baseline);
|
|
110
|
+
const env = environmentFingerprint({
|
|
111
|
+
worktree: params.worktree,
|
|
112
|
+
selectedSet: params.selectedSet,
|
|
113
|
+
capacity: params.capacity,
|
|
114
|
+
lockfile: params.lockfile,
|
|
115
|
+
nodeRuntime: params.nodeRuntime,
|
|
116
|
+
scope: params.scope,
|
|
117
|
+
verification: params.verification,
|
|
118
|
+
});
|
|
119
|
+
return {
|
|
120
|
+
gate: params.gate,
|
|
121
|
+
scope: params.scope ?? "battery",
|
|
122
|
+
worktree: realpathSync(params.worktree),
|
|
123
|
+
tree,
|
|
124
|
+
command: params.command,
|
|
125
|
+
baseline,
|
|
126
|
+
environment: env.fingerprint,
|
|
127
|
+
envParts: env.parts,
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
export function verificationIdentityKey(id) {
|
|
131
|
+
const cmdHash = createHash("sha256").update(id.command).digest("hex").slice(0, 16);
|
|
132
|
+
const gate = id.gate ?? "gate";
|
|
133
|
+
// Checkout location is diagnostic only. Scope separates each verifier's evidence policy.
|
|
134
|
+
const workingTree = createHash("sha256").update(id.tree).digest("hex");
|
|
135
|
+
return `${id.scope ?? "battery"}-${gate}-${workingTree}-${cmdHash}-${id.baseline}-${id.environment}`;
|
|
136
|
+
}
|
|
137
|
+
export function formatReusedDetails(originalDetails, id) {
|
|
138
|
+
const unadorned = originalDetails.replace(/^reused verdict \(identity: [^)]+\):\s*/, "");
|
|
139
|
+
const envDesc = id.envParts
|
|
140
|
+
? ` [node=${id.envParts.nodeRuntime}, lockfile=${id.envParts.lockfile}, capacity=${describeCapacity(id.envParts.capacity)}${id.envParts.selectedSet ? `, selected=${id.envParts.selectedSet.join(",")}` : ""}, protocol=${id.envParts.verification.protocol}, lifecycle=${id.envParts.verification.lifecycle} (${id.envParts.verification.source})]`
|
|
141
|
+
: "";
|
|
142
|
+
const gate = id.gate ?? "gate";
|
|
143
|
+
const prefix = `reused ${id.scope === "tip" ? "tip " : ""}verdict (identity: gate=${gate} tree=${id.tree}${id.worktree ? ` worktree=${id.worktree}` : ""} command=${id.command} baseline=${id.baseline} env=${id.environment}${envDesc})`;
|
|
144
|
+
return `${prefix}: ${unadorned}`;
|
|
145
|
+
}
|
|
146
|
+
// A reused GREEN names the reuse in its details. A reused RED keeps the fresh verdict's details
|
|
147
|
+
// byte-for-byte: the daemon's fingerprint cap (identicalGateFailures) compares normalized details
|
|
148
|
+
// across attempts, so a red whose details carried a reuse note would never read as the identical
|
|
149
|
+
// failure it is and would buy a free retry — the reuse is named on the row's meta and on the
|
|
150
|
+
// gate-reused-verdict note run-gates emits beside it, which the daemon journals as its own row.
|
|
151
|
+
export function formatReusedRow(cached, id) {
|
|
152
|
+
const reusedDetails = formatReusedDetails(cached.details, id);
|
|
153
|
+
return {
|
|
154
|
+
gate: id.gate ?? cached.gate,
|
|
155
|
+
pass: cached.pass,
|
|
156
|
+
details: cached.pass ? reusedDetails : cached.details,
|
|
157
|
+
capacity: cached.capacity,
|
|
158
|
+
meta: {
|
|
159
|
+
...cached.meta,
|
|
160
|
+
reused: true,
|
|
161
|
+
reusedDetails,
|
|
162
|
+
verificationIdentity: reusedIdentity(id),
|
|
163
|
+
},
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
export function reusedIdentity(id) {
|
|
167
|
+
return {
|
|
168
|
+
gate: id.gate ?? "gate",
|
|
169
|
+
scope: id.scope ?? "battery",
|
|
170
|
+
tree: id.tree,
|
|
171
|
+
...(id.worktree ? { worktree: id.worktree } : {}),
|
|
172
|
+
command: id.command,
|
|
173
|
+
baseline: id.baseline,
|
|
174
|
+
environment: id.environment,
|
|
175
|
+
key: verificationIdentityKey(id),
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
export function isInfraResult(result) {
|
|
179
|
+
if (result.meta?.infra === true)
|
|
180
|
+
return true;
|
|
181
|
+
if (result.meta?.classification === "infra")
|
|
182
|
+
return true;
|
|
183
|
+
if (result.meta?.kind === "ceiling-kill" || result.meta?.kind === "hang")
|
|
184
|
+
return true;
|
|
185
|
+
if ("cause" in result && result.cause === "infra")
|
|
186
|
+
return true;
|
|
187
|
+
if (typeof result.details === "string") {
|
|
188
|
+
const d = result.details.toLowerCase();
|
|
189
|
+
if (d.startsWith("infra;") || d.startsWith("infra: ") || d.includes("infrastructure blocked execution")) {
|
|
190
|
+
return true;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
return false;
|
|
194
|
+
}
|
|
195
|
+
export function resolveStateDir(worktree, artifactDir) {
|
|
196
|
+
if (artifactDir) {
|
|
197
|
+
const idx = artifactDir.lastIndexOf("/.tickmarkr");
|
|
198
|
+
if (idx !== -1) {
|
|
199
|
+
return artifactDir.slice(0, idx + "/.tickmarkr".length);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
const idx = worktree.lastIndexOf("/.tickmarkr");
|
|
203
|
+
if (idx !== -1) {
|
|
204
|
+
return worktree.slice(0, idx + "/.tickmarkr".length);
|
|
205
|
+
}
|
|
206
|
+
if (existsSync(join(worktree, ".tickmarkr"))) {
|
|
207
|
+
return join(worktree, ".tickmarkr");
|
|
208
|
+
}
|
|
209
|
+
try {
|
|
210
|
+
const commonDir = execSync("git rev-parse --git-common-dir", { cwd: worktree, encoding: "utf8" }).trim();
|
|
211
|
+
const resolved = resolve(worktree, commonDir);
|
|
212
|
+
const commonRoot = realpathSync(dirname(resolved));
|
|
213
|
+
if (existsSync(join(commonRoot, ".tickmarkr"))) {
|
|
214
|
+
return join(commonRoot, ".tickmarkr");
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
catch {
|
|
218
|
+
// ignore
|
|
219
|
+
}
|
|
220
|
+
return join(worktree, ".tickmarkr");
|
|
221
|
+
}
|
|
222
|
+
export function resolveVerdictStoreDir(stateDir) {
|
|
223
|
+
mkdirSync(stateDir, { recursive: true });
|
|
224
|
+
const gi = join(stateDir, ".gitignore");
|
|
225
|
+
if (!existsSync(gi)) {
|
|
226
|
+
try {
|
|
227
|
+
writeFileSync(gi, "*\n");
|
|
228
|
+
}
|
|
229
|
+
catch {
|
|
230
|
+
// ignore
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
const dir = join(stateDir, "verdicts");
|
|
234
|
+
mkdirSync(dir, { recursive: true });
|
|
235
|
+
return dir;
|
|
236
|
+
}
|
|
237
|
+
let globalStoreSequence = 0;
|
|
238
|
+
export function resetGlobalStoreSequenceForTests() {
|
|
239
|
+
globalStoreSequence = 0;
|
|
240
|
+
}
|
|
241
|
+
export class VerdictStore {
|
|
242
|
+
dir;
|
|
243
|
+
constructor(dir) {
|
|
244
|
+
this.dir = dir;
|
|
245
|
+
}
|
|
246
|
+
initSequenceFromDisk() {
|
|
247
|
+
if (!existsSync(this.dir))
|
|
248
|
+
return;
|
|
249
|
+
try {
|
|
250
|
+
for (const f of readdirSync(this.dir)) {
|
|
251
|
+
if (f.startsWith("verdict-") && f.endsWith(".json")) {
|
|
252
|
+
try {
|
|
253
|
+
const raw = JSON.parse(readFileSync(join(this.dir, f), "utf8"));
|
|
254
|
+
if (raw && typeof raw.sequence === "number" && raw.sequence > globalStoreSequence) {
|
|
255
|
+
globalStoreSequence = raw.sequence;
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
catch {
|
|
259
|
+
// ignore
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
catch {
|
|
265
|
+
// ignore
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
// R41: an identity whose lifecycle policy could not be measured is never answered and never
|
|
269
|
+
// stored — an unknown policy is not comparable to anything, so the battery runs the command.
|
|
270
|
+
static unknownPolicy(id) {
|
|
271
|
+
return id.envParts?.verification?.lifecycle === "unknown";
|
|
272
|
+
}
|
|
273
|
+
get(id) {
|
|
274
|
+
if (!id || !id.tree || VerdictStore.unknownPolicy(id))
|
|
275
|
+
return undefined;
|
|
276
|
+
const key = verificationIdentityKey(id);
|
|
277
|
+
const p = join(this.dir, `verdict-${key}.json`);
|
|
278
|
+
if (existsSync(p)) {
|
|
279
|
+
try {
|
|
280
|
+
const raw = JSON.parse(readFileSync(p, "utf8"));
|
|
281
|
+
if (raw && raw.verdict)
|
|
282
|
+
return raw.verdict;
|
|
283
|
+
}
|
|
284
|
+
catch {
|
|
285
|
+
return undefined;
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
return undefined;
|
|
289
|
+
}
|
|
290
|
+
set(id, verdict) {
|
|
291
|
+
if (!id || !id.tree || VerdictStore.unknownPolicy(id))
|
|
292
|
+
return false;
|
|
293
|
+
if (isInfraResult(verdict))
|
|
294
|
+
return false;
|
|
295
|
+
mkdirSync(this.dir, { recursive: true });
|
|
296
|
+
this.initSequenceFromDisk();
|
|
297
|
+
const key = verificationIdentityKey(id);
|
|
298
|
+
const bound = testCacheBound ?? DEFAULT_VERDICT_CACHE_BOUND;
|
|
299
|
+
const record = {
|
|
300
|
+
key,
|
|
301
|
+
identity: id,
|
|
302
|
+
verdict: {
|
|
303
|
+
gate: verdict.gate ?? id.gate ?? "gate",
|
|
304
|
+
pass: verdict.pass,
|
|
305
|
+
details: verdict.details,
|
|
306
|
+
...("exitCode" in verdict ? { exitCode: verdict.exitCode } : {}),
|
|
307
|
+
...(verdict.capacity ? { capacity: verdict.capacity } : {}),
|
|
308
|
+
...(verdict.meta ? { meta: verdict.meta } : {}),
|
|
309
|
+
},
|
|
310
|
+
timestamp: Date.now(),
|
|
311
|
+
sequence: ++globalStoreSequence,
|
|
312
|
+
};
|
|
313
|
+
const finalPath = join(this.dir, `verdict-${key}.json`);
|
|
314
|
+
const tmpPath = join(this.dir, `verdict-${key}.${process.pid}.${Date.now()}.${Math.random().toString(16).slice(2)}.tmp`);
|
|
315
|
+
writeFileSync(tmpPath, JSON.stringify(record, null, 2) + "\n");
|
|
316
|
+
try {
|
|
317
|
+
unlinkSync(finalPath);
|
|
318
|
+
}
|
|
319
|
+
catch {
|
|
320
|
+
// ignore
|
|
321
|
+
}
|
|
322
|
+
writeFileSync(finalPath, readFileSync(tmpPath));
|
|
323
|
+
try {
|
|
324
|
+
unlinkSync(tmpPath);
|
|
325
|
+
}
|
|
326
|
+
catch {
|
|
327
|
+
// ignore
|
|
328
|
+
}
|
|
329
|
+
this.evictOldest(bound);
|
|
330
|
+
return true;
|
|
331
|
+
}
|
|
332
|
+
size() {
|
|
333
|
+
if (!existsSync(this.dir))
|
|
334
|
+
return 0;
|
|
335
|
+
return readdirSync(this.dir).filter((f) => f.startsWith("verdict-") && f.endsWith(".json")).length;
|
|
336
|
+
}
|
|
337
|
+
clear() {
|
|
338
|
+
globalStoreSequence = 0;
|
|
339
|
+
if (!existsSync(this.dir))
|
|
340
|
+
return;
|
|
341
|
+
for (const f of readdirSync(this.dir)) {
|
|
342
|
+
if (f.startsWith("verdict-")) {
|
|
343
|
+
try {
|
|
344
|
+
unlinkSync(join(this.dir, f));
|
|
345
|
+
}
|
|
346
|
+
catch {
|
|
347
|
+
// ignore
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
evictOldest(bound) {
|
|
353
|
+
const limit = bound ?? testCacheBound ?? DEFAULT_VERDICT_CACHE_BOUND;
|
|
354
|
+
const dir = this.dir;
|
|
355
|
+
if (!existsSync(dir))
|
|
356
|
+
return 0;
|
|
357
|
+
const files = readdirSync(dir).filter((f) => f.startsWith("verdict-") && f.endsWith(".json"));
|
|
358
|
+
if (files.length <= limit)
|
|
359
|
+
return 0;
|
|
360
|
+
const entries = [];
|
|
361
|
+
for (const file of files) {
|
|
362
|
+
const p = join(dir, file);
|
|
363
|
+
try {
|
|
364
|
+
const raw = JSON.parse(readFileSync(p, "utf8"));
|
|
365
|
+
entries.push({
|
|
366
|
+
file,
|
|
367
|
+
sequence: typeof raw.sequence === "number" ? raw.sequence : 0,
|
|
368
|
+
timestamp: typeof raw.timestamp === "number" ? raw.timestamp : 0,
|
|
369
|
+
});
|
|
370
|
+
}
|
|
371
|
+
catch {
|
|
372
|
+
entries.push({ file, sequence: -1, timestamp: -1 });
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
entries.sort((a, b) => {
|
|
376
|
+
// Each write seeds its sequence from disk, including in a fresh process.
|
|
377
|
+
// Wall-clock corrections must not make the newest write the oldest entry.
|
|
378
|
+
if (a.sequence !== b.sequence)
|
|
379
|
+
return a.sequence - b.sequence;
|
|
380
|
+
return a.timestamp - b.timestamp;
|
|
381
|
+
});
|
|
382
|
+
let evicted = 0;
|
|
383
|
+
while (entries.length > limit) {
|
|
384
|
+
const oldest = entries.shift();
|
|
385
|
+
try {
|
|
386
|
+
unlinkSync(join(dir, oldest.file));
|
|
387
|
+
evicted++;
|
|
388
|
+
}
|
|
389
|
+
catch {
|
|
390
|
+
// ignore
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
return evicted;
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
export function getVerdictStore(stateDir, options) {
|
|
397
|
+
if (options?.bound !== undefined) {
|
|
398
|
+
setVerdictCacheBoundForTests(options.bound);
|
|
399
|
+
}
|
|
400
|
+
return new VerdictStore(resolveVerdictStoreDir(stateDir));
|
|
401
|
+
}
|
package/dist/gates/llm.d.ts
CHANGED
|
@@ -62,11 +62,14 @@ export interface LlmRunResult {
|
|
|
62
62
|
exitCode?: number;
|
|
63
63
|
timedOut: boolean;
|
|
64
64
|
launchNeverStarted?: boolean;
|
|
65
|
+
/** OBS-1039: seat-authored bytes stayed under REVIEW_SILENT_BYTE_FLOOR at the first liveness beat. */
|
|
66
|
+
silentAtBeat?: boolean;
|
|
65
67
|
seatAuthoredBytes?: number;
|
|
66
68
|
}
|
|
67
69
|
export declare const PROMPT_GLYPHS: readonly ["➜", "❯", "$", "%", ">>", ">"];
|
|
68
70
|
export declare function reviewSeatOutput(raw: string, nonce: string, adapterBannerRows?: readonly string[]): string;
|
|
69
71
|
export declare const REVIEW_FIRST_LIVENESS_MS = 30000;
|
|
72
|
+
export declare const REVIEW_SILENT_BYTE_FLOOR = 64;
|
|
70
73
|
export declare function runHeadless(adapter: WorkerAdapter, model: string, prompt: string, cwd: string, timeoutMs?: number): Promise<string>;
|
|
71
74
|
export declare function runViaDriver(adapter: WorkerAdapter, model: string, prompt: string, cwd: string, via: LlmVia, timeoutMs?: number): Promise<string>;
|
|
72
75
|
export declare function dewrapPaneVerdict(out: string, nonce: string): string;
|
package/dist/gates/llm.js
CHANGED
|
@@ -300,6 +300,10 @@ export function reviewSeatOutput(raw, nonce, adapterBannerRows = []) {
|
|
|
300
300
|
return trailer ? seat.slice(0, trailer.index) : seat;
|
|
301
301
|
}
|
|
302
302
|
export const REVIEW_FIRST_LIVENESS_MS = 30_000;
|
|
303
|
+
// OBS-1039: a seat that wrote ten bytes and went quiet escaped the zero-byte beat and sat to the
|
|
304
|
+
// ceiling. Below this many seat-authored bytes at the first beat the seat is `silent` — demoted and
|
|
305
|
+
// re-routed then, not at the ceiling. Pane path only; a headless runner buffers and keeps its ceiling.
|
|
306
|
+
export const REVIEW_SILENT_BYTE_FLOOR = 64;
|
|
303
307
|
async function runHeadlessDetailed(adapter, model, prompt, cwd, timeoutMs = 300000) {
|
|
304
308
|
const dir = mkdtempSync(join(tmpdir(), "tickmarkr-llm-"));
|
|
305
309
|
try {
|
|
@@ -356,6 +360,7 @@ async function runViaDriverDetailed(adapter, model, prompt, cwd, via, timeoutMs
|
|
|
356
360
|
let out;
|
|
357
361
|
let timedOut = false;
|
|
358
362
|
let launchNeverStarted = false;
|
|
363
|
+
let silentAtBeat = false;
|
|
359
364
|
let seatAuthoredBytes = 0;
|
|
360
365
|
const gatePrompt = prompt.startsWith("TICKMARKR-JUDGE") || prompt.startsWith("TICKMARKR-REVIEW");
|
|
361
366
|
if (!gatePrompt) {
|
|
@@ -412,6 +417,11 @@ async function runViaDriverDetailed(adapter, model, prompt, cwd, via, timeoutMs
|
|
|
412
417
|
forceClose = true;
|
|
413
418
|
break;
|
|
414
419
|
}
|
|
420
|
+
if (seatAuthoredBytes < REVIEW_SILENT_BYTE_FLOOR) {
|
|
421
|
+
silentAtBeat = true;
|
|
422
|
+
forceClose = true;
|
|
423
|
+
break;
|
|
424
|
+
}
|
|
415
425
|
}
|
|
416
426
|
// Producing reviews own their full ceiling; inactivity is not a review verdict.
|
|
417
427
|
if (reviewing)
|
|
@@ -458,6 +468,7 @@ async function runViaDriverDetailed(adapter, model, prompt, cwd, via, timeoutMs
|
|
|
458
468
|
...(Number.isFinite(exitCode) ? { exitCode } : {}),
|
|
459
469
|
timedOut,
|
|
460
470
|
launchNeverStarted,
|
|
471
|
+
silentAtBeat,
|
|
461
472
|
seatAuthoredBytes,
|
|
462
473
|
};
|
|
463
474
|
}
|
package/dist/gates/review.d.ts
CHANGED
|
@@ -64,6 +64,13 @@ export declare function matchClosureId(candidate: unknown, fingerprints: Iterabl
|
|
|
64
64
|
* all route through matchClosureId.
|
|
65
65
|
*/
|
|
66
66
|
export declare function isReviewClosureInvalid(v: Pick<ReviewVerdict, "resolved" | "reraised"> | null | undefined, priorIds: ReadonlySet<string> | readonly string[]): boolean;
|
|
67
|
+
/**
|
|
68
|
+
* OBS-1013 add.3: the reviewer ECHOED closure ids and at least one matches no carried fingerprint —
|
|
69
|
+
* it answered about the materials and missed the id (a retyped, truncated or paraphrased fingerprint).
|
|
70
|
+
* That is a no-verdict about the carried work (re-route), not a parse defect. A verdict that omits a
|
|
71
|
+
* list, carries a non-string or duplicates an id stays malformed: its shape, not its ids, is wrong.
|
|
72
|
+
*/
|
|
73
|
+
export declare function isReviewClosureMismatch(v: Pick<ReviewVerdict, "resolved" | "reraised"> | null | undefined, priorIds: ReadonlySet<string> | readonly string[]): boolean;
|
|
67
74
|
export type ReviewerFloorCause = "author-tier" | "task-floor" | "config" | "prior-reviewer";
|
|
68
75
|
/**
|
|
69
76
|
* RF-1 (OBS-922 add.2/3): the tier a reviewer must meet is the maximum of the author's tier, the
|
|
@@ -101,12 +108,30 @@ export declare function pickReviewer(author: Assignment, channels: BillingChanne
|
|
|
101
108
|
prefer?: string[], // v1.53 T2: review.prefer — reorders eligible channels, never changes eligibility
|
|
102
109
|
floor?: Tier, // task/config/prior floor from the caller; the author's own tier is ALWAYS applied here (RF-1)
|
|
103
110
|
history?: string[], // run-scoped picks, oldest to newest; empty preserves the established ranking
|
|
104
|
-
onSeat?: (seat: number, count: number) => void, demoted?: ReadonlySet<string>): BillingChannel | null;
|
|
105
|
-
export type ReviewUnparseableCause = VerdictUnparseableCause | "launch-never-started" | "truncated" | "silent";
|
|
111
|
+
onSeat?: (seat: number, count: number) => void, demoted?: ReadonlySet<string>, excludeVendors?: ReadonlySet<string>): BillingChannel | null;
|
|
112
|
+
export type ReviewUnparseableCause = VerdictUnparseableCause | "launch-never-started" | "truncated" | "silent" | "closure-mismatch";
|
|
106
113
|
/**
|
|
107
114
|
* This shows the reviewer what the task DECLARED, never what the diff may actually reach. The diff
|
|
108
115
|
* remains a separate stated input, so whether its touched paths fit the declaration stays a reviewer
|
|
109
116
|
* judgement rather than a guarantee made by this renderer.
|
|
110
117
|
*/
|
|
111
118
|
export declare function renderDeclaredWriteScope(files: ReadonlyArray<string>): string;
|
|
112
|
-
|
|
119
|
+
/**
|
|
120
|
+
* OBS-1033: the vendors of the seats that authored commits inside the accumulated diff. A seat is
|
|
121
|
+
* never handed its own earlier work to approve. A prior author not resolvable in the pool excludes
|
|
122
|
+
* its adapter's vendors instead (fail closed: the seat is known, its vendor is whatever it bills as).
|
|
123
|
+
*/
|
|
124
|
+
export declare function carriedAuthorVendors(channels: BillingChannel[], carriedAuthors?: readonly string[]): Set<string>;
|
|
125
|
+
/**
|
|
126
|
+
* OBS-1020: the compiled goal is the contract. After `resume --graph-changed` the worktree's copy of
|
|
127
|
+
* the spec is the pre-change text on the integration branch, so a reviewer that reads it grades a
|
|
128
|
+
* superseded contract. The daemon's repository root is where specs and planning records are current.
|
|
129
|
+
*/
|
|
130
|
+
export declare function renderGoalSection(goal: string, repoRoot?: string): string;
|
|
131
|
+
/**
|
|
132
|
+
* OBS-1013 add.3: each carried id is printed ONCE, verbatim, inside a fenced block the reviewer can
|
|
133
|
+
* copy; the notes follow in the same order. A reviewer that retyped a 600-byte id from prose lost
|
|
134
|
+
* closure on a typo and that read as malformed — the block is what a closure list is copied from.
|
|
135
|
+
*/
|
|
136
|
+
export declare function renderPriorMaterials(priorMaterials: readonly StructuredFinding[]): string;
|
|
137
|
+
export declare function reviewGate(task: Task, worktree: string, baseRef: string, author: Assignment, channels: BillingChannel[], adapters: WorkerAdapter[], cfg: TickmarkrConfig, via?: GateVia, excludeReviewers?: string[], artifactDir?: string, reviewHistory?: string[], demotedReviewers?: ReadonlySet<string>, carriedFindings?: readonly StructuredFinding[], priorReviewers?: readonly PriorReviewer[], carriedAuthors?: readonly string[]): Promise<GateResult>;
|