@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.
Files changed (46) hide show
  1. package/dist/adapters/types.d.ts +37 -0
  2. package/dist/adjudication.d.ts +210 -0
  3. package/dist/adjudication.js +392 -0
  4. package/dist/affected.d.ts +88 -0
  5. package/dist/affected.js +222 -0
  6. package/dist/capture-trace-types.d.ts +228 -0
  7. package/dist/capture-trace-types.js +23 -0
  8. package/dist/capture.d.ts +193 -0
  9. package/dist/capture.js +344 -0
  10. package/dist/execution-trace.d.ts +61 -0
  11. package/dist/execution-trace.js +299 -0
  12. package/dist/index.d.ts +9 -0
  13. package/dist/index.js +9 -0
  14. package/dist/instruction-coverage.d.ts +106 -0
  15. package/dist/instruction-coverage.js +253 -0
  16. package/dist/journal.d.ts +17 -0
  17. package/dist/lint.d.ts +16 -1
  18. package/dist/lint.js +52 -0
  19. package/dist/regate.js +80 -17
  20. package/dist/regrade.js +17 -3
  21. package/dist/report.d.ts +48 -0
  22. package/dist/report.js +39 -1
  23. package/dist/reps.d.ts +14 -1
  24. package/dist/reps.js +28 -2
  25. package/dist/rescore.js +11 -2
  26. package/dist/results.d.ts +128 -6
  27. package/dist/results.js +155 -6
  28. package/dist/run.d.ts +9 -1
  29. package/dist/run.js +129 -9
  30. package/dist/seeded.d.ts +11 -0
  31. package/dist/seeded.js +31 -7
  32. package/dist/sources.d.ts +26 -0
  33. package/dist/sources.js +82 -3
  34. package/dist/spec-write.d.ts +62 -0
  35. package/dist/spec-write.js +106 -0
  36. package/dist/spec.d.ts +29 -0
  37. package/dist/spec.js +55 -0
  38. package/dist/stability.d.ts +144 -0
  39. package/dist/stability.js +232 -0
  40. package/dist/trace-gates.d.ts +133 -0
  41. package/dist/trace-gates.js +519 -0
  42. package/dist/trends.d.ts +28 -0
  43. package/dist/trends.js +76 -61
  44. package/dist/workspace.d.ts +36 -0
  45. package/dist/workspace.js +61 -0
  46. 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;
@@ -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