@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
@@ -1,220 +0,0 @@
1
- import { mkdir, writeFile } from "node:fs/promises";
2
- import { dirname, join } from "node:path";
3
- import { canonicalJson } from "./identity";
4
- import { prepareFixtureBin } from "./fixture-bin";
5
- import { prepareGitHooks } from "./git-hooks";
6
- import { fixtureParts } from "./preparation";
7
-
8
- const MAX_COMMITS = 128;
9
- const MAX_FILES = 1024;
10
- const MAX_BYTES = 32 * 1024 * 1024;
11
- const MAX_GIT_OUTPUT = 64 * 1024;
12
-
13
- export interface GeneratedFixture {
14
- kind: "generated";
15
- commits: { message: string; files: Record<string, string> }[];
16
- files?: Record<string, string>;
17
- staged?: string[];
18
- commitFiles?: boolean;
19
- hooks?: Record<string, string>;
20
- bin?: Record<string, string>;
21
- }
22
-
23
- function validFiles(value: unknown): value is Record<string, string> {
24
- return (
25
- Boolean(value && typeof value === "object" && !Array.isArray(value)) &&
26
- Object.values(value as object).every((item) => typeof item === "string")
27
- );
28
- }
29
-
30
- function validPath(path: string): void {
31
- const parts = fixtureParts(path);
32
- if (parts.some((part) => part.toLowerCase() === ".git"))
33
- throw new Error("generated fixture cannot write repository metadata");
34
- }
35
-
36
- /** Freeze a bounded declarative history before any host or fixture work. */
37
- export function prepareGeneratedFixture(value: unknown): GeneratedFixture {
38
- if (!value || typeof value !== "object" || Array.isArray(value))
39
- throw new Error("invalid generated fixture");
40
- const fixture = value as Record<string, unknown>;
41
- if (
42
- fixture.kind !== "generated" ||
43
- Object.keys(fixture).some(
44
- (key) =>
45
- ![
46
- "kind",
47
- "commits",
48
- "files",
49
- "staged",
50
- "commitFiles",
51
- "hooks",
52
- "bin",
53
- ].includes(key),
54
- ) ||
55
- !Array.isArray(fixture.commits) ||
56
- fixture.commits.length > MAX_COMMITS ||
57
- (fixture.files !== undefined && !validFiles(fixture.files)) ||
58
- (fixture.staged !== undefined &&
59
- (!Array.isArray(fixture.staged) ||
60
- !fixture.staged.every((item: unknown) => typeof item === "string"))) ||
61
- (fixture.commitFiles !== undefined &&
62
- typeof fixture.commitFiles !== "boolean")
63
- )
64
- throw new Error("invalid generated fixture");
65
- prepareGitHooks(fixture.hooks);
66
- prepareFixtureBin(fixture.bin);
67
- let bytes = 0;
68
- let files = 0;
69
- const paths: string[] = [];
70
- for (const commit of fixture.commits) {
71
- if (
72
- !commit ||
73
- typeof commit !== "object" ||
74
- Array.isArray(commit) ||
75
- Object.keys(commit).some((key) => key !== "message" && key !== "files") ||
76
- typeof commit.message !== "string" ||
77
- !commit.message.trim() ||
78
- Buffer.byteLength(commit.message, "utf8") > 4096 ||
79
- !validFiles(commit.files) ||
80
- Object.keys(commit.files).length === 0
81
- )
82
- throw new Error("invalid generated fixture commit");
83
- bytes += Buffer.byteLength(commit.message, "utf8");
84
- for (const [path, content] of Object.entries(
85
- commit.files as Record<string, string>,
86
- )) {
87
- validPath(path);
88
- paths.push(path);
89
- files++;
90
- bytes +=
91
- Buffer.byteLength(path, "utf8") + Buffer.byteLength(content, "utf8");
92
- }
93
- }
94
- const overlay = (fixture.files ?? {}) as Record<string, string>;
95
- for (const [path, content] of Object.entries(overlay)) {
96
- validPath(path);
97
- paths.push(path);
98
- files++;
99
- bytes +=
100
- Buffer.byteLength(path, "utf8") + Buffer.byteLength(content, "utf8");
101
- }
102
- bytes += ((fixture.staged ?? []) as string[]).reduce(
103
- (size, path) => size + Buffer.byteLength(path, "utf8"),
104
- 0,
105
- );
106
- if (files > MAX_FILES || bytes > MAX_BYTES)
107
- throw new Error("generated fixture exceeds the size limit");
108
- const unique = [...new Set(paths)];
109
- const normalized = unique.map((path) => path.toLowerCase());
110
- for (let index = 0; index < normalized.length; index++) {
111
- for (let other = 0; other < index; other++) {
112
- if (
113
- normalized[index] === normalized[other] ||
114
- normalized[index]!.startsWith(`${normalized[other]}/`) ||
115
- normalized[other]!.startsWith(`${normalized[index]}/`)
116
- )
117
- throw new Error("generated fixture paths collide");
118
- }
119
- }
120
- const staged = (fixture.staged ?? []) as string[];
121
- if (
122
- new Set(staged.map((path) => path.toLowerCase())).size !== staged.length ||
123
- staged.some((path) => !Object.hasOwn(overlay, path)) ||
124
- (fixture.commitFiles === true && Object.keys(overlay).length === 0)
125
- )
126
- throw new Error("invalid generated fixture staging");
127
- return JSON.parse(canonicalJson(fixture)) as GeneratedFixture;
128
- }
129
-
130
- async function git(workspace: string, args: string[]): Promise<void> {
131
- async function bounded(stream: ReadableStream<Uint8Array>): Promise<void> {
132
- let size = 0;
133
- for await (const chunk of stream) {
134
- size += chunk.byteLength;
135
- if (size > MAX_GIT_OUTPUT)
136
- throw new Error("generated fixture Git output is too large");
137
- }
138
- }
139
- const proc = Bun.spawn(
140
- [
141
- "git",
142
- "-c",
143
- `core.hooksPath=${process.platform === "win32" ? "NUL" : "/dev/null"}`,
144
- ...args,
145
- ],
146
- {
147
- cwd: workspace,
148
- stdout: "pipe",
149
- stderr: "pipe",
150
- env: {
151
- PATH: process.env.PATH ?? "/usr/bin:/bin",
152
- ...(process.env.SystemRoot
153
- ? { SystemRoot: process.env.SystemRoot }
154
- : {}),
155
- GIT_CONFIG_NOSYSTEM: "1",
156
- GIT_CONFIG_GLOBAL: process.platform === "win32" ? "NUL" : "/dev/null",
157
- GIT_TERMINAL_PROMPT: "0",
158
- GIT_AUTHOR_NAME: "Sevro Fixture",
159
- GIT_AUTHOR_EMAIL: "fixture@sevro.invalid",
160
- GIT_COMMITTER_NAME: "Sevro Fixture",
161
- GIT_COMMITTER_EMAIL: "fixture@sevro.invalid",
162
- GIT_AUTHOR_DATE: "2000-01-01T00:00:00+00:00",
163
- GIT_COMMITTER_DATE: "2000-01-01T00:00:00+00:00",
164
- },
165
- },
166
- );
167
- try {
168
- const [, , code] = await Promise.all([
169
- bounded(proc.stdout),
170
- bounded(proc.stderr),
171
- proc.exited,
172
- ]);
173
- if (code !== 0) throw new Error();
174
- } catch {
175
- try {
176
- proc.kill("SIGKILL");
177
- } catch {
178
- // The process may have exited before the failure was observed.
179
- }
180
- await proc.exited;
181
- throw new Error("generated fixture Git operation failed");
182
- }
183
- }
184
-
185
- async function writeFiles(
186
- workspace: string,
187
- files: Record<string, string>,
188
- ): Promise<void> {
189
- for (const [path, content] of Object.entries(files)) {
190
- const target = join(workspace, ...fixtureParts(path));
191
- await mkdir(dirname(target), { recursive: true, mode: 0o700 });
192
- await writeFile(target, content, { flag: "w", mode: 0o600 });
193
- }
194
- }
195
-
196
- /** Build a deterministic local Git history, then apply working-tree state. */
197
- export async function materializeGeneratedFixture(
198
- fixture: GeneratedFixture,
199
- workspace: string,
200
- ): Promise<void> {
201
- await git(workspace, ["init", "--quiet", "--initial-branch=main"]);
202
- for (const commit of fixture.commits) {
203
- await writeFiles(workspace, commit.files);
204
- await git(workspace, ["add", "--", ...Object.keys(commit.files)]);
205
- await git(workspace, ["commit", "--quiet", "-m", commit.message]);
206
- }
207
- const overlay = fixture.files ?? {};
208
- await writeFiles(workspace, overlay);
209
- if (fixture.commitFiles) {
210
- await git(workspace, ["add", "--", ...Object.keys(overlay)]);
211
- await git(workspace, [
212
- "commit",
213
- "--quiet",
214
- "-m",
215
- "Add evaluation scaffolding",
216
- ]);
217
- }
218
- if (fixture.staged?.length)
219
- await git(workspace, ["add", "--", ...fixture.staged]);
220
- }
package/src/git-hooks.ts DELETED
@@ -1,63 +0,0 @@
1
- import { chmod, lstat, writeFile } from "node:fs/promises";
2
- import { join } from "node:path";
3
-
4
- const MAX_HOOKS = 32;
5
- const MAX_HOOK_BYTES = 1024 * 1024;
6
-
7
- /** Bound hook declarations and keep every name inside Git's hooks directory. */
8
- export function prepareGitHooks(value: unknown): Record<string, string> | null {
9
- if (value === undefined) return null;
10
- if (!value || typeof value !== "object" || Array.isArray(value))
11
- throw new Error("invalid fixture hooks");
12
- const hooks = value as Record<string, unknown>;
13
- const entries = Object.entries(hooks);
14
- if (
15
- entries.length > MAX_HOOKS ||
16
- entries.some(
17
- ([name, content]) =>
18
- !/^[a-z][a-z0-9-]*$/.test(name) ||
19
- name.length > 64 ||
20
- typeof content !== "string" ||
21
- !content ||
22
- content.includes("\0"),
23
- ) ||
24
- entries.reduce(
25
- (size, [name, content]) =>
26
- size +
27
- Buffer.byteLength(name, "utf8") +
28
- Buffer.byteLength(content as string, "utf8"),
29
- 0,
30
- ) > MAX_HOOK_BYTES
31
- )
32
- throw new Error("invalid fixture hooks");
33
- return hooks as Record<string, string>;
34
- }
35
-
36
- /** Install executable hooks only after fixture commits and overlays are ready. */
37
- export async function installGitHooks(
38
- hooks: Record<string, string> | undefined,
39
- workspace: string,
40
- ): Promise<void> {
41
- if (!hooks) return;
42
- const directory = join(workspace, ".git", "hooks");
43
- const root = await lstat(directory);
44
- if (!root.isDirectory() || root.isSymbolicLink())
45
- throw new Error("fixture Git hooks directory is invalid");
46
- for (const [name, content] of Object.entries(hooks)) {
47
- const target = join(directory, name);
48
- try {
49
- const existing = await lstat(target);
50
- if (!existing.isFile() || existing.isSymbolicLink())
51
- throw new Error("fixture Git hook target is invalid");
52
- } catch (error) {
53
- if (!(
54
- error instanceof Error &&
55
- "code" in error &&
56
- error.code === "ENOENT"
57
- ))
58
- throw error;
59
- }
60
- await writeFile(target, content, { mode: 0o755 });
61
- await chmod(target, 0o755);
62
- }
63
- }
@@ -1,218 +0,0 @@
1
- import { lstat, readFile, readdir, realpath } from "node:fs/promises";
2
- import { join } from "node:path";
3
-
4
- type Entry = Record<string, unknown>;
5
- const MAX_BYTES = 8 * 1024 * 1024;
6
- const MAX_AGENTS = 64;
7
- const MAX_SKILLS = 128;
8
-
9
- function record(value: unknown): value is Entry {
10
- return value !== null && typeof value === "object" && !Array.isArray(value);
11
- }
12
-
13
- function label(value: unknown): value is string {
14
- return typeof value === "string" && /^[A-Za-z0-9_.:-]{1,256}$/.test(value);
15
- }
16
-
17
- function identity(value: unknown): value is string {
18
- return typeof value === "string" && /^[A-Za-z0-9_-]{1,128}$/.test(value);
19
- }
20
-
21
- function blocks(entry: Entry): Entry[] {
22
- const content = record(entry.message) ? entry.message.content : null;
23
- return Array.isArray(content) ? content.filter(record) : [];
24
- }
25
-
26
- function entries(text: string): Entry[] {
27
- return text
28
- .split(/\r?\n/)
29
- .filter(Boolean)
30
- .map((line) => {
31
- const parsed: unknown = JSON.parse(line);
32
- if (!record(parsed)) throw new Error("invalid Claude session entry");
33
- return parsed;
34
- });
35
- }
36
-
37
- async function readSession(path: string): Promise<string> {
38
- const file = await lstat(path);
39
- if (!file.isFile() || file.isSymbolicLink() || file.size > MAX_BYTES)
40
- throw new Error("Claude session file is unavailable");
41
- return readFile(path, "utf8");
42
- }
43
-
44
- type NestedSkill = {
45
- ancestorToolUseId: string;
46
- skill: string;
47
- invocation: string;
48
- };
49
-
50
- type State = {
51
- sessionId: string;
52
- projectDir: string;
53
- seen: Set<string>;
54
- calls: NestedSkill[];
55
- };
56
-
57
- async function sidecarChild(
58
- state: State,
59
- toolUseId: string,
60
- parentAgentId: string | null,
61
- ): Promise<string> {
62
- const directory = join(state.projectDir, state.sessionId, "subagents");
63
- const names = (await readdir(directory)).filter((name) =>
64
- /^agent-[A-Za-z0-9_-]+\.meta\.json$/.test(name),
65
- );
66
- if (names.length > MAX_AGENTS) throw new Error("excessive Claude agents");
67
- const matches: string[] = [];
68
- for (const name of names) {
69
- const metadata: unknown = JSON.parse(
70
- await readSession(join(directory, name)),
71
- );
72
- if (
73
- record(metadata) &&
74
- metadata.toolUseId === toolUseId &&
75
- (metadata.parentAgentId ?? null) === parentAgentId
76
- )
77
- matches.push(name.slice(6, -10));
78
- }
79
- if (matches.length !== 1 || !identity(matches[0]))
80
- throw new Error("Claude child identity is unavailable");
81
- return matches[0]!;
82
- }
83
-
84
- async function childId(
85
- entry: Entry,
86
- state: State,
87
- toolUseId: string,
88
- parentAgentId: string | null,
89
- ): Promise<string> {
90
- const result = record(entry.toolUseResult) ? entry.toolUseResult : null;
91
- if (result) {
92
- if (result.status !== "completed" || !identity(result.agentId))
93
- throw new Error("Claude child did not complete");
94
- return result.agentId;
95
- }
96
- return sidecarChild(state, toolUseId, parentAgentId);
97
- }
98
-
99
- async function childSession(state: State, agentId: string) {
100
- const directory = join(state.projectDir, state.sessionId, "subagents");
101
- const path = join(directory, `agent-${agentId}.jsonl`);
102
- if ((await realpath(path)) !== path)
103
- throw new Error("Claude child session is redirected");
104
- return readSession(path);
105
- }
106
-
107
- function skillCall(block: Entry, ancestorToolUseId: string | null) {
108
- const invocation = record(block.input) ? block.input.skill : null;
109
- if (!label(invocation)) throw new Error("invalid Claude Skill call");
110
- return ancestorToolUseId
111
- ? {
112
- ancestorToolUseId,
113
- skill: invocation.split(":").at(-1)!,
114
- invocation,
115
- }
116
- : null;
117
- }
118
-
119
- async function visit(
120
- text: string,
121
- state: State,
122
- agentId: string | null,
123
- ancestorToolUseId: string | null,
124
- ): Promise<void> {
125
- const pending = new Set<string>();
126
- for (const entry of entries(text)) {
127
- if (entry.type !== "assistant" && entry.type !== "user") continue;
128
- if (
129
- entry.sessionId !== state.sessionId ||
130
- (entry.agentId ?? null) !== agentId
131
- )
132
- throw new Error("Claude session identity mismatch");
133
- for (const block of blocks(entry)) {
134
- if (entry.type === "assistant" && block.type === "tool_use") {
135
- if (block.name === "Skill") {
136
- const call = skillCall(block, ancestorToolUseId);
137
- if (call) state.calls.push(call);
138
- if (state.calls.length > MAX_SKILLS)
139
- throw new Error("excessive Claude Skill calls");
140
- }
141
- if (block.name === "Agent" || block.name === "Task") {
142
- if (!identity(block.id) || pending.has(block.id))
143
- throw new Error("invalid Claude Agent call");
144
- pending.add(block.id);
145
- }
146
- }
147
- if (
148
- entry.type === "user" &&
149
- block.type === "tool_result" &&
150
- identity(block.tool_use_id) &&
151
- pending.has(block.tool_use_id)
152
- ) {
153
- if (block.is_error === true)
154
- throw new Error("Claude child call failed");
155
- const child = await childId(entry, state, block.tool_use_id, agentId);
156
- if (state.seen.has(child) || state.seen.size >= MAX_AGENTS)
157
- throw new Error("repeated Claude child");
158
- state.seen.add(child);
159
- await visit(
160
- await childSession(state, child),
161
- state,
162
- child,
163
- ancestorToolUseId ?? block.tool_use_id,
164
- );
165
- pending.delete(block.tool_use_id);
166
- }
167
- }
168
- }
169
- if (pending.size) throw new Error("incomplete Claude Agent graph");
170
- }
171
-
172
- export async function claudeNestedSkillsObservation(
173
- stream: string,
174
- configRoot: string,
175
- workspace: string,
176
- ) {
177
- const unavailable = {
178
- id: "sevro.claude.nested-skills" as const,
179
- completeness: "partial" as const,
180
- data: { method: "native_session_graph", calls: [] as NestedSkill[] },
181
- };
182
- try {
183
- const sessions = [
184
- ...new Set(
185
- entries(stream)
186
- .filter((entry) => entry.type === "result")
187
- .map((entry) => entry.session_id),
188
- ),
189
- ];
190
- if (sessions.length !== 1 || !identity(sessions[0])) return unavailable;
191
- const canonicalWorkspace = await realpath(workspace);
192
- const canonicalConfigRoot = await realpath(configRoot);
193
- const projectDir = join(
194
- canonicalConfigRoot,
195
- "projects",
196
- canonicalWorkspace.replace(/[^A-Za-z0-9]/g, "-"),
197
- );
198
- const state: State = {
199
- sessionId: sessions[0],
200
- projectDir,
201
- seen: new Set(),
202
- calls: [],
203
- };
204
- await visit(
205
- await readSession(join(projectDir, `${state.sessionId}.jsonl`)),
206
- state,
207
- null,
208
- null,
209
- );
210
- return {
211
- id: "sevro.claude.nested-skills" as const,
212
- completeness: "complete" as const,
213
- data: { method: "native_session_graph", calls: state.calls },
214
- };
215
- } catch {
216
- return unavailable;
217
- }
218
- }