@bjoernrochel/sevro 0.1.0-rc.7 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (230) hide show
  1. package/CONTRIBUTING.md +22 -6
  2. package/README.md +5 -1
  3. package/docs/README.md +19 -15
  4. package/docs/architecture.md +138 -17
  5. package/docs/assets/README.md +5 -4
  6. package/docs/builtin-graders-v1.md +17 -3
  7. package/docs/codex-host.md +155 -49
  8. package/docs/contracts.md +2 -1
  9. package/docs/creating-cases.md +9 -2
  10. package/docs/development-cli.md +187 -19
  11. package/docs/documentation-quality.md +12 -2
  12. package/docs/evaluation-workflow.md +110 -0
  13. package/docs/extension-protocol-v1.md +26 -3
  14. package/docs/getting-started.md +23 -2
  15. package/docs/guide-evaluation.md +33 -1
  16. package/docs/identity-v1.md +25 -2
  17. package/docs/installing.md +11 -11
  18. package/docs/native-controls.md +76 -5
  19. package/docs/native-hosts.md +196 -7
  20. package/docs/reading-results.md +102 -0
  21. package/docs/recipes.md +489 -0
  22. package/docs/releases.md +31 -12
  23. package/docs/report-v1.md +29 -1
  24. package/docs/results-v1.md +141 -4
  25. package/docs/run-settings-v1.md +147 -0
  26. package/docs/runtime-v1.md +307 -18
  27. package/docs/troubleshooting.md +12 -12
  28. package/docs/typescript-quality.md +89 -26
  29. package/examples/recipes/candidate.ts +26 -0
  30. package/examples/recipes/cases/coding-change.json +21 -0
  31. package/examples/recipes/cases/host-tools.json +21 -0
  32. package/examples/recipes/cases/mise.json +21 -0
  33. package/examples/recipes/cases/node.json +21 -0
  34. package/examples/recipes/cases/python-uv.json +21 -0
  35. package/examples/recipes/cases/semantic.json +17 -0
  36. package/examples/recipes/coding-change/check.ts +23 -0
  37. package/examples/recipes/extension-artifacts.ts +65 -0
  38. package/examples/recipes/extension-methods.ts +174 -0
  39. package/examples/recipes/extension.ts +37 -0
  40. package/examples/recipes/repository-skill/.agents/skills/startup-guide/SKILL.md +7 -0
  41. package/examples/recipes/repository-skill/.claude/skills/startup-guide/SKILL.md +7 -0
  42. package/examples/recipes/repository-skill/README.md +3 -0
  43. package/examples/recipes/run-inputs.ts +188 -0
  44. package/examples/recipes/run-options.ts +131 -0
  45. package/examples/recipes/run.ts +63 -0
  46. package/examples/recipes/runtime/empty.json +1 -0
  47. package/examples/recipes/runtime/goals.json +1 -0
  48. package/examples/recipes/runtime/mise.json +6 -0
  49. package/examples/recipes/runtime/path.json +1 -0
  50. package/examples/recipes/runtime/plugins.json +4 -0
  51. package/examples/recipes/runtime/python-uv.json +10 -0
  52. package/examples/recipes/semantic-judge.ts +29 -0
  53. package/examples/templates/coding/adapter.ts +17 -0
  54. package/examples/templates/coding/case.json +15 -0
  55. package/examples/templates/repository-skill/.agents/skills/startup-guide/SKILL.md +6 -0
  56. package/examples/templates/repository-skill/extension-data.ts +64 -0
  57. package/examples/templates/repository-skill/extension.ts +31 -0
  58. package/examples/templates/response/adapter.ts +12 -0
  59. package/examples/templates/response/case.json +13 -0
  60. package/package.json +11 -5
  61. package/schemas/report-v1.schema.json +9 -0
  62. package/schemas/run-evidence-v1.schema.json +2 -1
  63. package/schemas/runtime-v1.schema.json +63 -7
  64. package/src/advisory.ts +2 -2
  65. package/src/candidate-artifacts.ts +206 -0
  66. package/src/candidate-codex-state.ts +185 -0
  67. package/src/case-validation.ts +73 -50
  68. package/src/cli/cancellation.ts +20 -0
  69. package/src/cli/command.ts +87 -0
  70. package/src/cli/completion.ts +15 -0
  71. package/src/cli/demo.ts +60 -0
  72. package/src/cli/evaluation.ts +55 -0
  73. package/src/cli/extension.ts +79 -0
  74. package/src/cli/help.ts +198 -0
  75. package/src/cli/hosts.ts +55 -0
  76. package/src/cli/init-settings.ts +30 -0
  77. package/src/cli/init.ts +111 -0
  78. package/src/{cli-invocation.ts → cli/invocation.ts} +214 -218
  79. package/src/cli/loading.ts +151 -0
  80. package/src/cli/preparation.ts +154 -0
  81. package/src/cli/presentation.ts +93 -0
  82. package/src/cli/progress.ts +6 -0
  83. package/src/cli/run-manifest.ts +128 -0
  84. package/src/cli/run-options.ts +403 -0
  85. package/src/cli/run-overrides.ts +69 -0
  86. package/src/cli/settings.ts +154 -0
  87. package/src/cli/summary.ts +43 -0
  88. package/src/cli.ts +13 -591
  89. package/src/contract-types.ts +94 -0
  90. package/src/engine.ts +13 -5
  91. package/src/evaluation/advisory.ts +150 -0
  92. package/src/{evaluation-check-plan.ts → evaluation/check-plan.ts} +103 -67
  93. package/src/evaluation/collection.ts +88 -0
  94. package/src/evaluation/context.ts +10 -0
  95. package/src/{evaluation-grading-host.ts → evaluation/grading-host.ts} +5 -5
  96. package/src/evaluation/grading.ts +235 -0
  97. package/src/{evaluation-invocation-plan.ts → evaluation/invocation-plan.ts} +147 -58
  98. package/src/{evaluation-plan.ts → evaluation/plan.ts} +208 -141
  99. package/src/evaluation/progress.ts +19 -0
  100. package/src/evaluation/resources.ts +241 -0
  101. package/src/evaluation/run.ts +206 -0
  102. package/src/{evaluation-semantic.ts → evaluation/semantic.ts} +133 -115
  103. package/src/evaluation/trial.ts +800 -0
  104. package/src/evaluation/types.ts +207 -0
  105. package/src/evidence/host-results.ts +85 -0
  106. package/src/evidence/retained-artifacts.ts +18 -0
  107. package/src/evidence/trial-writer.ts +203 -0
  108. package/src/evidence/usage.ts +17 -0
  109. package/src/extension-client.ts +34 -96
  110. package/src/extension-session.ts +24 -48
  111. package/src/{advisory-fixture.ts → fixtures/advisory.ts} +15 -76
  112. package/src/{fixture-bin.ts → fixtures/bin.ts} +1 -1
  113. package/src/{fixture-cleanup.ts → fixtures/cleanup.ts} +13 -7
  114. package/src/fixtures/create.ts +183 -0
  115. package/src/{generated-fixture.ts → fixtures/generated.ts} +10 -53
  116. package/src/{git-hooks.ts → fixtures/git-hooks.ts} +1 -1
  117. package/src/{preparation.ts → fixtures/preparation.ts} +6 -16
  118. package/src/{repository-fixture.ts → fixtures/repository.ts} +30 -64
  119. package/src/{fixture-setup.ts → fixtures/setup.ts} +33 -51
  120. package/src/{fixture-tools.ts → fixtures/tools.ts} +1 -1
  121. package/src/fixtures/workspace-state.ts +212 -0
  122. package/src/generated/cli-result.d.cts +2 -1
  123. package/src/generated/extension.d.cts +2 -1
  124. package/src/generated/report.cjs +1 -1
  125. package/src/generated/report.d.cts +2 -1
  126. package/src/generated/run-evidence.cjs +1 -1
  127. package/src/generated/run-evidence.d.cts +2 -1
  128. package/src/generated/runtime.cjs +1 -1
  129. package/src/generated/runtime.d.cts +2 -1
  130. package/src/git.ts +93 -0
  131. package/src/graders/git-head.ts +29 -84
  132. package/src/graders/shell.ts +57 -68
  133. package/src/host-diagnostic-redaction.ts +52 -0
  134. package/src/host-diagnostics.ts +331 -0
  135. package/src/hosts/claude-continuation.ts +57 -40
  136. package/src/hosts/claude-credential.ts +160 -63
  137. package/src/hosts/claude-events.ts +34 -18
  138. package/src/hosts/claude-keychain.ts +78 -21
  139. package/src/hosts/claude-login-recovery.ts +85 -0
  140. package/src/hosts/claude-login-refresh.ts +178 -0
  141. package/src/hosts/claude-login-store.ts +162 -0
  142. package/src/hosts/claude-login.ts +107 -0
  143. package/src/hosts/claude-native-goal.ts +15 -12
  144. package/src/hosts/claude-process.ts +10 -50
  145. package/src/hosts/claude-refresh-lock.ts +81 -0
  146. package/src/hosts/claude-repository-invocation.ts +9 -34
  147. package/src/hosts/claude-settings.ts +91 -14
  148. package/src/hosts/claude-skill-activations.ts +169 -0
  149. package/src/hosts/claude-state.ts +46 -16
  150. package/src/hosts/claude-tool-calls.ts +13 -10
  151. package/src/hosts/claude.ts +122 -73
  152. package/src/hosts/codex-app-server.ts +78 -52
  153. package/src/hosts/codex-configuration.ts +5 -10
  154. package/src/hosts/codex-exec-controls.ts +516 -0
  155. package/src/hosts/codex-marketplace.ts +12 -10
  156. package/src/hosts/codex-native-calls.ts +15 -122
  157. package/src/hosts/codex-process.ts +10 -54
  158. package/src/hosts/codex-profile.ts +50 -21
  159. package/src/hosts/codex-session-files.ts +1 -9
  160. package/src/hosts/codex-state.ts +141 -19
  161. package/src/hosts/codex.ts +221 -179
  162. package/src/hosts/isolation-common.ts +15 -11
  163. package/src/hosts/isolation-roots.ts +13 -20
  164. package/src/hosts/linux-sandbox.ts +27 -3
  165. package/src/hosts/mac-sandbox.ts +111 -18
  166. package/src/hosts/native-controls.ts +72 -22
  167. package/src/hosts/native-request.ts +55 -0
  168. package/src/hosts/process-lifecycle.ts +221 -45
  169. package/src/hosts/protected-worktrees.ts +1 -1
  170. package/src/hosts/skill-hook-catalog.ts +131 -0
  171. package/src/hosts/skill-hook-handler.ts +374 -0
  172. package/src/hosts/skill-hooks.ts +577 -0
  173. package/src/instrumentation.ts +7 -8
  174. package/src/native-transcript-state.ts +22 -3
  175. package/src/native-transcript-view.ts +52 -13
  176. package/src/native-transcripts.ts +34 -15
  177. package/src/paths.ts +22 -0
  178. package/src/process-lifecycle.ts +150 -0
  179. package/src/provenance.ts +27 -7
  180. package/src/prune.ts +300 -0
  181. package/src/report.ts +63 -36
  182. package/src/resolved-case.ts +1 -1
  183. package/src/results-lock.ts +42 -0
  184. package/src/results.ts +14 -18
  185. package/src/run-owner.ts +4 -4
  186. package/src/{runtime-config.ts → runtime/config.ts} +182 -44
  187. package/src/{runtime-discovery-apple.ts → runtime/discovery-apple.ts} +3 -3
  188. package/src/{runtime-discovery-providers.ts → runtime/discovery-providers.ts} +3 -9
  189. package/src/{runtime-discovery.ts → runtime/discovery.ts} +4 -4
  190. package/src/{runtime-hook-processes.ts → runtime/hook-processes.ts} +1 -1
  191. package/src/{runtime-hooks.ts → runtime/hooks.ts} +33 -22
  192. package/src/{runtime-paths.ts → runtime/paths.ts} +6 -18
  193. package/src/{runtime-query.ts → runtime/query.ts} +5 -5
  194. package/src/{runtime-seeds.ts → runtime/seeds.ts} +3 -3
  195. package/src/runtime/shared-cache.ts +130 -0
  196. package/src/{runtime-state.ts → runtime/state.ts} +28 -11
  197. package/src/schema-types.ts +73 -161
  198. package/src/shell.ts +4 -0
  199. package/src/transcript-retention.ts +146 -0
  200. package/src/value-guards.ts +22 -0
  201. package/docs/assets/sevro-logo.png +0 -0
  202. package/docs/assets/sevro-symbol.svg +0 -1
  203. package/docs/assets/sevro-wordmark.svg +0 -1
  204. package/docs/cache-ownership-validation.md +0 -49
  205. package/docs/claude-continuation-validation.md +0 -47
  206. package/docs/concurrency-validation.md +0 -86
  207. package/docs/configuration-root-validation.md +0 -70
  208. package/docs/engine-slice.md +0 -114
  209. package/docs/evidence/runtime-startup-30/README.md +0 -185
  210. package/docs/evidence/runtime-startup-30/after.json +0 -319
  211. package/docs/evidence/runtime-startup-30/baseline-timeout.jsonl +0 -2
  212. package/docs/evidence/runtime-startup-30/before.json +0 -403
  213. package/docs/evidence/runtime-startup-30/consumer-smoke.json +0 -63
  214. package/docs/evidence/runtime-startup-30/full-policy-diagnostic.jsonl +0 -3
  215. package/docs/evidence/runtime-startup-30/minimal-policy-diagnostic.jsonl +0 -3
  216. package/docs/evidence/runtime-startup-30/plugin-template-assessment.json +0 -20
  217. package/docs/evidence/runtime-startup-30/seedless-timeout.jsonl +0 -2
  218. package/docs/evidence.md +0 -17
  219. package/docs/fixture-cleanup-validation.md +0 -70
  220. package/docs/guide-verification.json +0 -239
  221. package/src/evaluation-context.ts +0 -257
  222. package/src/evaluation-fixture.ts +0 -313
  223. package/src/evaluation-run.ts +0 -198
  224. package/src/evaluation-trial.ts +0 -946
  225. package/src/evaluation-types.ts +0 -149
  226. package/src/hosts/claude-nested-skills.ts +0 -272
  227. package/src/hosts/codex-command-output.ts +0 -510
  228. package/src/hosts/codex-executor-command.ts +0 -291
  229. package/src/hosts/codex-skill-reads.ts +0 -609
  230. /package/src/{trial-scheduler.ts → evaluation/scheduler.ts} +0 -0
@@ -1,609 +0,0 @@
1
- import { isRecord, isUnknownArray } from "../value-guards";
2
- import type { RecoveredCommandOutput } from "./codex-command-output";
3
- import { nativeCommandOutputs } from "./codex-command-output";
4
- import { readFile, realpath } from "node:fs/promises";
5
- import { isAbsolute, relative, resolve, sep } from "node:path";
6
-
7
- const MAX_SKILL_BYTES = 1024 * 1024;
8
- const MAX_NATIVE_READ_ATTEMPTS = 64;
9
- const SKILL_PATH = /^\.agents\/skills\/([A-Za-z0-9._-]+)\/SKILL\.md$/;
10
- const PLUGIN_SKILL_PATH = /^skills\/([A-Za-z0-9._-]+)\/SKILL\.md$/;
11
-
12
- interface CodexItem {
13
- id?: unknown;
14
- type?: unknown;
15
- command?: unknown;
16
- status?: unknown;
17
- exit_code?: unknown;
18
- aggregated_output?: unknown;
19
- }
20
-
21
- interface CodexEvent {
22
- type?: unknown;
23
- item?: CodexItem;
24
- }
25
-
26
- function shellPayload(command: string): string {
27
- let payload = command;
28
- for (let depth = 0; depth < 2; depth++) {
29
- const wrapper = payload.match(
30
- /^(?:\/bin\/(?:ba|z)?sh\s+-l?c|lean-ctx\s+-c)\s+(["'])([\s\S]*)\1$/,
31
- );
32
- if (!wrapper) break;
33
- payload = wrapper.slice(2, 3).join("");
34
- }
35
- return payload;
36
- }
37
-
38
- interface DirectRead {
39
- path: string;
40
- firstLine?: number;
41
- lastLine?: number;
42
- }
43
-
44
- function capturedReadPath(match: RegExpMatchArray, first: number): string {
45
- return match
46
- .slice(first, first + 3)
47
- .filter(Boolean)
48
- .join("");
49
- }
50
- function validReadLines(firstLine: number, lastLine: number): boolean {
51
- return (
52
- Number.isSafeInteger(firstLine) &&
53
- firstLine >= 1 &&
54
- Number.isSafeInteger(lastLine) &&
55
- lastLine >= firstLine
56
- );
57
- }
58
- function sedRead(payload: string, path: string): DirectRead | null {
59
- const sed = payload.match(
60
- new RegExp(`^(?:/bin/)?sed\\s+-n\\s+['"]?(\\d+),(\\d+)p['"]?\\s+${path}$`),
61
- );
62
- if (!sed) return null;
63
- const firstLine = Number(sed[1]),
64
- lastLine = Number(sed[2]);
65
- if (!validReadLines(firstLine, lastLine)) return null;
66
- return { path: capturedReadPath(sed, 3), firstLine, lastLine };
67
- }
68
- function directRead(command: string): DirectRead | null {
69
- const payload = shellPayload(command).trim();
70
- const path = "(?:\"([^\"]+)\"|'([^']+)'|([^\\s'\"`$;&|<>]+))";
71
- return catRead(payload, path) ?? sedRead(payload, path);
72
- }
73
-
74
- function catRead(payload: string, path: string): DirectRead | null {
75
- const cat = payload.match(
76
- new RegExp(`^(?:/bin/)?cat\\s+(?:--\\s+)?(${path}(?:\\s+${path})*)$`),
77
- );
78
- if (!cat) return null;
79
- const paths = [
80
- ...cat.slice(1, 2).join("").matchAll(new RegExp(path, "g")),
81
- ].map((match) => capturedReadPath(match, 1));
82
- const skills = paths.filter((value) => /(?:^|\/)SKILL\.md$/.test(value));
83
- if (skills.length !== 1 || paths.length > MAX_NATIVE_READ_ATTEMPTS)
84
- return null;
85
- return { path: skills.join("") };
86
- }
87
-
88
- function readRange(body: string, read: DirectRead): [number, number] {
89
- if (read.firstLine === undefined || read.lastLine === undefined)
90
- return [0, body.length];
91
- const lines = body.match(/[^\n]*\n|[^\n]+$/g) ?? [];
92
- const start = lines.slice(0, read.firstLine - 1).join("").length;
93
- const end = lines.slice(0, read.lastLine).join("").length;
94
- return [start, end];
95
- }
96
-
97
- type VerifiedRead = {
98
- skill: string;
99
- path: string;
100
- range: [number, number];
101
- bodyLength: number;
102
- };
103
- function readDeclaration(
104
- item: CodexItem,
105
- ): { read: DirectRead; output: string } | null {
106
- if (typeof item.command !== "string") return null;
107
- const read = directRead(item.command);
108
- if (!read || typeof item.aggregated_output !== "string") return null;
109
- return { read, output: item.aggregated_output };
110
- }
111
- async function verifiedSkillRead(
112
- item: CodexItem,
113
- workspace: string,
114
- pluginRoots: string[],
115
- commandCwd = workspace,
116
- ): Promise<VerifiedRead | null> {
117
- const declaration = readDeclaration(item);
118
- if (!declaration) return null;
119
- return resolvedSkillRead(declaration, workspace, pluginRoots, commandCwd);
120
- }
121
- function mountedSkillName(
122
- actual: string,
123
- workspace: string,
124
- pluginRoots: string[],
125
- ): string | null {
126
- const mounted = relative(workspace, actual)
127
- .split(sep)
128
- .join("/")
129
- .match(SKILL_PATH);
130
- const plugin = pluginRoots
131
- .map((root) =>
132
- relative(root, actual).split(sep).join("/").match(PLUGIN_SKILL_PATH),
133
- )
134
- .find((match) => match !== null);
135
- const match = mounted ?? plugin;
136
- return match ? match.slice(1, 2).join("") : null;
137
- }
138
- async function verifiedSkillBody(
139
- actual: string,
140
- skill: string,
141
- ): Promise<string | null> {
142
- const body = await readFile(actual).catch(() => null);
143
- if (!body || body.byteLength > MAX_SKILL_BYTES) return null;
144
- const text = body.toString("utf8");
145
- if (!text.startsWith("---\n") || !text.includes(`\nname: ${skill}\n`))
146
- return null;
147
- return text;
148
- }
149
- async function resolvedSkillRead(
150
- declaration: { read: DirectRead; output: string },
151
- workspace: string,
152
- pluginRoots: string[],
153
- commandCwd: string,
154
- ): Promise<VerifiedRead | null> {
155
- const actual = await realpath(
156
- resolve(commandCwd, declaration.read.path),
157
- ).catch(() => null);
158
- if (!actual) return null;
159
- const skill = mountedSkillName(actual, workspace, pluginRoots);
160
- if (skill === null) return null;
161
- const text = await verifiedSkillBody(actual, skill);
162
- if (text === null) return null;
163
- return skillReadReceipt(actual, skill, declaration, text);
164
- }
165
- function skillReadReceipt(
166
- actual: string,
167
- skill: string,
168
- declaration: { read: DirectRead; output: string },
169
- text: string,
170
- ): VerifiedRead | null {
171
- const range = readRange(text, declaration.read);
172
- if (range[0] === range[1]) return null;
173
- if (!declaration.output.includes(text.slice(...range))) return null;
174
- return { skill, path: actual, range, bodyLength: text.length };
175
- }
176
-
177
- function fullCoverage(
178
- ranges: Array<[number, number]>,
179
- length: number,
180
- ): boolean {
181
- const ordered = ranges.sort((left, right) => left[0] - right[0]);
182
- let covered = 0;
183
- for (const [start, end] of ordered) {
184
- if (start > covered) return false;
185
- covered = Math.max(covered, end);
186
- }
187
- return covered === length;
188
- }
189
-
190
- interface NativeEntry {
191
- ordinal: number;
192
- payload: Record<string, unknown>;
193
- }
194
-
195
- export interface NativeReadDiagnostic {
196
- completeness: "complete" | "partial";
197
- observedSkills: string[];
198
- completedReads: Array<{ skill: string; ordinal: number }>;
199
- recoverySources: Array<{
200
- ordinal: number;
201
- nativeOutput: boolean;
202
- yieldedChunks: number;
203
- completedCall: boolean;
204
- literalCommandCall: boolean;
205
- }>;
206
- commandExecutions: number;
207
- readAttempts: number;
208
- truncated: boolean;
209
- }
210
-
211
- function nativeShellArguments(value: string[]): boolean {
212
- return value.length >= 3 && (value[1] === "-lc" || value[1] === "-c");
213
- }
214
- function nativeCommandText(value: unknown): string | null {
215
- if (typeof value === "string") return value;
216
- if (
217
- !isUnknownArray(value) ||
218
- !value.every((part): part is string => typeof part === "string")
219
- )
220
- return null;
221
- return nativeShellArguments(value)
222
- ? value.slice(2).join(" ")
223
- : value.join(" ");
224
- }
225
-
226
- type Coverage = { ranges: Array<[number, number]>; bodyLength: number };
227
- interface CoverageState {
228
- attempted: Map<string, Coverage>;
229
- observedSkills: string[];
230
- }
231
- interface NativeReadState extends CoverageState {
232
- completedReads: NativeReadDiagnostic["completedReads"];
233
- recoverySources: NativeReadDiagnostic["recoverySources"];
234
- commandExecutions: number;
235
- readAttempts: number;
236
- partial: boolean;
237
- truncated: boolean;
238
- }
239
- interface ReadContext {
240
- workspace: string;
241
- pluginRoots: string[];
242
- recovered: Map<number, RecoveredCommandOutput>;
243
- }
244
- function nativeReadState(): NativeReadState {
245
- return {
246
- attempted: new Map(),
247
- observedSkills: [],
248
- completedReads: [],
249
- recoverySources: [],
250
- commandExecutions: 0,
251
- readAttempts: 0,
252
- partial: false,
253
- truncated: false,
254
- };
255
- }
256
-
257
- /** Verify native skill-file reads without retaining commands or output. */
258
- export async function codexNativeReadDiagnostic(
259
- entries: NativeEntry[],
260
- workspace: string,
261
- installedPluginRoots: string[] = [],
262
- ): Promise<NativeReadDiagnostic> {
263
- const canonicalWorkspace = await realpath(workspace);
264
- const pluginRoots = await Promise.all(
265
- installedPluginRoots.map((root) => realpath(root)),
266
- );
267
- const context: ReadContext = {
268
- workspace: canonicalWorkspace,
269
- pluginRoots,
270
- recovered: nativeCommandOutputs(entries),
271
- };
272
- const state = nativeReadState();
273
- for (const entry of entries)
274
- if (!(await nativeReadEntry(entry, context, state))) break;
275
- return nativeReadResult(state);
276
- }
277
-
278
- function nativeCommandItem(
279
- payload: Record<string, unknown>,
280
- ): (CodexItem & { cwd?: unknown }) | null {
281
- if (
282
- payload.type !== "item_completed" ||
283
- !isRecord(payload.item) ||
284
- payload.item.type !== "CommandExecution"
285
- )
286
- return null;
287
- return payload.item;
288
- }
289
-
290
- async function nativeReadEntry(
291
- { ordinal, payload }: NativeEntry,
292
- context: ReadContext,
293
- state: NativeReadState,
294
- ): Promise<boolean> {
295
- const item = nativeCommandItem(payload);
296
- if (!item) return true;
297
- state.commandExecutions++;
298
- const command = nativeCommandText(item.command);
299
- if (!command) {
300
- state.partial = true;
301
- return true;
302
- }
303
- return processNativeRead(item, command, ordinal, context, state);
304
- }
305
-
306
- function countNativeRead(state: NativeReadState): boolean {
307
- state.readAttempts++;
308
- if (state.readAttempts <= MAX_NATIVE_READ_ATTEMPTS) return true;
309
- state.readAttempts = MAX_NATIVE_READ_ATTEMPTS;
310
- state.truncated = true;
311
- state.partial = true;
312
- return false;
313
- }
314
-
315
- async function processNativeRead(
316
- item: CodexItem & { cwd?: unknown },
317
- command: string,
318
- ordinal: number,
319
- context: ReadContext,
320
- state: NativeReadState,
321
- ): Promise<boolean> {
322
- if (!command.includes("SKILL.md")) return true;
323
- if (!countNativeRead(state)) return false;
324
- const recovered = context.recovered.get(ordinal);
325
- state.recoverySources.push({
326
- ordinal,
327
- nativeOutput: hasNativeOutput(item),
328
- ...recoveryFlags(recovered),
329
- });
330
- await completeNativeRead(item, command, ordinal, context, state);
331
- return true;
332
- }
333
- function hasNativeOutput(item: CodexItem): boolean {
334
- return (
335
- typeof item.aggregated_output === "string" &&
336
- item.aggregated_output.length > 0
337
- );
338
- }
339
- function recoveryFlags(recovered: RecoveredCommandOutput | undefined) {
340
- return {
341
- yieldedChunks: recovered?.chunks ?? 0,
342
- completedCall: recovered?.completedCall === true,
343
- literalCommandCall: recovered?.literalCommandCall === true,
344
- };
345
- }
346
- function successfulReadCommand(item: CodexItem): boolean {
347
- return typeof item.exit_code === "number" && item.status === "completed";
348
- }
349
- async function nativeReadCwd(
350
- cwd: unknown,
351
- workspace: string,
352
- ): Promise<string | null> {
353
- if (cwd === undefined) return workspace;
354
- if (typeof cwd !== "string" || !isAbsolute(cwd)) return null;
355
- return realpath(cwd).catch(() => null);
356
- }
357
- function nativeOutput(item: CodexItem): string {
358
- return typeof item.aggregated_output === "string"
359
- ? item.aggregated_output
360
- : "";
361
- }
362
- function combinedNativeOutput(
363
- item: CodexItem,
364
- recovered: RecoveredCommandOutput | undefined,
365
- ): string {
366
- return (recovered?.output ?? "") + nativeOutput(item);
367
- }
368
- async function completeNativeRead(
369
- item: CodexItem & { cwd?: unknown },
370
- command: string,
371
- ordinal: number,
372
- context: ReadContext,
373
- state: NativeReadState,
374
- ): Promise<void> {
375
- if (!successfulReadCommand(item)) {
376
- state.partial = true;
377
- return;
378
- }
379
- const cwd = await nativeReadCwd(item.cwd, context.workspace);
380
- if (!cwd) {
381
- state.partial = true;
382
- return;
383
- }
384
- const read = await verifiedSkillRead(
385
- {
386
- ...item,
387
- command,
388
- aggregated_output: combinedNativeOutput(
389
- item,
390
- context.recovered.get(ordinal),
391
- ),
392
- },
393
- context.workspace,
394
- context.pluginRoots,
395
- cwd,
396
- );
397
- if (!read) {
398
- state.partial = true;
399
- return;
400
- }
401
- retainNativeRead(state, read, ordinal);
402
- }
403
- function continuingNativeCoverage(
404
- state: NativeReadState,
405
- read: VerifiedRead,
406
- ): Coverage {
407
- const prior = state.attempted.get(read.path);
408
- return prior && !fullCoverage(prior.ranges, prior.bodyLength)
409
- ? prior
410
- : { ranges: [], bodyLength: read.bodyLength };
411
- }
412
- function appendReadCoverage(
413
- state: CoverageState,
414
- read: VerifiedRead,
415
- coverage: Coverage,
416
- ): boolean {
417
- coverage.ranges.push(read.range);
418
- state.attempted.set(read.path, coverage);
419
- return fullCoverage(coverage.ranges, coverage.bodyLength);
420
- }
421
- function retainNativeRead(
422
- state: NativeReadState,
423
- read: VerifiedRead,
424
- ordinal: number,
425
- ): void {
426
- const coverage = continuingNativeCoverage(state, read);
427
- if (appendReadCoverage(state, read, coverage)) {
428
- state.completedReads.push({ skill: read.skill, ordinal });
429
- if (!state.observedSkills.includes(read.skill))
430
- state.observedSkills.push(read.skill);
431
- }
432
- }
433
- function completeCoverage(state: CoverageState): boolean {
434
- return [...state.attempted.values()].every(({ ranges, bodyLength }) =>
435
- fullCoverage(ranges, bodyLength),
436
- );
437
- }
438
- function nativeReadResult(state: NativeReadState): NativeReadDiagnostic {
439
- return {
440
- completeness:
441
- !state.partial && completeCoverage(state) ? "complete" : "partial",
442
- observedSkills: state.observedSkills,
443
- completedReads: state.completedReads,
444
- recoverySources: state.recoverySources,
445
- commandExecutions: state.commandExecutions,
446
- readAttempts: state.readAttempts,
447
- truncated: state.truncated,
448
- };
449
- }
450
-
451
- interface StreamReadState extends CoverageState {
452
- pending: Set<string>;
453
- partial: boolean;
454
- completedTurn: boolean;
455
- }
456
- type RecoveredRead = { command: string; output: string };
457
- function streamReadState(): StreamReadState {
458
- return {
459
- attempted: new Map(),
460
- observedSkills: [],
461
- pending: new Set(),
462
- partial: false,
463
- completedTurn: false,
464
- };
465
- }
466
- function readEvent(line: string): CodexEvent {
467
- const value: unknown = JSON.parse(line);
468
- if (!isRecord(value)) throw new Error("Invalid Codex skill-read event");
469
- return {
470
- type: value.type,
471
- item: isRecord(value.item) ? value.item : undefined,
472
- };
473
- }
474
- function recoveredReadItem(
475
- item: CodexItem | undefined,
476
- recoveredReads: Map<string, RecoveredRead>,
477
- ): CodexItem | undefined {
478
- if (typeof item?.id !== "string") return item;
479
- return correlatedReadItem(item, recoveredReads.get(item.id));
480
- }
481
- function correlatedReadItem(
482
- item: CodexItem,
483
- recovered: RecoveredRead | undefined,
484
- ): CodexItem {
485
- if (
486
- !recovered ||
487
- typeof item.command !== "string" ||
488
- shellPayload(item.command) !== shellPayload(recovered.command)
489
- )
490
- return item;
491
- return { ...item, aggregated_output: recovered.output + nativeOutput(item) };
492
- }
493
- function observeStreamTurn(event: CodexEvent, state: StreamReadState): void {
494
- if (event.type === "turn.completed") state.completedTurn = true;
495
- if (event.type === "turn.failed") state.partial = true;
496
- }
497
- async function streamReadEvent(
498
- event: CodexEvent,
499
- item: CodexItem | undefined,
500
- context: Pick<ReadContext, "workspace" | "pluginRoots">,
501
- state: StreamReadState,
502
- ): Promise<void> {
503
- observeStreamTurn(event, state);
504
- if (!item || item.type !== "command_execution") return;
505
- if (event.type === "item.started") {
506
- pendingStreamRead(item, state);
507
- return;
508
- }
509
- if (event.type !== "item.completed") return;
510
- await completeStreamRead(item, context, state);
511
- }
512
- function pendingStreamRead(item: CodexItem, state: StreamReadState): void {
513
- if (typeof item.command === "string" && item.command.includes("SKILL.md")) {
514
- if (typeof item.id === "string") state.pending.add(item.id);
515
- else state.partial = true;
516
- }
517
- }
518
- function eligibleCompletedRead(
519
- item: CodexItem,
520
- state: StreamReadState,
521
- ): boolean {
522
- if (typeof item.id === "string") state.pending.delete(item.id);
523
- if (typeof item.command !== "string") {
524
- state.partial = true;
525
- return false;
526
- }
527
- if (!item.command.includes("SKILL.md")) return false;
528
- if (!successfulReadCommand(item)) {
529
- state.partial = true;
530
- return false;
531
- }
532
- return true;
533
- }
534
- async function completeStreamRead(
535
- item: CodexItem,
536
- context: Pick<ReadContext, "workspace" | "pluginRoots">,
537
- state: StreamReadState,
538
- ): Promise<void> {
539
- if (!eligibleCompletedRead(item, state)) return;
540
- const read = await verifiedSkillRead(
541
- item,
542
- context.workspace,
543
- context.pluginRoots,
544
- );
545
- if (!read) {
546
- state.partial = true;
547
- return;
548
- }
549
- const coverage = state.attempted.get(read.path) ?? {
550
- ranges: [],
551
- bodyLength: read.bodyLength,
552
- };
553
- retainStreamRead(state, read, coverage);
554
- }
555
-
556
- function retainStreamRead(
557
- state: StreamReadState,
558
- read: VerifiedRead,
559
- coverage: Coverage,
560
- ): void {
561
- if (
562
- appendReadCoverage(state, read, coverage) &&
563
- !state.observedSkills.includes(read.skill)
564
- )
565
- state.observedSkills.push(read.skill);
566
- }
567
- function streamReadCompleteness(
568
- state: StreamReadState,
569
- ): "complete" | "partial" {
570
- return state.completedTurn &&
571
- !state.partial &&
572
- state.pending.size === 0 &&
573
- completeCoverage(state)
574
- ? "complete"
575
- : "partial";
576
- }
577
- function streamReadResult(state: StreamReadState) {
578
- return {
579
- id: "sevro.codex.skill-reads" as const,
580
- completeness: streamReadCompleteness(state),
581
- data: {
582
- method: "skill_file_read_probe",
583
- primarySkill: state.observedSkills[0] ?? null,
584
- observedSkills: state.observedSkills,
585
- },
586
- };
587
- }
588
-
589
- /** Retain only ordered mounted-skill names, never commands or skill bodies. */
590
- export async function codexSkillReadObservation(
591
- stream: string,
592
- workspace: string,
593
- installedPluginRoots: string[] = [],
594
- recoveredReads: Map<string, RecoveredRead> = new Map(),
595
- ) {
596
- const state = streamReadState();
597
- const canonicalWorkspace = await realpath(workspace);
598
- const pluginRoots = await Promise.all(
599
- installedPluginRoots.map((root) => realpath(root)),
600
- );
601
- const context = { workspace: canonicalWorkspace, pluginRoots };
602
- for (const line of stream.split("\n")) {
603
- if (!line.trim()) continue;
604
- const event = readEvent(line);
605
- const item = recoveredReadItem(event.item, recoveredReads);
606
- await streamReadEvent(event, item, context, state);
607
- }
608
- return streamReadResult(state);
609
- }