tickmarkr 1.86.0 → 1.89.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/adapters/catalog.d.ts +30 -1
- package/dist/adapters/catalog.js +58 -2
- package/dist/adapters/fake.d.ts +2 -1
- package/dist/adapters/fake.js +7 -0
- package/dist/adapters/grok.js +11 -0
- package/dist/adapters/kimi.d.ts +2 -1
- package/dist/adapters/kimi.js +36 -0
- package/dist/adapters/opencode.js +17 -0
- package/dist/adapters/pi.js +11 -0
- package/dist/adapters/prompt.js +8 -1
- package/dist/adapters/registry.d.ts +10 -2
- package/dist/adapters/registry.js +126 -67
- package/dist/adapters/types.d.ts +34 -3
- package/dist/adapters/types.js +99 -1
- package/dist/cli/commands/approve.d.ts +2 -0
- package/dist/cli/commands/approve.js +104 -84
- package/dist/cli/commands/compile.d.ts +1 -1
- package/dist/cli/commands/compile.js +29 -12
- package/dist/cli/commands/init.js +1 -1
- package/dist/cli/commands/plan.d.ts +1 -1
- package/dist/cli/commands/plan.js +21 -2
- package/dist/cli/commands/report.js +49 -0
- package/dist/cli/commands/resume.js +7 -1
- package/dist/cli/commands/status.js +298 -96
- package/dist/cli/harness.d.ts +13 -0
- package/dist/cli/harness.js +50 -0
- package/dist/compile/collateral.js +4 -4
- package/dist/compile/index.d.ts +14 -3
- package/dist/compile/index.js +36 -10
- package/dist/compile/native.js +108 -25
- package/dist/drivers/subprocess.d.ts +6 -1
- package/dist/drivers/subprocess.js +9 -4
- package/dist/gates/acceptance.d.ts +21 -1
- package/dist/gates/acceptance.js +67 -22
- package/dist/gates/artifact-manifest.d.ts +119 -0
- package/dist/gates/artifact-manifest.js +357 -0
- package/dist/gates/baseline.d.ts +6 -0
- package/dist/gates/baseline.js +52 -7
- package/dist/gates/llm.js +37 -26
- package/dist/gates/review.d.ts +16 -11
- package/dist/gates/review.js +44 -150
- package/dist/gates/run-gates.d.ts +1 -0
- package/dist/gates/run-gates.js +145 -9
- package/dist/graph/schema.d.ts +3 -1
- package/dist/graph/schema.js +4 -1
- package/dist/route/preference.d.ts +1 -1
- package/dist/route/preference.js +8 -1
- package/dist/run/consult.js +14 -1
- package/dist/run/daemon.d.ts +42 -0
- package/dist/run/daemon.js +2322 -1963
- package/dist/run/git.d.ts +50 -0
- package/dist/run/git.js +113 -2
- package/dist/run/interactive-seed.d.ts +6 -2
- package/dist/run/interactive-seed.js +72 -5
- package/dist/run/journal.d.ts +9 -1
- package/dist/run/journal.js +99 -9
- package/dist/run/lock.d.ts +11 -0
- package/dist/run/lock.js +97 -6
- package/dist/run/outcome.d.ts +50 -0
- package/dist/run/outcome.js +152 -0
- package/dist/run/protocol.d.ts +460 -0
- package/dist/run/protocol.js +433 -0
- package/dist/run/supervision.d.ts +29 -0
- package/dist/run/supervision.js +189 -0
- package/fixtures/gateway-models.json +1 -0
- package/fixtures/wrapped-acceptance.native.md +29 -0
- package/package.json +1 -1
- package/schema/rungraph.schema.json +21 -2
- package/skills/tickmarkr-overseer/SKILL.md +366 -4
- package/skills/tickmarkr-overseer/scripts/watch-artifacts.sh +95 -8
- package/skills/tickmarkr-overseer/scripts/watch-contamination.sh +77 -0
- package/skills/tickmarkr-overseer/scripts/watch-context.sh +86 -0
- package/skills/tickmarkr-overseer/scripts/watch-parks.sh +96 -0
- package/skills/tickmarkr-overseer/scripts/watch-pending-input.sh +183 -0
|
@@ -0,0 +1,357 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Generated artifacts are not inferred from their directory or extension. A
|
|
3
|
+
* path receives capture accounting only when its manifest row names a
|
|
4
|
+
* registered producer and repeats that producer's current provenance exactly.
|
|
5
|
+
*/
|
|
6
|
+
const GOLDEN_PROVENANCE = {
|
|
7
|
+
source: "src/tui/cockpit/capture.ts",
|
|
8
|
+
entrypoint: "regenerateGoldenFrames",
|
|
9
|
+
revision: "golden-frame-v1",
|
|
10
|
+
};
|
|
11
|
+
const COLOUR_PROVENANCE = {
|
|
12
|
+
source: "src/tui/cockpit/capture.ts",
|
|
13
|
+
entrypoint: "regenerateColourFrames",
|
|
14
|
+
revision: "colour-frame-v1",
|
|
15
|
+
};
|
|
16
|
+
export const CAPTURE_PRODUCERS = [
|
|
17
|
+
{ id: "cockpit-golden-frames", provenance: GOLDEN_PROVENANCE },
|
|
18
|
+
{ id: "cockpit-colour-frames", provenance: COLOUR_PROVENANCE },
|
|
19
|
+
];
|
|
20
|
+
const goldenFrameNames = [
|
|
21
|
+
"run.width-stacked.80x24.txt",
|
|
22
|
+
"run.width-folded-keys.100x24.txt",
|
|
23
|
+
"run.width-three-column.140x24.txt",
|
|
24
|
+
"run.height-14.140x14.txt",
|
|
25
|
+
"run.height-18.140x18.txt",
|
|
26
|
+
"run.height-24.140x24.txt",
|
|
27
|
+
"run.height-40.140x40.txt",
|
|
28
|
+
"run.no-colour.140x24.txt",
|
|
29
|
+
"run.non-tty.140x24.txt",
|
|
30
|
+
"run.ci.140x24.txt",
|
|
31
|
+
"setup.width-stacked.80x24.txt",
|
|
32
|
+
"setup.width-folded-keys.100x24.txt",
|
|
33
|
+
"setup.width-three-column.140x24.txt",
|
|
34
|
+
"setup.height-14.140x14.txt",
|
|
35
|
+
"setup.height-18.140x18.txt",
|
|
36
|
+
"setup.height-24.140x24.txt",
|
|
37
|
+
"setup.height-40.140x40.txt",
|
|
38
|
+
"setup.no-colour.140x24.txt",
|
|
39
|
+
"setup.non-tty.140x24.txt",
|
|
40
|
+
"setup.ci.140x24.txt",
|
|
41
|
+
];
|
|
42
|
+
const colourFrameNames = [
|
|
43
|
+
"run-20260718-000943.colour.140x24.txt",
|
|
44
|
+
"run-20260718-000943.no-colour.140x24.txt",
|
|
45
|
+
"run-20260725-025004.interrupted.colour.140x24.txt",
|
|
46
|
+
];
|
|
47
|
+
const provenanceCopy = (provenance) => ({ ...provenance });
|
|
48
|
+
export const CAPTURE_ARTIFACT_MANIFEST = {
|
|
49
|
+
version: 1,
|
|
50
|
+
producers: CAPTURE_PRODUCERS,
|
|
51
|
+
artifacts: [
|
|
52
|
+
...goldenFrameNames.map((fixture) => ({
|
|
53
|
+
path: `tests/fixtures/cockpit/frames/${fixture}`,
|
|
54
|
+
producer: "cockpit-golden-frames",
|
|
55
|
+
provenance: provenanceCopy(GOLDEN_PROVENANCE),
|
|
56
|
+
})),
|
|
57
|
+
...colourFrameNames.map((fixture) => ({
|
|
58
|
+
path: `tests/fixtures/cockpit/colour/${fixture}`,
|
|
59
|
+
producer: "cockpit-colour-frames",
|
|
60
|
+
provenance: provenanceCopy(COLOUR_PROVENANCE),
|
|
61
|
+
})),
|
|
62
|
+
],
|
|
63
|
+
};
|
|
64
|
+
/** Compatibility name for the pre-manifest gate API; now derived from one manifest. */
|
|
65
|
+
export const REGENERABLE_CAPTURE_PATHS = CAPTURE_ARTIFACT_MANIFEST.artifacts
|
|
66
|
+
.map((artifact) => artifact.path);
|
|
67
|
+
export const PROTECTED_EVIDENCE_PREFIXES = [
|
|
68
|
+
"tests/fixtures/cockpit/anchors/",
|
|
69
|
+
"tests/fixtures/cockpit/sources/",
|
|
70
|
+
"tests/fixtures/cockpit/colour/sources/",
|
|
71
|
+
];
|
|
72
|
+
export function isProtectedEvidence(path) {
|
|
73
|
+
return PROTECTED_EVIDENCE_PREFIXES.some((prefix) => path.startsWith(prefix));
|
|
74
|
+
}
|
|
75
|
+
const isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
76
|
+
function isCanonicalRepoPath(value) {
|
|
77
|
+
if (typeof value !== "string" || value.length === 0)
|
|
78
|
+
return false;
|
|
79
|
+
if (value.startsWith("/") || value.includes("\\"))
|
|
80
|
+
return false;
|
|
81
|
+
const parts = value.split("/");
|
|
82
|
+
return parts.every((part) => part.length > 0 && part !== "." && part !== "..");
|
|
83
|
+
}
|
|
84
|
+
function parseProvenance(value) {
|
|
85
|
+
if (!isRecord(value))
|
|
86
|
+
return null;
|
|
87
|
+
const { source, entrypoint, revision } = value;
|
|
88
|
+
if (!isCanonicalRepoPath(source))
|
|
89
|
+
return null;
|
|
90
|
+
if (typeof entrypoint !== "string" || !/^[A-Za-z_$][\w$]*$/.test(entrypoint))
|
|
91
|
+
return null;
|
|
92
|
+
if (typeof revision !== "string" || !/^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(revision))
|
|
93
|
+
return null;
|
|
94
|
+
return { source, entrypoint, revision };
|
|
95
|
+
}
|
|
96
|
+
const sameProvenance = (left, right) => left.source === right.source
|
|
97
|
+
&& left.entrypoint === right.entrypoint
|
|
98
|
+
&& left.revision === right.revision;
|
|
99
|
+
function indexManifest(manifest) {
|
|
100
|
+
if (!isRecord(manifest) || manifest.version !== 1) {
|
|
101
|
+
return "artifact manifest version must be 1";
|
|
102
|
+
}
|
|
103
|
+
if (!Array.isArray(manifest.producers) || !Array.isArray(manifest.artifacts)) {
|
|
104
|
+
return "artifact manifest producers and artifacts must be arrays";
|
|
105
|
+
}
|
|
106
|
+
const producers = new Map();
|
|
107
|
+
for (const [index, raw] of manifest.producers.entries()) {
|
|
108
|
+
if (!isRecord(raw) || typeof raw.id !== "string" || !raw.id.trim()) {
|
|
109
|
+
return `artifact manifest producer ${index} is malformed`;
|
|
110
|
+
}
|
|
111
|
+
const provenance = parseProvenance(raw.provenance);
|
|
112
|
+
if (!provenance)
|
|
113
|
+
return `artifact manifest producer ${raw.id} has malformed provenance`;
|
|
114
|
+
if (producers.has(raw.id))
|
|
115
|
+
return `artifact manifest producer ${raw.id} is duplicated`;
|
|
116
|
+
producers.set(raw.id, { id: raw.id, provenance });
|
|
117
|
+
}
|
|
118
|
+
const artifacts = new Map();
|
|
119
|
+
for (const [index, raw] of manifest.artifacts.entries()) {
|
|
120
|
+
if (!isRecord(raw) || !isCanonicalRepoPath(raw.path)
|
|
121
|
+
|| typeof raw.producer !== "string" || !raw.producer.trim()) {
|
|
122
|
+
return `artifact manifest entry ${index} is malformed`;
|
|
123
|
+
}
|
|
124
|
+
const provenance = parseProvenance(raw.provenance);
|
|
125
|
+
if (!provenance)
|
|
126
|
+
return `artifact manifest entry ${raw.path} has malformed provenance`;
|
|
127
|
+
if (artifacts.has(raw.path))
|
|
128
|
+
return `artifact manifest path ${raw.path} is duplicated`;
|
|
129
|
+
artifacts.set(raw.path, { path: raw.path, producer: raw.producer, provenance });
|
|
130
|
+
}
|
|
131
|
+
return { producers, artifacts };
|
|
132
|
+
}
|
|
133
|
+
export function classifyArtifactPath(path, manifest = CAPTURE_ARTIFACT_MANIFEST) {
|
|
134
|
+
// Protected evidence wins even over a forged manifest row.
|
|
135
|
+
if (isProtectedEvidence(path)) {
|
|
136
|
+
return { path, kind: "logic", reason: "protected-evidence" };
|
|
137
|
+
}
|
|
138
|
+
const indexed = indexManifest(manifest);
|
|
139
|
+
if (typeof indexed === "string") {
|
|
140
|
+
return { path, kind: "logic", reason: "malformed-manifest", error: indexed };
|
|
141
|
+
}
|
|
142
|
+
const artifact = indexed.artifacts.get(path);
|
|
143
|
+
if (!artifact)
|
|
144
|
+
return { path, kind: "logic", reason: "unmanifested" };
|
|
145
|
+
const producer = indexed.producers.get(artifact.producer);
|
|
146
|
+
if (!producer) {
|
|
147
|
+
return {
|
|
148
|
+
path,
|
|
149
|
+
kind: "logic",
|
|
150
|
+
reason: "missing-producer",
|
|
151
|
+
error: `capture producer ${artifact.producer} is not registered`,
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
if (!sameProvenance(artifact.provenance, producer.provenance)) {
|
|
155
|
+
return {
|
|
156
|
+
path,
|
|
157
|
+
kind: "logic",
|
|
158
|
+
reason: "stale-provenance",
|
|
159
|
+
error: `capture provenance for ${path} does not match producer ${producer.id}`,
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
return {
|
|
163
|
+
path,
|
|
164
|
+
kind: "capture",
|
|
165
|
+
reason: "manifest-provenance",
|
|
166
|
+
producer: producer.id,
|
|
167
|
+
provenance: producer.provenance,
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
const SET_ASIDE_RECEIPT = /^set aside: regenerable capture (.+?) — \d+ bytes withheld\b/m;
|
|
171
|
+
/** The path named by a citable capture receipt, or null when there is none. */
|
|
172
|
+
export function setAsideReceiptPath(section) {
|
|
173
|
+
return SET_ASIDE_RECEIPT.exec(section)?.[1] ?? null;
|
|
174
|
+
}
|
|
175
|
+
const DIFF_SECTIONS = /(?=^diff --git )/m;
|
|
176
|
+
function deletedPath(section) {
|
|
177
|
+
if (!/^deleted file mode /m.test(section))
|
|
178
|
+
return null;
|
|
179
|
+
const oldPath = /^--- (.+)$/m.exec(section)?.[1]
|
|
180
|
+
?? /^Binary files (.+) and \/dev\/null differ$/m.exec(section)?.[1];
|
|
181
|
+
if (!oldPath || oldPath === "/dev/null")
|
|
182
|
+
return null;
|
|
183
|
+
return unquoteGitPath(oldPath).replace(/^a\//, "");
|
|
184
|
+
}
|
|
185
|
+
/**
|
|
186
|
+
* Whole-file source deletions retain their operation fact instead of spending
|
|
187
|
+
* the reader cap on bytes that no longer exist. Capture receipts and protected
|
|
188
|
+
* evidence are deliberately exempt from this older reduction.
|
|
189
|
+
*/
|
|
190
|
+
export function reviewableLogicDiff(diff) {
|
|
191
|
+
return diff.split(DIFF_SECTIONS).map((section) => {
|
|
192
|
+
if (setAsideReceiptPath(section))
|
|
193
|
+
return section;
|
|
194
|
+
const path = deletedPath(section);
|
|
195
|
+
if (!path || isProtectedEvidence(path))
|
|
196
|
+
return section;
|
|
197
|
+
return `deleted file: ${path}\n`;
|
|
198
|
+
}).join("");
|
|
199
|
+
}
|
|
200
|
+
function unquoteGitPath(raw) {
|
|
201
|
+
const value = raw.trim();
|
|
202
|
+
if (!value.startsWith('"') || !value.endsWith('"'))
|
|
203
|
+
return value;
|
|
204
|
+
try {
|
|
205
|
+
return JSON.parse(value);
|
|
206
|
+
}
|
|
207
|
+
catch {
|
|
208
|
+
return value.slice(1, -1);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
function diffSidePath(raw) {
|
|
212
|
+
const value = unquoteGitPath(raw);
|
|
213
|
+
if (value === "/dev/null")
|
|
214
|
+
return null;
|
|
215
|
+
return value.replace(/^[ab]\//, "");
|
|
216
|
+
}
|
|
217
|
+
function gitHeaderTokens(raw) {
|
|
218
|
+
return raw.match(/"(?:\\.|[^"\\])*"|\S+/g) ?? [];
|
|
219
|
+
}
|
|
220
|
+
function parseContentSection(section) {
|
|
221
|
+
const lines = section.split("\n");
|
|
222
|
+
const minus = lines.findIndex((line) => line.startsWith("--- "));
|
|
223
|
+
if (minus === -1 || !lines[minus + 1]?.startsWith("+++ "))
|
|
224
|
+
return null;
|
|
225
|
+
const body = lines.slice(minus + 2);
|
|
226
|
+
if (!body.some((line) => line.startsWith("@@ ")))
|
|
227
|
+
return null;
|
|
228
|
+
return {
|
|
229
|
+
lines,
|
|
230
|
+
minus,
|
|
231
|
+
sides: [
|
|
232
|
+
diffSidePath(lines[minus].slice(4)),
|
|
233
|
+
diffSidePath(lines[minus + 1].slice(4)),
|
|
234
|
+
],
|
|
235
|
+
body,
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
function sectionPaths(section, parsed) {
|
|
239
|
+
if (parsed)
|
|
240
|
+
return [...new Set(parsed.sides.filter((path) => path !== null))];
|
|
241
|
+
const renamedFrom = /^rename from (.+)$/m.exec(section)?.[1];
|
|
242
|
+
const renamedTo = /^rename to (.+)$/m.exec(section)?.[1];
|
|
243
|
+
if (renamedFrom || renamedTo) {
|
|
244
|
+
return [...new Set([renamedFrom, renamedTo].filter((path) => path !== undefined).map(unquoteGitPath))];
|
|
245
|
+
}
|
|
246
|
+
const header = /^diff --git (.+)$/m.exec(section)?.[1];
|
|
247
|
+
if (!header)
|
|
248
|
+
return [];
|
|
249
|
+
const tokens = gitHeaderTokens(header);
|
|
250
|
+
if (tokens.length !== 2)
|
|
251
|
+
return [];
|
|
252
|
+
return [...new Set(tokens.map(diffSidePath).filter((path) => path !== null))];
|
|
253
|
+
}
|
|
254
|
+
function hunkPayload(body, sign) {
|
|
255
|
+
return body
|
|
256
|
+
.filter((line) => line.startsWith(sign) || line.startsWith("\\"))
|
|
257
|
+
.map((line) => line[0] === sign ? line.slice(1) : line)
|
|
258
|
+
.join("\n");
|
|
259
|
+
}
|
|
260
|
+
function kindOnlyPaths(sections) {
|
|
261
|
+
const removed = new Map();
|
|
262
|
+
const added = new Map();
|
|
263
|
+
for (const section of sections) {
|
|
264
|
+
const parsed = parseContentSection(section);
|
|
265
|
+
if (!parsed)
|
|
266
|
+
continue;
|
|
267
|
+
const [before, after] = parsed.sides;
|
|
268
|
+
if (before && !after)
|
|
269
|
+
removed.set(before, hunkPayload(parsed.body, "-"));
|
|
270
|
+
else if (after && !before)
|
|
271
|
+
added.set(after, hunkPayload(parsed.body, "+"));
|
|
272
|
+
}
|
|
273
|
+
return new Set([...removed]
|
|
274
|
+
.filter(([path, payload]) => added.get(path) === payload)
|
|
275
|
+
.map(([path]) => path));
|
|
276
|
+
}
|
|
277
|
+
function sameCaptureProducer(classifications) {
|
|
278
|
+
if (!classifications.length || classifications.some((row) => row.kind !== "capture"))
|
|
279
|
+
return false;
|
|
280
|
+
const [first] = classifications;
|
|
281
|
+
return first !== undefined && classifications.every((row) => row.kind === "capture"
|
|
282
|
+
&& row.producer === first.producer
|
|
283
|
+
&& sameProvenance(row.provenance, first.provenance));
|
|
284
|
+
}
|
|
285
|
+
/**
|
|
286
|
+
* Classify and compact a Git diff once. Invalid manifest facts remain ordinary
|
|
287
|
+
* logic. Valid capture content is replaced by one citable receipt, while its
|
|
288
|
+
* exact UTF-8 payload remains charged to the separate capture bucket.
|
|
289
|
+
*/
|
|
290
|
+
export function measureArtifactDiff(diff, manifest = CAPTURE_ARTIFACT_MANIFEST) {
|
|
291
|
+
if (!diff.includes("diff --git ")) {
|
|
292
|
+
return {
|
|
293
|
+
rendered: diff,
|
|
294
|
+
logicBytes: Buffer.byteLength(diff, "utf8"),
|
|
295
|
+
captureBytes: 0,
|
|
296
|
+
sections: [],
|
|
297
|
+
};
|
|
298
|
+
}
|
|
299
|
+
const rawSections = diff.split(/(?=^diff --git )/m);
|
|
300
|
+
const kindOnly = kindOnlyPaths(rawSections);
|
|
301
|
+
const rendered = [];
|
|
302
|
+
const measurements = [];
|
|
303
|
+
for (const section of rawSections) {
|
|
304
|
+
const parsed = parseContentSection(section);
|
|
305
|
+
const paths = sectionPaths(section, parsed);
|
|
306
|
+
const classifications = paths.map((path) => classifyArtifactPath(path, manifest));
|
|
307
|
+
const validCapture = sameCaptureProducer(classifications);
|
|
308
|
+
const producer = validCapture ? classifications[0] : undefined;
|
|
309
|
+
const isKindOnly = paths.some((path) => kindOnly.has(path));
|
|
310
|
+
if (!parsed || !validCapture || !producer || isKindOnly) {
|
|
311
|
+
rendered.push(section);
|
|
312
|
+
measurements.push({
|
|
313
|
+
paths,
|
|
314
|
+
kind: validCapture ? "capture" : "logic",
|
|
315
|
+
reason: validCapture
|
|
316
|
+
? parsed ? "content-identical-kind-change" : "file-operation-only"
|
|
317
|
+
: classifications.map((row) => row.reason).join(",") || "unparsed-diff-section",
|
|
318
|
+
...(producer ? { producer: producer.producer, provenance: producer.provenance } : {}),
|
|
319
|
+
logicBytes: Buffer.byteLength(section, "utf8"),
|
|
320
|
+
captureBytes: 0,
|
|
321
|
+
});
|
|
322
|
+
continue;
|
|
323
|
+
}
|
|
324
|
+
const withheld = parsed.lines.slice(parsed.minus).join("\n");
|
|
325
|
+
const captureBytes = Buffer.byteLength(withheld, "utf8");
|
|
326
|
+
const receiptPath = paths.at(-1);
|
|
327
|
+
const receipt = `set aside: regenerable capture ${receiptPath} — ${captureBytes} bytes withheld (producer ${producer.producer}; provenance ${producer.provenance.source}#${producer.provenance.entrypoint}@${producer.provenance.revision})`;
|
|
328
|
+
const compact = `${parsed.lines.slice(0, parsed.minus).join("\n")}\n${receipt}\n`;
|
|
329
|
+
rendered.push(compact);
|
|
330
|
+
measurements.push({
|
|
331
|
+
paths,
|
|
332
|
+
kind: "capture",
|
|
333
|
+
reason: "manifest-provenance",
|
|
334
|
+
producer: producer.producer,
|
|
335
|
+
provenance: producer.provenance,
|
|
336
|
+
logicBytes: Buffer.byteLength(compact, "utf8"),
|
|
337
|
+
captureBytes,
|
|
338
|
+
});
|
|
339
|
+
}
|
|
340
|
+
return {
|
|
341
|
+
rendered: rendered.join(""),
|
|
342
|
+
logicBytes: measurements.reduce((sum, row) => sum + row.logicBytes, 0),
|
|
343
|
+
captureBytes: measurements.reduce((sum, row) => sum + row.captureBytes, 0),
|
|
344
|
+
sections: measurements,
|
|
345
|
+
};
|
|
346
|
+
}
|
|
347
|
+
/** The original API now delegates to the provenance-backed measurement. */
|
|
348
|
+
export function setAsideRegenerableCaptures(diff) {
|
|
349
|
+
return measureArtifactDiff(diff).rendered;
|
|
350
|
+
}
|
|
351
|
+
// The default corpus is about 134 KiB. Capture bytes are bounded, never free;
|
|
352
|
+
// the floor avoids making a deliberately lowered logic cap reintroduce Q14.
|
|
353
|
+
export const MIN_CAPTURE_DIFF_CAP = 1_000_000;
|
|
354
|
+
export const CAPTURE_DIFF_CAP_MULTIPLIER = 4;
|
|
355
|
+
export function captureDiffCapFor(logicCap) {
|
|
356
|
+
return Math.max(MIN_CAPTURE_DIFF_CAP, logicCap * CAPTURE_DIFF_CAP_MULTIPLIER);
|
|
357
|
+
}
|
package/dist/gates/baseline.d.ts
CHANGED
|
@@ -14,6 +14,12 @@ export interface BaselineWarning {
|
|
|
14
14
|
commands: string[];
|
|
15
15
|
reason: string;
|
|
16
16
|
}
|
|
17
|
+
export type FailureClassification = "regression" | "infra";
|
|
18
|
+
/**
|
|
19
|
+
* What a nonzero runner exit is evidence OF. `undefined` when the output names neither — the
|
|
20
|
+
* unreadable-runner case the existing fail-closed path already owns.
|
|
21
|
+
*/
|
|
22
|
+
export declare function classifyFailureOutput(output: string): FailureClassification | undefined;
|
|
17
23
|
export declare const UNRECOGNIZED_FAILURE = "<unrecognized failure output>";
|
|
18
24
|
export declare function fingerprint(output: string): string[];
|
|
19
25
|
export declare function detectGateCommands(repoRoot: string, cfg: TickmarkrConfig): Record<string, string>;
|
package/dist/gates/baseline.js
CHANGED
|
@@ -57,6 +57,30 @@ const namesFailure = (l) => FAIL_ANCHOR_RE.test(l) || RUNNER_FAIL_RE.test(l) ||
|
|
|
57
57
|
const isFailureShaped = (l) => namesFailure(l) || SUMMARY_FAIL_RE.test(l) || ERROR_ANCHOR_RE.test(l)
|
|
58
58
|
|| TSC_ERROR_RE.test(l) || LINTER_ERROR_RE.test(l);
|
|
59
59
|
const VOCAB_RE = /\b(?:error|fail(?:ed|ure|ing)?)\b/i;
|
|
60
|
+
// T9 — the infra/regression discriminator. A runner that died because the MACHINE ran out of
|
|
61
|
+
// processes, file descriptors or memory never finished asking the question, so its nonzero exit is
|
|
62
|
+
// not evidence about the work. But the reverse mistake is the expensive one: a real regression that
|
|
63
|
+
// happens to be printed next to an errno token must never be laundered into "infra" and forgiven.
|
|
64
|
+
// So the errno tokens below classify a line as infra only when NOTHING on that line also names a
|
|
65
|
+
// test-level failure — "AssertionError after spawn EAGAIN" names one and is a regression; "spawn
|
|
66
|
+
// EAGAIN" and "Error: spawn EAGAIN" name none and are infra. One regression line anywhere in the
|
|
67
|
+
// output makes the whole output a regression, whatever else the runner printed.
|
|
68
|
+
const INFRA_RE = /\bE(?:AGAIN|MFILE|NFILE|NOMEM|NOSPC)\b|JavaScript heap out of memory|Cannot allocate memory|Resource temporarily unavailable/;
|
|
69
|
+
// A named error CLASS ("AssertionError", "TypeError", "MyDomainError") — never bare "Error", which
|
|
70
|
+
// is what an errno report itself is headed with (`Error: spawn EAGAIN`). The prefix is required.
|
|
71
|
+
const ERROR_CLASS_RE = /\b[A-Za-z][A-Za-z0-9]*Error\b/;
|
|
72
|
+
const isInfraLine = (l) => INFRA_RE.test(l) && !ERROR_CLASS_RE.test(l) && !namesFailure(l) && !SUMMARY_FAIL_RE.test(l);
|
|
73
|
+
const namesRegression = (l) => (isFailureShaped(l) || ERROR_CLASS_RE.test(l)) && !isInfraLine(l);
|
|
74
|
+
/**
|
|
75
|
+
* What a nonzero runner exit is evidence OF. `undefined` when the output names neither — the
|
|
76
|
+
* unreadable-runner case the existing fail-closed path already owns.
|
|
77
|
+
*/
|
|
78
|
+
export function classifyFailureOutput(output) {
|
|
79
|
+
const lines = output.split("\n").map((l) => l.replace(ANSI_RE, "")).filter((l) => !PASS_LINE_RE.test(l));
|
|
80
|
+
if (lines.some(namesRegression))
|
|
81
|
+
return "regression";
|
|
82
|
+
return lines.some(isInfraLine) ? "infra" : undefined;
|
|
83
|
+
}
|
|
60
84
|
const normalizeLine = (l) => l.replace(/\d+/g, "#").replace(/\s+/g, " ").trim();
|
|
61
85
|
// A failing command whose output holds no shape any runner here names. The marker is content-free and
|
|
62
86
|
// constant: downstream consumers (tip verify journals fingerprint counts) still see that the command
|
|
@@ -205,6 +229,21 @@ export async function compareToBaseline(cwd, commands, baseline, enabled) {
|
|
|
205
229
|
continue;
|
|
206
230
|
}
|
|
207
231
|
const raw = (r.stdout + "\n" + r.stderr).split(cwd).join("");
|
|
232
|
+
// T9: classify BEFORE the baseline diff, and record it on every nonzero result. An infra-only
|
|
233
|
+
// exit means the runner never completed a suite, so there is nothing to forgive and nothing
|
|
234
|
+
// verified — it fails, and `meta.infra` marks it so the merge predicate cannot read it as a
|
|
235
|
+
// satisfied gate even if some future producer reports it as a pass. Baseline forgiveness stays
|
|
236
|
+
// exactly where it belongs: on failures the runner actually reported and the baseline already had.
|
|
237
|
+
const classification = classifyFailureOutput(raw);
|
|
238
|
+
if (classification === "infra") {
|
|
239
|
+
results.push({
|
|
240
|
+
gate: name,
|
|
241
|
+
pass: false,
|
|
242
|
+
details: `exit ${r.code} on infrastructure alone — the runner never completed a suite, so this gate verified nothing:\n${unrecognizedEvidence(raw) || raw.trim().split("\n").slice(0, 10).join("\n")}`,
|
|
243
|
+
meta: { classification, infra: true },
|
|
244
|
+
});
|
|
245
|
+
continue;
|
|
246
|
+
}
|
|
208
247
|
const known = new Set((baseline.commands[name]?.fingerprints ?? []).map(renormalize));
|
|
209
248
|
// OBS-42: diagnostic headings enrich fingerprints but cannot invalidate legacy baselines.
|
|
210
249
|
const current = fingerprint(raw);
|
|
@@ -225,16 +264,22 @@ export async function compareToBaseline(cwd, commands, baseline, enabled) {
|
|
|
225
264
|
gate: name,
|
|
226
265
|
pass: false,
|
|
227
266
|
details: evidence ? `${closed}\nunrecognized output:\n${evidence}` : closed,
|
|
267
|
+
...(classification ? { meta: { classification } } : {}),
|
|
228
268
|
});
|
|
229
269
|
continue;
|
|
230
270
|
}
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
: {
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
271
|
+
if (failing.length) {
|
|
272
|
+
const headlined = headlineDetails(raw, failing);
|
|
273
|
+
const meta = { ...headlined.meta, ...(classification ? { classification } : {}) };
|
|
274
|
+
results.push({ gate: name, pass: false, details: headlined.details, ...(Object.keys(meta).length ? { meta } : {}) });
|
|
275
|
+
continue;
|
|
276
|
+
}
|
|
277
|
+
results.push({
|
|
278
|
+
gate: name,
|
|
279
|
+
pass: true,
|
|
280
|
+
details: `exit ${r.code} but only pre-existing failures (forgiven)${unreadable ? " — no failure shape recognized in this output, so a new failure from this runner is invisible to the baseline gate" : ""}`,
|
|
281
|
+
...(classification ? { meta: { classification } } : {}),
|
|
282
|
+
});
|
|
238
283
|
}
|
|
239
284
|
return results;
|
|
240
285
|
}
|
package/dist/gates/llm.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { AsyncLocalStorage } from "node:async_hooks";
|
|
2
2
|
import { randomBytes } from "node:crypto";
|
|
3
|
-
import { mkdtempSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
4
4
|
import { tmpdir } from "node:os";
|
|
5
5
|
import { join } from "node:path";
|
|
6
6
|
import { formatOwnedName, parseOwnedName } from "../drivers/types.js";
|
|
@@ -104,36 +104,47 @@ export async function captureLlmOutput(run) {
|
|
|
104
104
|
return { value, outputs };
|
|
105
105
|
}
|
|
106
106
|
export async function runHeadless(adapter, model, prompt, cwd, timeoutMs = 300000) {
|
|
107
|
-
const
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
107
|
+
const dir = mkdtempSync(join(tmpdir(), "tickmarkr-llm-"));
|
|
108
|
+
try {
|
|
109
|
+
const pf = join(dir, "prompt.md");
|
|
110
|
+
writeFileSync(pf, prompt);
|
|
111
|
+
const r = await sh(adapter.headlessCommand(pf, model), cwd, timeoutMs);
|
|
112
|
+
return r.stdout + "\n" + r.stderr;
|
|
113
|
+
}
|
|
114
|
+
finally {
|
|
115
|
+
rmSync(dir, { recursive: true, force: true });
|
|
116
|
+
}
|
|
111
117
|
}
|
|
112
118
|
// v1.1 default path: the same headless CLI call, but dispatched through the driver
|
|
113
119
|
// as a visible named agent (herdr pane), with the quote-split completion wrapper.
|
|
114
120
|
export async function runViaDriver(adapter, model, prompt, cwd, via, timeoutMs = 300000) {
|
|
115
121
|
const dir = mkdtempSync(join(tmpdir(), "tickmarkr-llm-"));
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
122
|
+
try {
|
|
123
|
+
const pf = join(dir, "prompt.md");
|
|
124
|
+
writeFileSync(pf, prompt);
|
|
125
|
+
const scriptPath = join(dir, "dispatch.sh");
|
|
126
|
+
// OBS-50: bootstrap in a script beside the prompt — pane sees one short bash line + banner, not the raw inline command
|
|
127
|
+
const nonce = extractPromptNonce(prompt) ?? generateVerdictNonce();
|
|
128
|
+
writeFileSync(scriptPath, [
|
|
129
|
+
"export BASH_SILENCE_DEPRECATION_WARNING=1",
|
|
130
|
+
bannerShell(),
|
|
131
|
+
adapter.headlessCommand(pf, model),
|
|
132
|
+
gateExitTrailer(nonce),
|
|
133
|
+
].join("\n"));
|
|
134
|
+
const slot = await via.driver.slot(cwd, rolePaneNameFromPrompt(prompt, via.name), via.label ? { label: via.label } : undefined);
|
|
135
|
+
via.onSlot?.(slot);
|
|
136
|
+
await via.driver.run(slot, paneDispatchCommand(scriptPath));
|
|
137
|
+
// nonce-suffixed exit only: a displayed bare "TICKMARKR_EXIT:" or another call's marker must not
|
|
138
|
+
// false-complete — same guard the worker path uses (daemon.ts:330-331).
|
|
139
|
+
await via.driver.waitOutput(slot, `TICKMARKR_EXIT_${nonce}:\\d`, timeoutMs, { regex: true });
|
|
140
|
+
const out = await via.driver.read(slot, 400);
|
|
141
|
+
if (!via.keep)
|
|
142
|
+
await via.driver.close(slot);
|
|
143
|
+
return dewrapPaneVerdict(out, nonce);
|
|
144
|
+
}
|
|
145
|
+
finally {
|
|
146
|
+
rmSync(dir, { recursive: true, force: true });
|
|
147
|
+
}
|
|
137
148
|
}
|
|
138
149
|
// OBS-155: a TUI renders the verdict as a bullet and HARD-wraps it at pane width with a 2-space
|
|
139
150
|
// continuation indent, splitting words mid-token — so literal newlines land inside JSON string
|
package/dist/gates/review.d.ts
CHANGED
|
@@ -4,6 +4,8 @@ import { type Task } from "../graph/schema.js";
|
|
|
4
4
|
import { type GateVia } from "./llm.js";
|
|
5
5
|
import type { GateResult } from "./types.js";
|
|
6
6
|
import { type VerdictUnparseableCause } from "./verdict-cause.js";
|
|
7
|
+
import { type ArtifactDiffMeasurement, type ArtifactDiffSection } from "./artifact-manifest.js";
|
|
8
|
+
export { isProtectedEvidence, PROTECTED_EVIDENCE_PREFIXES, REGENERABLE_CAPTURE_PATHS, setAsideReceiptPath, setAsideRegenerableCaptures, } from "./artifact-manifest.js";
|
|
7
9
|
export type ReviewSeverity = "material" | "minor";
|
|
8
10
|
export interface ReviewFinding {
|
|
9
11
|
note: string;
|
|
@@ -21,13 +23,6 @@ export interface ReviewVerdict {
|
|
|
21
23
|
body: string;
|
|
22
24
|
}>;
|
|
23
25
|
}
|
|
24
|
-
export declare const REGENERABLE_CAPTURE_PATHS: readonly string[];
|
|
25
|
-
export declare const PROTECTED_EVIDENCE_PREFIXES: readonly ["tests/fixtures/cockpit/anchors/", "tests/fixtures/cockpit/sources/", "tests/fixtures/cockpit/colour/sources/"];
|
|
26
|
-
export declare function isProtectedEvidence(path: string): boolean;
|
|
27
|
-
/** The `{path}` a set-aside receipt names, or null if this section carries no receipt. */
|
|
28
|
-
export declare function setAsideReceiptPath(section: string): string | null;
|
|
29
|
-
/** Replace the content of every section confined to the regenerable frame corpora with a receipt. */
|
|
30
|
-
export declare function setAsideRegenerableCaptures(diff: string): string;
|
|
31
26
|
/**
|
|
32
27
|
* The paths this task's diff ACTUALLY touched. `-z` so a path carrying spaces or non-ASCII bytes is
|
|
33
28
|
* never mangled by git's quoting, `--no-renames` so a rename reports BOTH sides: a file renamed OUT of
|
|
@@ -35,11 +30,21 @@ export declare function setAsideRegenerableCaptures(diff: string): string;
|
|
|
35
30
|
*/
|
|
36
31
|
export declare function changedPaths(worktree: string, baseRef: string): Promise<string[]>;
|
|
37
32
|
export declare function mirrorsVersionOnly(worktree: string, baseRef: string, path: string): Promise<boolean>;
|
|
38
|
-
export
|
|
39
|
-
full: string;
|
|
40
|
-
forCap: string;
|
|
41
|
-
|
|
33
|
+
export type TaskDiffMeasurement = {
|
|
34
|
+
readonly full: string;
|
|
35
|
+
readonly forCap: string;
|
|
36
|
+
/** Strict-cap UTF-8 bytes left after capture payloads become receipts. */
|
|
37
|
+
readonly logicBytes: number;
|
|
38
|
+
/** UTF-8 bytes withheld by those receipts and charged to the larger cap. */
|
|
39
|
+
readonly captureBytes: number;
|
|
40
|
+
readonly classifications: readonly ArtifactDiffSection[];
|
|
41
|
+
readonly fullMeasurement: ArtifactDiffMeasurement;
|
|
42
|
+
readonly capMeasurement: ArtifactDiffMeasurement;
|
|
43
|
+
};
|
|
44
|
+
export declare function fetchTaskDiff(worktree: string, baseRef: string): Promise<TaskDiffMeasurement>;
|
|
42
45
|
export declare function checkDiffCap(gate: string, measured: number, cap: number, prefix?: string): GateResult | null;
|
|
46
|
+
/** Apply the strict reviewable-logic cap and the finite, larger capture cap independently. */
|
|
47
|
+
export declare function checkTaskDiffCaps(gate: string, measured: Pick<TaskDiffMeasurement, "logicBytes" | "captureBytes">, logicCap: number, prefix?: string): GateResult | null;
|
|
43
48
|
export declare function isDiffCapPark(result: GateResult): boolean;
|
|
44
49
|
export declare function diffCapParkReason(results: GateResult[]): string | null;
|
|
45
50
|
export declare function modelId(model: string): string;
|