@skill-harness/core 0.5.0 → 0.7.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/types.d.ts +37 -0
- package/dist/adjudication.d.ts +210 -0
- package/dist/adjudication.js +392 -0
- package/dist/affected.d.ts +88 -0
- package/dist/affected.js +222 -0
- package/dist/capture-trace-types.d.ts +228 -0
- package/dist/capture-trace-types.js +23 -0
- package/dist/capture.d.ts +193 -0
- package/dist/capture.js +344 -0
- package/dist/execution-trace.d.ts +61 -0
- package/dist/execution-trace.js +299 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.js +9 -0
- package/dist/instruction-coverage.d.ts +106 -0
- package/dist/instruction-coverage.js +253 -0
- package/dist/journal.d.ts +17 -0
- package/dist/lint.d.ts +16 -1
- package/dist/lint.js +52 -0
- package/dist/regate.js +80 -17
- package/dist/regrade.js +17 -3
- package/dist/report.d.ts +48 -0
- package/dist/report.js +39 -1
- package/dist/reps.d.ts +14 -1
- package/dist/reps.js +28 -2
- package/dist/rescore.js +11 -2
- package/dist/results.d.ts +128 -6
- package/dist/results.js +155 -6
- package/dist/run.d.ts +9 -1
- package/dist/run.js +129 -9
- package/dist/seeded.d.ts +11 -0
- package/dist/seeded.js +31 -7
- package/dist/sources.d.ts +26 -0
- package/dist/sources.js +82 -3
- package/dist/spec-write.d.ts +62 -0
- package/dist/spec-write.js +106 -0
- package/dist/spec.d.ts +29 -0
- package/dist/spec.js +55 -0
- package/dist/stability.d.ts +144 -0
- package/dist/stability.js +232 -0
- package/dist/trace-gates.d.ts +133 -0
- package/dist/trace-gates.js +519 -0
- package/dist/trends.d.ts +28 -0
- package/dist/trends.js +76 -61
- package/dist/workspace.d.ts +36 -0
- package/dist/workspace.js +61 -0
- package/package.json +1 -1
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import type { Scenario } from "./spec.js";
|
|
2
|
+
/**
|
|
3
|
+
* Which scenarios could plausibly be affected by a change.
|
|
4
|
+
*
|
|
5
|
+
* The governing asymmetry: **an under-inclusive set is dangerous and an
|
|
6
|
+
* over-inclusive one is merely expensive.** Missing a regression means shipping
|
|
7
|
+
* it; running extra scenarios costs tokens. So every ambiguity resolves toward
|
|
8
|
+
* selecting more, and anything the mapping cannot explain selects everything.
|
|
9
|
+
*
|
|
10
|
+
* An affected run is always partial and can never report SHIP. It is an
|
|
11
|
+
* iteration tool — a full run is still what clears a skill for publishing.
|
|
12
|
+
*/
|
|
13
|
+
export type SelectionReason = {
|
|
14
|
+
kind: "covers";
|
|
15
|
+
detail: string;
|
|
16
|
+
} | {
|
|
17
|
+
kind: "critical";
|
|
18
|
+
} | {
|
|
19
|
+
kind: "under-pressure";
|
|
20
|
+
} | {
|
|
21
|
+
kind: "stimulus-changed";
|
|
22
|
+
detail: string;
|
|
23
|
+
} | {
|
|
24
|
+
kind: "unmapped-change";
|
|
25
|
+
detail: string;
|
|
26
|
+
} | {
|
|
27
|
+
kind: "no-covers-declared";
|
|
28
|
+
};
|
|
29
|
+
export interface SelectedScenario {
|
|
30
|
+
id: string;
|
|
31
|
+
reasons: SelectionReason[];
|
|
32
|
+
}
|
|
33
|
+
export interface AffectedResult {
|
|
34
|
+
selected: SelectedScenario[];
|
|
35
|
+
/** True when the mapping could not be trusted and everything was selected. */
|
|
36
|
+
conservative: boolean;
|
|
37
|
+
/** Human-readable account of why, when `conservative`. */
|
|
38
|
+
conservativeReason: string | null;
|
|
39
|
+
/** Files in the diff whose changed lines map to no covered section. A file nothing `covers` at all is not listed here — it selects everything (see `isInstructionText`) or is skipped as source. */
|
|
40
|
+
unmappedFiles: string[];
|
|
41
|
+
}
|
|
42
|
+
export interface DiffHunk {
|
|
43
|
+
file: string;
|
|
44
|
+
/**
|
|
45
|
+
* 1-based first changed line in the NEW file.
|
|
46
|
+
*
|
|
47
|
+
* For a pure deletion git emits `@@ -2,3 +1,0 @@`, so this is the line the
|
|
48
|
+
* deletion sits AFTER — 1 in that example, not 0. `selectAffected` maps
|
|
49
|
+
* sections from the real value; "fixing" it to 0 to match an earlier version
|
|
50
|
+
* of this comment would break deletion→section mapping.
|
|
51
|
+
*/
|
|
52
|
+
start: number;
|
|
53
|
+
/** Number of changed lines; 0 for a pure deletion at `start`. */
|
|
54
|
+
count: number;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Parse `git diff --unified=0` hunk headers.
|
|
58
|
+
*
|
|
59
|
+
* `--unified=0` matters: with context lines the hunk range covers unchanged text,
|
|
60
|
+
* and a change at the top of one section would be attributed to the section above
|
|
61
|
+
* it too. Over-selection is the safe direction, but only when it is *reasoned*
|
|
62
|
+
* over-selection rather than an artefact of a flag.
|
|
63
|
+
*/
|
|
64
|
+
export declare function parseDiffHunks(diff: string): DiffHunk[];
|
|
65
|
+
/** Files the diff touched (added, modified or deleted). */
|
|
66
|
+
export declare function parseDiffFiles(diff: string): string[];
|
|
67
|
+
export interface AffectedOptions {
|
|
68
|
+
scenarios: Scenario[];
|
|
69
|
+
/** Dir `covers` and fixture paths resolve against. */
|
|
70
|
+
specDir: string;
|
|
71
|
+
/** Unified diff text (`git diff --unified=0 <base>`). */
|
|
72
|
+
diff: string;
|
|
73
|
+
/** Repo root the diff paths are relative to. */
|
|
74
|
+
repoRoot: string;
|
|
75
|
+
}
|
|
76
|
+
/** Run `git diff --unified=0 <base>` in a repo. Empty string when git fails. */
|
|
77
|
+
export declare function gitDiff(repoRoot: string, base: string): Promise<string>;
|
|
78
|
+
/**
|
|
79
|
+
* Select the scenarios a change could affect.
|
|
80
|
+
*
|
|
81
|
+
* Always unions in every critical and every B-series scenario, whatever the diff
|
|
82
|
+
* said. Those are the ship gates: if the mapping is wrong — and a mapping built
|
|
83
|
+
* from author-written labels can be — the scenarios that decide releases are the
|
|
84
|
+
* worst possible ones to skip.
|
|
85
|
+
*/
|
|
86
|
+
export declare function selectAffected(opts: AffectedOptions): AffectedResult;
|
|
87
|
+
/** One line per selected scenario, naming every reason it was picked. */
|
|
88
|
+
export declare function formatAffected(result: AffectedResult, total: number): string;
|
package/dist/affected.js
ADDED
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
2
|
+
import { resolve, dirname, relative } from "node:path";
|
|
3
|
+
import { parseSections, sectionAtLine, parseCoversRef } from "./instruction-coverage.js";
|
|
4
|
+
import { exec } from "./util/exec.js";
|
|
5
|
+
/**
|
|
6
|
+
* Parse `git diff --unified=0` hunk headers.
|
|
7
|
+
*
|
|
8
|
+
* `--unified=0` matters: with context lines the hunk range covers unchanged text,
|
|
9
|
+
* and a change at the top of one section would be attributed to the section above
|
|
10
|
+
* it too. Over-selection is the safe direction, but only when it is *reasoned*
|
|
11
|
+
* over-selection rather than an artefact of a flag.
|
|
12
|
+
*/
|
|
13
|
+
export function parseDiffHunks(diff) {
|
|
14
|
+
const hunks = [];
|
|
15
|
+
let file = null;
|
|
16
|
+
for (const line of diff.split("\n")) {
|
|
17
|
+
if (line.startsWith("+++ ")) {
|
|
18
|
+
const p = line.slice(4).trim();
|
|
19
|
+
file = p === "/dev/null" ? null : p.replace(/^b\//, "");
|
|
20
|
+
continue;
|
|
21
|
+
}
|
|
22
|
+
if (!line.startsWith("@@") || file === null)
|
|
23
|
+
continue;
|
|
24
|
+
const m = /@@ -\d+(?:,\d+)? \+(\d+)(?:,(\d+))? @@/.exec(line);
|
|
25
|
+
if (!m)
|
|
26
|
+
continue;
|
|
27
|
+
hunks.push({ file, start: Number(m[1]), count: m[2] === undefined ? 1 : Number(m[2]) });
|
|
28
|
+
}
|
|
29
|
+
return hunks;
|
|
30
|
+
}
|
|
31
|
+
/** Files the diff touched (added, modified or deleted). */
|
|
32
|
+
export function parseDiffFiles(diff) {
|
|
33
|
+
const files = new Set();
|
|
34
|
+
for (const line of diff.split("\n")) {
|
|
35
|
+
const m = /^diff --git a\/(.+?) b\/(.+)$/.exec(line);
|
|
36
|
+
if (m) {
|
|
37
|
+
files.add(m[1]);
|
|
38
|
+
files.add(m[2]);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
return [...files];
|
|
42
|
+
}
|
|
43
|
+
/** Run `git diff --unified=0 <base>` in a repo. Empty string when git fails. */
|
|
44
|
+
export async function gitDiff(repoRoot, base) {
|
|
45
|
+
const r = await exec("git", ["diff", "--unified=0", base], { cwd: repoRoot, timeoutMs: 60_000 });
|
|
46
|
+
if (r.code !== 0)
|
|
47
|
+
throw new Error(`git diff --unified=0 ${base} failed: ${r.stderr.trim() || `exit ${r.code}`}`);
|
|
48
|
+
return r.stdout;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Select the scenarios a change could affect.
|
|
52
|
+
*
|
|
53
|
+
* Always unions in every critical and every B-series scenario, whatever the diff
|
|
54
|
+
* said. Those are the ship gates: if the mapping is wrong — and a mapping built
|
|
55
|
+
* from author-written labels can be — the scenarios that decide releases are the
|
|
56
|
+
* worst possible ones to skip.
|
|
57
|
+
*/
|
|
58
|
+
export function selectAffected(opts) {
|
|
59
|
+
const { scenarios, specDir, diff, repoRoot } = opts;
|
|
60
|
+
const reasons = new Map();
|
|
61
|
+
const add = (id, reason) => {
|
|
62
|
+
const list = reasons.get(id) ?? [];
|
|
63
|
+
list.push(reason);
|
|
64
|
+
reasons.set(id, list);
|
|
65
|
+
};
|
|
66
|
+
const selectAll = (why) => {
|
|
67
|
+
for (const s of scenarios)
|
|
68
|
+
if (!reasons.has(s.id))
|
|
69
|
+
add(s.id, { kind: "unmapped-change", detail: why });
|
|
70
|
+
return {
|
|
71
|
+
selected: [...reasons.entries()].map(([id, rs]) => ({ id, reasons: rs })),
|
|
72
|
+
conservative: true,
|
|
73
|
+
conservativeReason: why,
|
|
74
|
+
unmappedFiles: [],
|
|
75
|
+
};
|
|
76
|
+
};
|
|
77
|
+
// The ship gates, unconditionally.
|
|
78
|
+
for (const s of scenarios) {
|
|
79
|
+
if (s.critical)
|
|
80
|
+
add(s.id, { kind: "critical" });
|
|
81
|
+
if (/^B/i.test(s.id))
|
|
82
|
+
add(s.id, { kind: "under-pressure" });
|
|
83
|
+
}
|
|
84
|
+
const hunks = parseDiffHunks(diff);
|
|
85
|
+
const changedFiles = parseDiffFiles(diff);
|
|
86
|
+
// A scenario with no `covers` cannot be excluded by a coverage mapping — there
|
|
87
|
+
// is nothing to consult. Selecting it is the only honest answer.
|
|
88
|
+
for (const s of scenarios) {
|
|
89
|
+
if (!s.covers || s.covers.length === 0)
|
|
90
|
+
add(s.id, { kind: "no-covers-declared" });
|
|
91
|
+
}
|
|
92
|
+
// Stimulus files: a changed fixture, post-test, agent file or extension changes
|
|
93
|
+
// what the scenario RUNS, regardless of any instruction text.
|
|
94
|
+
const stimulusFiles = (s) => {
|
|
95
|
+
const files = [];
|
|
96
|
+
if (s.fixture)
|
|
97
|
+
files.push(s.fixture);
|
|
98
|
+
if (s.assert?.post_test)
|
|
99
|
+
files.push(s.assert.post_test);
|
|
100
|
+
if (s.systemPromptFile)
|
|
101
|
+
files.push(s.systemPromptFile);
|
|
102
|
+
for (const e of s.extensions ?? [])
|
|
103
|
+
files.push(e);
|
|
104
|
+
return files;
|
|
105
|
+
};
|
|
106
|
+
const changedAbs = new Set(changedFiles.map((f) => resolve(repoRoot, f)));
|
|
107
|
+
for (const s of scenarios) {
|
|
108
|
+
for (const f of stimulusFiles(s)) {
|
|
109
|
+
const abs = resolve(specDir, f);
|
|
110
|
+
// Directory-ish match too: a fixture is a tree, and a change anywhere in it
|
|
111
|
+
// counts.
|
|
112
|
+
const hit = [...changedAbs].some((c) => c === abs || c.startsWith(`${abs}/`));
|
|
113
|
+
if (hit)
|
|
114
|
+
add(s.id, { kind: "stimulus-changed", detail: f });
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
// Reverse the covers map: changed line → section → scenarios.
|
|
118
|
+
const sectionsFor = new Map();
|
|
119
|
+
const load = (abs) => {
|
|
120
|
+
if (sectionsFor.has(abs))
|
|
121
|
+
return sectionsFor.get(abs);
|
|
122
|
+
const parsed = existsSync(abs) ? parseSections(readFileSync(abs, "utf8")) : null;
|
|
123
|
+
sectionsFor.set(abs, parsed);
|
|
124
|
+
return parsed;
|
|
125
|
+
};
|
|
126
|
+
const coversIndex = new Map(); // "abs#slug" | "abs" -> scenario ids
|
|
127
|
+
for (const s of scenarios) {
|
|
128
|
+
for (const raw of s.covers ?? []) {
|
|
129
|
+
const ref = parseCoversRef(raw);
|
|
130
|
+
const abs = resolve(specDir, ref.file);
|
|
131
|
+
const key = ref.slug === undefined ? abs : `${abs}#${ref.slug}`;
|
|
132
|
+
coversIndex.set(key, [...(coversIndex.get(key) ?? []), s.id]);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
// The skill's own directory, minus its `tests/` subtree. Markdown here is
|
|
136
|
+
// instruction text the model reads; markdown elsewhere in the repo is not.
|
|
137
|
+
const skillRoot = dirname(specDir);
|
|
138
|
+
const isInstructionText = (abs) => abs.startsWith(`${skillRoot}/`) && !abs.startsWith(`${specDir}/`) && /\.(?:md|markdown)$/i.test(abs);
|
|
139
|
+
const unmappedFiles = new Set();
|
|
140
|
+
for (const hunk of hunks) {
|
|
141
|
+
const abs = resolve(repoRoot, hunk.file);
|
|
142
|
+
// Only instruction files participate; a changed source file is not a section.
|
|
143
|
+
const referenced = [...coversIndex.keys()].some((k) => k === abs || k.startsWith(`${abs}#`));
|
|
144
|
+
if (!referenced) {
|
|
145
|
+
// "No `covers` mentions it" is not the same as "it cannot matter". For
|
|
146
|
+
// instruction text inside the skill, it is the opposite: every scenario
|
|
147
|
+
// reads the skill, and nobody claimed this prose — so the mapping has no
|
|
148
|
+
// basis for ruling it out. `continue` here deselected scenarios the edit
|
|
149
|
+
// could well have broken, and did not even reach `unmappedFiles`, so the
|
|
150
|
+
// output said nothing about it. Under-inclusion is the one failure this
|
|
151
|
+
// module exists to avoid.
|
|
152
|
+
if (isInstructionText(abs)) {
|
|
153
|
+
return selectAll(`${relative(repoRoot, abs) || hunk.file} is instruction text that no scenario \`covers\` — the mapping cannot rule it out`);
|
|
154
|
+
}
|
|
155
|
+
continue;
|
|
156
|
+
}
|
|
157
|
+
const sections = load(abs);
|
|
158
|
+
if (sections === null) {
|
|
159
|
+
// The file is referenced but gone — a rename or delete. Nothing can be
|
|
160
|
+
// mapped, and guessing would be worse than admitting it.
|
|
161
|
+
return selectAll(`${hunk.file} is referenced by \`covers\` but is not readable — it may have been renamed or deleted`);
|
|
162
|
+
}
|
|
163
|
+
for (const id of coversIndex.get(abs) ?? [])
|
|
164
|
+
add(id, { kind: "covers", detail: `${hunk.file} (whole file)` });
|
|
165
|
+
const lines = hunk.count === 0 ? [hunk.start] : Array.from({ length: hunk.count }, (_, i) => hunk.start + i);
|
|
166
|
+
let mappedAny = false;
|
|
167
|
+
for (const line of lines) {
|
|
168
|
+
const section = sectionAtLine(sections, line);
|
|
169
|
+
if (!section)
|
|
170
|
+
continue; // preamble before the first heading
|
|
171
|
+
const ids = coversIndex.get(`${abs}#${section.slug}`) ?? [];
|
|
172
|
+
for (const id of ids)
|
|
173
|
+
add(id, { kind: "covers", detail: `${hunk.file}#${section.slug}` });
|
|
174
|
+
if (ids.length > 0)
|
|
175
|
+
mappedAny = true;
|
|
176
|
+
}
|
|
177
|
+
if (!mappedAny && (coversIndex.get(abs) ?? []).length === 0)
|
|
178
|
+
unmappedFiles.add(hunk.file);
|
|
179
|
+
}
|
|
180
|
+
// A wholesale rewrite defeats line mapping: every line looks changed, and the
|
|
181
|
+
// sections that "match" are an artefact of the rewrite's shape.
|
|
182
|
+
const rewritten = hunks.filter((h) => h.count > 200);
|
|
183
|
+
if (rewritten.length > 0) {
|
|
184
|
+
return selectAll(`${rewritten[0].file} changed by ${rewritten[0].count} lines in one hunk — too large to map to sections reliably`);
|
|
185
|
+
}
|
|
186
|
+
return {
|
|
187
|
+
selected: [...reasons.entries()].map(([id, rs]) => ({ id, reasons: rs })),
|
|
188
|
+
conservative: false,
|
|
189
|
+
conservativeReason: null,
|
|
190
|
+
unmappedFiles: [...unmappedFiles],
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
/** One line per selected scenario, naming every reason it was picked. */
|
|
194
|
+
export function formatAffected(result, total) {
|
|
195
|
+
const out = [];
|
|
196
|
+
if (result.conservative) {
|
|
197
|
+
out.push(`selecting ALL ${total} scenario(s): ${result.conservativeReason}`);
|
|
198
|
+
}
|
|
199
|
+
else {
|
|
200
|
+
out.push(`selected ${result.selected.length}/${total} scenario(s):`);
|
|
201
|
+
}
|
|
202
|
+
for (const s of [...result.selected].sort((a, b) => a.id.localeCompare(b.id))) {
|
|
203
|
+
out.push(` ${s.id} ${s.reasons.map(describe).join(", ")}`);
|
|
204
|
+
}
|
|
205
|
+
if (result.unmappedFiles.length) {
|
|
206
|
+
out.push(` note: changes in ${result.unmappedFiles.join(", ")} map to no covered section`);
|
|
207
|
+
}
|
|
208
|
+
out.push("");
|
|
209
|
+
out.push("an affected run is partial and never reports SHIP — a full run still gates a release");
|
|
210
|
+
return out.join("\n");
|
|
211
|
+
}
|
|
212
|
+
function describe(r) {
|
|
213
|
+
switch (r.kind) {
|
|
214
|
+
case "covers": return `covers ${r.detail}`;
|
|
215
|
+
case "critical": return "critical (always run)";
|
|
216
|
+
case "under-pressure": return "B-series (always run)";
|
|
217
|
+
case "stimulus-changed": return `stimulus changed: ${r.detail}`;
|
|
218
|
+
case "unmapped-change": return `conservative: ${r.detail}`;
|
|
219
|
+
case "no-covers-declared": return "declares no `covers` — cannot be ruled out";
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
//# sourceMappingURL=affected.js.map
|
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Data contracts for the pi-native regression capture program.
|
|
3
|
+
*
|
|
4
|
+
* Types only — no behavior. Phase 0 of
|
|
5
|
+
* `docs/superpowers/plans/2026-08-07-pi-native-regression-capture-program.md`
|
|
6
|
+
* fixes the shapes before any parser, capture UI, or gate evaluator is written,
|
|
7
|
+
* because both shapes get persisted and a persisted shape is expensive to move.
|
|
8
|
+
*
|
|
9
|
+
* Both carry an explicit version. The pi event stream they derive from is not a
|
|
10
|
+
* stable public contract (measured against pi 0.83.0; see
|
|
11
|
+
* `packages/adapters/test/fixtures/pi-json/README.md`), so a reader must be able
|
|
12
|
+
* to tell which pi produced an artifact and refuse one it does not understand
|
|
13
|
+
* rather than silently misreading it.
|
|
14
|
+
*/
|
|
15
|
+
import type { ModelRef, RunMode } from "./adapters/types.js";
|
|
16
|
+
export declare const EXECUTION_TRACE_VERSION = 2;
|
|
17
|
+
/**
|
|
18
|
+
* Tool arguments after sanitization.
|
|
19
|
+
*
|
|
20
|
+
* `unknown` rather than `any`: an assertion evaluator must narrow before
|
|
21
|
+
* comparing, and the safe-DSL operators (equals/contains/matches/…) are the only
|
|
22
|
+
* things allowed to inspect these.
|
|
23
|
+
*/
|
|
24
|
+
export type SanitizedArgs = Record<string, unknown>;
|
|
25
|
+
/**
|
|
26
|
+
* One tool call, correlated across pi's `tool_execution_start` /
|
|
27
|
+
* `tool_execution_end` pair.
|
|
28
|
+
*
|
|
29
|
+
* Correlation is by `toolCallId` and nothing else. Measured: pi executes tool
|
|
30
|
+
* calls batched in one assistant message CONCURRENTLY, and emits their `end`
|
|
31
|
+
* events in completion order, not issue order — three `bash` calls sleeping
|
|
32
|
+
* 6s/1s/3s started SLOW,FAST,MID and ended FAST,MID,SLOW. Any parser that pairs
|
|
33
|
+
* by position is wrong.
|
|
34
|
+
*/
|
|
35
|
+
export interface TraceToolCall {
|
|
36
|
+
/** pi's `toolCallId`. Unique within a trace; the only sound correlation key. */
|
|
37
|
+
id: string;
|
|
38
|
+
/** Registered tool name, e.g. `read`, `bash`, `Agent`. */
|
|
39
|
+
name: string;
|
|
40
|
+
/** Sanitized call arguments. Secrets and oversized values are redacted. */
|
|
41
|
+
args: SanitizedArgs;
|
|
42
|
+
/** 0-based position in ISSUE order (the order pi emitted `tool_execution_start`). */
|
|
43
|
+
issueIndex: number;
|
|
44
|
+
/** 0-based position in COMPLETION order. Differs from `issueIndex` under parallelism. */
|
|
45
|
+
completionIndex: number;
|
|
46
|
+
/** pi's `isError` on the `tool_execution_end` event. */
|
|
47
|
+
isError: boolean;
|
|
48
|
+
/**
|
|
49
|
+
* Bounded metadata about the result body — never the body itself.
|
|
50
|
+
*
|
|
51
|
+
* Full tool results are not persisted: they routinely contain file contents,
|
|
52
|
+
* command output, and absolute paths (a failing `read` embeds the full path in
|
|
53
|
+
* its error string). A gate that needs to assert on content asserts on the
|
|
54
|
+
* workspace instead.
|
|
55
|
+
*/
|
|
56
|
+
result: TraceResultMeta;
|
|
57
|
+
}
|
|
58
|
+
export interface TraceResultMeta {
|
|
59
|
+
/** Byte length of the serialized result content. */
|
|
60
|
+
bytes: number;
|
|
61
|
+
/** SHA-256 of the serialized result content, so identity is checkable without the body. */
|
|
62
|
+
sha256: string;
|
|
63
|
+
/**
|
|
64
|
+
* The tool's own structured `details`, when it returned any.
|
|
65
|
+
*
|
|
66
|
+
* Measured: a value a tool returns as `details` survives verbatim into
|
|
67
|
+
* `tool_execution_end.result.details`. That is the one stable structured
|
|
68
|
+
* channel an extension can expose deliberately — unlike prose in `content`,
|
|
69
|
+
* which is formatting, not contract. Retained only when it is small and free
|
|
70
|
+
* of redaction hits; absent otherwise.
|
|
71
|
+
*/
|
|
72
|
+
details?: Record<string, unknown>;
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* A trace of one subject execution — one `pi` invocation.
|
|
76
|
+
*
|
|
77
|
+
* A multi-turn scenario produces ONE TRACE PER TURN, because each `pi` call
|
|
78
|
+
* emits an independent stream containing only that invocation's messages (the
|
|
79
|
+
* conversation carries in the session dir, not in the event stream). This
|
|
80
|
+
* mirrors the loop the adapter already runs.
|
|
81
|
+
*/
|
|
82
|
+
export interface ExecutionTraceV1 {
|
|
83
|
+
trace_version: typeof EXECUTION_TRACE_VERSION;
|
|
84
|
+
/** `pi --version` at capture time. Null when it could not be determined. */
|
|
85
|
+
pi_version: string | null;
|
|
86
|
+
/** The model under test. */
|
|
87
|
+
subject: ModelRef;
|
|
88
|
+
scenario_id: string;
|
|
89
|
+
mode: RunMode;
|
|
90
|
+
/** 0-based repetition index within a run. */
|
|
91
|
+
rep: number;
|
|
92
|
+
/** 0-based turn index; always 0 for a single-turn scenario. */
|
|
93
|
+
turn: number;
|
|
94
|
+
/**
|
|
95
|
+
* The assistant's final answer text for this invocation.
|
|
96
|
+
*
|
|
97
|
+
* Deliberately the FINAL assistant message only, not every assistant text
|
|
98
|
+
* block. Measured: pi's print mode emits exactly this, and it is what today's
|
|
99
|
+
* adapter hands the judge — concatenating interim text blocks would feed the
|
|
100
|
+
* judge narration ("Let me read that file…") that the current transcript has
|
|
101
|
+
* never contained, changing grades on scenarios nobody edited.
|
|
102
|
+
*/
|
|
103
|
+
final_text: string;
|
|
104
|
+
/** Every tool call in this invocation, in issue order. */
|
|
105
|
+
tool_calls: TraceToolCall[];
|
|
106
|
+
/**
|
|
107
|
+
* Workspace paths whose content changed during the invocation, relative to the
|
|
108
|
+
* workspace root. Evidence for `unchanged_paths` assertions. Bounded to the
|
|
109
|
+
* isolated workspace: writes outside it are not observable and never claimed.
|
|
110
|
+
*
|
|
111
|
+
* **Tri-state, and the third state is the point.** `null` means the workspace
|
|
112
|
+
* was never observed — there was none, or the snapshot could not be taken.
|
|
113
|
+
* `[]` means observed, and nothing changed. Collapsing the two made a safety
|
|
114
|
+
* gate report green from evidence that does not exist: a run whose observation
|
|
115
|
+
* failed recorded `objective: ERROR` honestly, and `regate` then read the saved
|
|
116
|
+
* `[]` and re-graded it to PASS, re-stamping the source hash so `lint` called
|
|
117
|
+
* the result current.
|
|
118
|
+
*
|
|
119
|
+
* Version 2 exists for exactly this widening: a v1 reader must decline a v2
|
|
120
|
+
* trace rather than read `null` as empty.
|
|
121
|
+
*/
|
|
122
|
+
changed_paths: string[] | null;
|
|
123
|
+
/**
|
|
124
|
+
* Reported token cost of this invocation, when pi provided it.
|
|
125
|
+
*
|
|
126
|
+
* pi's `usage.cost` carries real per-message costs. Recorded for spend
|
|
127
|
+
* disclosure, never for grading.
|
|
128
|
+
*/
|
|
129
|
+
cost_usd: number | null;
|
|
130
|
+
/** SHA-256 over the deterministic serialization of this trace, minus this field. */
|
|
131
|
+
trace_sha256?: string;
|
|
132
|
+
}
|
|
133
|
+
export declare const CAPTURE_SCHEMA_VERSION = 1;
|
|
134
|
+
/** Why a conversation was captured. */
|
|
135
|
+
export type CaptureClassification = "failure" | "good_example";
|
|
136
|
+
/** Where a capture is in the human review pipeline. */
|
|
137
|
+
export type CapturePromotionStatus = "pending" | "promoted" | "rejected";
|
|
138
|
+
/** What the human confirmed as responsible for the behavior. */
|
|
139
|
+
export interface CaptureTarget {
|
|
140
|
+
kind: "skill" | "subagent";
|
|
141
|
+
/** Path relative to the skills root or repo root — never absolute. */
|
|
142
|
+
path: string;
|
|
143
|
+
/** SHA-256 of the target's content at capture time, so drift is detectable. */
|
|
144
|
+
content_sha256: string;
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* Provenance for a capture.
|
|
148
|
+
*
|
|
149
|
+
* Hashed, not absolute: an absolute session path identifies a machine and a
|
|
150
|
+
* user, and the capture file is meant to be committed. The hash is enough to
|
|
151
|
+
* recognize the same session again locally, which is all provenance needs to do.
|
|
152
|
+
*/
|
|
153
|
+
export interface CaptureProvenance {
|
|
154
|
+
session_sha256: string;
|
|
155
|
+
/** Indices of the selected turn range within the active branch, inclusive. */
|
|
156
|
+
turn_range: {
|
|
157
|
+
start: number;
|
|
158
|
+
end: number;
|
|
159
|
+
};
|
|
160
|
+
subject?: ModelRef;
|
|
161
|
+
git_commit?: string;
|
|
162
|
+
/** True when the working tree was dirty at capture time. */
|
|
163
|
+
git_dirty?: boolean;
|
|
164
|
+
}
|
|
165
|
+
/**
|
|
166
|
+
* A reviewed conversation awaiting promotion into a scenario.
|
|
167
|
+
*
|
|
168
|
+
* Lives under `<skill>/tests/captures/`, NOT in `specification.yaml`. A pending
|
|
169
|
+
* capture is not a test: putting it in the spec would drag it into ship-bar
|
|
170
|
+
* totals, staleness, lift, and stability, and the alternative — a `draft: true`
|
|
171
|
+
* flag every runner, scorer and linter has to honor — is a state that only has
|
|
172
|
+
* to be forgotten once to either corrupt a grade or silently drop a real
|
|
173
|
+
* scenario from a release run.
|
|
174
|
+
*/
|
|
175
|
+
export interface CaptureCaseV1 {
|
|
176
|
+
capture_schema: typeof CAPTURE_SCHEMA_VERSION;
|
|
177
|
+
id: string;
|
|
178
|
+
created: string;
|
|
179
|
+
classification: CaptureClassification;
|
|
180
|
+
/** Sanitized user turns only. Assistant prose is evidence, never an oracle. */
|
|
181
|
+
turns: string[];
|
|
182
|
+
/** Human-written, in their own words. Required — a capture with no expectation is not reviewable. */
|
|
183
|
+
expected_behavior: string;
|
|
184
|
+
/** Editable draft checklist. Offline-derived by default; LLM drafting is opt-in and costs tokens. */
|
|
185
|
+
checklist: string[];
|
|
186
|
+
target: CaptureTarget;
|
|
187
|
+
provenance: CaptureProvenance;
|
|
188
|
+
status: CapturePromotionStatus;
|
|
189
|
+
/** Set once promoted: the scenario id appended to `specification.yaml`. */
|
|
190
|
+
scenario_id?: string;
|
|
191
|
+
/**
|
|
192
|
+
* `covers` refs for this pending case, so `coverage` can park it against the
|
|
193
|
+
* instructions it is about before anyone promotes it.
|
|
194
|
+
*
|
|
195
|
+
* Derived from `target` — the author has already been made to choose which
|
|
196
|
+
* instructions are responsible, and asking again in different words would be
|
|
197
|
+
* asking the same question twice. File granularity, not `#section`: the target
|
|
198
|
+
* choice attributes a file, and inventing a section would be a guess the
|
|
199
|
+
* session cannot support.
|
|
200
|
+
*
|
|
201
|
+
* Written relative to the SPEC dir (`<skill>/tests`), matching how a scenario's
|
|
202
|
+
* own `covers` resolve. `coverage` read this field from the start; nothing ever
|
|
203
|
+
* wrote it, so every pending case parked against nothing and the feature was
|
|
204
|
+
* inert.
|
|
205
|
+
*/
|
|
206
|
+
covers?: string[];
|
|
207
|
+
}
|
|
208
|
+
/**
|
|
209
|
+
* Local-only evidence sidecar for a capture.
|
|
210
|
+
*
|
|
211
|
+
* Written to `<skill>/tests/captures/.local/` and git-ignored by default. It may
|
|
212
|
+
* hold a sanitized assistant excerpt and tool-call summaries to help a human
|
|
213
|
+
* review the capture, and must NEVER hold hidden thinking, complete tool-result
|
|
214
|
+
* bodies, or the effective system prompt.
|
|
215
|
+
*
|
|
216
|
+
* pi's stream carries thinking in `message_end`, `turn_end` AND `agent_end`, so
|
|
217
|
+
* dropping it is an explicit filter at every one of those, not a side effect of
|
|
218
|
+
* reading the convenient field.
|
|
219
|
+
*/
|
|
220
|
+
export interface CaptureEvidenceV1 {
|
|
221
|
+
capture_id: string;
|
|
222
|
+
/** Sanitized, truncated excerpt of the assistant text of every selected turn, joined. */
|
|
223
|
+
assistant_excerpt: string;
|
|
224
|
+
/** Tool name, error state and redacted arguments — no bodies. */
|
|
225
|
+
tool_calls: Array<Pick<TraceToolCall, "name" | "isError"> & {
|
|
226
|
+
args: SanitizedArgs;
|
|
227
|
+
}>;
|
|
228
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Data contracts for the pi-native regression capture program.
|
|
3
|
+
*
|
|
4
|
+
* Types only — no behavior. Phase 0 of
|
|
5
|
+
* `docs/superpowers/plans/2026-08-07-pi-native-regression-capture-program.md`
|
|
6
|
+
* fixes the shapes before any parser, capture UI, or gate evaluator is written,
|
|
7
|
+
* because both shapes get persisted and a persisted shape is expensive to move.
|
|
8
|
+
*
|
|
9
|
+
* Both carry an explicit version. The pi event stream they derive from is not a
|
|
10
|
+
* stable public contract (measured against pi 0.83.0; see
|
|
11
|
+
* `packages/adapters/test/fixtures/pi-json/README.md`), so a reader must be able
|
|
12
|
+
* to tell which pi produced an artifact and refuse one it does not understand
|
|
13
|
+
* rather than silently misreading it.
|
|
14
|
+
*/
|
|
15
|
+
// ---------------------------------------------------------------------------
|
|
16
|
+
// Execution trace
|
|
17
|
+
// ---------------------------------------------------------------------------
|
|
18
|
+
export const EXECUTION_TRACE_VERSION = 2;
|
|
19
|
+
// ---------------------------------------------------------------------------
|
|
20
|
+
// Capture case
|
|
21
|
+
// ---------------------------------------------------------------------------
|
|
22
|
+
export const CAPTURE_SCHEMA_VERSION = 1;
|
|
23
|
+
//# sourceMappingURL=capture-trace-types.js.map
|