@skill-harness/core 0.16.0 → 0.18.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 (93) hide show
  1. package/README.md +1 -2
  2. package/dist/adapters/types.d.ts +0 -12
  3. package/dist/capture-trace-types.d.ts +0 -96
  4. package/dist/capture-trace-types.js +0 -4
  5. package/dist/command-cost.d.ts +1 -1
  6. package/dist/command-cost.js +0 -5
  7. package/dist/execution-trace.js +1 -1
  8. package/dist/index.d.ts +3 -23
  9. package/dist/index.js +3 -23
  10. package/dist/instruction-coverage.d.ts +0 -7
  11. package/dist/instruction-coverage.js +5 -7
  12. package/dist/journal.d.ts +0 -8
  13. package/dist/metrics.d.ts +18 -0
  14. package/dist/metrics.js +28 -0
  15. package/dist/redaction.d.ts +6 -0
  16. package/dist/redaction.js +55 -0
  17. package/dist/regate.js +6 -71
  18. package/dist/regrade.d.ts +1 -3
  19. package/dist/regrade.js +1 -21
  20. package/dist/report.d.ts +1 -15
  21. package/dist/report.js +1 -13
  22. package/dist/reps.d.ts +1 -2
  23. package/dist/reps.js +0 -2
  24. package/dist/results.d.ts +0 -9
  25. package/dist/results.js +2 -19
  26. package/dist/run.js +11 -72
  27. package/dist/seeded.d.ts +1 -7
  28. package/dist/seeded.js +5 -11
  29. package/dist/sources.js +4 -9
  30. package/dist/spec.d.ts +0 -13
  31. package/dist/spec.js +6 -48
  32. package/dist/trajectory-events.d.ts +58 -0
  33. package/dist/trajectory-events.js +119 -0
  34. package/package.json +1 -1
  35. package/schemas/results-v2.schema.json +376 -75
  36. package/schemas/specification-v1.schema.json +128 -174
  37. package/dist/adjudication.d.ts +0 -219
  38. package/dist/adjudication.js +0 -422
  39. package/dist/adoption.d.ts +0 -128
  40. package/dist/adoption.js +0 -102
  41. package/dist/affected.d.ts +0 -88
  42. package/dist/affected.js +0 -222
  43. package/dist/capture.d.ts +0 -193
  44. package/dist/capture.js +0 -344
  45. package/dist/comparison.d.ts +0 -97
  46. package/dist/comparison.js +0 -189
  47. package/dist/factory-calibration.d.ts +0 -63
  48. package/dist/factory-calibration.js +0 -115
  49. package/dist/intervention-results.d.ts +0 -6
  50. package/dist/intervention-results.js +0 -52
  51. package/dist/intervention.d.ts +0 -123
  52. package/dist/intervention.js +0 -218
  53. package/dist/investigation.d.ts +0 -94
  54. package/dist/investigation.js +0 -148
  55. package/dist/judge-agreement.d.ts +0 -19
  56. package/dist/judge-agreement.js +0 -31
  57. package/dist/qualification-capture.d.ts +0 -44
  58. package/dist/qualification-capture.js +0 -163
  59. package/dist/qualification-config.d.ts +0 -233
  60. package/dist/qualification-config.js +0 -694
  61. package/dist/qualification-lock.d.ts +0 -15
  62. package/dist/qualification-lock.js +0 -87
  63. package/dist/qualification-oauth-directory.d.ts +0 -79
  64. package/dist/qualification-oauth-directory.js +0 -444
  65. package/dist/qualification-panel-store.d.ts +0 -14
  66. package/dist/qualification-panel-store.js +0 -188
  67. package/dist/qualification-panels.d.ts +0 -99
  68. package/dist/qualification-panels.js +0 -91
  69. package/dist/qualification-process.d.ts +0 -13
  70. package/dist/qualification-process.js +0 -190
  71. package/dist/qualification-runner.d.ts +0 -203
  72. package/dist/qualification-runner.js +0 -2037
  73. package/dist/qualification-store.d.ts +0 -187
  74. package/dist/qualification-store.js +0 -833
  75. package/dist/qualification-validation.d.ts +0 -12
  76. package/dist/qualification-validation.js +0 -8
  77. package/dist/screen.d.ts +0 -31
  78. package/dist/screen.js +0 -105
  79. package/dist/trajectory-gates.d.ts +0 -137
  80. package/dist/trajectory-gates.js +0 -576
  81. package/dist/trusted-host-supervision.d.ts +0 -25
  82. package/dist/trusted-host-supervision.js +0 -94
  83. package/dist/work-capture-schema.d.ts +0 -38
  84. package/dist/work-capture-schema.js +0 -33
  85. package/dist/work-capture.d.ts +0 -43
  86. package/dist/work-capture.js +0 -81
  87. package/dist/work-signals.d.ts +0 -55
  88. package/dist/work-signals.js +0 -71
  89. package/schemas/qualification-cell-result-v1.schema.json +0 -32
  90. package/schemas/qualification-config-v1.schema.json +0 -244
  91. package/schemas/qualification-invocation-request-v1.schema.json +0 -59
  92. package/schemas/qualification-judge-panel-result-v1.schema.json +0 -42
  93. package/schemas/trajectory-event-v1.schema.json +0 -80
@@ -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[];