@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,6 +1,8 @@
1
+ import type { Stats } from "node:fs";
1
2
  import { lstat, readFile, realpath } from "node:fs/promises";
2
3
  import { join } from "node:path";
3
4
  import type { HostAdapter } from "../engine";
5
+ import { isRecord } from "../value-guards";
4
6
  import type { ClaudeToolCallsObservation } from "./claude-tool-calls";
5
7
 
6
8
  type Entry = Record<string, unknown>;
@@ -11,27 +13,23 @@ export interface ClaudeRepositoryInvocation {
11
13
  prompt: string;
12
14
  }
13
15
 
14
- function record(value: unknown): value is Entry {
15
- return value !== null && typeof value === "object" && !Array.isArray(value);
16
- }
17
-
18
16
  function entries(text: string): Entry[] {
19
17
  return text
20
18
  .split(/\r?\n/)
21
19
  .filter((line) => line.trim())
22
20
  .map((line) => {
23
21
  const value: unknown = JSON.parse(line);
24
- if (!record(value)) throw new Error("invalid Claude session entry");
22
+ if (!isRecord(value)) throw new Error("invalid Claude session entry");
25
23
  return value;
26
24
  });
27
25
  }
28
26
 
29
27
  function content(entry: Entry): string {
30
- const blocks = record(entry.message) ? entry.message.content : null;
28
+ const blocks = isRecord(entry.message) ? entry.message.content : null;
31
29
  if (typeof blocks === "string") return blocks;
32
30
  if (!Array.isArray(blocks)) return "";
33
31
  return blocks
34
- .filter(record)
32
+ .filter(isRecord)
35
33
  .filter((block) => block.type === "text")
36
34
  .map((block) => (typeof block.text === "string" ? block.text : ""))
37
35
  .join("\n");
@@ -42,18 +40,9 @@ export async function verifyClaudeRepositoryInvocation(
42
40
  request: Parameters<HostAdapter["run"]>[0],
43
41
  ): Promise<ClaudeRepositoryInvocation> {
44
42
  const selected = request.explicitSkillInvocation;
45
- if (!selected || selected.scope !== "repository")
46
- throw new Error("Claude repository invocation is not declared");
43
+ requireRepositorySelection(selected);
47
44
  const { skillName, token } = selected;
48
- if (
49
- !/^[A-Za-z0-9._-]+$/.test(skillName) ||
50
- [".", ".."].includes(skillName) ||
51
- token !== `/${skillName}` ||
52
- request.prompt.split(token).length - 1 !== 1 ||
53
- (request.prompt !== token &&
54
- !request.prompt.startsWith(token + " ") &&
55
- !request.prompt.startsWith(token + "\n"))
56
- )
45
+ if (!validRepositoryDeclaration(skillName, token, request.prompt))
57
46
  throw new Error("invalid Claude repository skill invocation");
58
47
  let path = await realpath(request.workspace);
59
48
  for (const [index, part] of [
@@ -64,13 +53,7 @@ export async function verifyClaudeRepositoryInvocation(
64
53
  ].entries()) {
65
54
  path = join(path, part);
66
55
  const entry = await lstat(path).catch(() => null);
67
- if (
68
- !entry ||
69
- entry.isSymbolicLink() ||
70
- (index === 3
71
- ? !entry.isFile() || entry.size > 1024 * 1024
72
- : !entry.isDirectory())
73
- )
56
+ if (!safeSkillEntry(entry, index))
74
57
  throw new Error(
75
58
  "invoked Claude repository skill is unavailable or unsafe",
76
59
  );
@@ -83,76 +66,159 @@ export async function verifyClaudeRepositoryInvocation(
83
66
  };
84
67
  }
85
68
 
69
+ function requireRepositorySelection(
70
+ selected: Parameters<HostAdapter["run"]>[0]["explicitSkillInvocation"],
71
+ ): asserts selected is Extract<
72
+ NonNullable<Parameters<HostAdapter["run"]>[0]["explicitSkillInvocation"]>,
73
+ { scope: "repository" }
74
+ > {
75
+ if (!selected || selected.scope !== "repository")
76
+ throw new Error("Claude repository invocation is not declared");
77
+ }
78
+
79
+ type Receipt = { accepted: boolean | null; reason: string };
80
+ type MatchInput = ClaudeRepositoryInvocation & {
81
+ transcript: string;
82
+ sessionId: string;
83
+ };
84
+ type AssistantBoundary = { before: Entry[] } | { receipt: Receipt };
85
+ type CommandBoundary = { called: Entry } | { receipt: Receipt };
86
+
87
+ function validRepositoryDeclaration(
88
+ skillName: string,
89
+ token: string,
90
+ prompt: string,
91
+ ): boolean {
92
+ return (
93
+ validRepositorySkillName(skillName) &&
94
+ token === `/${skillName}` &&
95
+ prompt.split(token).length - 1 === 1 &&
96
+ repositoryPromptMatches(prompt, token)
97
+ );
98
+ }
99
+ function validRepositorySkillName(name: string): boolean {
100
+ return /^[A-Za-z0-9._-]+$/.test(name) && ![".", ".."].includes(name);
101
+ }
102
+ function repositoryPromptMatches(prompt: string, token: string): boolean {
103
+ return (
104
+ prompt === token ||
105
+ prompt.startsWith(token + " ") ||
106
+ prompt.startsWith(token + "\n")
107
+ );
108
+ }
109
+ function safeSkillEntry(entry: Stats | null, index: number): boolean {
110
+ if (!entry || entry.isSymbolicLink()) return false;
111
+ return index === 3
112
+ ? entry.isFile() && entry.size <= 1024 * 1024
113
+ : entry.isDirectory();
114
+ }
115
+
86
116
  /** Match one exact command and complete body before the assistant starts. */
87
- export function matchClaudeRepositoryInvocation(
88
- input: ClaudeRepositoryInvocation & {
89
- transcript: string;
90
- sessionId: string;
91
- },
92
- ): { accepted: boolean | null; reason: string } {
117
+ export function matchClaudeRepositoryInvocation(input: MatchInput): Receipt {
93
118
  let native: Entry[];
94
119
  try {
95
120
  native = entries(input.transcript);
96
121
  } catch {
97
122
  return { accepted: null, reason: "malformed native session" };
98
123
  }
124
+ const boundary = assistantBoundary(native, input.sessionId);
125
+ if ("receipt" in boundary) return boundary.receipt;
126
+ return matchCommandAndBody(boundary.before, input);
127
+ }
128
+
129
+ function assistantBoundary(
130
+ native: Entry[],
131
+ sessionId: string,
132
+ ): AssistantBoundary {
99
133
  const firstAssistant = native.findIndex(
100
134
  (entry) => entry.type === "assistant",
101
135
  );
102
- if (firstAssistant < 0)
103
- return { accepted: null, reason: "missing assistant turn" };
104
- const assistant = native[firstAssistant]!;
105
- if (assistant.sessionId !== input.sessionId || assistant.isSidechain === true)
106
- return { accepted: false, reason: "assistant session differs" };
107
- const before = native.slice(0, firstAssistant);
108
- const token = `/${input.skillName}`;
109
- if (
110
- input.prompt !== token &&
111
- !input.prompt.startsWith(token + " ") &&
112
- !input.prompt.startsWith(token + "\n")
113
- )
114
- return { accepted: false, reason: "prompt differs from mounted command" };
136
+ const assistant = native.at(firstAssistant);
137
+ if (firstAssistant < 0 || assistant === undefined)
138
+ return { receipt: { accepted: null, reason: "missing assistant turn" } };
139
+ if (assistant.sessionId !== sessionId || assistant.isSidechain === true)
140
+ return {
141
+ receipt: { accepted: false, reason: "assistant session differs" },
142
+ };
143
+ return { before: native.slice(0, firstAssistant) };
144
+ }
145
+
146
+ function nativeCommand(input: MatchInput, token: string): string {
115
147
  const args = input.prompt.slice(token.length).trim();
116
- const command =
148
+ return (
117
149
  `<command-message>${input.skillName}</command-message>\n<command-name>${token}</command-name>` +
118
- (args ? `\n<command-args>${args}</command-args>` : "");
150
+ (args ? `\n<command-args>${args}</command-args>` : "")
151
+ );
152
+ }
153
+
154
+ function matchCommandAndBody(before: Entry[], input: MatchInput): Receipt {
155
+ const token = `/${input.skillName}`;
156
+ if (!repositoryPromptMatches(input.prompt, token))
157
+ return { accepted: false, reason: "prompt differs from mounted command" };
158
+ const command = commandBoundary(before, input, token);
159
+ if ("receipt" in command) return command.receipt;
160
+ return mountedBodyReceipt(before, input, command.called);
161
+ }
162
+
163
+ function commandBoundary(
164
+ before: Entry[],
165
+ input: MatchInput,
166
+ token: string,
167
+ ): CommandBoundary {
119
168
  const commands = before.filter(
120
169
  (entry) =>
121
170
  entry.type === "user" &&
122
171
  content(entry).includes(`<command-name>${token}</command-name>`),
123
172
  );
124
- if (commands.length !== 1)
125
- return { accepted: false, reason: "missing or duplicate native command" };
126
- const called = commands[0]!;
127
- if (
128
- called.sessionId !== input.sessionId ||
129
- called.isMeta === true ||
130
- called.isSidechain === true ||
131
- content(called) !== command
132
- )
133
- return { accepted: false, reason: "command session or arguments differ" };
173
+ const [called] = commands;
174
+ if (commands.length !== 1 || called === undefined)
175
+ return {
176
+ receipt: {
177
+ accepted: false,
178
+ reason: "missing or duplicate native command",
179
+ },
180
+ };
181
+ if (!commandCorrelated(called, input, nativeCommand(input, token)))
182
+ return {
183
+ receipt: {
184
+ accepted: false,
185
+ reason: "command session or arguments differ",
186
+ },
187
+ };
188
+ return { called };
189
+ }
190
+
191
+ function commandCorrelated(
192
+ called: Entry,
193
+ input: MatchInput,
194
+ command: string,
195
+ ): boolean {
196
+ return (
197
+ called.sessionId === input.sessionId &&
198
+ called.isMeta !== true &&
199
+ called.isSidechain !== true &&
200
+ content(called) === command
201
+ );
202
+ }
203
+
204
+ function mountedBodyReceipt(
205
+ before: Entry[],
206
+ input: MatchInput,
207
+ called: Entry,
208
+ ): Receipt {
134
209
  const body = input.skillText
135
210
  .replace(/^---\r?\n[\s\S]*?\r?\n---\r?\n/, "")
136
211
  .trim();
137
212
  if (!body) return { accepted: null, reason: "empty mounted body" };
138
213
  const prefix = `Base directory for this skill: ${input.skillDir}\n\n${body}`;
139
- const expanded = before.filter(
140
- (entry) =>
141
- entry.type === "user" &&
142
- (content(entry) === prefix || content(entry).startsWith(prefix + "\n")),
143
- );
144
- if (expanded.length !== 1)
214
+ const expanded = before.filter((entry) => mountedBodyMatches(entry, prefix));
215
+ const [loaded] = expanded;
216
+ if (expanded.length !== 1 || loaded === undefined)
145
217
  return {
146
218
  accepted: false,
147
219
  reason: "missing, partial, or duplicate mounted body",
148
220
  };
149
- const loaded = expanded[0]!;
150
- if (
151
- loaded.sessionId !== input.sessionId ||
152
- loaded.isMeta !== true ||
153
- loaded.isSidechain === true ||
154
- before.indexOf(loaded) <= before.indexOf(called)
155
- )
221
+ if (!bodyCorrelated(loaded, input.sessionId, before, called))
156
222
  return { accepted: false, reason: "mounted body is uncorrelated" };
157
223
  return {
158
224
  accepted: true,
@@ -160,6 +226,27 @@ export function matchClaudeRepositoryInvocation(
160
226
  };
161
227
  }
162
228
 
229
+ function mountedBodyMatches(entry: Entry, prefix: string): boolean {
230
+ return (
231
+ entry.type === "user" &&
232
+ (content(entry) === prefix || content(entry).startsWith(prefix + "\n"))
233
+ );
234
+ }
235
+
236
+ function bodyCorrelated(
237
+ loaded: Entry,
238
+ sessionId: string,
239
+ before: Entry[],
240
+ called: Entry,
241
+ ): boolean {
242
+ return (
243
+ loaded.sessionId === sessionId &&
244
+ loaded.isMeta === true &&
245
+ loaded.isSidechain !== true &&
246
+ before.indexOf(loaded) > before.indexOf(called)
247
+ );
248
+ }
249
+
163
250
  /** Read only the completed bound native session; retain no arguments or body. */
164
251
  export async function claudeRepositoryInvocationObservation(input: {
165
252
  invocation: ClaudeRepositoryInvocation;
@@ -173,55 +260,10 @@ export async function claudeRepositoryInvocationObservation(input: {
173
260
  reason: "native session evidence unavailable",
174
261
  };
175
262
  try {
176
- const sessions = [
177
- ...new Set(
178
- entries(input.stream)
179
- .filter(
180
- (entry) =>
181
- entry.type === "result" ||
182
- (entry.type === "system" && entry.subtype === "init"),
183
- )
184
- .map((entry) => entry.session_id),
185
- ),
186
- ];
187
- if (
188
- sessions.length === 1 &&
189
- typeof sessions[0] === "string" &&
190
- /^[A-Za-z0-9_-]{1,128}$/.test(sessions[0])
191
- ) {
192
- const workspace = await realpath(input.workspace);
193
- const path = join(
194
- await realpath(input.configRoot),
195
- "projects",
196
- workspace.replace(/[^A-Za-z0-9]/g, "-"),
197
- `${sessions[0]}.jsonl`,
198
- );
199
- const entry = await lstat(path);
200
- if (
201
- entry.isFile() &&
202
- !entry.isSymbolicLink() &&
203
- entry.size <= 8 * 1024 * 1024 &&
204
- (await realpath(path)) === path
205
- ) {
206
- receipt = matchClaudeRepositoryInvocation({
207
- ...input.invocation,
208
- sessionId: sessions[0],
209
- transcript: await readFile(path, "utf8"),
210
- });
211
- }
212
- }
263
+ receipt = await repositoryReceipt(input);
213
264
  } catch {
214
265
  /* Unavailable or malformed native evidence stays partial. */
215
266
  }
216
- const observed = input.tools.data.calls
217
- .filter((call) => call.name === "Skill")
218
- .map((call) => call.skill);
219
- const observedSkills = [
220
- ...new Set([
221
- ...(receipt.accepted === true ? [input.invocation.skillName] : []),
222
- ...observed,
223
- ]),
224
- ];
225
267
  return {
226
268
  id: "sevro.claude.repository-invocation" as const,
227
269
  completeness:
@@ -232,8 +274,57 @@ export async function claudeRepositoryInvocationObservation(input: {
232
274
  method: "native_repository_command",
233
275
  ...receipt,
234
276
  skill: input.invocation.skillName,
235
- primarySkill: observedSkills[0] ?? null,
236
- observedSkills,
237
277
  },
238
278
  };
239
279
  }
280
+
281
+ function repositorySessionId(stream: string): string | null {
282
+ const sessions = [
283
+ ...new Set(
284
+ entries(stream)
285
+ .filter(
286
+ (entry) =>
287
+ entry.type === "result" ||
288
+ (entry.type === "system" && entry.subtype === "init"),
289
+ )
290
+ .map((entry) => entry.session_id),
291
+ ),
292
+ ];
293
+ const [id] = sessions;
294
+ if (
295
+ sessions.length !== 1 ||
296
+ typeof id !== "string" ||
297
+ !/^[A-Za-z0-9_-]{1,128}$/.test(id)
298
+ )
299
+ return null;
300
+ return id;
301
+ }
302
+
303
+ async function repositoryReceipt(
304
+ input: Parameters<typeof claudeRepositoryInvocationObservation>[0],
305
+ ): Promise<Receipt> {
306
+ const sessionId = repositorySessionId(input.stream);
307
+ if (sessionId === null)
308
+ return { accepted: null, reason: "native session evidence unavailable" };
309
+ const workspace = await realpath(input.workspace);
310
+ const path = join(
311
+ await realpath(input.configRoot),
312
+ "projects",
313
+ workspace.replace(/[^A-Za-z0-9]/g, "-"),
314
+ `${sessionId}.jsonl`,
315
+ );
316
+ const entry = await lstat(path);
317
+ if (!regularTranscript(entry) || (await realpath(path)) !== path)
318
+ return { accepted: null, reason: "native session evidence unavailable" };
319
+ return matchClaudeRepositoryInvocation({
320
+ ...input.invocation,
321
+ sessionId,
322
+ transcript: await readFile(path, "utf8"),
323
+ });
324
+ }
325
+
326
+ function regularTranscript(entry: Stats): boolean {
327
+ return (
328
+ entry.isFile() && !entry.isSymbolicLink() && entry.size <= 8 * 1024 * 1024
329
+ );
330
+ }