@bjoernrochel/sevro 0.1.0-rc.2 → 0.1.0-rc.21

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 (243) hide show
  1. package/CONTRIBUTING.md +170 -0
  2. package/LICENSE +63 -136
  3. package/README.md +83 -66
  4. package/docs/README.md +44 -0
  5. package/docs/acknowledgements.md +14 -0
  6. package/docs/architecture.md +146 -0
  7. package/docs/assets/README.md +27 -0
  8. package/docs/assets/sevro-logo.png +0 -0
  9. package/docs/assets/sevro-symbol.svg +1 -0
  10. package/docs/assets/sevro-wordmark.svg +1 -0
  11. package/docs/builtin-graders-v1.md +41 -13
  12. package/docs/codex-host.md +184 -52
  13. package/docs/contracts.md +19 -0
  14. package/docs/creating-cases.md +139 -0
  15. package/docs/development-cli.md +190 -30
  16. package/docs/documentation-quality.md +61 -0
  17. package/docs/evaluation-workflow.md +110 -0
  18. package/docs/evidence/code-mode-controls-71.md +64 -0
  19. package/docs/evidence/code-mode-programs-85.md +64 -0
  20. package/docs/evidence/codex-state-73/README.md +204 -0
  21. package/docs/evidence/codex-state-73/darrow-migration.patch +182 -0
  22. package/docs/evidence/codex-state-73/results.json +661 -0
  23. package/docs/evidence/local-http-74.md +104 -0
  24. package/docs/evidence/runtime-startup-30/README.md +185 -0
  25. package/docs/evidence/runtime-startup-30/after.json +319 -0
  26. package/docs/evidence/runtime-startup-30/baseline-timeout.jsonl +2 -0
  27. package/docs/evidence/runtime-startup-30/before.json +403 -0
  28. package/docs/evidence/runtime-startup-30/consumer-smoke.json +63 -0
  29. package/docs/evidence/runtime-startup-30/full-policy-diagnostic.jsonl +3 -0
  30. package/docs/evidence/runtime-startup-30/minimal-policy-diagnostic.jsonl +3 -0
  31. package/docs/evidence/runtime-startup-30/plugin-template-assessment.json +20 -0
  32. package/docs/evidence/runtime-startup-30/seedless-timeout.jsonl +2 -0
  33. package/docs/evidence/skill-reads-70/results.json +823 -0
  34. package/docs/evidence.md +21 -0
  35. package/docs/extension-protocol-v1.md +50 -16
  36. package/docs/fixture-cleanup-validation.md +3 -0
  37. package/docs/getting-started.md +102 -0
  38. package/docs/guide-evaluation.md +89 -0
  39. package/docs/guide-verification.json +239 -0
  40. package/docs/identity-v1.md +35 -1
  41. package/docs/installing.md +88 -0
  42. package/docs/licenses/BUSL-1.1-0.1.0-rc.2.txt +159 -0
  43. package/docs/licensing.md +42 -0
  44. package/docs/native-controls.md +76 -5
  45. package/docs/native-hosts.md +228 -0
  46. package/docs/reading-results.md +141 -0
  47. package/docs/recipes.md +489 -0
  48. package/docs/releases.md +81 -40
  49. package/docs/report-v1.md +29 -1
  50. package/docs/results-v1.md +180 -0
  51. package/docs/run-settings-v1.md +147 -0
  52. package/docs/running-evaluations.md +31 -0
  53. package/docs/runtime-v1.md +614 -0
  54. package/docs/specs/repository-guide.md +58 -0
  55. package/docs/troubleshooting.md +30 -0
  56. package/docs/typescript-quality.md +234 -0
  57. package/docs/usability-study.md +114 -0
  58. package/examples/basic/host.ts +2 -2
  59. package/examples/recipes/candidate.ts +26 -0
  60. package/examples/recipes/cases/coding-change.json +21 -0
  61. package/examples/recipes/cases/host-tools.json +21 -0
  62. package/examples/recipes/cases/mise.json +21 -0
  63. package/examples/recipes/cases/node.json +21 -0
  64. package/examples/recipes/cases/python-uv.json +21 -0
  65. package/examples/recipes/cases/semantic.json +17 -0
  66. package/examples/recipes/coding-change/check.ts +23 -0
  67. package/examples/recipes/extension-artifacts.ts +65 -0
  68. package/examples/recipes/extension-methods.ts +174 -0
  69. package/examples/recipes/extension.ts +37 -0
  70. package/examples/recipes/repository-skill/.agents/skills/startup-guide/SKILL.md +7 -0
  71. package/examples/recipes/repository-skill/.claude/skills/startup-guide/SKILL.md +7 -0
  72. package/examples/recipes/repository-skill/README.md +3 -0
  73. package/examples/recipes/run-inputs.ts +188 -0
  74. package/examples/recipes/run-options.ts +131 -0
  75. package/examples/recipes/run.ts +63 -0
  76. package/examples/recipes/runtime/empty.json +1 -0
  77. package/examples/recipes/runtime/goals.json +1 -0
  78. package/examples/recipes/runtime/mise.json +6 -0
  79. package/examples/recipes/runtime/path.json +1 -0
  80. package/examples/recipes/runtime/plugins.json +4 -0
  81. package/examples/recipes/runtime/python-uv.json +10 -0
  82. package/examples/recipes/semantic-judge.ts +29 -0
  83. package/examples/templates/coding/adapter.ts +17 -0
  84. package/examples/templates/coding/case.json +15 -0
  85. package/examples/templates/repository-skill/.agents/skills/startup-guide/SKILL.md +6 -0
  86. package/examples/templates/repository-skill/extension-data.ts +64 -0
  87. package/examples/templates/repository-skill/extension.ts +31 -0
  88. package/examples/templates/response/adapter.ts +12 -0
  89. package/examples/templates/response/case.json +13 -0
  90. package/package.json +39 -6
  91. package/schemas/report-v1.schema.json +9 -0
  92. package/schemas/run-evidence-v1.schema.json +2 -1
  93. package/schemas/runtime-v1.schema.json +193 -0
  94. package/src/advisory.ts +10 -5
  95. package/src/candidate-artifacts.ts +208 -0
  96. package/src/candidate-codex-state.ts +185 -0
  97. package/src/case-validation.ts +105 -0
  98. package/src/cli/cancellation.ts +20 -0
  99. package/src/cli/command.ts +81 -0
  100. package/src/cli/completion.ts +15 -0
  101. package/src/cli/demo.ts +59 -0
  102. package/src/cli/evaluation.ts +55 -0
  103. package/src/cli/extension.ts +79 -0
  104. package/src/cli/help.ts +222 -0
  105. package/src/cli/hosts.ts +59 -0
  106. package/src/cli/init-settings.ts +30 -0
  107. package/src/cli/init.ts +111 -0
  108. package/src/cli/invocation.ts +627 -0
  109. package/src/cli/loading.ts +151 -0
  110. package/src/cli/preparation.ts +163 -0
  111. package/src/cli/presentation.ts +91 -0
  112. package/src/cli/progress.ts +6 -0
  113. package/src/cli/run-manifest.ts +133 -0
  114. package/src/cli/run-options.ts +101 -0
  115. package/src/cli/run-overrides.ts +83 -0
  116. package/src/cli/settings.ts +160 -0
  117. package/src/cli/summary.ts +43 -0
  118. package/src/cli.ts +13 -843
  119. package/src/contract-types.ts +94 -0
  120. package/src/engine.ts +19 -2373
  121. package/src/evaluation/advisory.ts +150 -0
  122. package/src/evaluation/check-plan.ts +510 -0
  123. package/src/evaluation/collection.ts +88 -0
  124. package/src/evaluation/context.ts +10 -0
  125. package/src/evaluation/grading-host.ts +39 -0
  126. package/src/evaluation/grading.ts +258 -0
  127. package/src/evaluation/invocation-plan.ts +548 -0
  128. package/src/evaluation/plan.ts +736 -0
  129. package/src/evaluation/progress.ts +19 -0
  130. package/src/evaluation/resources.ts +241 -0
  131. package/src/evaluation/run.ts +206 -0
  132. package/src/{trial-scheduler.ts → evaluation/scheduler.ts} +3 -1
  133. package/src/evaluation/semantic.ts +312 -0
  134. package/src/evaluation/trial.ts +866 -0
  135. package/src/evaluation/types.ts +207 -0
  136. package/src/evidence/host-results.ts +92 -0
  137. package/src/evidence/retained-artifacts.ts +18 -0
  138. package/src/evidence/trial-writer.ts +203 -0
  139. package/src/evidence/usage.ts +17 -0
  140. package/src/extension-client.ts +228 -148
  141. package/src/extension-session.ts +326 -195
  142. package/src/fixtures/advisory.ts +338 -0
  143. package/src/fixtures/bin.ts +57 -0
  144. package/src/{fixture-cleanup.ts → fixtures/cleanup.ts} +13 -7
  145. package/src/fixtures/create.ts +187 -0
  146. package/src/fixtures/generated.ts +300 -0
  147. package/src/fixtures/git-hooks.ts +24 -0
  148. package/src/fixtures/preparation.ts +270 -0
  149. package/src/{repository-fixture.ts → fixtures/repository.ts} +207 -85
  150. package/src/fixtures/setup.ts +224 -0
  151. package/src/fixtures/tools.ts +88 -0
  152. package/src/fixtures/workspace-state.ts +212 -0
  153. package/src/generated/cli-result.cjs +2 -0
  154. package/src/generated/cli-result.d.cts +3 -0
  155. package/src/generated/extension.cjs +2 -0
  156. package/src/generated/extension.d.cts +3 -0
  157. package/src/generated/report.cjs +2 -0
  158. package/src/generated/report.d.cts +3 -0
  159. package/src/generated/run-evidence.cjs +2 -0
  160. package/src/generated/run-evidence.d.cts +3 -0
  161. package/src/generated/runtime.cjs +2 -0
  162. package/src/generated/runtime.d.cts +3 -0
  163. package/src/graders/artifact.ts +56 -23
  164. package/src/graders/git-head.ts +79 -75
  165. package/src/graders/output.ts +222 -140
  166. package/src/graders/semantic.ts +102 -43
  167. package/src/graders/shell.ts +379 -207
  168. package/src/host-diagnostic-redaction.ts +52 -0
  169. package/src/host-diagnostics.ts +319 -0
  170. package/src/hosts/claude-continuation.ts +63 -18
  171. package/src/hosts/claude-credential.ts +43 -62
  172. package/src/hosts/claude-events.ts +86 -51
  173. package/src/hosts/claude-keychain.ts +59 -0
  174. package/src/hosts/claude-native-goal.ts +57 -33
  175. package/src/hosts/claude-process.ts +111 -0
  176. package/src/hosts/claude-repository-invocation.ts +208 -117
  177. package/src/hosts/claude-settings.ts +240 -21
  178. package/src/hosts/claude-skill-activations.ts +166 -0
  179. package/src/hosts/claude-state.ts +403 -0
  180. package/src/hosts/claude-tool-calls.ts +171 -106
  181. package/src/hosts/claude.ts +561 -451
  182. package/src/hosts/codex-app-server.ts +326 -178
  183. package/src/hosts/codex-configuration.ts +44 -18
  184. package/src/hosts/codex-events.ts +97 -60
  185. package/src/hosts/codex-exec-controls.ts +516 -0
  186. package/src/hosts/codex-invocation.ts +70 -0
  187. package/src/hosts/codex-marketplace.ts +283 -0
  188. package/src/hosts/codex-native-calls.ts +546 -500
  189. package/src/hosts/codex-process.ts +202 -0
  190. package/src/hosts/codex-profile.ts +257 -28
  191. package/src/hosts/codex-session-files.ts +106 -0
  192. package/src/hosts/codex-state.ts +492 -0
  193. package/src/hosts/codex.ts +783 -832
  194. package/src/hosts/isolation-common.ts +94 -0
  195. package/src/hosts/isolation-roots.ts +54 -27
  196. package/src/hosts/isolation.ts +10 -0
  197. package/src/hosts/linux-sandbox.ts +159 -0
  198. package/src/hosts/mac-sandbox.ts +187 -61
  199. package/src/hosts/native-controls.ts +152 -62
  200. package/src/hosts/process-lifecycle.ts +37 -0
  201. package/src/hosts/protected-worktrees.ts +51 -19
  202. package/src/hosts/skill-hook-catalog.ts +131 -0
  203. package/src/hosts/skill-hook-handler.ts +374 -0
  204. package/src/hosts/skill-hooks.ts +580 -0
  205. package/src/hosts/workspace-fingerprint.ts +55 -35
  206. package/src/identity.ts +102 -48
  207. package/src/instrumentation.ts +109 -48
  208. package/src/native-transcript-state.ts +131 -0
  209. package/src/native-transcript-view.ts +90 -0
  210. package/src/native-transcripts.ts +286 -0
  211. package/src/process-lifecycle.ts +150 -0
  212. package/src/provenance.ts +161 -81
  213. package/src/prune.ts +215 -0
  214. package/src/report.ts +172 -140
  215. package/src/resolved-case.ts +33 -0
  216. package/src/results-lock.ts +29 -0
  217. package/src/results.ts +133 -86
  218. package/src/run-owner.ts +154 -63
  219. package/src/runtime/config.ts +456 -0
  220. package/src/runtime/discovery-apple.ts +90 -0
  221. package/src/runtime/discovery-providers.ts +63 -0
  222. package/src/runtime/discovery.ts +70 -0
  223. package/src/runtime/hook-processes.ts +97 -0
  224. package/src/runtime/hooks.ts +476 -0
  225. package/src/runtime/paths.ts +313 -0
  226. package/src/runtime/query.ts +85 -0
  227. package/src/runtime/seeds.ts +134 -0
  228. package/src/runtime/shared-cache.ts +143 -0
  229. package/src/runtime/state.ts +143 -0
  230. package/src/schema-types.ts +198 -0
  231. package/src/schema.ts +15 -19
  232. package/src/transcript-retention.ts +146 -0
  233. package/src/value-guards.ts +20 -0
  234. package/src/advisory-fixture.ts +0 -282
  235. package/src/fixture-bin.ts +0 -87
  236. package/src/fixture-setup.ts +0 -163
  237. package/src/generated-fixture.ts +0 -220
  238. package/src/git-hooks.ts +0 -63
  239. package/src/hosts/claude-nested-skills.ts +0 -218
  240. package/src/hosts/codex-command-output.ts +0 -399
  241. package/src/hosts/codex-executor-command.ts +0 -195
  242. package/src/hosts/codex-skill-reads.ts +0 -391
  243. package/src/preparation.ts +0 -211
@@ -0,0 +1,374 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { open, readFile, stat, writeFile } from "node:fs/promises";
3
+ import { join } from "node:path";
4
+ import { isRecord } from "../value-guards";
5
+
6
+ const MAX_INPUT_BYTES = 8 * 1024 * 1024;
7
+ const MAX_RECEIPT_BYTES = 4 * 1024 * 1024;
8
+
9
+ export interface HookSkill {
10
+ invocation: string;
11
+ sha256: string;
12
+ body: string;
13
+ }
14
+ export interface SkillHookConfiguration {
15
+ host: "codex";
16
+ receipts: string;
17
+ skills: HookSkill[];
18
+ catalogComplete: boolean;
19
+ }
20
+ export interface SkillHookReceipt {
21
+ event: string;
22
+ sessionId: string | null;
23
+ turnId: string | null;
24
+ toolUseId: string | null;
25
+ toolName: string | null;
26
+ invocations: string[];
27
+ ambiguousInvocations: string[];
28
+ bodyRanges?: SkillBodyRanges[];
29
+ partial: boolean;
30
+ }
31
+ export interface SkillBodyRanges {
32
+ invocation: string;
33
+ sha256: string;
34
+ ranges: [number, number][];
35
+ }
36
+
37
+ const MIN_PAGE_CHARACTERS = 32;
38
+ interface MatchBudget {
39
+ comparisons: number;
40
+ anchors: number;
41
+ }
42
+
43
+ function spend(budget: MatchBudget, count: number) {
44
+ budget.comparisons -= count;
45
+ if (budget.comparisons < 0)
46
+ throw new Error("skill page matching exceeds limit");
47
+ }
48
+
49
+ function spendAnchor(budget: MatchBudget) {
50
+ budget.anchors--;
51
+ if (budget.anchors < 0) throw new Error("skill page anchors exceed limit");
52
+ }
53
+
54
+ function matchingLength(
55
+ body: string,
56
+ text: string,
57
+ bodyStart: number,
58
+ textStart: number,
59
+ direction: number,
60
+ budget: MatchBudget,
61
+ ): number {
62
+ const available =
63
+ direction === 1
64
+ ? Math.min(body.length - bodyStart, text.length - textStart)
65
+ : Math.min(bodyStart + 1, textStart + 1);
66
+ for (let length = 0; length < available; length++) {
67
+ spend(budget, 1);
68
+ if (
69
+ body[bodyStart + direction * length] !==
70
+ text[textStart + direction * length]
71
+ )
72
+ return length;
73
+ }
74
+ return available;
75
+ }
76
+
77
+ function lineOffsets(body: string) {
78
+ const offsets = new Map<string, number[]>();
79
+ for (const line of body.matchAll(/[^\n]*(?:\n|$)/g)) {
80
+ if (!line[0].trim()) continue;
81
+ const positions = offsets.get(line[0]) ?? [];
82
+ positions.push(line.index);
83
+ offsets.set(line[0], positions);
84
+ }
85
+ return offsets;
86
+ }
87
+
88
+ function anchoredRange(
89
+ body: string,
90
+ text: string,
91
+ bodyStart: number,
92
+ textStart: number,
93
+ anchorLength: number,
94
+ budget: MatchBudget,
95
+ ) {
96
+ spendAnchor(budget);
97
+ const before = matchingLength(
98
+ body,
99
+ text,
100
+ bodyStart - 1,
101
+ textStart - 1,
102
+ -1,
103
+ budget,
104
+ );
105
+ const after = matchingLength(
106
+ body,
107
+ text,
108
+ bodyStart + anchorLength,
109
+ textStart + anchorLength,
110
+ 1,
111
+ budget,
112
+ );
113
+ return {
114
+ start: bodyStart - before,
115
+ end: bodyStart + anchorLength + after,
116
+ outputEnd: textStart + anchorLength + after,
117
+ };
118
+ }
119
+
120
+ function uniqueRange(
121
+ body: string,
122
+ start: number,
123
+ end: number,
124
+ budget: MatchBudget,
125
+ ) {
126
+ spend(budget, body.length * 2);
127
+ const fragment = body.slice(start, end);
128
+ return (
129
+ body.indexOf(fragment) === start && body.indexOf(fragment, start + 1) === -1
130
+ );
131
+ }
132
+
133
+ function lineRanges(
134
+ body: string,
135
+ text: string,
136
+ line: RegExpExecArray,
137
+ positions: number[],
138
+ budget: MatchBudget,
139
+ ) {
140
+ const result = {
141
+ found: false,
142
+ ranges: [] as [number, number][],
143
+ matchedUntil: 0,
144
+ };
145
+ for (const offset of positions) {
146
+ const range = anchoredRange(
147
+ body,
148
+ text,
149
+ offset,
150
+ line.index,
151
+ line[0].length,
152
+ budget,
153
+ );
154
+ if (range.end - range.start < MIN_PAGE_CHARACTERS) continue;
155
+ result.found = true;
156
+ if (uniqueRange(body, range.start, range.end, budget))
157
+ result.ranges.push([range.start, range.end]);
158
+ result.matchedUntil = Math.max(result.matchedUntil, range.outputEnd);
159
+ }
160
+ return result;
161
+ }
162
+
163
+ function textRanges(body: string, text: string, budget: MatchBudget) {
164
+ const offsets = lineOffsets(body);
165
+ const result = { found: false, ranges: [] as [number, number][] };
166
+ let matchedUntil = 0;
167
+ for (const line of text.matchAll(/[^\n]*(?:\n|$)/g)) {
168
+ if (line.index < matchedUntil) continue;
169
+ const match = lineRanges(
170
+ body,
171
+ text,
172
+ line,
173
+ offsets.get(line[0]) ?? [],
174
+ budget,
175
+ );
176
+ result.found ||= match.found;
177
+ result.ranges.push(...match.ranges);
178
+ matchedUntil = Math.max(matchedUntil, match.matchedUntil);
179
+ }
180
+ return result;
181
+ }
182
+
183
+ function skillRanges(
184
+ skill: HookSkill,
185
+ texts: string[],
186
+ budget: MatchBudget,
187
+ ): SkillBodyRanges[] {
188
+ const matches = texts.map((text) => textRanges(skill.body, text, budget));
189
+ if (!matches.some((match) => match.found)) return [];
190
+ return [
191
+ {
192
+ invocation: skill.invocation,
193
+ sha256: skill.sha256,
194
+ ranges: matches.flatMap((match) => match.ranges),
195
+ },
196
+ ];
197
+ }
198
+
199
+ function label(value: unknown): string | null {
200
+ return typeof value === "string" && /^[A-Za-z0-9_.:-]{1,256}$/.test(value)
201
+ ? value
202
+ : null;
203
+ }
204
+
205
+ function responseTexts(value: unknown, depth = 0): string[] {
206
+ if (depth > 16) throw new Error("skill hook response nesting exceeds limit");
207
+ if (typeof value === "string") return [value];
208
+ return responseValues(value).flatMap((child) =>
209
+ responseTexts(child, depth + 1),
210
+ );
211
+ }
212
+
213
+ function responseValues(value: unknown): unknown[] {
214
+ if (Array.isArray(value)) return value;
215
+ return isRecord(value) ? Object.values(value) : [];
216
+ }
217
+
218
+ function completeBodyRanges(
219
+ text: string,
220
+ skill: HookSkill,
221
+ budget: MatchBudget,
222
+ ): [number, number][] {
223
+ spend(budget, text.length);
224
+ const ranges: [number, number][] = [];
225
+ let start = text.indexOf(skill.body);
226
+ while (start !== -1) {
227
+ spendAnchor(budget);
228
+ ranges.push([start, start + skill.body.length]);
229
+ start = text.indexOf(skill.body, start + 1);
230
+ }
231
+ return ranges;
232
+ }
233
+
234
+ function textOutsideRanges(text: string, ranges: [number, number][]) {
235
+ const remaining: string[] = [];
236
+ let cursor = 0;
237
+ for (const [start, end] of ranges.sort((a, b) => a[0] - b[0])) {
238
+ if (start > cursor) remaining.push(text.slice(cursor, start));
239
+ cursor = Math.max(cursor, end);
240
+ }
241
+ remaining.push(text.slice(cursor));
242
+ return remaining;
243
+ }
244
+
245
+ function textsOutsideCompleteBodies(
246
+ texts: string[],
247
+ skills: HookSkill[],
248
+ budget: MatchBudget,
249
+ ) {
250
+ return texts.flatMap((text) =>
251
+ textOutsideRanges(
252
+ text,
253
+ skills.flatMap((skill) => completeBodyRanges(text, skill, budget)),
254
+ ),
255
+ );
256
+ }
257
+
258
+ function invocations(
259
+ event: Record<string, unknown>,
260
+ configuration: SkillHookConfiguration,
261
+ ): {
262
+ invocations: string[];
263
+ ambiguousInvocations: string[];
264
+ bodyRanges?: SkillBodyRanges[];
265
+ } {
266
+ if (event.hook_event_name !== "PostToolUse")
267
+ return { invocations: [], ambiguousInvocations: [] };
268
+ const texts = responseTexts(event.tool_response);
269
+ const matched = configuration.skills.filter((skill) =>
270
+ texts.some((text) => text.includes(skill.body)),
271
+ );
272
+ const ambiguous = matched.filter((skill) =>
273
+ matched.some(
274
+ (other) =>
275
+ other.sha256 === skill.sha256 && other.invocation !== skill.invocation,
276
+ ),
277
+ );
278
+ const ambiguousNames = new Set(ambiguous.map((skill) => skill.invocation));
279
+ const unambiguous = matched.filter(
280
+ (skill) => !ambiguousNames.has(skill.invocation),
281
+ );
282
+ const budget = { comparisons: 4 * MAX_INPUT_BYTES, anchors: 4096 };
283
+ const remainingTexts = textsOutsideCompleteBodies(texts, unambiguous, budget);
284
+ const bodyRanges = configuration.skills
285
+ .filter((skill) => !matched.includes(skill))
286
+ .flatMap((skill) => skillRanges(skill, remainingTexts, budget));
287
+ return {
288
+ invocations: [...new Set(unambiguous.map((skill) => skill.invocation))],
289
+ ambiguousInvocations: [...ambiguousNames],
290
+ ...(bodyRanges.length ? { bodyRanges } : {}),
291
+ };
292
+ }
293
+
294
+ export function skillHookReceipt(
295
+ input: unknown,
296
+ configuration: SkillHookConfiguration,
297
+ ): SkillHookReceipt {
298
+ if (!isRecord(input) || typeof input.hook_event_name !== "string")
299
+ throw new Error("invalid skill hook input");
300
+ const receipt = {
301
+ event: input.hook_event_name,
302
+ sessionId: label(input.session_id),
303
+ turnId: label(input.turn_id),
304
+ toolUseId: label(input.tool_use_id),
305
+ toolName: label(input.tool_name),
306
+ ...invocations(input, configuration),
307
+ partial: false,
308
+ };
309
+ receipt.partial =
310
+ !validReceiptIdentity(receipt) || receipt.ambiguousInvocations.length > 0;
311
+ return receipt;
312
+ }
313
+
314
+ function validReceiptIdentity(receipt: SkillHookReceipt): boolean {
315
+ if (!receipt.sessionId) return false;
316
+ return (
317
+ !receipt.event.includes("ToolUse") ||
318
+ (receipt.toolUseId !== null && receipt.toolName !== null)
319
+ );
320
+ }
321
+
322
+ async function boundedInput(): Promise<unknown> {
323
+ const chunks: Uint8Array[] = [];
324
+ let bytes = 0;
325
+ for await (const chunk of Bun.stdin.stream()) {
326
+ bytes += chunk.byteLength;
327
+ if (bytes > MAX_INPUT_BYTES) throw new Error("skill hook input too large");
328
+ chunks.push(chunk);
329
+ }
330
+ return JSON.parse(Buffer.concat(chunks).toString("utf8")) as unknown;
331
+ }
332
+
333
+ async function appendReceipt(root: string, receipt: SkillHookReceipt) {
334
+ const path = join(root, "events.jsonl");
335
+ const size = await stat(path).then(
336
+ (file) => file.size,
337
+ () => 0,
338
+ );
339
+ if (size > MAX_RECEIPT_BYTES) {
340
+ await writeFile(join(root, "partial"), "limit\n", { mode: 0o600 });
341
+ return;
342
+ }
343
+ const file = await open(path, "a", 0o600);
344
+ try {
345
+ await file.write(JSON.stringify(receipt) + "\n");
346
+ } finally {
347
+ await file.close();
348
+ }
349
+ }
350
+
351
+ /** A silent observer: errors affect evidence availability, never tool decisions. */
352
+ export async function runSkillHook(configurationPath: string): Promise<void> {
353
+ const configuration = JSON.parse(
354
+ await readFile(configurationPath, "utf8"),
355
+ ) as SkillHookConfiguration;
356
+ try {
357
+ await appendReceipt(
358
+ configuration.receipts,
359
+ skillHookReceipt(await boundedInput(), configuration),
360
+ );
361
+ } catch {
362
+ await writeFile(join(configuration.receipts, "partial"), randomUUID(), {
363
+ mode: 0o600,
364
+ });
365
+ }
366
+ }
367
+
368
+ if (import.meta.main) {
369
+ try {
370
+ await runSkillHook(process.argv[2] ?? "");
371
+ } catch {
372
+ // Missing receipts remain unavailable; hook failures never alter the task.
373
+ }
374
+ }