tickmarkr 2.5.3 → 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.d.ts +4 -0
- 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/plan.js +144 -3
- package/dist/cli/commands/resume.js +6 -2
- package/dist/cli/commands/run.js +21 -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/compile/collateral.d.ts +2 -0
- package/dist/compile/collateral.js +50 -9
- package/dist/compile/ownership.d.ts +7 -0
- package/dist/compile/ownership.js +59 -22
- package/dist/config/config.d.ts +23 -8
- package/dist/config/config.js +42 -28
- 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/herdr.d.ts +5 -0
- package/dist/drivers/herdr.js +14 -3
- 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/drivers/types.d.ts +1 -0
- 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/review.d.ts +35 -2
- package/dist/gates/review.js +64 -16
- package/dist/gates/run-gates.d.ts +5 -0
- package/dist/gates/run-gates.js +134 -18
- 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.d.ts +13 -0
- package/dist/route/router.js +95 -17
- package/dist/run/daemon.d.ts +13 -0
- package/dist/run/daemon.js +398 -96
- package/dist/run/git.d.ts +35 -1
- package/dist/run/git.js +111 -10
- 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 +83 -31
- 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 +180 -99
package/dist/drivers/types.d.ts
CHANGED
|
@@ -106,6 +106,7 @@ export interface ExecutorDriver {
|
|
|
106
106
|
narrateWith?(narrate: (event: JournalEvent) => void): void;
|
|
107
107
|
worktree(repo: string, branch: string, baseRef: string): Promise<string>;
|
|
108
108
|
narrator?: (cwd: string, command: string, runId?: string) => Promise<Slot>;
|
|
109
|
+
retireLostWatch?: (slot: Slot) => Promise<void>;
|
|
109
110
|
/** Best-effort projection of a task's lifecycle onto the execution host. */
|
|
110
111
|
project?: (taskId: string, state: "in-progress" | "in-review" | "completed") => Promise<void>;
|
|
111
112
|
reconcile?: (desired: Set<string>, runId: string, opts?: PanesToCloseOpts) => Promise<void>;
|
package/dist/gates/baseline.d.ts
CHANGED
|
@@ -37,6 +37,9 @@ export interface BaselineCommand {
|
|
|
37
37
|
impliedParallelism?: number | null;
|
|
38
38
|
/** The slowest per-file entry named by the runner; null when per-file timing is unavailable. */
|
|
39
39
|
longestFile?: BaselineFileDuration | null;
|
|
40
|
+
/** VL-1: every per-file duration the runner named, so a later per-file hang budget (test-manifest.ts)
|
|
41
|
+
* can be derived per file rather than from one slowest-file number; null when unavailable. */
|
|
42
|
+
fileDurations?: BaselineFileDuration[] | null;
|
|
40
43
|
/** The ceiling that measurement implies, persisted so every later battery uses the same number. */
|
|
41
44
|
ceilingMs?: number;
|
|
42
45
|
/**
|
package/dist/gates/baseline.js
CHANGED
|
@@ -230,13 +230,14 @@ function fileTiming(output, wallClockMs) {
|
|
|
230
230
|
files.push({ file: match[1], durationMs });
|
|
231
231
|
}
|
|
232
232
|
if (!files.length)
|
|
233
|
-
return { fileDurationSumMs: null, impliedParallelism: null, longestFile: null };
|
|
233
|
+
return { fileDurationSumMs: null, impliedParallelism: null, longestFile: null, fileDurations: null };
|
|
234
234
|
const fileDurationSumMs = files.reduce((sum, entry) => sum + entry.durationMs, 0);
|
|
235
235
|
const longestFile = files.reduce((longest, entry) => entry.durationMs > longest.durationMs ? entry : longest);
|
|
236
236
|
return {
|
|
237
237
|
fileDurationSumMs,
|
|
238
238
|
impliedParallelism: wallClockMs > 0 ? fileDurationSumMs / wallClockMs : null,
|
|
239
239
|
longestFile,
|
|
240
|
+
fileDurations: files,
|
|
240
241
|
};
|
|
241
242
|
}
|
|
242
243
|
// A failing command whose output holds no shape any runner here names. The marker is content-free and
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import type { Baseline } from "./baseline.js";
|
|
2
|
+
import type { GateResult } from "./types.js";
|
|
3
|
+
import { type RunCapacity } from "../run/git.js";
|
|
4
|
+
export declare const DEFAULT_VERDICT_CACHE_BOUND = 128;
|
|
5
|
+
export declare function setVerdictCacheBoundForTests(bound: number | undefined): void;
|
|
6
|
+
export declare function resetVerdictCacheBoundForTests(): void;
|
|
7
|
+
export declare const LOCKFILES: readonly ["package-lock.json", "npm-shrinkwrap.json", "pnpm-lock.yaml", "yarn.lock", "bun.lock", "bun.lockb"];
|
|
8
|
+
export declare function lockfileHash(worktree: string): string;
|
|
9
|
+
export declare function canonicalJson(obj: unknown): string;
|
|
10
|
+
export declare function baselineIdentity(baseline?: Baseline): string;
|
|
11
|
+
export declare function getWorktreeTree(worktree: string): Promise<string>;
|
|
12
|
+
export interface GateEnvironmentInput {
|
|
13
|
+
nodeRuntime?: string;
|
|
14
|
+
lockfile?: string;
|
|
15
|
+
worktree?: string;
|
|
16
|
+
capacity?: RunCapacity;
|
|
17
|
+
selectedSet?: readonly string[];
|
|
18
|
+
scope?: VerificationScope;
|
|
19
|
+
}
|
|
20
|
+
export declare function environmentFingerprint(env: GateEnvironmentInput): {
|
|
21
|
+
fingerprint: string;
|
|
22
|
+
parts: {
|
|
23
|
+
nodeRuntime: string;
|
|
24
|
+
lockfile: string;
|
|
25
|
+
capacity: RunCapacity;
|
|
26
|
+
selectedSet?: readonly string[];
|
|
27
|
+
};
|
|
28
|
+
};
|
|
29
|
+
export type VerificationScope = "battery" | "tip" | "standalone";
|
|
30
|
+
export interface VerificationIdentity {
|
|
31
|
+
/** the gate the verdict answers — build/lint/test may share one command and are still three verdicts */
|
|
32
|
+
gate?: string;
|
|
33
|
+
/** which verifier produced it: a battery gate row or an integration-tip row (own report, own forgiveness rule) */
|
|
34
|
+
scope?: VerificationScope;
|
|
35
|
+
/** Checkout location is diagnostic metadata; equal trees share verdicts across worktrees. */
|
|
36
|
+
worktree?: string;
|
|
37
|
+
tree: string;
|
|
38
|
+
command: string;
|
|
39
|
+
baseline: string;
|
|
40
|
+
environment: string;
|
|
41
|
+
envParts?: {
|
|
42
|
+
nodeRuntime: string;
|
|
43
|
+
lockfile: string;
|
|
44
|
+
capacity: RunCapacity;
|
|
45
|
+
selectedSet?: readonly string[];
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
export declare function computeVerificationIdentity(params: {
|
|
49
|
+
worktree: string;
|
|
50
|
+
gate: string;
|
|
51
|
+
scope?: VerificationScope;
|
|
52
|
+
command: string;
|
|
53
|
+
baseline?: Baseline;
|
|
54
|
+
selectedSet?: readonly string[];
|
|
55
|
+
capacity?: RunCapacity;
|
|
56
|
+
tree?: string;
|
|
57
|
+
lockfile?: string;
|
|
58
|
+
nodeRuntime?: string;
|
|
59
|
+
}): Promise<VerificationIdentity | undefined>;
|
|
60
|
+
export declare function verificationIdentityKey(id: VerificationIdentity): string;
|
|
61
|
+
export declare function formatReusedDetails(originalDetails: string, id: VerificationIdentity): string;
|
|
62
|
+
export interface CachedVerdict {
|
|
63
|
+
gate: string;
|
|
64
|
+
pass: boolean;
|
|
65
|
+
details: string;
|
|
66
|
+
exitCode?: number;
|
|
67
|
+
meta?: Record<string, unknown>;
|
|
68
|
+
capacity?: RunCapacity;
|
|
69
|
+
}
|
|
70
|
+
export declare function formatReusedRow(cached: CachedVerdict, id: VerificationIdentity): GateResult;
|
|
71
|
+
export declare function reusedIdentity(id: VerificationIdentity): Record<string, unknown>;
|
|
72
|
+
export declare function isInfraResult(result: GateResult | {
|
|
73
|
+
meta?: Record<string, unknown>;
|
|
74
|
+
details?: string;
|
|
75
|
+
cause?: string;
|
|
76
|
+
exitCode?: number;
|
|
77
|
+
}): boolean;
|
|
78
|
+
export declare function resolveStateDir(worktree: string, artifactDir?: string): string;
|
|
79
|
+
export declare function resolveVerdictStoreDir(stateDir: string): string;
|
|
80
|
+
export interface StoredVerdictRecord {
|
|
81
|
+
key: string;
|
|
82
|
+
identity: VerificationIdentity;
|
|
83
|
+
verdict: CachedVerdict;
|
|
84
|
+
timestamp: number;
|
|
85
|
+
sequence: number;
|
|
86
|
+
}
|
|
87
|
+
export declare function resetGlobalStoreSequenceForTests(): void;
|
|
88
|
+
export declare class VerdictStore {
|
|
89
|
+
readonly dir: string;
|
|
90
|
+
constructor(dir: string);
|
|
91
|
+
private initSequenceFromDisk;
|
|
92
|
+
get(id?: VerificationIdentity): CachedVerdict | undefined;
|
|
93
|
+
set(id: VerificationIdentity | undefined, verdict: GateResult | CachedVerdict): boolean;
|
|
94
|
+
size(): number;
|
|
95
|
+
clear(): void;
|
|
96
|
+
evictOldest(bound?: number): number;
|
|
97
|
+
}
|
|
98
|
+
export declare function getVerdictStore(stateDir: string, options?: {
|
|
99
|
+
bound?: number;
|
|
100
|
+
}): VerdictStore;
|
|
@@ -0,0 +1,389 @@
|
|
|
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 } 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 payload = canonicalJson({
|
|
87
|
+
nodeRuntime,
|
|
88
|
+
lockfile,
|
|
89
|
+
capacity,
|
|
90
|
+
selectedSet: selectedSet ?? null,
|
|
91
|
+
scope: env.scope ?? "battery",
|
|
92
|
+
});
|
|
93
|
+
const fingerprint = createHash("sha256").update(payload).digest("hex").slice(0, 16);
|
|
94
|
+
return {
|
|
95
|
+
fingerprint,
|
|
96
|
+
parts: { nodeRuntime, lockfile, capacity, selectedSet },
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
export async function computeVerificationIdentity(params) {
|
|
100
|
+
const tree = params.tree ?? (await getWorktreeTree(params.worktree));
|
|
101
|
+
if (!tree)
|
|
102
|
+
return undefined;
|
|
103
|
+
const baseline = baselineIdentity(params.baseline);
|
|
104
|
+
const env = environmentFingerprint({
|
|
105
|
+
worktree: params.worktree,
|
|
106
|
+
selectedSet: params.selectedSet,
|
|
107
|
+
capacity: params.capacity,
|
|
108
|
+
lockfile: params.lockfile,
|
|
109
|
+
nodeRuntime: params.nodeRuntime,
|
|
110
|
+
scope: params.scope,
|
|
111
|
+
});
|
|
112
|
+
return {
|
|
113
|
+
gate: params.gate,
|
|
114
|
+
scope: params.scope ?? "battery",
|
|
115
|
+
worktree: realpathSync(params.worktree),
|
|
116
|
+
tree,
|
|
117
|
+
command: params.command,
|
|
118
|
+
baseline,
|
|
119
|
+
environment: env.fingerprint,
|
|
120
|
+
envParts: env.parts,
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
export function verificationIdentityKey(id) {
|
|
124
|
+
const cmdHash = createHash("sha256").update(id.command).digest("hex").slice(0, 16);
|
|
125
|
+
const gate = id.gate ?? "gate";
|
|
126
|
+
// Checkout location is diagnostic only. Scope separates each verifier's evidence policy.
|
|
127
|
+
const workingTree = createHash("sha256").update(id.tree).digest("hex");
|
|
128
|
+
return `${id.scope ?? "battery"}-${gate}-${workingTree}-${cmdHash}-${id.baseline}-${id.environment}`;
|
|
129
|
+
}
|
|
130
|
+
export function formatReusedDetails(originalDetails, id) {
|
|
131
|
+
const unadorned = originalDetails.replace(/^reused verdict \(identity: [^)]+\):\s*/, "");
|
|
132
|
+
const envDesc = id.envParts
|
|
133
|
+
? ` [node=${id.envParts.nodeRuntime}, lockfile=${id.envParts.lockfile}, capacity=${describeCapacity(id.envParts.capacity)}${id.envParts.selectedSet ? `, selected=${id.envParts.selectedSet.join(",")}` : ""}]`
|
|
134
|
+
: "";
|
|
135
|
+
const gate = id.gate ?? "gate";
|
|
136
|
+
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})`;
|
|
137
|
+
return `${prefix}: ${unadorned}`;
|
|
138
|
+
}
|
|
139
|
+
// A reused GREEN names the reuse in its details. A reused RED keeps the fresh verdict's details
|
|
140
|
+
// byte-for-byte: the daemon's fingerprint cap (identicalGateFailures) compares normalized details
|
|
141
|
+
// across attempts, so a red whose details carried a reuse note would never read as the identical
|
|
142
|
+
// failure it is and would buy a free retry — the reuse is named on the row's meta and on the
|
|
143
|
+
// gate-reused-verdict note run-gates emits beside it, which the daemon journals as its own row.
|
|
144
|
+
export function formatReusedRow(cached, id) {
|
|
145
|
+
const reusedDetails = formatReusedDetails(cached.details, id);
|
|
146
|
+
return {
|
|
147
|
+
gate: id.gate ?? cached.gate,
|
|
148
|
+
pass: cached.pass,
|
|
149
|
+
details: cached.pass ? reusedDetails : cached.details,
|
|
150
|
+
capacity: cached.capacity,
|
|
151
|
+
meta: {
|
|
152
|
+
...cached.meta,
|
|
153
|
+
reused: true,
|
|
154
|
+
reusedDetails,
|
|
155
|
+
verificationIdentity: reusedIdentity(id),
|
|
156
|
+
},
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
export function reusedIdentity(id) {
|
|
160
|
+
return {
|
|
161
|
+
gate: id.gate ?? "gate",
|
|
162
|
+
scope: id.scope ?? "battery",
|
|
163
|
+
tree: id.tree,
|
|
164
|
+
...(id.worktree ? { worktree: id.worktree } : {}),
|
|
165
|
+
command: id.command,
|
|
166
|
+
baseline: id.baseline,
|
|
167
|
+
environment: id.environment,
|
|
168
|
+
key: verificationIdentityKey(id),
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
export function isInfraResult(result) {
|
|
172
|
+
if (result.meta?.infra === true)
|
|
173
|
+
return true;
|
|
174
|
+
if (result.meta?.classification === "infra")
|
|
175
|
+
return true;
|
|
176
|
+
if (result.meta?.kind === "ceiling-kill" || result.meta?.kind === "hang")
|
|
177
|
+
return true;
|
|
178
|
+
if ("cause" in result && result.cause === "infra")
|
|
179
|
+
return true;
|
|
180
|
+
if (typeof result.details === "string") {
|
|
181
|
+
const d = result.details.toLowerCase();
|
|
182
|
+
if (d.startsWith("infra;") || d.startsWith("infra: ") || d.includes("infrastructure blocked execution")) {
|
|
183
|
+
return true;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
return false;
|
|
187
|
+
}
|
|
188
|
+
export function resolveStateDir(worktree, artifactDir) {
|
|
189
|
+
if (artifactDir) {
|
|
190
|
+
const idx = artifactDir.lastIndexOf("/.tickmarkr");
|
|
191
|
+
if (idx !== -1) {
|
|
192
|
+
return artifactDir.slice(0, idx + "/.tickmarkr".length);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
const idx = worktree.lastIndexOf("/.tickmarkr");
|
|
196
|
+
if (idx !== -1) {
|
|
197
|
+
return worktree.slice(0, idx + "/.tickmarkr".length);
|
|
198
|
+
}
|
|
199
|
+
if (existsSync(join(worktree, ".tickmarkr"))) {
|
|
200
|
+
return join(worktree, ".tickmarkr");
|
|
201
|
+
}
|
|
202
|
+
try {
|
|
203
|
+
const commonDir = execSync("git rev-parse --git-common-dir", { cwd: worktree, encoding: "utf8" }).trim();
|
|
204
|
+
const resolved = resolve(worktree, commonDir);
|
|
205
|
+
const commonRoot = realpathSync(dirname(resolved));
|
|
206
|
+
if (existsSync(join(commonRoot, ".tickmarkr"))) {
|
|
207
|
+
return join(commonRoot, ".tickmarkr");
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
catch {
|
|
211
|
+
// ignore
|
|
212
|
+
}
|
|
213
|
+
return join(worktree, ".tickmarkr");
|
|
214
|
+
}
|
|
215
|
+
export function resolveVerdictStoreDir(stateDir) {
|
|
216
|
+
mkdirSync(stateDir, { recursive: true });
|
|
217
|
+
const gi = join(stateDir, ".gitignore");
|
|
218
|
+
if (!existsSync(gi)) {
|
|
219
|
+
try {
|
|
220
|
+
writeFileSync(gi, "*\n");
|
|
221
|
+
}
|
|
222
|
+
catch {
|
|
223
|
+
// ignore
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
const dir = join(stateDir, "verdicts");
|
|
227
|
+
mkdirSync(dir, { recursive: true });
|
|
228
|
+
return dir;
|
|
229
|
+
}
|
|
230
|
+
let globalStoreSequence = 0;
|
|
231
|
+
export function resetGlobalStoreSequenceForTests() {
|
|
232
|
+
globalStoreSequence = 0;
|
|
233
|
+
}
|
|
234
|
+
export class VerdictStore {
|
|
235
|
+
dir;
|
|
236
|
+
constructor(dir) {
|
|
237
|
+
this.dir = dir;
|
|
238
|
+
}
|
|
239
|
+
initSequenceFromDisk() {
|
|
240
|
+
if (!existsSync(this.dir))
|
|
241
|
+
return;
|
|
242
|
+
try {
|
|
243
|
+
for (const f of readdirSync(this.dir)) {
|
|
244
|
+
if (f.startsWith("verdict-") && f.endsWith(".json")) {
|
|
245
|
+
try {
|
|
246
|
+
const raw = JSON.parse(readFileSync(join(this.dir, f), "utf8"));
|
|
247
|
+
if (raw && typeof raw.sequence === "number" && raw.sequence > globalStoreSequence) {
|
|
248
|
+
globalStoreSequence = raw.sequence;
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
catch {
|
|
252
|
+
// ignore
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
catch {
|
|
258
|
+
// ignore
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
get(id) {
|
|
262
|
+
if (!id || !id.tree)
|
|
263
|
+
return undefined;
|
|
264
|
+
const key = verificationIdentityKey(id);
|
|
265
|
+
const p = join(this.dir, `verdict-${key}.json`);
|
|
266
|
+
if (existsSync(p)) {
|
|
267
|
+
try {
|
|
268
|
+
const raw = JSON.parse(readFileSync(p, "utf8"));
|
|
269
|
+
if (raw && raw.verdict)
|
|
270
|
+
return raw.verdict;
|
|
271
|
+
}
|
|
272
|
+
catch {
|
|
273
|
+
return undefined;
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
return undefined;
|
|
277
|
+
}
|
|
278
|
+
set(id, verdict) {
|
|
279
|
+
if (!id || !id.tree)
|
|
280
|
+
return false;
|
|
281
|
+
if (isInfraResult(verdict))
|
|
282
|
+
return false;
|
|
283
|
+
mkdirSync(this.dir, { recursive: true });
|
|
284
|
+
this.initSequenceFromDisk();
|
|
285
|
+
const key = verificationIdentityKey(id);
|
|
286
|
+
const bound = testCacheBound ?? DEFAULT_VERDICT_CACHE_BOUND;
|
|
287
|
+
const record = {
|
|
288
|
+
key,
|
|
289
|
+
identity: id,
|
|
290
|
+
verdict: {
|
|
291
|
+
gate: verdict.gate ?? id.gate ?? "gate",
|
|
292
|
+
pass: verdict.pass,
|
|
293
|
+
details: verdict.details,
|
|
294
|
+
...("exitCode" in verdict ? { exitCode: verdict.exitCode } : {}),
|
|
295
|
+
...(verdict.capacity ? { capacity: verdict.capacity } : {}),
|
|
296
|
+
...(verdict.meta ? { meta: verdict.meta } : {}),
|
|
297
|
+
},
|
|
298
|
+
timestamp: Date.now(),
|
|
299
|
+
sequence: ++globalStoreSequence,
|
|
300
|
+
};
|
|
301
|
+
const finalPath = join(this.dir, `verdict-${key}.json`);
|
|
302
|
+
const tmpPath = join(this.dir, `verdict-${key}.${process.pid}.${Date.now()}.${Math.random().toString(16).slice(2)}.tmp`);
|
|
303
|
+
writeFileSync(tmpPath, JSON.stringify(record, null, 2) + "\n");
|
|
304
|
+
try {
|
|
305
|
+
unlinkSync(finalPath);
|
|
306
|
+
}
|
|
307
|
+
catch {
|
|
308
|
+
// ignore
|
|
309
|
+
}
|
|
310
|
+
writeFileSync(finalPath, readFileSync(tmpPath));
|
|
311
|
+
try {
|
|
312
|
+
unlinkSync(tmpPath);
|
|
313
|
+
}
|
|
314
|
+
catch {
|
|
315
|
+
// ignore
|
|
316
|
+
}
|
|
317
|
+
this.evictOldest(bound);
|
|
318
|
+
return true;
|
|
319
|
+
}
|
|
320
|
+
size() {
|
|
321
|
+
if (!existsSync(this.dir))
|
|
322
|
+
return 0;
|
|
323
|
+
return readdirSync(this.dir).filter((f) => f.startsWith("verdict-") && f.endsWith(".json")).length;
|
|
324
|
+
}
|
|
325
|
+
clear() {
|
|
326
|
+
globalStoreSequence = 0;
|
|
327
|
+
if (!existsSync(this.dir))
|
|
328
|
+
return;
|
|
329
|
+
for (const f of readdirSync(this.dir)) {
|
|
330
|
+
if (f.startsWith("verdict-")) {
|
|
331
|
+
try {
|
|
332
|
+
unlinkSync(join(this.dir, f));
|
|
333
|
+
}
|
|
334
|
+
catch {
|
|
335
|
+
// ignore
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
evictOldest(bound) {
|
|
341
|
+
const limit = bound ?? testCacheBound ?? DEFAULT_VERDICT_CACHE_BOUND;
|
|
342
|
+
const dir = this.dir;
|
|
343
|
+
if (!existsSync(dir))
|
|
344
|
+
return 0;
|
|
345
|
+
const files = readdirSync(dir).filter((f) => f.startsWith("verdict-") && f.endsWith(".json"));
|
|
346
|
+
if (files.length <= limit)
|
|
347
|
+
return 0;
|
|
348
|
+
const entries = [];
|
|
349
|
+
for (const file of files) {
|
|
350
|
+
const p = join(dir, file);
|
|
351
|
+
try {
|
|
352
|
+
const raw = JSON.parse(readFileSync(p, "utf8"));
|
|
353
|
+
entries.push({
|
|
354
|
+
file,
|
|
355
|
+
sequence: typeof raw.sequence === "number" ? raw.sequence : 0,
|
|
356
|
+
timestamp: typeof raw.timestamp === "number" ? raw.timestamp : 0,
|
|
357
|
+
});
|
|
358
|
+
}
|
|
359
|
+
catch {
|
|
360
|
+
entries.push({ file, sequence: -1, timestamp: -1 });
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
entries.sort((a, b) => {
|
|
364
|
+
// Each write seeds its sequence from disk, including in a fresh process.
|
|
365
|
+
// Wall-clock corrections must not make the newest write the oldest entry.
|
|
366
|
+
if (a.sequence !== b.sequence)
|
|
367
|
+
return a.sequence - b.sequence;
|
|
368
|
+
return a.timestamp - b.timestamp;
|
|
369
|
+
});
|
|
370
|
+
let evicted = 0;
|
|
371
|
+
while (entries.length > limit) {
|
|
372
|
+
const oldest = entries.shift();
|
|
373
|
+
try {
|
|
374
|
+
unlinkSync(join(dir, oldest.file));
|
|
375
|
+
evicted++;
|
|
376
|
+
}
|
|
377
|
+
catch {
|
|
378
|
+
// ignore
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
return evicted;
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
export function getVerdictStore(stateDir, options) {
|
|
385
|
+
if (options?.bound !== undefined) {
|
|
386
|
+
setVerdictCacheBoundForTests(options.bound);
|
|
387
|
+
}
|
|
388
|
+
return new VerdictStore(resolveVerdictStoreDir(stateDir));
|
|
389
|
+
}
|
package/dist/gates/review.d.ts
CHANGED
|
@@ -64,9 +64,42 @@ 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
|
+
export type ReviewerFloorCause = "author-tier" | "task-floor" | "config" | "prior-reviewer";
|
|
68
|
+
/**
|
|
69
|
+
* RF-1 (OBS-922 add.2/3): the tier a reviewer must meet is the maximum of the author's tier, the
|
|
70
|
+
* task-declared floor, a configured `review.floor` tier and, on a second round or a retry, the prior
|
|
71
|
+
* reviewer's tier. The cause names the input that reached the maximum (earlier inputs win a tie, so a
|
|
72
|
+
* floor the author's tier already satisfies is attributed to the author).
|
|
73
|
+
*/
|
|
74
|
+
export declare function resolveReviewerFloor(authorTier: Tier, taskFloor?: Tier, configFloor?: Tier, priorReviewerTier?: Tier): {
|
|
75
|
+
floor: Tier;
|
|
76
|
+
cause: ReviewerFloorCause;
|
|
77
|
+
};
|
|
78
|
+
/** A prior reviewer of THIS task: a channel key, or a journaled row's key plus the tier it was DISPATCHED at. */
|
|
79
|
+
export type PriorReviewer = string | {
|
|
80
|
+
reviewer: string;
|
|
81
|
+
tier?: unknown;
|
|
82
|
+
};
|
|
83
|
+
/**
|
|
84
|
+
* The highest tier among the task's prior reviewers — the prior reviewer's tier for RF-1. A recorded
|
|
85
|
+
* dispatch tier is historical evidence and wins over the current pool; an unrecorded one falls back to
|
|
86
|
+
* the seat's channel; a seat neither establishes (it left the pool on resume, or the journal holds
|
|
87
|
+
* garbage) holds frontier — fail closed, never silently dropped.
|
|
88
|
+
*/
|
|
89
|
+
export declare function priorReviewerTier(channels: BillingChannel[], priorReviewers?: readonly PriorReviewer[]): Tier | undefined;
|
|
90
|
+
/**
|
|
91
|
+
* The gate's floor: author tier, task floor, review.floor (a tier — `worker` names none) and the seats
|
|
92
|
+
* the caller names as THIS TASK's prior reviewers (earlier rounds' seats, a flaked seat). Eligibility
|
|
93
|
+
* exclusions are NOT evidence — a retry bans a flaked seat's whole adapter, and those sibling channels
|
|
94
|
+
* never reviewed — and neither is the run-scoped LRU rotation history, which names unrelated tasks' seats.
|
|
95
|
+
*/
|
|
96
|
+
export declare function gateReviewerFloor(task: Pick<Task, "routingHints">, cfg: TickmarkrConfig, author: Assignment, channels: BillingChannel[], priorReviewers?: readonly PriorReviewer[]): {
|
|
97
|
+
floor: Tier;
|
|
98
|
+
cause: ReviewerFloorCause;
|
|
99
|
+
};
|
|
67
100
|
export declare function pickReviewer(author: Assignment, channels: BillingChannel[], exclude?: string[], // v1.1 failover: reviewer channels that already produced garbage for this task
|
|
68
101
|
prefer?: string[], // v1.53 T2: review.prefer — reorders eligible channels, never changes eligibility
|
|
69
|
-
floor?: Tier, // task
|
|
102
|
+
floor?: Tier, // task/config/prior floor from the caller; the author's own tier is ALWAYS applied here (RF-1)
|
|
70
103
|
history?: string[], // run-scoped picks, oldest to newest; empty preserves the established ranking
|
|
71
104
|
onSeat?: (seat: number, count: number) => void, demoted?: ReadonlySet<string>): BillingChannel | null;
|
|
72
105
|
export type ReviewUnparseableCause = VerdictUnparseableCause | "launch-never-started" | "truncated" | "silent";
|
|
@@ -76,4 +109,4 @@ export type ReviewUnparseableCause = VerdictUnparseableCause | "launch-never-sta
|
|
|
76
109
|
* judgement rather than a guarantee made by this renderer.
|
|
77
110
|
*/
|
|
78
111
|
export declare function renderDeclaredWriteScope(files: ReadonlyArray<string>): string;
|
|
79
|
-
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[]): Promise<GateResult>;
|
|
112
|
+
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[]): Promise<GateResult>;
|