@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
@@ -0,0 +1,516 @@
1
+ import { createRequire } from "node:module";
2
+ import type ts from "typescript";
3
+ import { isRecord, isUnknownArray } from "../value-guards";
4
+
5
+ const require = createRequire(import.meta.url);
6
+ let parser: typeof ts | undefined;
7
+ function typescript(): typeof ts {
8
+ return (parser ??= require("typescript") as typeof ts);
9
+ }
10
+
11
+ interface Entry {
12
+ ordinal: number;
13
+ payload: Record<string, unknown>;
14
+ }
15
+ interface SubmittedCall {
16
+ index: number;
17
+ namespace: "functions" | "collaboration" | "other";
18
+ name: string;
19
+ }
20
+ export interface ExecCallReceipt {
21
+ ordinal: number;
22
+ completeness: "complete" | "partial";
23
+ calls: SubmittedCall[];
24
+ inspectionReason?:
25
+ | "unsupported_source"
26
+ | "malformed_source"
27
+ | "source_limit"
28
+ | "foreign_namespace"
29
+ | null;
30
+ execution?: "completed" | "failed" | "incomplete";
31
+ output?: "complete" | "truncated" | "unavailable";
32
+ }
33
+
34
+ function toolNamespace(name: string): SubmittedCall["namespace"] {
35
+ if (
36
+ [
37
+ "spawn_agent",
38
+ "list_agents",
39
+ "wait_agent",
40
+ "followup_task",
41
+ "send_message",
42
+ "interrupt_agent",
43
+ ].includes(name)
44
+ )
45
+ return "collaboration";
46
+ if (
47
+ [
48
+ "create_goal",
49
+ "get_goal",
50
+ "update_goal",
51
+ "exec_command",
52
+ "write_stdin",
53
+ ].includes(name)
54
+ )
55
+ return "functions";
56
+ return "other";
57
+ }
58
+ function toolName(node: ts.Expression): string | undefined {
59
+ const ts = typescript();
60
+ if (!ts.isPropertyAccessExpression(node)) return undefined;
61
+ if (!ts.isIdentifier(node.expression) || node.expression.text !== "tools")
62
+ return undefined;
63
+ return /^[A-Za-z_][A-Za-z0-9_]{0,63}$/.test(node.name.text)
64
+ ? node.name.text
65
+ : undefined;
66
+ }
67
+ function literal(node: ts.Expression): boolean {
68
+ const ts = typescript();
69
+ if (ts.isStringLiteralLike(node) || ts.isNumericLiteral(node)) return true;
70
+ return [
71
+ ts.SyntaxKind.TrueKeyword,
72
+ ts.SyntaxKind.FalseKeyword,
73
+ ts.SyntaxKind.NullKeyword,
74
+ ].includes(node.kind);
75
+ }
76
+ function member(node: ts.Expression, owner: string, names: string[]): boolean {
77
+ const ts = typescript();
78
+ return (
79
+ ts.isPropertyAccessExpression(node) &&
80
+ ts.isIdentifier(node.expression) &&
81
+ node.expression.text === owner &&
82
+ names.includes(node.name.text)
83
+ );
84
+ }
85
+ function localName(node: ts.BindingName): string | undefined {
86
+ const ts = typescript();
87
+ if (!ts.isIdentifier(node)) return undefined;
88
+ return [
89
+ "tools",
90
+ "text",
91
+ "store",
92
+ "Promise",
93
+ "load",
94
+ "JSON",
95
+ "String",
96
+ ].includes(node.text)
97
+ ? undefined
98
+ : node.text;
99
+ }
100
+ class Submission {
101
+ readonly calls: SubmittedCall[] = [];
102
+ private readonly locals = new Set<string>();
103
+ private readonly collections = new Set<string>();
104
+ private readonly indices = new Set<string>();
105
+ private nodes = 0;
106
+ expression(node: ts.Expression): boolean {
107
+ const ts = typescript();
108
+ if (++this.nodes > 2048) return false;
109
+ switch (true) {
110
+ case literal(node):
111
+ return true;
112
+ case ts.isTemplateExpression(node):
113
+ return node.templateSpans.every((span) =>
114
+ this.expression(span.expression),
115
+ );
116
+ case ts.isBinaryExpression(node):
117
+ return this.composition(node);
118
+ case ts.isAwaitExpression(node):
119
+ return this.expression(node.expression);
120
+ case ts.isObjectLiteralExpression(node):
121
+ return node.properties.every((property) => this.property(property));
122
+ case ts.isArrayLiteralExpression(node):
123
+ return node.elements.every((element) => this.expression(element));
124
+ case ts.isIdentifier(node):
125
+ return this.locals.has(node.text);
126
+ case ts.isPropertyAccessExpression(node):
127
+ return this.localProperty(node);
128
+ case ts.isCallExpression(node):
129
+ return this.call(node);
130
+ case ts.isConditionalExpression(node):
131
+ return [node.condition, node.whenTrue, node.whenFalse].every((part) =>
132
+ this.outputExpression(part),
133
+ );
134
+ case ts.isElementAccessExpression(node):
135
+ return (
136
+ this.collection(node.expression) &&
137
+ typescript().isIdentifier(node.argumentExpression) &&
138
+ this.indices.has(node.argumentExpression.text)
139
+ );
140
+ default:
141
+ return false;
142
+ }
143
+ }
144
+ private property(node: ts.ObjectLiteralElementLike): boolean {
145
+ const ts = typescript();
146
+ if (ts.isSpreadAssignment(node))
147
+ return this.outputExpression(node.expression);
148
+ if (ts.isShorthandPropertyAssignment(node)) return this.shorthand(node);
149
+ if (!ts.isPropertyAssignment(node) || ts.isComputedPropertyName(node.name))
150
+ return false;
151
+ return this.expression(node.initializer);
152
+ }
153
+ private shorthand(node: ts.ShorthandPropertyAssignment): boolean {
154
+ return !node.objectAssignmentInitializer && this.expression(node.name);
155
+ }
156
+ private composition(node: ts.BinaryExpression): boolean {
157
+ if (
158
+ [
159
+ typescript().SyntaxKind.EqualsEqualsEqualsToken,
160
+ typescript().SyntaxKind.ExclamationEqualsEqualsToken,
161
+ ].includes(node.operatorToken.kind)
162
+ )
163
+ return (
164
+ this.outputExpression(node.left) && this.outputExpression(node.right)
165
+ );
166
+ return (
167
+ node.operatorToken.kind === typescript().SyntaxKind.PlusToken &&
168
+ this.expression(node.left) &&
169
+ this.expression(node.right)
170
+ );
171
+ }
172
+ private call(node: ts.CallExpression): boolean {
173
+ if (this.outputEach(node)) return true;
174
+ if (!node.arguments.every((argument) => this.expression(argument)))
175
+ return false;
176
+ return helperCall(node.expression) || this.retainToolCall(node.expression);
177
+ }
178
+ private retainToolCall(expression: ts.Expression): boolean {
179
+ const name = toolName(expression);
180
+ if (!name || this.calls.length >= 128) return false;
181
+ this.calls.push({
182
+ index: this.calls.length,
183
+ namespace: toolNamespace(name),
184
+ name,
185
+ });
186
+ return name !== "exec";
187
+ }
188
+ statement(node: ts.Statement): boolean {
189
+ const ts = typescript();
190
+ if (ts.isVariableStatement(node))
191
+ return this.declarations(node.declarationList);
192
+ if (ts.isForOfStatement(node)) return this.outputLoop(node);
193
+ if (ts.isForStatement(node)) return this.numericOutputLoop(node);
194
+ return ts.isExpressionStatement(node) && this.expression(node.expression);
195
+ }
196
+ private localProperty(node: ts.PropertyAccessExpression): boolean {
197
+ return (
198
+ this.expression(node.expression) &&
199
+ !["constructor", "__proto__", "prototype"].includes(node.name.text)
200
+ );
201
+ }
202
+ private declarations(list: ts.VariableDeclarationList): boolean {
203
+ const ts = typescript();
204
+ if (!(list.flags & ts.NodeFlags.Const)) return false;
205
+ return list.declarations.every((declaration) =>
206
+ this.declaration(declaration),
207
+ );
208
+ }
209
+ private declaration(node: ts.VariableDeclaration): boolean {
210
+ if (!node.initializer) return false;
211
+ const names = this.bindingNames(node.name, node.initializer);
212
+ if (!names) return false;
213
+ if (!this.expression(node.initializer)) return false;
214
+ names.forEach((name) => this.locals.add(name));
215
+ this.retainCollection(node.name, node.initializer, names);
216
+ return true;
217
+ }
218
+ private retainCollection(
219
+ name: ts.BindingName,
220
+ initializer: ts.Expression,
221
+ names: string[],
222
+ ): void {
223
+ if (localName(name) && collectionExpression(initializer))
224
+ names.forEach((name) => this.collections.add(name));
225
+ }
226
+ private bindingNames(
227
+ node: ts.BindingName,
228
+ initializer: ts.Expression,
229
+ ): string[] | undefined {
230
+ const ts = typescript();
231
+ if (ts.isIdentifier(node)) {
232
+ const name = this.declaredName(node);
233
+ return name ? [name] : undefined;
234
+ }
235
+ if (!ts.isArrayBindingPattern(node) || !collectionExpression(initializer))
236
+ return undefined;
237
+ return this.batchNames(node);
238
+ }
239
+ private batchNames(node: ts.ArrayBindingPattern): string[] | undefined {
240
+ const names = node.elements.map((element) => this.batchBinding(element));
241
+ if (!names.every((name): name is string => name !== undefined))
242
+ return undefined;
243
+ return new Set(names).size === names.length ? names : undefined;
244
+ }
245
+ private batchBinding(node: ts.ArrayBindingElement): string | undefined {
246
+ const ts = typescript();
247
+ if (!ts.isBindingElement(node)) return undefined;
248
+ if (node.dotDotDotToken || node.initializer || node.propertyName)
249
+ return undefined;
250
+ return this.declaredName(node.name);
251
+ }
252
+ private declaredName(node: ts.BindingName): string | undefined {
253
+ const name = localName(node);
254
+ return name && !this.locals.has(name) ? name : undefined;
255
+ }
256
+ private outputEach(node: ts.CallExpression): boolean {
257
+ const ts = typescript();
258
+ const [callback] = node.arguments;
259
+ if (
260
+ node.arguments.length !== 1 ||
261
+ !callback ||
262
+ !ts.isIdentifier(callback) ||
263
+ callback.text !== "text"
264
+ )
265
+ return false;
266
+ return [...this.collections].some((name) =>
267
+ member(node.expression, name, ["forEach"]),
268
+ );
269
+ }
270
+ private outputLoop(node: ts.ForOfStatement): boolean {
271
+ const ts = typescript();
272
+ if (!this.collection(node.expression)) return false;
273
+ if (!ts.isVariableDeclarationList(node.initializer)) return false;
274
+ const name = loopVariable(node.initializer);
275
+ if (!name || this.locals.has(name)) return false;
276
+ this.locals.add(name);
277
+ const valid = this.outputBody(node.statement);
278
+ this.locals.delete(name);
279
+ return valid;
280
+ }
281
+ private outputExpression(node: ts.Expression): boolean {
282
+ const count = this.calls.length;
283
+ return this.expression(node) && this.calls.length === count;
284
+ }
285
+ private numericOutputLoop(node: ts.ForStatement): boolean {
286
+ const name = numericLoopVariable(node);
287
+ if (!name || this.locals.has(name)) return false;
288
+ const condition = node.condition as ts.BinaryExpression;
289
+ const bound = condition.right as ts.PropertyAccessExpression;
290
+ if (!this.collection(bound.expression)) return false;
291
+ this.locals.add(name);
292
+ this.indices.add(name);
293
+ const valid = this.outputBody(node.statement);
294
+ this.locals.delete(name);
295
+ this.indices.delete(name);
296
+ return valid;
297
+ }
298
+ private outputBody(node: ts.Statement): boolean {
299
+ const ts = typescript();
300
+ if (ts.isBlock(node))
301
+ return node.statements.every((statement) => this.outputBody(statement));
302
+ if (ts.isIfStatement(node)) return this.outputConditional(node);
303
+ return (
304
+ ts.isExpressionStatement(node) && this.outputExpression(node.expression)
305
+ );
306
+ }
307
+ private outputConditional(node: ts.IfStatement): boolean {
308
+ return (
309
+ this.outputExpression(node.expression) &&
310
+ this.outputBody(node.thenStatement) &&
311
+ (!node.elseStatement || this.outputBody(node.elseStatement))
312
+ );
313
+ }
314
+ private collection(node: ts.Expression): boolean {
315
+ const ts = typescript();
316
+ return ts.isIdentifier(node) && this.collections.has(node.text);
317
+ }
318
+ }
319
+ function helperCall(node: ts.Expression): boolean {
320
+ const ts = typescript();
321
+ if (ts.isIdentifier(node) && ["text", "store", "String"].includes(node.text))
322
+ return true;
323
+ return (
324
+ member(node, "Promise", ["all", "allSettled"]) ||
325
+ member(node, "JSON", ["stringify"])
326
+ );
327
+ }
328
+ function namedIdentifier(
329
+ node: ts.Expression | undefined,
330
+ name: string,
331
+ ): boolean {
332
+ return (
333
+ node !== undefined && typescript().isIdentifier(node) && node.text === name
334
+ );
335
+ }
336
+ function collectionExpression(node: ts.Expression): boolean {
337
+ const ts = typescript();
338
+ if (!ts.isAwaitExpression(node) || !ts.isCallExpression(node.expression))
339
+ return false;
340
+ return member(node.expression.expression, "Promise", ["all", "allSettled"]);
341
+ }
342
+ function loopVariable(list: ts.VariableDeclarationList): string | undefined {
343
+ const ts = typescript();
344
+ if (!(list.flags & ts.NodeFlags.Const) || list.declarations.length !== 1)
345
+ return undefined;
346
+ const declaration = list.declarations[0];
347
+ return declaration && !declaration.initializer
348
+ ? localName(declaration.name)
349
+ : undefined;
350
+ }
351
+ function numericLoopVariable(node: ts.ForStatement): string | undefined {
352
+ const ts = typescript();
353
+ if (!node.initializer || !ts.isVariableDeclarationList(node.initializer))
354
+ return undefined;
355
+ const name = numericBinding(node.initializer);
356
+ return name && numericLoopBounds(node, name) ? name : undefined;
357
+ }
358
+ function numericBinding(list: ts.VariableDeclarationList): string | undefined {
359
+ if (
360
+ !(list.flags & typescript().NodeFlags.Let) ||
361
+ list.declarations.length !== 1
362
+ )
363
+ return undefined;
364
+ const declaration = list.declarations[0];
365
+ return declaration && zeroInitializer(declaration.initializer)
366
+ ? localName(declaration.name)
367
+ : undefined;
368
+ }
369
+ function zeroInitializer(node: ts.Expression | undefined): boolean {
370
+ return (
371
+ node !== undefined &&
372
+ typescript().isNumericLiteral(node) &&
373
+ node.text === "0"
374
+ );
375
+ }
376
+ function numericLoopBounds(node: ts.ForStatement, name: string): boolean {
377
+ const ts = typescript();
378
+ const condition = node.condition;
379
+ if (!condition || !ts.isBinaryExpression(condition)) return false;
380
+ return (
381
+ numericCondition(condition, name) &&
382
+ numericIncrement(node.incrementor, name)
383
+ );
384
+ }
385
+ function numericCondition(node: ts.BinaryExpression, name: string): boolean {
386
+ return (
387
+ node.operatorToken.kind === typescript().SyntaxKind.LessThanToken &&
388
+ namedIdentifier(node.left, name) &&
389
+ lengthProperty(node.right)
390
+ );
391
+ }
392
+ function lengthProperty(node: ts.Expression): boolean {
393
+ return (
394
+ typescript().isPropertyAccessExpression(node) && node.name.text === "length"
395
+ );
396
+ }
397
+ function numericIncrement(
398
+ node: ts.Expression | undefined,
399
+ name: string,
400
+ ): boolean {
401
+ return (
402
+ node !== undefined &&
403
+ typescript().isPostfixUnaryExpression(node) &&
404
+ node.operator === typescript().SyntaxKind.PlusPlusToken &&
405
+ namedIdentifier(node.operand, name)
406
+ );
407
+ }
408
+ function inspectedSource(code: unknown): {
409
+ submission?: Submission;
410
+ reason: ExecCallReceipt["inspectionReason"];
411
+ } {
412
+ if (typeof code !== "string") return { reason: "unsupported_source" };
413
+ if (code.length > 65536) return { reason: "source_limit" };
414
+ const ts = typescript();
415
+ const source = ts.createSourceFile(
416
+ "submitted.js",
417
+ code,
418
+ ts.ScriptTarget.Latest,
419
+ false,
420
+ ts.ScriptKind.JS,
421
+ );
422
+ const diagnostics = (
423
+ source as ts.SourceFile & { parseDiagnostics: readonly ts.Diagnostic[] }
424
+ ).parseDiagnostics;
425
+ if (diagnostics.length !== 0) return { reason: "malformed_source" };
426
+ const submission = new Submission();
427
+ return source.statements.every((statement) => submission.statement(statement))
428
+ ? { submission, reason: null }
429
+ : { reason: "unsupported_source" };
430
+ }
431
+ function outputBlocks(output: unknown): unknown[] {
432
+ if (isUnknownArray(output)) return output;
433
+ if (typeof output !== "string") return [];
434
+ try {
435
+ const blocks: unknown = JSON.parse(output);
436
+ return isUnknownArray(blocks) ? blocks : [];
437
+ } catch {
438
+ return [];
439
+ }
440
+ }
441
+ function completeTextBlock(block: unknown): boolean {
442
+ if (!isRecord(block) || !["text", "input_text"].includes(String(block.type)))
443
+ return false;
444
+ return typeof block.text === "string";
445
+ }
446
+ function completedHeader(block: unknown): boolean {
447
+ return (
448
+ isRecord(block) &&
449
+ typeof block.text === "string" &&
450
+ /^Script completed\nWall time [0-9]+(?:\.[0-9]+)? seconds\nOutput:\n$/.test(
451
+ block.text,
452
+ )
453
+ );
454
+ }
455
+ function submissionOutput(entry: Entry, entries: Entry[]): unknown[] {
456
+ const id = entry.payload.call_id;
457
+ if (typeof id !== "string") return [];
458
+ const matches = entries.filter((other) => other.payload.call_id === id);
459
+ if (matches.length !== 2) return [];
460
+ const result = matches.find(
461
+ (other) => other.payload.type === "custom_tool_call_output",
462
+ );
463
+ return result && result.ordinal > entry.ordinal
464
+ ? outputBlocks(result.payload.output)
465
+ : [];
466
+ }
467
+ function outputEvidence(
468
+ blocks: unknown[],
469
+ ): Pick<ExecCallReceipt, "execution" | "output"> {
470
+ if (!blocks.length || !blocks.every(completeTextBlock))
471
+ return { execution: "incomplete", output: "unavailable" };
472
+ const header = blocks[0] as { text: string };
473
+ const execution = executionStatus(header);
474
+ const truncated = blocks.some((block) =>
475
+ (block as { text: string }).text.includes("Warning: truncated output"),
476
+ );
477
+ return { execution, output: truncated ? "truncated" : "complete" };
478
+ }
479
+ function executionStatus(header: {
480
+ text: string;
481
+ }): ExecCallReceipt["execution"] {
482
+ if (completedHeader(header)) return "completed";
483
+ return /^Script failed\b/.test(header.text) ? "failed" : "incomplete";
484
+ }
485
+ function nativeExecNamespace(namespace: unknown): boolean {
486
+ return namespace === undefined || namespace === "functions";
487
+ }
488
+ /** Static inspection only: submitted code is never executed by the observer. */
489
+ export function codexExecCallReceipts(entries: Entry[]): ExecCallReceipt[] {
490
+ return entries
491
+ .filter(
492
+ ({ payload }) =>
493
+ payload.type === "custom_tool_call" && payload.name === "exec",
494
+ )
495
+ .map((entry) => ({
496
+ ordinal: entry.ordinal,
497
+ ...inspectionEvidence(entry),
498
+ ...outputEvidence(submissionOutput(entry, entries)),
499
+ }));
500
+ }
501
+ function inspectionEvidence(
502
+ entry: Entry,
503
+ ): Pick<ExecCallReceipt, "completeness" | "calls" | "inspectionReason"> {
504
+ if (!nativeExecNamespace(entry.payload.namespace))
505
+ return {
506
+ completeness: "partial",
507
+ calls: [],
508
+ inspectionReason: "foreign_namespace",
509
+ };
510
+ const inspected = inspectedSource(entry.payload.input);
511
+ return {
512
+ completeness: inspected.submission ? "complete" : "partial",
513
+ calls: inspected.submission?.calls ?? [],
514
+ inspectionReason: inspected.reason,
515
+ };
516
+ }
@@ -1,10 +1,12 @@
1
1
  import type { Dirent } from "node:fs";
2
2
  import { readFile, readdir, realpath, stat } from "node:fs/promises";
3
- import { isAbsolute, join, relative, sep } from "node:path";
4
- import { fixtureParts } from "../preparation";
3
+ import { join, relative, sep } from "node:path";
4
+ import { insideRoot as inside } from "../paths";
5
+ import { fixtureParts } from "../fixtures/preparation";
5
6
  import type { HostAdapter } from "../engine";
6
7
  import { runCodexProcess } from "./codex-process";
7
8
  import { isRecord, isUnknownArray } from "../value-guards";
9
+ import type { HostDiagnostics } from "../host-diagnostics";
8
10
  type Request = Parameters<HostAdapter["run"]>[0];
9
11
  type Declaration = NonNullable<Request["codexMarketplace"]>;
10
12
  interface InstallContext {
@@ -12,15 +14,9 @@ interface InstallContext {
12
14
  env: Record<string, string>;
13
15
  pluginCacheRoot: string;
14
16
  request: Request;
17
+ diagnostics: HostDiagnostics;
15
18
  }
16
19
 
17
- function inside(root: string, path: string): boolean {
18
- const child = relative(root, path);
19
- return (
20
- child === "" ||
21
- (child !== ".." && !child.startsWith(`..${sep}`) && !isAbsolute(child))
22
- );
23
- }
24
20
  function validPluginNames(declaration: Declaration): boolean {
25
21
  const names = declaration.pluginNames;
26
22
  return (
@@ -174,20 +170,24 @@ function verifyPackageEntry(
174
170
  }
175
171
 
176
172
  async function pluginCommand(context: InstallContext, args: string[]) {
177
- return runCodexProcess({
173
+ const result = await runCodexProcess({
174
+ diagnostics: context.diagnostics,
178
175
  argv: [context.binary, "plugin", ...args, "--json"],
179
176
  cwd: context.request.workspace,
180
177
  env: context.env,
181
178
  timeoutMs: 30_000,
182
179
  signal: context.request.signal,
183
180
  });
181
+ return result;
184
182
  }
185
183
  export async function installCodexPlugins(
186
184
  context: InstallContext,
187
185
  ): Promise<string[]> {
188
186
  const declaration = context.request.codexMarketplace;
189
187
  if (!declaration) return [];
188
+ context.diagnostics.stage = "marketplace-validation";
190
189
  const root = await marketplaceRoot(context.request.workspace, declaration);
190
+ context.diagnostics.stage = "marketplace-install";
191
191
  const added = await pluginCommand(context, ["marketplace", "add", root]);
192
192
  if (added.code !== 0)
193
193
  throw new Error("Codex local marketplace installation failed");
@@ -201,12 +201,14 @@ async function installPlugin(
201
201
  declaration: Declaration,
202
202
  pluginName: string,
203
203
  ): Promise<string> {
204
+ context.diagnostics.stage = "plugin-install";
204
205
  const installed = await pluginCommand(context, [
205
206
  "add",
206
207
  `${pluginName}@${declaration.marketplaceName}`,
207
208
  ]);
208
209
  if (installed.code !== 0)
209
210
  throw new Error("Codex local plugin installation failed");
211
+ context.diagnostics.stage = "plugin-receipt";
210
212
  const path = await installedPluginPath(
211
213
  installed.out,
212
214
  declaration.marketplaceName,