@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,167 +1,110 @@
1
- import { observeClaudeNativeGoal } from "./claude-native-goal";
2
1
  import { existsSync } from "node:fs";
3
2
  import {
4
- cp,
5
- mkdir,
6
- mkdtemp,
7
- readFile,
8
- realpath,
9
- rm,
10
- stat,
11
- writeFile,
12
- } from "node:fs/promises";
13
- import { tmpdir } from "node:os";
14
- import { delimiter, dirname, isAbsolute, join, relative, sep } from "node:path";
15
- import type { HostAdapter } from "../engine";
16
- import { claudeNativeControls } from "./native-controls";
17
- import { fixtureParts } from "../preparation";
18
- import { stageClaudeAuthentication } from "./claude-credential";
3
+ requireCandidateRuntime,
4
+ requireCandidateArtifactRoots,
5
+ } from "../candidate-artifacts";
6
+ import { readFile, realpath, rm, stat } from "node:fs/promises";
7
+ import { dirname, isAbsolute, join } from "node:path";
8
+ import type { HostAdapter, HostResult } from "../engine";
9
+ import {
10
+ allocateNativeState,
11
+ requireNativeTranscriptView,
12
+ } from "../native-transcript-state";
13
+ import {
14
+ failedNativeTranscripts,
15
+ retainEnabledNativeTranscripts,
16
+ } from "../native-transcripts";
17
+ import { fixtureParts } from "../fixtures/preparation";
18
+ import {
19
+ prepareHookMounts,
20
+ releaseRuntimeHooks,
21
+ runtimeHookObservation,
22
+ runtimeHooksEnabled,
23
+ type HookMounts,
24
+ } from "../runtime/hooks";
25
+ import { insideRuntimeRoot as inside } from "../runtime/paths";
26
+ import { runtimeObservations } from "../runtime/state";
27
+ import { isOptionalNonblankString, isRecord } from "../value-guards";
28
+ import { runClaudeTurns, type ClaudeSession } from "./claude-continuation";
19
29
  import { summarizeClaudeEvents } from "./claude-events";
20
- import { claudeNestedSkillsObservation } from "./claude-nested-skills";
30
+ import { observeClaudeNativeGoal } from "./claude-native-goal";
31
+ import { runClaudeProcess } from "./claude-process";
21
32
  import {
22
33
  claudeRepositoryInvocationObservation,
23
34
  verifyClaudeRepositoryInvocation,
24
35
  } from "./claude-repository-invocation";
25
- import { claudeHostSettings } from "./claude-settings";
36
+ import { prepareClaudeState, type ClaudeHostOptions } from "./claude-state";
37
+ import {
38
+ cleanupNativeState,
39
+ createHostDiagnostics,
40
+ diagnoseHostRequest,
41
+ diagnosticConfiguration,
42
+ diagnosticCredentialEnvironment,
43
+ diagnosticStage,
44
+ readDiagnosticCredentials,
45
+ retainReturnedHostFailure,
46
+ type HostDiagnostics,
47
+ } from "../host-diagnostics";
48
+ import { CLAUDE_AUTH_VARIABLES } from "./claude-credential";
26
49
  import { claudeToolCallsObservation } from "./claude-tool-calls";
27
- import { evaluationProtectedRoots } from "./isolation-roots";
28
- import { runClaudeTurns, type ClaudeSession } from "./claude-continuation";
50
+ import { claudeSkillActivationsObservation } from "./claude-skill-activations";
51
+ import { claudeNativeControls } from "./native-controls";
52
+ export type { ClaudeHostOptions } from "./claude-state";
29
53
 
30
- const MAX_EVENT_BYTES = 8 * 1024 * 1024;
31
- const MAX_STDERR_BYTES = 64 * 1024;
32
54
  const DEFAULT_TIMEOUT_MS = 10 * 60_000;
33
55
 
34
- export interface ClaudeHostOptions {
35
- binary: string;
36
- model: string;
37
- effort: string;
38
- projectRoot: string;
39
- resultsRoot: string;
40
- additionalProtectedRoots: string[];
41
- credentialFile?: string;
42
- uvCacheDir?: string;
43
- toolchainBinDir?: string;
44
- projectSettings?: boolean;
45
- timeoutMs?: number;
46
- }
47
-
48
- function inside(root: string, path: string): boolean {
49
- const child = relative(root, path);
50
- return (
51
- child === "" ||
52
- (child !== ".." && !child.startsWith(`..${sep}`) && !isAbsolute(child))
53
- );
54
- }
55
-
56
- function minimalRoots(roots: string[]): string[] {
57
- return roots.filter(
58
- (root) => !roots.some((other) => other !== root && inside(other, root)),
59
- );
60
- }
61
-
62
- function stop(proc: Bun.Subprocess): void {
63
- try {
64
- if (process.platform !== "win32") process.kill(-proc.pid, "SIGKILL");
65
- else proc.kill("SIGKILL");
66
- } catch {
67
- proc.kill("SIGKILL");
68
- }
69
- }
70
-
71
- async function boundedStream(
72
- stream: ReadableStream<Uint8Array>,
73
- limit = MAX_EVENT_BYTES,
74
- ): Promise<string> {
75
- const reader = stream.getReader();
76
- const chunks: Uint8Array[] = [];
77
- let size = 0;
78
- while (true) {
79
- const { value, done } = await reader.read();
80
- if (done) break;
81
- size += value.byteLength;
82
- if (size > limit)
83
- throw new Error("Claude process output exceeds its limit");
84
- chunks.push(value);
85
- }
86
- return new TextDecoder("utf-8", { fatal: true }).decode(
87
- Buffer.concat(chunks),
88
- );
89
- }
90
-
91
- async function runProcess(options: {
92
- argv: string[];
93
- cwd: string;
94
- env: Record<string, string>;
95
- timeoutMs: number;
96
- signal?: AbortSignal;
97
- }): Promise<{ code: number; out: string; err: string }> {
98
- if (options.signal?.aborted) throw new Error("Claude run cancelled");
99
- const proc = Bun.spawn(options.argv, {
100
- cwd: options.cwd,
101
- env: options.env,
102
- detached: process.platform !== "win32",
103
- stdin: "ignore",
104
- stdout: "pipe",
105
- stderr: "pipe",
106
- });
107
- let timer: ReturnType<typeof setTimeout> | undefined;
108
- let cancel: (() => void) | undefined;
109
- try {
110
- const completed = Promise.all([
111
- boundedStream(proc.stdout),
112
- boundedStream(proc.stderr, MAX_STDERR_BYTES),
113
- proc.exited,
114
- ]).then(([out, err, code]) => ({ out, err, code }));
115
- const timeout = new Promise<never>((_resolve, reject) => {
116
- timer = setTimeout(
117
- () => reject(new Error("Claude run timed out")),
118
- options.timeoutMs,
119
- );
120
- });
121
- const aborted = new Promise<never>((_resolve, reject) => {
122
- if (!options.signal) return;
123
- cancel = () => reject(new Error("Claude run cancelled"));
124
- if (options.signal.aborted) cancel();
125
- else options.signal.addEventListener("abort", cancel, { once: true });
126
- });
127
- return await Promise.race([completed, timeout, aborted]);
128
- } catch (error) {
129
- stop(proc);
130
- await proc.exited;
131
- throw error;
132
- } finally {
133
- if (timer) clearTimeout(timer);
134
- if (cancel) options.signal?.removeEventListener("abort", cancel);
135
- }
136
- }
56
+ type Request = Parameters<HostAdapter["run"]>[0];
57
+ type PluginDeclaration = NonNullable<Request["claudePluginDirs"]>;
58
+ type PluginInvocation = NonNullable<Request["explicitSkillInvocation"]> & {
59
+ scope?: "plugin";
60
+ };
61
+ type RepositoryInvocation = Awaited<
62
+ ReturnType<typeof verifyClaudeRepositoryInvocation>
63
+ >;
64
+ type Execution = Awaited<ReturnType<typeof runClaudeTurns>>;
137
65
 
138
66
  async function pluginDirectories(
139
67
  workspace: string,
140
- declaration: NonNullable<
141
- Parameters<HostAdapter["run"]>[0]["claudePluginDirs"]
142
- >,
68
+ declaration: PluginDeclaration,
143
69
  ): Promise<string[]> {
144
70
  const root = await realpath(workspace);
71
+ requireUniquePluginDeclaration(declaration);
72
+ const paths = new Set(declaration.artifactPaths);
73
+ const directories: string[] = [];
74
+ for (const relativeRoot of declaration.artifactRoots)
75
+ directories.push(await declaredPluginDirectory(root, relativeRoot, paths));
76
+ requireDisjointPluginDirectories(directories);
77
+ for (const artifactPath of paths)
78
+ await verifyPluginArtifact(root, artifactPath, declaration, directories);
79
+ return directories;
80
+ }
81
+
82
+ function requireUniquePluginDeclaration(declaration: PluginDeclaration): void {
145
83
  if (
146
84
  new Set(declaration.artifactRoots).size !==
147
85
  declaration.artifactRoots.length ||
148
86
  new Set(declaration.artifactPaths).size !== declaration.artifactPaths.length
149
87
  )
150
88
  throw new Error("Claude plugin declaration contains duplicates");
151
- const paths = new Set(declaration.artifactPaths);
152
- const directories: string[] = [];
153
- for (const relativeRoot of declaration.artifactRoots) {
154
- fixtureParts(relativeRoot);
155
- const manifest = `${relativeRoot}/.claude-plugin/plugin.json`;
156
- if (!paths.has(manifest))
157
- throw new Error("Claude plugin manifest is undeclared");
158
- const actual = await realpath(join(root, relativeRoot));
159
- if (!inside(root, actual))
160
- throw new Error("Claude plugin directory escapes workspace");
161
- if (!(await stat(join(actual, ".claude-plugin", "plugin.json"))).isFile())
162
- throw new Error("Claude plugin manifest is missing");
163
- directories.push(actual);
164
- }
89
+ }
90
+
91
+ async function declaredPluginDirectory(
92
+ root: string,
93
+ relativeRoot: string,
94
+ paths: Set<string>,
95
+ ): Promise<string> {
96
+ fixtureParts(relativeRoot);
97
+ if (!paths.has(`${relativeRoot}/.claude-plugin/plugin.json`))
98
+ throw new Error("Claude plugin manifest is undeclared");
99
+ const actual = await realpath(join(root, relativeRoot));
100
+ if (!inside(root, actual))
101
+ throw new Error("Claude plugin directory escapes workspace");
102
+ if (!(await stat(join(actual, ".claude-plugin", "plugin.json"))).isFile())
103
+ throw new Error("Claude plugin manifest is missing");
104
+ return actual;
105
+ }
106
+
107
+ function requireDisjointPluginDirectories(directories: string[]): void {
165
108
  if (new Set(directories).size !== directories.length)
166
109
  throw new Error("Claude plugin directories overlap");
167
110
  if (
@@ -170,94 +113,154 @@ async function pluginDirectories(
170
113
  )
171
114
  )
172
115
  throw new Error("Claude plugin directories overlap");
173
- for (const artifactPath of paths) {
174
- fixtureParts(artifactPath);
175
- const index = declaration.artifactRoots.findIndex((item) =>
176
- artifactPath.startsWith(`${item}/`),
177
- );
178
- if (index < 0)
179
- throw new Error("Claude plugin artifact is outside a declared directory");
180
- if (!inside(directories[index]!, await realpath(join(root, artifactPath))))
181
- throw new Error("Claude plugin artifact escapes workspace");
182
- }
183
- return directories;
184
116
  }
185
117
 
186
- async function verifyInvocation(
187
- request: Parameters<HostAdapter["run"]>[0],
118
+ async function verifyPluginArtifact(
119
+ root: string,
120
+ artifactPath: string,
121
+ declaration: PluginDeclaration,
188
122
  directories: string[],
189
123
  ): Promise<void> {
124
+ fixtureParts(artifactPath);
125
+ const index = declaration.artifactRoots.findIndex((item) =>
126
+ artifactPath.startsWith(`${item}/`),
127
+ );
128
+ const directory = directories.at(index);
129
+ if (index < 0 || directory === undefined)
130
+ throw new Error("Claude plugin artifact is outside a declared directory");
131
+ if (!inside(directory, await realpath(join(root, artifactPath))))
132
+ throw new Error("Claude plugin artifact escapes workspace");
133
+ }
134
+
135
+ function pluginSelection(request: Request): PluginInvocation | null {
190
136
  const selected = request.explicitSkillInvocation;
191
- if (!selected) return;
192
- if (selected.scope === "repository") return;
193
- const { pluginName, skillName, token } = selected;
137
+ if (!selected || selected.scope === "repository") return null;
138
+ return selected;
139
+ }
140
+
141
+ function requirePluginInvocation(
142
+ selected: PluginInvocation,
143
+ prompt: string,
144
+ ): void {
145
+ const { token, pluginName, skillName } = selected;
194
146
  if (
195
147
  token !== `/${pluginName}:${skillName}` ||
196
- request.prompt.split(token).length - 1 !== 1 ||
148
+ prompt.split(token).length - 1 !== 1 ||
197
149
  !/^[a-z][a-z0-9-]*$/.test(pluginName) ||
198
150
  !/^[A-Za-z0-9._-]+$/.test(skillName)
199
151
  )
200
152
  throw new Error("invalid Claude explicit skill invocation");
153
+ }
154
+
155
+ function skillPathDeclared(
156
+ request: Request,
157
+ index: number,
158
+ skillName: string,
159
+ ): boolean {
160
+ const relativeRoot = request.claudePluginDirs?.artifactRoots[index];
161
+ return !!request.claudePluginDirs?.artifactPaths.includes(
162
+ `${relativeRoot}/skills/${skillName}/SKILL.md`,
163
+ );
164
+ }
165
+
166
+ async function verifyInvocation(
167
+ request: Request,
168
+ directories: string[],
169
+ ): Promise<void> {
170
+ const selected = pluginSelection(request);
171
+ if (!selected) return;
172
+ requirePluginInvocation(selected, request.prompt);
201
173
  for (const [index, root] of directories.entries()) {
202
- const relativeRoot = request.claudePluginDirs?.artifactRoots[index];
203
- if (
204
- !request.claudePluginDirs?.artifactPaths.includes(
205
- `${relativeRoot}/skills/${skillName}/SKILL.md`,
206
- )
207
- )
208
- continue;
209
- let manifest: unknown;
210
- try {
211
- manifest = JSON.parse(
212
- await readFile(join(root, ".claude-plugin", "plugin.json"), "utf8"),
213
- );
214
- } catch {
215
- throw new Error("invalid Claude plugin manifest");
216
- }
217
- if (
218
- manifest &&
219
- typeof manifest === "object" &&
220
- "name" in manifest &&
221
- manifest.name === pluginName &&
222
- (
223
- await stat(join(root, "skills", skillName, "SKILL.md")).catch(
224
- () => null,
225
- )
226
- )?.isFile()
227
- )
228
- return;
174
+ if (!skillPathDeclared(request, index, selected.skillName)) continue;
175
+ if (await mountedPluginSkill(root, selected)) return;
229
176
  }
230
177
  throw new Error("invoked Claude skill is absent from the package");
231
178
  }
232
179
 
180
+ async function mountedPluginSkill(
181
+ root: string,
182
+ selected: PluginInvocation,
183
+ ): Promise<boolean> {
184
+ let manifest: unknown;
185
+ try {
186
+ manifest = JSON.parse(
187
+ await readFile(join(root, ".claude-plugin", "plugin.json"), "utf8"),
188
+ );
189
+ } catch (error) {
190
+ throw new Error("invalid Claude plugin manifest", { cause: error });
191
+ }
192
+ if (!isRecord(manifest) || manifest.name !== selected.pluginName)
193
+ return false;
194
+ const skill = await stat(
195
+ join(root, "skills", selected.skillName, "SKILL.md"),
196
+ ).catch(() => null);
197
+ return !!skill?.isFile();
198
+ }
199
+
200
+ function validClaudeOptions(
201
+ options: ClaudeHostOptions,
202
+ timeoutMs: number,
203
+ ): boolean {
204
+ return (
205
+ availableClaudeSandbox() &&
206
+ requiredClaudePaths(options) &&
207
+ optionalClaudePaths(options) &&
208
+ !!options.model &&
209
+ validClaudeEffortTimeout(options.effort, timeoutMs)
210
+ );
211
+ }
212
+ function availableClaudeSandbox(): boolean {
213
+ if (process.platform === "darwin") return existsSync("/usr/bin/sandbox-exec");
214
+ return (
215
+ process.platform === "linux" &&
216
+ existsSync("/usr/bin/bwrap") &&
217
+ existsSync("/usr/bin/socat")
218
+ );
219
+ }
220
+ function requiredClaudePaths(options: ClaudeHostOptions): boolean {
221
+ return [
222
+ options.binary,
223
+ options.projectRoot,
224
+ options.resultsRoot,
225
+ ...options.additionalProtectedRoots,
226
+ ].every(isAbsolute);
227
+ }
228
+ function optionalClaudePaths(options: ClaudeHostOptions): boolean {
229
+ return [
230
+ options.credentialFile,
231
+ options.uvCacheDir,
232
+ options.toolchainBinDir,
233
+ ].every((path) => path === undefined || isAbsolute(path));
234
+ }
235
+ function validClaudeEffortTimeout(effort: string, timeoutMs: number): boolean {
236
+ return (
237
+ !!effort &&
238
+ Number.isSafeInteger(timeoutMs) &&
239
+ timeoutMs >= 1 &&
240
+ timeoutMs <= 30 * 60_000
241
+ );
242
+ }
243
+
233
244
  /** Run Claude with explicit plugins and its native tool sandbox. */
234
245
  export function createClaudeHost(options: ClaudeHostOptions): HostAdapter {
235
246
  const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
236
- if (
237
- process.platform !== "darwin" ||
238
- !existsSync("/usr/bin/sandbox-exec") ||
239
- !isAbsolute(options.binary) ||
240
- !isAbsolute(options.projectRoot) ||
241
- !isAbsolute(options.resultsRoot) ||
242
- options.additionalProtectedRoots.some((root) => !isAbsolute(root)) ||
243
- (options.credentialFile !== undefined &&
244
- !isAbsolute(options.credentialFile)) ||
245
- (options.uvCacheDir !== undefined && !isAbsolute(options.uvCacheDir)) ||
246
- (options.toolchainBinDir !== undefined &&
247
- !isAbsolute(options.toolchainBinDir)) ||
248
- !options.model ||
249
- !options.effort ||
250
- !Number.isSafeInteger(timeoutMs) ||
251
- timeoutMs < 1 ||
252
- timeoutMs > 30 * 60_000
253
- )
247
+ if (!validClaudeOptions(options, timeoutMs))
254
248
  throw new Error("invalid Claude host configuration");
255
249
  return {
256
250
  id: "sevro.host.claude",
257
251
  model: options.model,
258
252
  effort: options.effort,
253
+ ...(options.retainHostDiagnostics
254
+ ? {
255
+ configuration: diagnosticConfiguration(options.retainHostDiagnostics),
256
+ }
257
+ : {}),
259
258
  hostCapabilities: [
260
259
  "sevro.host.native-goal",
260
+ "sevro.host.runtime",
261
+ "sevro.host.native-transcripts",
262
+ "sevro.host.hooks",
263
+ "sevro.host.skill-activations",
261
264
  "sevro.claude.plugin-dirs",
262
265
  "sevro.claude.explicit-invocation",
263
266
  ...(options.projectSettings
@@ -265,256 +268,363 @@ export function createClaudeHost(options: ClaudeHostOptions): HostAdapter {
265
268
  : []),
266
269
  "sevro.claude.tool-calls",
267
270
  "sevro.host.native-controls",
268
- "sevro.claude.nested-skills",
269
271
  "sevro.host.continuation",
270
272
  "sevro.claude.continuation",
271
273
  ],
272
- async run(request) {
273
- if (
274
- request.followUpPrompt !== undefined &&
275
- (typeof request.followUpPrompt !== "string" ||
276
- !request.followUpPrompt.trim())
277
- )
278
- throw new Error("Claude follow-up prompt must be nonempty");
279
- if (request.instrumentation?.length || request.condition !== "passive")
280
- throw new Error("Claude enforcement instrumentation is unavailable");
281
- if (
282
- request.fixtureBinDir !== undefined &&
283
- request.fixtureBinDir !== join(request.workspace, ".git", "fixture-bin")
284
- )
285
- throw new Error("fixture binary path is outside the workspace");
286
- const pluginDirs = request.claudePluginDirs
287
- ? await pluginDirectories(request.workspace, request.claudePluginDirs)
288
- : [];
289
- await verifyInvocation(request, pluginDirs);
290
- if (
291
- request.explicitSkillInvocation?.scope === "repository" &&
292
- !options.projectSettings
293
- )
294
- throw new Error(
295
- "Claude repository invocation requires project setting sources",
296
- );
297
- const repositoryInvocation =
298
- request.explicitSkillInvocation?.scope === "repository"
299
- ? await verifyClaudeRepositoryInvocation(request)
300
- : null;
301
- const stateRoot = await mkdtemp(join(tmpdir(), "sevro-claude-state-"));
302
- try {
303
- await mkdir(join(stateRoot, "private"), { mode: 0o700 });
304
- const privateRoot = await realpath(join(stateRoot, "private"));
305
- const home = join(stateRoot, "home");
306
- const temp = join(stateRoot, "tmp");
307
- await Promise.all(
308
- [home, temp].map((path) => mkdir(path, { mode: 0o700 })),
309
- );
310
- const authentication = await stageClaudeAuthentication(
311
- join(privateRoot, "config"),
312
- options.credentialFile,
313
- );
314
- const credential = authentication.credentialFile;
315
- const protectedRoots = minimalRoots(
316
- await evaluationProtectedRoots({
317
- workspace: request.workspace,
318
- projectRoot: options.projectRoot,
319
- resultsRoot: options.resultsRoot,
320
- additionalRoots: options.additionalProtectedRoots,
321
- }),
322
- );
323
- if (protectedRoots.some((root) => inside(root, credential)))
324
- throw new Error("Claude private state overlaps a protected root");
325
- const binaryRoot = await realpath(options.binary);
326
- if (protectedRoots.some((root) => inside(root, binaryRoot)))
327
- throw new Error("Claude executable resides inside a protected root");
328
- const toolchainBinDir = options.toolchainBinDir
329
- ? await realpath(options.toolchainBinDir)
330
- : null;
331
- if (
332
- toolchainBinDir &&
333
- (protectedRoots.some((root) => inside(root, toolchainBinDir)) ||
334
- !(await stat(toolchainBinDir)).isDirectory())
335
- )
336
- throw new Error(
337
- "Claude toolchain directory is unavailable or protected",
338
- );
339
- let runtimeRoot: string | null = null;
340
- if (options.uvCacheDir) {
341
- const cacheRoot = await realpath(options.uvCacheDir);
342
- if (
343
- protectedRoots.some((root) => inside(root, cacheRoot)) ||
344
- !(await stat(cacheRoot)).isDirectory()
345
- )
346
- throw new Error("Claude UV cache is unavailable or protected");
347
- runtimeRoot = join(request.workspace, ".git", "sevro-runtime");
348
- await mkdir(runtimeRoot, { recursive: true, mode: 0o700 });
349
- await cp(cacheRoot, join(runtimeRoot, "uv-cache"), {
350
- recursive: true,
351
- force: false,
352
- errorOnExist: true,
353
- });
354
- }
355
- const settingsPath = join(privateRoot, "settings.json");
356
- await writeFile(
357
- settingsPath,
358
- JSON.stringify(
359
- claudeHostSettings(
360
- privateRoot,
361
- credential,
362
- pluginDirs,
363
- protectedRoots,
364
- ),
365
- ),
366
- { flag: "wx", mode: 0o600 },
367
- );
368
- const env: Record<string, string> = {
369
- ...authentication.environment,
370
- CLAUDE_CODE_SUBPROCESS_ENV_SCRUB: "1",
371
- PATH: [
372
- request.fixtureBinDir,
373
- toolchainBinDir,
374
- process.env.PATH ?? "/usr/bin:/bin",
375
- ]
376
- .filter(Boolean)
377
- .join(delimiter),
378
- LANG: process.env.LANG ?? "C",
379
- HOME: runtimeRoot ? join(runtimeRoot, "host-home") : home,
380
- TMPDIR: temp,
381
- CLAUDE_CONFIG_DIR: dirname(credential),
382
- NO_COLOR: "1",
383
- ...(runtimeRoot
384
- ? {
385
- UV_CACHE_DIR: join(runtimeRoot, "uv-cache"),
386
- UV_PROJECT_ENVIRONMENT: join(
387
- runtimeRoot,
388
- "project-environment",
389
- ),
390
- UV_OFFLINE: "1",
391
- PYTHONDONTWRITEBYTECODE: "1",
392
- }
393
- : {}),
394
- };
395
- if (runtimeRoot) await mkdir(env.HOME!, { mode: 0o700 });
396
- const runTurn = (prompt: string, session?: ClaudeSession) =>
397
- runProcess({
398
- argv: [
399
- options.binary,
400
- "-p",
401
- prompt,
402
- "--output-format",
403
- "stream-json",
404
- "--verbose",
405
- "--model",
406
- options.model,
407
- "--effort",
408
- options.effort,
409
- "--permission-mode",
410
- "dontAsk",
411
- "--tools",
412
- "Bash,Read,Edit,Skill,Agent",
413
- "--setting-sources",
414
- options.projectSettings ? "project" : "",
415
- "--settings",
416
- settingsPath,
417
- "--strict-mcp-config",
418
- "--mcp-config",
419
- '{"mcpServers":{}}',
420
- "--no-chrome",
421
- ...(session ? [session.option, session.id] : []),
422
- ...pluginDirs.flatMap((path) => ["--plugin-dir", path]),
423
- ],
424
- cwd: request.workspace,
425
- env,
426
- timeoutMs,
427
- signal: request.signal,
428
- });
429
- const execution = await runClaudeTurns({
430
- prompt: request.prompt,
431
- followUpPrompt: request.followUpPrompt,
432
- workspace: request.workspace,
433
- run: runTurn,
434
- });
435
- const summary = summarizeClaudeEvents(execution.out, execution.code);
436
- const nestedSkills = await claudeNestedSkillsObservation(
437
- execution.out,
438
- dirname(credential),
439
- request.workspace,
440
- );
441
- const toolCalls = claudeToolCallsObservation(
442
- execution.out,
443
- execution.code,
444
- );
445
- const repositoryObservation = repositoryInvocation
446
- ? await claudeRepositoryInvocationObservation({
447
- invocation: repositoryInvocation,
448
- stream: execution.initialOut ?? execution.out,
449
- configRoot: dirname(credential),
450
- workspace: request.workspace,
451
- tools: execution.initialOut
452
- ? claudeToolCallsObservation(execution.initialOut, 0)
453
- : toolCalls,
454
- })
455
- : null;
456
- return {
457
- finalMessage: summary.finalMessage,
458
- complete: summary.complete,
459
- executionFailed: !summary.complete,
460
- observations: [
461
- toolCalls,
462
- await observeClaudeNativeGoal(
463
- dirname(credential),
464
- execution.followUpOut ?? execution.out,
465
- ),
466
- nestedSkills,
467
- claudeNativeControls(execution.out, execution.code),
468
- ...(repositoryObservation ? [repositoryObservation] : []),
469
- ...(execution.continuation ? [execution.continuation] : []),
470
- ],
471
- artifacts: [
472
- {
473
- id: "sevro.claude.events",
474
- bytes: Buffer.from(execution.out, "utf8"),
475
- },
476
- ...(execution.initialOut !== undefined
477
- ? [
478
- {
479
- id: "sevro.claude.initial-events",
480
- bytes: Buffer.from(execution.initialOut, "utf8"),
481
- },
482
- ]
483
- : []),
484
- ...(execution.followUpOut !== undefined
485
- ? [
486
- {
487
- id: "sevro.claude.follow-up-events",
488
- bytes: Buffer.from(execution.followUpOut, "utf8"),
489
- },
490
- ]
491
- : []),
492
- ...(execution.err
493
- ? [
494
- {
495
- id: "sevro.claude.stderr",
496
- bytes: Buffer.from(execution.err, "utf8"),
497
- },
498
- ]
499
- : []),
500
- ],
501
- actualCondition: "passive" as const,
502
- inputTokens:
503
- execution.sessionResultsBound === false
504
- ? null
505
- : summary.inputTokens,
506
- outputTokens:
507
- execution.sessionResultsBound === false
508
- ? null
509
- : summary.outputTokens,
510
- costUsd:
511
- execution.sessionResultsBound === false ? null : summary.costUsd,
512
- usageComplete:
513
- execution.sessionResultsBound !== false && summary.usageComplete,
514
- };
515
- } finally {
516
- await rm(stateRoot, { recursive: true, force: true });
517
- }
274
+ run(request) {
275
+ return runClaudeRequest(options, timeoutMs, request);
518
276
  },
519
277
  };
520
278
  }
279
+
280
+ function requireClaudeFollowUp(request: Request): void {
281
+ if (!isOptionalNonblankString(request.followUpPrompt))
282
+ throw new Error("Claude follow-up prompt must be nonempty");
283
+ }
284
+ function requirePassiveClaudeRequest(request: Request): void {
285
+ if (request.instrumentation?.length || request.condition !== "passive")
286
+ throw new Error("Claude enforcement instrumentation is unavailable");
287
+ requireClaudeFixtureBin(request);
288
+ }
289
+
290
+ function requireClaudeFixtureBin(request: Request): void {
291
+ if (
292
+ request.fixtureBinDir !== undefined &&
293
+ request.fixtureBinDir !== join(request.workspace, ".git", "fixture-bin")
294
+ )
295
+ throw new Error("fixture binary path is outside the workspace");
296
+ }
297
+
298
+ async function repositoryMount(
299
+ request: Request,
300
+ options: ClaudeHostOptions,
301
+ ): Promise<RepositoryInvocation | null> {
302
+ if (request.explicitSkillInvocation?.scope !== "repository") return null;
303
+ if (!options.projectSettings)
304
+ throw new Error(
305
+ "Claude repository invocation requires project setting sources",
306
+ );
307
+ return verifyClaudeRepositoryInvocation(request);
308
+ }
309
+
310
+ async function runClaudeRequest(
311
+ options: ClaudeHostOptions,
312
+ timeoutMs: number,
313
+ request: Request,
314
+ ): Promise<HostResult> {
315
+ const diagnostics = createHostDiagnostics(
316
+ "claude",
317
+ options.retainHostDiagnostics,
318
+ request,
319
+ );
320
+ return diagnoseHostRequest(diagnostics, async () => {
321
+ diagnosticStage(diagnostics, "request-validation");
322
+ await readDiagnosticCredentials(diagnostics, options.credentialFile);
323
+ return executeClaudeRequest(options, timeoutMs, request, diagnostics);
324
+ });
325
+ }
326
+
327
+ async function executeClaudeRequest(
328
+ options: ClaudeHostOptions,
329
+ timeoutMs: number,
330
+ request: Request,
331
+ diagnostics?: HostDiagnostics,
332
+ ): Promise<HostResult> {
333
+ const { pluginDirs, repositoryInvocation } = await validateClaudeRequest(
334
+ options,
335
+ request,
336
+ );
337
+ diagnosticStage(diagnostics, "state-allocation");
338
+ const stateRoot = await allocateNativeState(
339
+ "claude-",
340
+ request.runtimePolicy ? request.workspace : undefined,
341
+ );
342
+ let hooks: HookMounts | undefined;
343
+ try {
344
+ diagnosticStage(diagnostics, "state-preparation");
345
+ const state = await prepareClaudeState(
346
+ stateRoot,
347
+ options,
348
+ request,
349
+ pluginDirs,
350
+ );
351
+ await readDiagnosticCredentials(diagnostics, state.credential);
352
+ diagnosticCredentialEnvironment(
353
+ diagnostics,
354
+ state.env,
355
+ CLAUDE_AUTH_VARIABLES,
356
+ );
357
+ hooks = state.hooks;
358
+ const run = claudeProcessRunner(
359
+ options,
360
+ timeoutMs,
361
+ request,
362
+ state,
363
+ diagnostics,
364
+ );
365
+ const execution = await runClaudeTurns({
366
+ prompt: request.prompt,
367
+ followUpPrompt: request.followUpPrompt,
368
+ workspace: request.workspace,
369
+ run,
370
+ });
371
+ const result = await claudeResult(
372
+ execution,
373
+ claudeObservationRoot(stateRoot, state.credential, request),
374
+ request,
375
+ repositoryInvocation,
376
+ );
377
+ await retainReturnedHostFailure(
378
+ diagnostics,
379
+ result,
380
+ "Claude host execution did not complete",
381
+ );
382
+ await addHookObservation(result, state.hooks);
383
+ await retainEnabledNativeTranscripts(
384
+ result,
385
+ request.runtimePolicy,
386
+ join(stateRoot, "native-home", "projects"),
387
+ "claude",
388
+ request.runtimeRole,
389
+ );
390
+ return result;
391
+ } catch (cause) {
392
+ return await failedNativeTranscripts(
393
+ cause,
394
+ request.runtimePolicy,
395
+ join(stateRoot, "native-home", "projects"),
396
+ "claude",
397
+ request.runtimeRole,
398
+ diagnostics,
399
+ );
400
+ } finally {
401
+ await cleanupNativeState(diagnostics, async () => {
402
+ await releaseRuntimeHooks(hooks);
403
+ await rm(stateRoot, { recursive: true, force: true });
404
+ });
405
+ }
406
+ }
407
+
408
+ function claudeProcessRunner(
409
+ options: ClaudeHostOptions,
410
+ timeoutMs: number,
411
+ request: Request,
412
+ state: Awaited<ReturnType<typeof prepareClaudeState>>,
413
+ diagnostics: HostDiagnostics | undefined,
414
+ ) {
415
+ return (prompt: string, session?: ClaudeSession) => {
416
+ diagnosticStage(diagnostics, "claude-execution");
417
+ return runClaudeProcess({
418
+ diagnostics,
419
+ argv: claudeTurnArgs(
420
+ options,
421
+ prompt,
422
+ session,
423
+ state.settingsPath,
424
+ state.pluginDirs,
425
+ ),
426
+ cwd: request.workspace,
427
+ env: state.env,
428
+ timeoutMs,
429
+ signal: request.signal,
430
+ });
431
+ };
432
+ }
433
+
434
+ async function validateClaudeRequest(
435
+ options: ClaudeHostOptions,
436
+ request: Request,
437
+ ) {
438
+ requireClaudeFollowUp(request);
439
+ await requireNativeTranscriptView(request.candidateTranscriptRoot);
440
+ await requireCandidateRuntime(
441
+ request.candidateRuntimeRoot,
442
+ request.runtimeRole,
443
+ );
444
+ await requireCandidateArtifactRoots(request.candidateArtifactRoots);
445
+ requirePassiveClaudeRequest(request);
446
+ const pluginDirs = request.claudePluginDirs
447
+ ? await pluginDirectories(request.workspace, request.claudePluginDirs)
448
+ : [];
449
+ await verifyInvocation(request, pluginDirs);
450
+ requireIsolatedHookSources(request, options);
451
+ const repositoryInvocation = await repositoryMount(request, options);
452
+ return { pluginDirs, repositoryInvocation };
453
+ }
454
+
455
+ function requireIsolatedHookSources(
456
+ request: Request,
457
+ options: ClaudeHostOptions,
458
+ ): void {
459
+ if (runtimeHooksEnabled(request.runtimePolicy) && options.projectSettings)
460
+ throw new Error("native goals require isolated hook sources");
461
+ }
462
+
463
+ async function addHookObservation(
464
+ result: HostResult,
465
+ hooks: Awaited<ReturnType<typeof prepareHookMounts>> | undefined,
466
+ ): Promise<void> {
467
+ if (!hooks) return;
468
+ result.observations?.push(await runtimeHookObservation(hooks));
469
+ }
470
+
471
+ function claudeTurnArgs(
472
+ options: ClaudeHostOptions,
473
+ prompt: string,
474
+ session: ClaudeSession | undefined,
475
+ settingsPath: string,
476
+ pluginDirs: string[],
477
+ ): string[] {
478
+ return [
479
+ options.binary,
480
+ "-p",
481
+ prompt,
482
+ "--output-format",
483
+ "stream-json",
484
+ "--verbose",
485
+ "--model",
486
+ options.model,
487
+ "--effort",
488
+ options.effort,
489
+ "--permission-mode",
490
+ "dontAsk",
491
+ "--tools",
492
+ "Bash,Read,Edit,Skill,Agent",
493
+ "--setting-sources",
494
+ options.projectSettings ? "project" : "",
495
+ "--settings",
496
+ settingsPath,
497
+ "--strict-mcp-config",
498
+ "--mcp-config",
499
+ '{"mcpServers":{}}',
500
+ "--no-chrome",
501
+ ...(session ? [session.option, session.id] : []),
502
+ ...pluginDirs.flatMap((path) => ["--plugin-dir", path]),
503
+ ];
504
+ }
505
+
506
+ function claudeObservationRoot(
507
+ stateRoot: string,
508
+ credential: string,
509
+ request: Request,
510
+ ): string {
511
+ return request.runtimePolicy?.nativeTranscripts === true
512
+ ? join(stateRoot, "native-home")
513
+ : dirname(credential);
514
+ }
515
+
516
+ async function repositoryObservation(
517
+ execution: Execution,
518
+ configRoot: string,
519
+ request: Request,
520
+ invocation: RepositoryInvocation | null,
521
+ tools: ReturnType<typeof claudeToolCallsObservation>,
522
+ ) {
523
+ if (!invocation) return null;
524
+ return claudeRepositoryInvocationObservation({
525
+ invocation,
526
+ stream: execution.initialOut ?? execution.out,
527
+ configRoot: configRoot,
528
+ workspace: request.workspace,
529
+ tools: execution.initialOut
530
+ ? claudeToolCallsObservation(execution.initialOut, 0)
531
+ : tools,
532
+ });
533
+ }
534
+
535
+ async function claudeObservations(
536
+ execution: Execution,
537
+ configRoot: string,
538
+ request: Request,
539
+ invocation: RepositoryInvocation | null,
540
+ ) {
541
+ const tools = claudeToolCallsObservation(execution.out, execution.code);
542
+ const repository = await repositoryObservation(
543
+ execution,
544
+ configRoot,
545
+ request,
546
+ invocation,
547
+ tools,
548
+ );
549
+ return [
550
+ tools,
551
+ ...runtimeObservations(request.runtimePolicy, request.runtimeRole),
552
+ await observeClaudeNativeGoal(
553
+ configRoot,
554
+ execution.followUpOut ?? execution.out,
555
+ ),
556
+ claudeNativeControls(execution.out, execution.code),
557
+ ...(repository ? [repository] : []),
558
+ ...(execution.continuation ? [execution.continuation] : []),
559
+ claudeSkillActivationsObservation(execution.out, execution.code),
560
+ ];
561
+ }
562
+
563
+ function claudeArtifacts(execution: Execution) {
564
+ return [
565
+ { id: "sevro.claude.events", bytes: Buffer.from(execution.out, "utf8") },
566
+ ...(execution.initialOut !== undefined
567
+ ? [
568
+ {
569
+ id: "sevro.claude.initial-events",
570
+ bytes: Buffer.from(execution.initialOut, "utf8"),
571
+ },
572
+ ]
573
+ : []),
574
+ ...(execution.followUpOut !== undefined
575
+ ? [
576
+ {
577
+ id: "sevro.claude.follow-up-events",
578
+ bytes: Buffer.from(execution.followUpOut, "utf8"),
579
+ },
580
+ ]
581
+ : []),
582
+ ...(execution.err
583
+ ? [
584
+ {
585
+ id: "sevro.claude.stderr",
586
+ bytes: Buffer.from(execution.err, "utf8"),
587
+ },
588
+ ]
589
+ : []),
590
+ ];
591
+ }
592
+
593
+ function measuredClaudeUsage(
594
+ summary: ReturnType<typeof summarizeClaudeEvents>,
595
+ execution: Execution,
596
+ ) {
597
+ return {
598
+ inputTokens:
599
+ execution.sessionResultsBound === false ? null : summary.inputTokens,
600
+ outputTokens:
601
+ execution.sessionResultsBound === false ? null : summary.outputTokens,
602
+ costUsd: execution.sessionResultsBound === false ? null : summary.costUsd,
603
+ usageComplete:
604
+ execution.sessionResultsBound !== false && summary.usageComplete,
605
+ };
606
+ }
607
+
608
+ async function claudeResult(
609
+ execution: Execution,
610
+ configRoot: string,
611
+ request: Request,
612
+ invocation: RepositoryInvocation | null,
613
+ ): Promise<HostResult> {
614
+ const summary = summarizeClaudeEvents(execution.out, execution.code);
615
+ const observations = await claudeObservations(
616
+ execution,
617
+ configRoot,
618
+ request,
619
+ invocation,
620
+ );
621
+ return {
622
+ finalMessage: summary.finalMessage,
623
+ complete: summary.complete,
624
+ executionFailed: !summary.complete,
625
+ observations,
626
+ artifacts: claudeArtifacts(execution),
627
+ actualCondition: "passive",
628
+ ...measuredClaudeUsage(summary, execution),
629
+ };
630
+ }