@skill-harness/core 0.17.0 → 0.19.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/README.md +1 -2
- package/dist/adapters/types.d.ts +2 -12
- package/dist/capture-trace-types.d.ts +8 -102
- package/dist/capture-trace-types.js +0 -4
- package/dist/command-cost.d.ts +1 -1
- package/dist/command-cost.js +0 -5
- package/dist/execution-trace.d.ts +0 -16
- package/dist/execution-trace.js +31 -42
- package/dist/index.d.ts +3 -23
- package/dist/index.js +3 -23
- package/dist/instruction-coverage.d.ts +0 -7
- package/dist/instruction-coverage.js +5 -7
- package/dist/journal.d.ts +0 -8
- package/dist/metrics.d.ts +19 -0
- package/dist/metrics.js +28 -0
- package/dist/redaction.d.ts +6 -0
- package/dist/redaction.js +55 -0
- package/dist/regate.js +6 -71
- package/dist/regrade.d.ts +1 -3
- package/dist/regrade.js +1 -21
- package/dist/report.d.ts +1 -15
- package/dist/report.js +1 -13
- package/dist/reps.d.ts +1 -2
- package/dist/reps.js +33 -10
- package/dist/results.d.ts +10 -9
- package/dist/results.js +3 -19
- package/dist/run.js +15 -74
- package/dist/seeded.d.ts +1 -7
- package/dist/seeded.js +5 -11
- package/dist/sources.js +4 -9
- package/dist/spec.d.ts +0 -13
- package/dist/spec.js +6 -48
- package/dist/trajectory-events.d.ts +58 -0
- package/dist/trajectory-events.js +119 -0
- package/package.json +1 -1
- package/schemas/results-v2.schema.json +394 -75
- package/schemas/results-v3.schema.json +18 -0
- package/schemas/specification-v1.schema.json +128 -174
- package/dist/adjudication.d.ts +0 -219
- package/dist/adjudication.js +0 -422
- package/dist/adoption.d.ts +0 -128
- package/dist/adoption.js +0 -102
- package/dist/affected.d.ts +0 -88
- package/dist/affected.js +0 -222
- package/dist/capture.d.ts +0 -193
- package/dist/capture.js +0 -344
- package/dist/comparison.d.ts +0 -97
- package/dist/comparison.js +0 -189
- package/dist/factory-calibration.d.ts +0 -63
- package/dist/factory-calibration.js +0 -115
- package/dist/intervention-results.d.ts +0 -6
- package/dist/intervention-results.js +0 -52
- package/dist/intervention.d.ts +0 -123
- package/dist/intervention.js +0 -218
- package/dist/investigation.d.ts +0 -94
- package/dist/investigation.js +0 -148
- package/dist/judge-agreement.d.ts +0 -19
- package/dist/judge-agreement.js +0 -31
- package/dist/qualification-capture.d.ts +0 -44
- package/dist/qualification-capture.js +0 -163
- package/dist/qualification-config.d.ts +0 -233
- package/dist/qualification-config.js +0 -694
- package/dist/qualification-lock.d.ts +0 -15
- package/dist/qualification-lock.js +0 -87
- package/dist/qualification-oauth-directory.d.ts +0 -79
- package/dist/qualification-oauth-directory.js +0 -444
- package/dist/qualification-panel-store.d.ts +0 -14
- package/dist/qualification-panel-store.js +0 -188
- package/dist/qualification-panels.d.ts +0 -99
- package/dist/qualification-panels.js +0 -91
- package/dist/qualification-process.d.ts +0 -13
- package/dist/qualification-process.js +0 -190
- package/dist/qualification-runner.d.ts +0 -203
- package/dist/qualification-runner.js +0 -2037
- package/dist/qualification-store.d.ts +0 -187
- package/dist/qualification-store.js +0 -833
- package/dist/qualification-validation.d.ts +0 -12
- package/dist/qualification-validation.js +0 -8
- package/dist/screen.d.ts +0 -31
- package/dist/screen.js +0 -105
- package/dist/trajectory-gates.d.ts +0 -137
- package/dist/trajectory-gates.js +0 -576
- package/dist/trusted-host-supervision.d.ts +0 -25
- package/dist/trusted-host-supervision.js +0 -94
- package/dist/work-capture-schema.d.ts +0 -38
- package/dist/work-capture-schema.js +0 -33
- package/dist/work-capture.d.ts +0 -43
- package/dist/work-capture.js +0 -81
- package/dist/work-signals.d.ts +0 -55
- package/dist/work-signals.js +0 -71
- package/schemas/qualification-cell-result-v1.schema.json +0 -32
- package/schemas/qualification-config-v1.schema.json +0 -244
- package/schemas/qualification-invocation-request-v1.schema.json +0 -59
- package/schemas/qualification-judge-panel-result-v1.schema.json +0 -42
- package/schemas/trajectory-event-v1.schema.json +0 -80
package/dist/affected.d.ts
DELETED
|
@@ -1,88 +0,0 @@
|
|
|
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
DELETED
|
@@ -1,222 +0,0 @@
|
|
|
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
|
package/dist/capture.d.ts
DELETED
|
@@ -1,193 +0,0 @@
|
|
|
1
|
-
import type { CaptureCaseV1, CaptureClassification, CaptureTarget, SanitizedArgs } from "./capture-trace-types.js";
|
|
2
|
-
/**
|
|
3
|
-
* Projection and sanitization for `/skill-harness capture` — turning a live pi
|
|
4
|
-
* conversation into a reviewable regression case.
|
|
5
|
-
*
|
|
6
|
-
* Everything here is pure: entries in, plain data out. The pi extension owns the
|
|
7
|
-
* UI and the filesystem; this module owns the rules that decide what a capture
|
|
8
|
-
* is allowed to contain. That split is deliberate — the privacy rules are the
|
|
9
|
-
* part that must be unit-testable without a running agent.
|
|
10
|
-
*
|
|
11
|
-
* Nothing in here calls a model. Checklist drafting is offline by default; any
|
|
12
|
-
* LLM assistance is the extension's business and requires its own confirmation.
|
|
13
|
-
*/
|
|
14
|
-
export interface SessionContentBlock {
|
|
15
|
-
type: string;
|
|
16
|
-
text?: string;
|
|
17
|
-
thinking?: string;
|
|
18
|
-
name?: string;
|
|
19
|
-
arguments?: unknown;
|
|
20
|
-
[k: string]: unknown;
|
|
21
|
-
}
|
|
22
|
-
export interface SessionMessage {
|
|
23
|
-
role: string;
|
|
24
|
-
content?: SessionContentBlock[];
|
|
25
|
-
toolName?: string;
|
|
26
|
-
isError?: boolean;
|
|
27
|
-
[k: string]: unknown;
|
|
28
|
-
}
|
|
29
|
-
/**
|
|
30
|
-
* One entry from a pi session file.
|
|
31
|
-
*
|
|
32
|
-
* Entries form a linked list through `parentId`, NOT a flat log: forking and
|
|
33
|
-
* rewinding leave sibling chains in the same file. Anything that reads a session
|
|
34
|
-
* without resolving the branch first will happily mix two alternate histories
|
|
35
|
-
* into one "conversation".
|
|
36
|
-
*/
|
|
37
|
-
export interface SessionEntry {
|
|
38
|
-
type: string;
|
|
39
|
-
id?: string;
|
|
40
|
-
parentId?: string | null;
|
|
41
|
-
timestamp?: string;
|
|
42
|
-
message?: SessionMessage;
|
|
43
|
-
[k: string]: unknown;
|
|
44
|
-
}
|
|
45
|
-
/**
|
|
46
|
-
* The chain of entries ending at `leafId`, in root→leaf order.
|
|
47
|
-
*
|
|
48
|
-
* With no `leafId`, the leaf is the last entry in file order — which is what
|
|
49
|
-
* "the conversation I am currently in" means. Entries with no `id` (the
|
|
50
|
-
* `session` header) are not part of the chain and are dropped.
|
|
51
|
-
*
|
|
52
|
-
* Cycles cannot occur in a well-formed file, but a corrupted one must not hang
|
|
53
|
-
* the agent, so the walk is bounded by the entry count.
|
|
54
|
-
*/
|
|
55
|
-
export declare function activeBranch(entries: SessionEntry[], leafId?: string): SessionEntry[];
|
|
56
|
-
/** A tool call as it is summarized for review — never the result body. */
|
|
57
|
-
export interface ToolCallSummary {
|
|
58
|
-
name: string;
|
|
59
|
-
args: SanitizedArgs;
|
|
60
|
-
isError: boolean;
|
|
61
|
-
/**
|
|
62
|
-
* pi's `toolCallId`, when the entry carried one.
|
|
63
|
-
*
|
|
64
|
-
* Correlation key for the matching result. Parallel tool calls complete out of
|
|
65
|
-
* issue order (measured on pi 0.83.0), so pairing by position is wrong the
|
|
66
|
-
* moment a conversation uses parallelism — which is exactly the kind of
|
|
67
|
-
* conversation worth capturing.
|
|
68
|
-
*/
|
|
69
|
-
id?: string;
|
|
70
|
-
/** Byte length of the result content, when the result was present. */
|
|
71
|
-
resultBytes?: number;
|
|
72
|
-
/** SHA-256 of the result content, so identity is checkable without the body. */
|
|
73
|
-
resultSha256?: string;
|
|
74
|
-
}
|
|
75
|
-
/**
|
|
76
|
-
* One user message and everything the agent did in reply, up to the next user
|
|
77
|
-
* message.
|
|
78
|
-
*
|
|
79
|
-
* This is the selection unit for capture. pi exposes no public API for arbitrary
|
|
80
|
-
* mouse-highlighted transcript text, so turn-level contiguous selection is the
|
|
81
|
-
* supported contract — not a limitation we can quietly widen later.
|
|
82
|
-
*/
|
|
83
|
-
export interface LogicalTurn {
|
|
84
|
-
index: number;
|
|
85
|
-
user: string;
|
|
86
|
-
/** Assistant text with thinking removed. Evidence for the human, never an oracle. */
|
|
87
|
-
assistantText: string;
|
|
88
|
-
toolCalls: ToolCallSummary[];
|
|
89
|
-
/** Entry ids covered by this turn. Local-only — never written to a committed capture. */
|
|
90
|
-
entryIds: string[];
|
|
91
|
-
}
|
|
92
|
-
/** Extract visible text from content blocks. Thinking is dropped unconditionally. */
|
|
93
|
-
export declare function visibleText(blocks: SessionContentBlock[] | undefined): string;
|
|
94
|
-
/**
|
|
95
|
-
* Group an already-branch-resolved entry list into logical turns.
|
|
96
|
-
*
|
|
97
|
-
* Entries that are not messages (`model_change`, `thinking_level_change`,
|
|
98
|
-
* compaction markers, custom extension entries) are skipped rather than
|
|
99
|
-
* guessed at: an unknown entry type is not evidence about the conversation.
|
|
100
|
-
* Anything before the first user message is dropped — it belongs to no turn.
|
|
101
|
-
*/
|
|
102
|
-
export declare function projectTurns(entries: SessionEntry[], homeDir?: string): LogicalTurn[];
|
|
103
|
-
/** Values longer than this are truncated rather than persisted whole. */
|
|
104
|
-
export declare const MAX_VALUE_CHARS = 2000;
|
|
105
|
-
/**
|
|
106
|
-
* Redact secrets and machine paths from a free-text string.
|
|
107
|
-
*
|
|
108
|
-
* Home directories are replaced because a capture is meant to be committed, and
|
|
109
|
-
* `/home/<name>/…` identifies a person as surely as a token identifies an
|
|
110
|
-
* account. `homeDir` is a parameter rather than a read of `process.env.HOME` so
|
|
111
|
-
* the behavior is testable and does not differ between a developer's machine
|
|
112
|
-
* and CI.
|
|
113
|
-
*/
|
|
114
|
-
export declare function redactText(input: string, homeDir?: string): string;
|
|
115
|
-
/** Truncate with an explicit marker — silent truncation reads as complete evidence. */
|
|
116
|
-
export declare function truncate(input: string, max?: number): string;
|
|
117
|
-
/**
|
|
118
|
-
* Sanitize tool-call arguments: drop secret-named values, redact secret-shaped
|
|
119
|
-
* ones, truncate the oversized, and refuse to recurse without bound.
|
|
120
|
-
*/
|
|
121
|
-
export declare function redactArgs(args: unknown, homeDir?: string, depth?: number): SanitizedArgs;
|
|
122
|
-
/** Stable, sortable, collision-resistant enough for a per-skill directory. */
|
|
123
|
-
export declare function captureId(seed: string, existing?: readonly string[]): string;
|
|
124
|
-
export interface BuildCaptureOptions {
|
|
125
|
-
turns: LogicalTurn[];
|
|
126
|
-
/** Inclusive selected range over `turns`. */
|
|
127
|
-
range: {
|
|
128
|
-
start: number;
|
|
129
|
-
end: number;
|
|
130
|
-
};
|
|
131
|
-
classification: CaptureClassification;
|
|
132
|
-
expectedBehavior: string;
|
|
133
|
-
checklist: string[];
|
|
134
|
-
target: CaptureTarget;
|
|
135
|
-
sessionPath: string;
|
|
136
|
-
created: string;
|
|
137
|
-
subject?: {
|
|
138
|
-
provider: string;
|
|
139
|
-
model: string;
|
|
140
|
-
};
|
|
141
|
-
gitCommit?: string;
|
|
142
|
-
gitDirty?: boolean;
|
|
143
|
-
homeDir?: string;
|
|
144
|
-
existingIds?: readonly string[];
|
|
145
|
-
}
|
|
146
|
-
/**
|
|
147
|
-
* Assemble a reviewed capture case.
|
|
148
|
-
*
|
|
149
|
-
* Only the USER turns are carried into the case: they are the stimulus, and they
|
|
150
|
-
* are the only part that can be replayed. The assistant's historical prose is
|
|
151
|
-
* evidence for the human writing the expectation, never an exact-output oracle —
|
|
152
|
-
* so it lives in the git-ignored local sidecar, not here.
|
|
153
|
-
*
|
|
154
|
-
* The session path is hashed rather than stored: an absolute path names a
|
|
155
|
-
* machine and a user, and a hash is enough to recognize the same session again.
|
|
156
|
-
*/
|
|
157
|
-
export declare function buildCaptureCase(opts: BuildCaptureOptions): CaptureCaseV1;
|
|
158
|
-
/**
|
|
159
|
-
* Project a reviewed capture into a scenario object for `appendScenario`.
|
|
160
|
-
*
|
|
161
|
-
* Deliberately minimal: id, title, turns, checklist. No `critical`, no gates, no
|
|
162
|
-
* fixture. Promotion is the moment a human takes responsibility for a test, and
|
|
163
|
-
* a capture cannot know whether the behavior it saw is ship-blocking — guessing
|
|
164
|
-
* `critical: true` here would let a captured one-off silently gate a release.
|
|
165
|
-
*/
|
|
166
|
-
export declare function captureToScenario(capture: CaptureCaseV1, scenarioId: string, title: string): Record<string, unknown>;
|
|
167
|
-
/**
|
|
168
|
-
* Draft an orchestration assertion from subagent calls seen in the captured range.
|
|
169
|
-
*
|
|
170
|
-
* Returns null when the range contains none, so the capture UI can skip the
|
|
171
|
-
* question entirely rather than offering an empty form.
|
|
172
|
-
*
|
|
173
|
-
* Deliberately proposes only `agent` and `count` — never `task_contains`. The
|
|
174
|
-
* task text that happened to be sent is not the task text that is *required*;
|
|
175
|
-
* turning one observed handoff into a required substring would manufacture a
|
|
176
|
-
* brittle assertion the author never reasoned about. Required context is a
|
|
177
|
-
* judgement, so the UI asks.
|
|
178
|
-
*/
|
|
179
|
-
export declare function draftSubagentAssertion(turns: LogicalTurn[], toolNames?: readonly string[]): {
|
|
180
|
-
tool: string;
|
|
181
|
-
agent: string;
|
|
182
|
-
count: {
|
|
183
|
-
min: number;
|
|
184
|
-
};
|
|
185
|
-
} | null;
|
|
186
|
-
/**
|
|
187
|
-
* Offline first-draft checklist from the human's expectation.
|
|
188
|
-
*
|
|
189
|
-
* Splits on sentence and bullet boundaries. This is a typing shortcut for the
|
|
190
|
-
* editor step, not an understanding of the text — the UI always opens the result
|
|
191
|
-
* for correction, and the plan makes LLM drafting a separate, paid, opt-in path.
|
|
192
|
-
*/
|
|
193
|
-
export declare function draftChecklist(expectedBehavior: string): string[];
|