@hassangameryt/freeflow 0.4.0 → 0.6.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 (285) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +2 -1
  3. package/.codex-plugin/plugin.json +2 -1
  4. package/README.md +150 -155
  5. package/capabilities/cognitive-routing/SKILL.md +229 -0
  6. package/capabilities/context-virtualization/SKILL.md +103 -0
  7. package/capabilities/conversation-history/SKILL.md +53 -0
  8. package/command-surface.json +17 -13
  9. package/hooks/freeflow-runtime-context.mjs +393 -159
  10. package/hooks/hooks.json +26 -1
  11. package/hooks/tests/check-runtime-context-hook.sh +223 -54
  12. package/package.json +25 -16
  13. package/pi-extension/dist/cognitive-routing/commands.js +96 -0
  14. package/pi-extension/dist/cognitive-routing/config.js +236 -0
  15. package/pi-extension/dist/cognitive-routing/controller.js +862 -0
  16. package/pi-extension/dist/cognitive-routing/history.js +439 -0
  17. package/pi-extension/dist/cognitive-routing/host.js +126 -0
  18. package/pi-extension/dist/cognitive-routing/runtime.js +43 -0
  19. package/pi-extension/dist/cognitive-routing/tool.js +211 -0
  20. package/pi-extension/dist/cognitive-routing/types.js +6 -0
  21. package/pi-extension/dist/context-virtualization/commands.js +89 -0
  22. package/pi-extension/dist/context-virtualization/projector.js +42 -0
  23. package/pi-extension/dist/context-virtualization/runtime.js +356 -0
  24. package/pi-extension/dist/context-virtualization/state.js +57 -0
  25. package/pi-extension/dist/context-virtualization/types.js +10 -0
  26. package/pi-extension/dist/conversation-history/passages.js +66 -0
  27. package/pi-extension/dist/conversation-history/ranking.js +105 -0
  28. package/pi-extension/dist/conversation-history/runtime.js +226 -0
  29. package/pi-extension/dist/conversation-history/search.js +56 -0
  30. package/pi-extension/dist/conversation-history/types.js +1 -0
  31. package/pi-extension/dist/freeflow-context/resolver.js +120 -0
  32. package/pi-extension/dist/freeflow-context/runtime.js +80 -0
  33. package/pi-extension/dist/freeflow-context/source-projector.js +95 -0
  34. package/pi-extension/dist/freeflow-context/tool.js +308 -0
  35. package/pi-extension/dist/freeflow-context/types.js +12 -0
  36. package/pi-extension/dist/freeflow-context/visibility.js +28 -0
  37. package/pi-extension/dist/index.js +516 -98
  38. package/pi-extension/dist/runtime/runtime-context.js +318 -271
  39. package/pi-extension/dist/runtime/runtime-identity.js +5 -0
  40. package/pi-extension/dist/settings/settings-tui.js +78 -0
  41. package/pi-extension/dist/settings/settings-ui.js +593 -563
  42. package/pi-extension/dist/ui/text-component.js +57 -0
  43. package/pi-extension/src/cognitive-routing/commands.ts +119 -0
  44. package/pi-extension/src/cognitive-routing/config.ts +307 -0
  45. package/pi-extension/src/cognitive-routing/controller.ts +1118 -0
  46. package/pi-extension/src/cognitive-routing/history.ts +638 -0
  47. package/pi-extension/src/cognitive-routing/host.ts +172 -0
  48. package/pi-extension/src/cognitive-routing/runtime.ts +57 -0
  49. package/pi-extension/src/cognitive-routing/tool.ts +272 -0
  50. package/pi-extension/src/cognitive-routing/types.ts +89 -0
  51. package/pi-extension/src/context-virtualization/commands.ts +111 -0
  52. package/pi-extension/src/context-virtualization/projector.ts +62 -0
  53. package/pi-extension/src/context-virtualization/runtime.ts +406 -0
  54. package/pi-extension/src/context-virtualization/state.ts +79 -0
  55. package/pi-extension/src/context-virtualization/types.ts +92 -0
  56. package/pi-extension/src/conversation-history/passages.ts +81 -0
  57. package/pi-extension/src/conversation-history/ranking.ts +155 -0
  58. package/pi-extension/src/conversation-history/runtime.ts +250 -0
  59. package/pi-extension/src/conversation-history/search.ts +79 -0
  60. package/pi-extension/src/conversation-history/types.ts +31 -0
  61. package/pi-extension/src/freeflow-context/resolver.ts +139 -0
  62. package/pi-extension/src/freeflow-context/runtime.ts +100 -0
  63. package/pi-extension/src/freeflow-context/source-projector.ts +106 -0
  64. package/pi-extension/src/freeflow-context/tool.ts +360 -0
  65. package/pi-extension/src/freeflow-context/types.ts +70 -0
  66. package/pi-extension/src/freeflow-context/visibility.ts +53 -0
  67. package/pi-extension/src/index.ts +553 -101
  68. package/pi-extension/src/runtime/runtime-context.ts +339 -281
  69. package/pi-extension/src/runtime/runtime-identity.ts +8 -0
  70. package/pi-extension/src/settings/settings-tui.ts +93 -0
  71. package/pi-extension/src/settings/settings-ui.ts +700 -613
  72. package/pi-extension/src/ui/text-component.ts +66 -0
  73. package/pi-extension/tests/cognitive-routing/commands.test.js +134 -0
  74. package/pi-extension/tests/cognitive-routing/controller.test.js +1099 -0
  75. package/pi-extension/tests/cognitive-routing/extension-lifecycle.test.js +1071 -0
  76. package/pi-extension/tests/cognitive-routing/history.test.js +274 -0
  77. package/pi-extension/tests/cognitive-routing/host-fixture.js +4 -0
  78. package/pi-extension/tests/cognitive-routing/host-identity.test.js +63 -0
  79. package/pi-extension/tests/cognitive-routing/profile-resolution.test.js +236 -0
  80. package/pi-extension/tests/cognitive-routing/tool.test.js +230 -0
  81. package/pi-extension/tests/context-virtualization/extension.test.js +749 -0
  82. package/pi-extension/tests/context-virtualization/projector.test.js +110 -0
  83. package/pi-extension/tests/context-virtualization/runtime.test.js +216 -0
  84. package/pi-extension/tests/conversation-history/runtime.test.js +98 -0
  85. package/pi-extension/tests/conversation-history/search.test.js +97 -0
  86. package/pi-extension/tests/integration/pi-extension.test.js +928 -2047
  87. package/pi-extension/tests/integration/prompt-architecture.test.js +305 -0
  88. package/runtime/prompts/cognitive-routing.md +9 -0
  89. package/runtime/prompts/context-virtualization.md +3 -0
  90. package/runtime/prompts/conversation-history.md +3 -0
  91. package/runtime/prompts/core.md +13 -0
  92. package/runtime/prompts/interaction-contract.md +7 -0
  93. package/runtime/prompts/skills.md +65 -0
  94. package/skills/action-selection/SKILL.md +91 -0
  95. package/skills/bypass/SKILL.md +3 -2
  96. package/skills/commit-work/SKILL.md +1 -1
  97. package/skills/commit-work/references/staging-decisions.md +1 -1
  98. package/skills/decision-gate/SKILL.md +1 -1
  99. package/skills/design-for-depth/SKILL.md +14 -33
  100. package/skills/design-for-depth/references/design-pressure-signals.md +1 -1
  101. package/skills/design-for-depth/references/interface-design-loop.md +1 -1
  102. package/skills/design-for-depth/references/software-design-philosophy.md +1 -1
  103. package/skills/diagnose-failure/SKILL.md +20 -14
  104. package/skills/diagnose-failure/references/diagnostic-loop-catalog.md +1 -3
  105. package/skills/diagnose-failure/references/{flaky-and-performance.md → flaky-and-performance-diagnosis.md} +1 -1
  106. package/skills/discuss/SKILL.md +34 -22
  107. package/skills/discuss/references/discussion-continuity.md +71 -0
  108. package/skills/evaluate-skill/SKILL.md +5 -5
  109. package/skills/evaluate-skill/references/{definition-schema.md → evaluation-definition-schema.md} +42 -2
  110. package/skills/evaluate-skill/references/execution-and-evidence.md +8 -3
  111. package/skills/evaluate-skill/scripts/lib/definitions.mjs +114 -3
  112. package/skills/evaluate-skill/scripts/lib/grade-interaction.mjs +156 -1
  113. package/skills/evaluate-skill/scripts/lib/grade.mjs +9 -1
  114. package/skills/evaluate-skill/scripts/lib/pi.mjs +147 -32
  115. package/skills/evaluate-skill/scripts/lib/rpc.mjs +4 -4
  116. package/skills/evaluate-skill/scripts/lib/runner.mjs +36 -14
  117. package/skills/evaluate-skill/scripts/lib/sandbox.mjs +92 -0
  118. package/skills/evaluate-skill/scripts/pi-guard.mjs +1 -1
  119. package/skills/evaluate-skill/scripts/pi-observer.mjs +66 -0
  120. package/skills/evaluate-skill/scripts/skill-eval.mjs +6 -5
  121. package/skills/execute-work/SKILL.md +18 -15
  122. package/skills/execute-work/references/code-practices.md +1 -1
  123. package/skills/execute-work/references/execute-work-edges.md +42 -0
  124. package/skills/finish-branch/SKILL.md +2 -2
  125. package/skills/finish-branch/references/integration-options.md +1 -1
  126. package/skills/handoff/SKILL.md +3 -3
  127. package/skills/launch-work/SKILL.md +1 -1
  128. package/skills/launch-work/references/launch-readiness.md +2 -2
  129. package/skills/migration-work/SKILL.md +2 -2
  130. package/skills/migration-work/references/migration-lifecycle.md +1 -1
  131. package/skills/mode-contract/SKILL.md +32 -10
  132. package/skills/release-work/SKILL.md +1 -1
  133. package/skills/release-work/references/release-evidence.md +1 -1
  134. package/skills/review-artifact/SKILL.md +28 -26
  135. package/skills/review-work/SKILL.md +28 -26
  136. package/skills/review-work/references/{reviewer-prompt.md → independent-work-reviewer-contract.md} +1 -1
  137. package/skills/review-work/references/security-risk-lens.md +1 -1
  138. package/skills/setup-freeflow/SKILL.md +6 -10
  139. package/skills/setup-freeflow/references/activation-contract.md +5 -10
  140. package/skills/setup-freeflow/references/host-setup.md +3 -7
  141. package/skills/simplify-code/SKILL.md +1 -1
  142. package/skills/simplify-code/references/simplification-patterns.md +1 -1
  143. package/skills/tdd/SKILL.md +18 -38
  144. package/skills/tdd/references/test-design.md +2 -2
  145. package/skills/track-work/SKILL.md +362 -58
  146. package/skills/track-work/scripts/lib/codec.mjs +570 -0
  147. package/skills/track-work/scripts/lib/commands.mjs +568 -0
  148. package/skills/track-work/scripts/lib/contract.mjs +303 -0
  149. package/skills/track-work/scripts/lib/model.mjs +506 -0
  150. package/skills/track-work/scripts/lib/operations.mjs +965 -0
  151. package/skills/track-work/scripts/lib/record-store.mjs +264 -0
  152. package/skills/track-work/scripts/lib/result.mjs +37 -0
  153. package/skills/track-work/scripts/lib/rewrites.mjs +740 -0
  154. package/skills/track-work/scripts/lib/schema.mjs +277 -0
  155. package/skills/track-work/scripts/lib/source-inventory.mjs +162 -0
  156. package/skills/track-work/scripts/lib/views.mjs +443 -0
  157. package/skills/track-work/scripts/lib/workspace.mjs +134 -0
  158. package/skills/track-work/scripts/working-record.mjs +122 -0
  159. package/skills/verify-work/SKILL.md +14 -2
  160. package/skills/verify-work/references/browser-runtime-evidence.md +1 -1
  161. package/skills/verify-work/references/integration-evidence.md +1 -1
  162. package/skills/verify-work/references/performance-evidence.md +1 -1
  163. package/skills/workflow/SKILL.md +105 -53
  164. package/skills/workflow/references/domain-skill-composition.md +22 -42
  165. package/skills/write-plan/SKILL.md +1 -1
  166. package/skills/write-skill/SKILL.md +3 -2
  167. package/skills/write-skill/references/progressive-disclosure.md +17 -3
  168. package/pi-extension/dist/output-router/host-producer-identification.js +0 -232
  169. package/pi-extension/dist/output-router/native-safety-net.js +0 -216
  170. package/pi-extension/dist/output-router/observed-tool-routing.js +0 -122
  171. package/pi-extension/dist/output-router/renderers.js +0 -1023
  172. package/pi-extension/dist/output-router/router-tools.js +0 -554
  173. package/pi-extension/dist/output-router/schemas.js +0 -383
  174. package/pi-extension/dist/output-router/status.js +0 -744
  175. package/pi-extension/dist/output-router/utils.js +0 -502
  176. package/pi-extension/src/output-router/host-producer-identification.ts +0 -256
  177. package/pi-extension/src/output-router/native-safety-net.ts +0 -248
  178. package/pi-extension/src/output-router/observed-tool-routing.ts +0 -134
  179. package/pi-extension/src/output-router/renderers.ts +0 -1108
  180. package/pi-extension/src/output-router/router-tools.ts +0 -579
  181. package/pi-extension/src/output-router/schemas.ts +0 -387
  182. package/pi-extension/src/output-router/status.ts +0 -791
  183. package/pi-extension/src/output-router/utils.ts +0 -554
  184. package/pi-extension/tests/output-router/observed-producers.test.js +0 -132
  185. package/pi-extension/tests/output-router/search.test.js +0 -722
  186. package/router/dist/benchmarks/benchmark-harness.d.ts +0 -35
  187. package/router/dist/benchmarks/benchmark-harness.js +0 -92
  188. package/router/dist/benchmarks/benchmarks.d.ts +0 -109
  189. package/router/dist/benchmarks/benchmarks.js +0 -979
  190. package/router/dist/benchmarks/codex-qa-benchmarks.d.ts +0 -95
  191. package/router/dist/benchmarks/codex-qa-benchmarks.js +0 -500
  192. package/router/dist/benchmarks/command-benchmarks.d.ts +0 -143
  193. package/router/dist/benchmarks/command-benchmarks.js +0 -698
  194. package/router/dist/benchmarks/context-mode-normalized-benchmarks.d.ts +0 -82
  195. package/router/dist/benchmarks/context-mode-normalized-benchmarks.js +0 -773
  196. package/router/dist/benchmarks/context-mode-real-deep-benchmark.d.ts +0 -132
  197. package/router/dist/benchmarks/context-mode-real-deep-benchmark.js +0 -1380
  198. package/router/dist/benchmarks/context-saving-baseline.d.ts +0 -60
  199. package/router/dist/benchmarks/context-saving-baseline.js +0 -470
  200. package/router/dist/benchmarks/index-benchmarks.d.ts +0 -103
  201. package/router/dist/benchmarks/index-benchmarks.js +0 -862
  202. package/router/dist/benchmarks/storage-policy-benchmarks.d.ts +0 -101
  203. package/router/dist/benchmarks/storage-policy-benchmarks.js +0 -618
  204. package/router/dist/benchmarks/vault-index-benchmarks.d.ts +0 -56
  205. package/router/dist/benchmarks/vault-index-benchmarks.js +0 -322
  206. package/router/dist/config/config.d.ts +0 -103
  207. package/router/dist/config/config.js +0 -632
  208. package/router/dist/config/router-contract.d.ts +0 -10
  209. package/router/dist/config/router-contract.js +0 -54
  210. package/router/dist/config/schema.d.ts +0 -27
  211. package/router/dist/config/schema.js +0 -648
  212. package/router/dist/config/types.d.ts +0 -573
  213. package/router/dist/config/types.js +0 -41
  214. package/router/dist/evidence/bounded-evidence.d.ts +0 -40
  215. package/router/dist/evidence/bounded-evidence.js +0 -323
  216. package/router/dist/evidence/evidence-range-selector.d.ts +0 -24
  217. package/router/dist/evidence/evidence-range-selector.js +0 -232
  218. package/router/dist/evidence/evidence-search.d.ts +0 -27
  219. package/router/dist/evidence/evidence-search.js +0 -490
  220. package/router/dist/evidence/evidence.d.ts +0 -38
  221. package/router/dist/evidence/evidence.js +0 -200
  222. package/router/dist/evidence/failure-contracts.d.ts +0 -37
  223. package/router/dist/evidence/failure-contracts.js +0 -166
  224. package/router/dist/evidence/line-ranges.d.ts +0 -24
  225. package/router/dist/evidence/line-ranges.js +0 -22
  226. package/router/dist/experiments/local-index.d.ts +0 -64
  227. package/router/dist/experiments/local-index.js +0 -581
  228. package/router/dist/index.d.ts +0 -30
  229. package/router/dist/index.js +0 -32
  230. package/router/dist/local/local-traversal.d.ts +0 -18
  231. package/router/dist/local/local-traversal.js +0 -334
  232. package/router/dist/processing/engine.d.ts +0 -132
  233. package/router/dist/processing/engine.js +0 -533
  234. package/router/dist/processing/reducers.d.ts +0 -280
  235. package/router/dist/processing/reducers.js +0 -1673
  236. package/router/dist/processing/renderers.d.ts +0 -31
  237. package/router/dist/processing/renderers.js +0 -195
  238. package/router/dist/processing/scripts.d.ts +0 -91
  239. package/router/dist/processing/scripts.js +0 -386
  240. package/router/dist/repo/repo-traversal.d.ts +0 -18
  241. package/router/dist/repo/repo-traversal.js +0 -245
  242. package/router/dist/routing/observed-reducers.d.ts +0 -41
  243. package/router/dist/routing/observed-reducers.js +0 -661
  244. package/router/dist/routing/observed-routing.d.ts +0 -26
  245. package/router/dist/routing/observed-routing.js +0 -378
  246. package/router/dist/routing/parsers.d.ts +0 -16
  247. package/router/dist/routing/parsers.js +0 -521
  248. package/router/dist/routing/run-filters.d.ts +0 -60
  249. package/router/dist/routing/run-filters.js +0 -238
  250. package/router/dist/routing/run-reducers.d.ts +0 -49
  251. package/router/dist/routing/run-reducers.js +0 -122
  252. package/router/dist/sandbox/adapter-roots.d.ts +0 -46
  253. package/router/dist/sandbox/adapter-roots.js +0 -66
  254. package/router/dist/sandbox/eryx-python-adapter.d.ts +0 -13
  255. package/router/dist/sandbox/eryx-python-adapter.js +0 -868
  256. package/router/dist/sandbox/jq-wasm-adapter.d.ts +0 -9
  257. package/router/dist/sandbox/jq-wasm-adapter.js +0 -429
  258. package/router/dist/sandbox/quickjs-wasi-adapter.d.ts +0 -13
  259. package/router/dist/sandbox/quickjs-wasi-adapter.js +0 -384
  260. package/router/dist/sandbox/script-sandbox.d.ts +0 -134
  261. package/router/dist/sandbox/script-sandbox.js +0 -337
  262. package/router/dist/setup/script-transform-adapters.d.ts +0 -32
  263. package/router/dist/setup/script-transform-adapters.js +0 -278
  264. package/router/dist/tools/batch.d.ts +0 -33
  265. package/router/dist/tools/batch.js +0 -586
  266. package/router/dist/tools/run.d.ts +0 -62
  267. package/router/dist/tools/run.js +0 -1304
  268. package/router/dist/tools/search.d.ts +0 -66
  269. package/router/dist/tools/search.js +0 -1364
  270. package/router/dist/transform/engine.d.ts +0 -168
  271. package/router/dist/transform/engine.js +0 -2072
  272. package/router/dist/vault/vault-index.d.ts +0 -125
  273. package/router/dist/vault/vault-index.js +0 -416
  274. package/router/dist/vault/vault.d.ts +0 -138
  275. package/router/dist/vault/vault.js +0 -630
  276. package/runtime/interaction-contract.md +0 -16
  277. package/skills/discuss/references/checkpoints.md +0 -66
  278. package/skills/execute-work/references/execution-loop.md +0 -77
  279. package/skills/output-router/SKILL.md +0 -206
  280. package/skills/output-router/references/safety-policy.md +0 -40
  281. package/skills/setup-freeflow/references/output-router-setup.md +0 -110
  282. package/skills/track-work/references/working-record-schema.md +0 -378
  283. package/skills/workflow/references/workflow-loop.md +0 -92
  284. /package/skills/handoff/references/{templates.md → handoff-templates.md} +0 -0
  285. /package/skills/review-artifact/references/{reviewer-prompt.md → independent-artifact-reviewer-contract.md} +0 -0
@@ -1,4 +1,5 @@
1
- import { realpath } from "node:fs/promises";
1
+ import { createHash } from "node:crypto";
2
+ import { readFile, realpath } from "node:fs/promises";
2
3
  import path from "node:path";
3
4
 
4
5
  import {
@@ -16,6 +17,10 @@ import {
16
17
  } from "./grade-support.mjs";
17
18
 
18
19
  const ACTIVATION_EXPECTATIONS = new Set(["never", "on-turn", "by-turn", "not-before-turn"]);
20
+ const TOOL_CALL_EXPECTATIONS = new Set(["called", "succeeded", "failed", "not-called"]);
21
+ const CONTEXT_TEXT_EXPECTATIONS = new Set(["contains", "not-contains", "equals"]);
22
+ const CONTEXT_SURFACES = new Set(["system-prompt", "provider-context"]);
23
+ const CONTEXT_REQUEST_SELECTORS = new Set(["first", "last"]);
19
24
 
20
25
  export async function gradeSkillRead(group, runs, expectation) {
21
26
  const turn = expectation.turn ?? null;
@@ -100,6 +105,95 @@ export async function gradeResourceRead(group, runs, expectation) {
100
105
  );
101
106
  }
102
107
 
108
+ export async function gradeContextText(group, runs, expectation) {
109
+ const turn = expectation.turn ?? null;
110
+ const surface = expectation.surface ?? "system-prompt";
111
+ const request = expectation.request ?? "last";
112
+ if (
113
+ !validBodyExpectation(group, expectation, turn) ||
114
+ !CONTEXT_SURFACES.has(surface) ||
115
+ (!CONTEXT_REQUEST_SELECTORS.has(request) && !Number.isInteger(request)) ||
116
+ (Number.isInteger(request) && request < 1) ||
117
+ !CONTEXT_TEXT_EXPECTATIONS.has(expectation.expect) ||
118
+ typeof expectation.value !== "string"
119
+ ) {
120
+ return { error: "invalid context-text expectation" };
121
+ }
122
+
123
+ const expected = { expect: expectation.expect, value: expectation.value, surface, request, turn };
124
+ const run = runs[expectation.variant];
125
+ if (!run || run.state !== "complete") return unavailableCheck(expectation, expected);
126
+ const observed = await contextObservation(run, turn, surface, request);
127
+ if (observed === null) return unavailableCheck(expectation, expected);
128
+ const passed = textMatches(expectation.expect, expectation.value, observed.text);
129
+ return completedCheck(expectation, expected, compactContextObservation(observed, passed), passed);
130
+ }
131
+
132
+ export async function gradeToolCall(group, runs, expectation) {
133
+ const turn = expectation.turn ?? null;
134
+ const contains = expectation.argumentContains ?? [];
135
+ const notContains = expectation.argumentNotContains ?? [];
136
+ if (
137
+ !validBodyExpectation(group, expectation, turn) ||
138
+ typeof expectation.tool !== "string" ||
139
+ expectation.tool.trim() === "" ||
140
+ !TOOL_CALL_EXPECTATIONS.has(expectation.expect) ||
141
+ !Array.isArray(contains) ||
142
+ !contains.every((value) => typeof value === "string") ||
143
+ !Array.isArray(notContains) ||
144
+ !notContains.every((value) => typeof value === "string")
145
+ ) {
146
+ return { error: "invalid tool-call expectation" };
147
+ }
148
+
149
+ const expected = {
150
+ tool: expectation.tool,
151
+ expect: expectation.expect,
152
+ turn,
153
+ argumentContains: contains,
154
+ argumentNotContains: notContains,
155
+ };
156
+ const run = runs[expectation.variant];
157
+ if (!run || run.state !== "complete") return unavailableCheck(expectation, expected);
158
+
159
+ const calls = toolCalls(run, turn).filter((call) => call.toolName === expectation.tool);
160
+ const matchingCalls = calls.filter((call) => {
161
+ const argumentsText = JSON.stringify(call.args ?? null);
162
+ return (
163
+ contains.every((value) => argumentsText.includes(value)) &&
164
+ notContains.every((value) => !argumentsText.includes(value))
165
+ );
166
+ });
167
+ const succeededCalls = matchingCalls.filter((call) => call.completed === true && call.isError !== true);
168
+ const failedCalls = matchingCalls.filter((call) => call.completed === true && call.isError === true);
169
+ let passed;
170
+ if (expectation.expect === "called") {
171
+ passed = matchingCalls.length > 0;
172
+ } else if (expectation.expect === "succeeded") {
173
+ passed = succeededCalls.length > 0;
174
+ } else if (expectation.expect === "failed") {
175
+ passed = failedCalls.length > 0;
176
+ } else if (contains.length > 0 || notContains.length > 0) {
177
+ passed = matchingCalls.length === 0;
178
+ } else {
179
+ passed = calls.length === 0;
180
+ }
181
+ const observed = {
182
+ tool: expectation.tool,
183
+ turn,
184
+ callCount: calls.length,
185
+ matchingCallCount: matchingCalls.length,
186
+ succeededCount: succeededCalls.length,
187
+ failedCount: failedCalls.length,
188
+ calls: calls.map((call) => ({
189
+ args: call.args ?? null,
190
+ completed: call.completed ?? null,
191
+ isError: call.isError ?? null,
192
+ })),
193
+ };
194
+ return completedCheck(expectation, expected, observed, passed);
195
+ }
196
+
103
197
  export async function gradeResponseText(group, runs, expectation) {
104
198
  const turn = expectation.turn ?? null;
105
199
  if (
@@ -123,6 +217,67 @@ export async function gradeResponseText(group, runs, expectation) {
123
217
  );
124
218
  }
125
219
 
220
+ function compactContextObservation(observation, matched) {
221
+ return {
222
+ surface: observation.surface,
223
+ turn: observation.turn,
224
+ request: observation.request,
225
+ characters: observation.characters,
226
+ sha256: createHash("sha256").update(observation.text, "utf8").digest("hex"),
227
+ matched,
228
+ preview: observation.text.slice(0, 240),
229
+ };
230
+ }
231
+
232
+ async function contextObservation(run, turn, surface, request) {
233
+ const observations = await loadContextObservations(run);
234
+ if (observations === null) return null;
235
+ const matching = observations.filter((entry) => entry.surface === surface);
236
+ const inTurn = turn === null ? matching : matching.filter((entry) => entry.turn === turn);
237
+ if (inTurn.length === 0) return null;
238
+ let observation;
239
+ if (request === "first") observation = inTurn[0];
240
+ else if (request === "last") observation = inTurn.at(-1);
241
+ else observation = inTurn.find((entry) => (entry.requestInTurn ?? 1) === request);
242
+ const text = surface === "system-prompt" ? (observation?.text ?? observation?.systemPrompt) : observation?.text;
243
+ if (!observation || typeof text !== "string") return null;
244
+ return {
245
+ surface,
246
+ turn: observation.turn ?? turn,
247
+ request: observation.requestInTurn ?? 1,
248
+ characters: observation.characters ?? text.length,
249
+ text,
250
+ };
251
+ }
252
+
253
+ async function loadContextObservations(run) {
254
+ const artifact = run.contextObservationArtifact;
255
+ if (!artifact || typeof artifact.path !== "string" || typeof artifact.sha256 !== "string") return null;
256
+ const contents = await readFile(artifact.path, "utf8");
257
+ const actualHash = createHash("sha256").update(contents, "utf8").digest("hex");
258
+ if (actualHash !== artifact.sha256) throw new Error("context observation artifact changed after run persistence");
259
+ const observations = [];
260
+ for (const line of contents.split("\n").filter(Boolean)) {
261
+ try {
262
+ observations.push(JSON.parse(line));
263
+ } catch (error) {
264
+ throw new Error(
265
+ `context observation artifact is malformed: ${error instanceof Error ? error.message : String(error)}`,
266
+ );
267
+ }
268
+ }
269
+ return observations;
270
+ }
271
+
272
+ function toolCalls(run, turn) {
273
+ if (turn === null) {
274
+ if (Array.isArray(run.turns)) return run.turns.flatMap((entry) => entry.toolActivity ?? []);
275
+ return Array.isArray(run.toolActivity) ? run.toolActivity : [];
276
+ }
277
+ if (!Array.isArray(run.turns)) return turn === 1 && Array.isArray(run.toolActivity) ? run.toolActivity : [];
278
+ return run.turns.find((entry) => entry.turn === turn)?.toolActivity ?? [];
279
+ }
280
+
126
281
  function activationMatches(expectation, observed) {
127
282
  if (expectation.expect === "never") return !observed.targetRead;
128
283
  if (!Number.isInteger(expectation.turn) || expectation.turn < 1) return false;
@@ -1,5 +1,11 @@
1
1
  import { gradeComparisons } from "./grade-comparison.mjs";
2
- import { gradeResourceRead, gradeResponseText, gradeSkillRead } from "./grade-interaction.mjs";
2
+ import {
3
+ gradeContextText,
4
+ gradeResourceRead,
5
+ gradeResponseText,
6
+ gradeSkillRead,
7
+ gradeToolCall,
8
+ } from "./grade-interaction.mjs";
3
9
  import { gradeChangedPaths, gradeFileText, gradeJson, gradePath } from "./grade-workspace.mjs";
4
10
 
5
11
  const GRADERS = new Map([
@@ -10,6 +16,8 @@ const GRADERS = new Map([
10
16
  ["file-text", gradeFileText],
11
17
  ["json", gradeJson],
12
18
  ["response-text", gradeResponseText],
19
+ ["context-text", gradeContextText],
20
+ ["tool-call", gradeToolCall],
13
21
  ]);
14
22
 
15
23
  /**
@@ -1,7 +1,8 @@
1
+ import { createHash } from "node:crypto";
1
2
  import { spawn } from "node:child_process";
2
3
  import { once } from "node:events";
3
4
  import { createWriteStream } from "node:fs";
4
- import { realpath } from "node:fs/promises";
5
+ import { mkdir, readFile, realpath } from "node:fs/promises";
5
6
  import path from "node:path";
6
7
  import { fileURLToPath } from "node:url";
7
8
 
@@ -21,12 +22,32 @@ import {
21
22
  fingerprintDirectory,
22
23
  materializeEnvironment,
23
24
  materializeFixture,
25
+ materializeRuntime,
24
26
  snapshotWorkspace,
25
27
  verifyEnvironment,
28
+ verifyRuntime,
26
29
  workspaceChanges,
27
30
  } from "./sandbox.mjs";
28
31
 
29
32
  const guardExtension = fileURLToPath(new URL("../pi-guard.mjs", import.meta.url));
33
+ const observerExtension = fileURLToPath(new URL("../pi-observer.mjs", import.meta.url));
34
+ const HOST_COMMANDS = { pi: "pi", piflow: "piflow" };
35
+ const BASE_PROCESS_ENVIRONMENT_KEYS = [
36
+ "PATH",
37
+ "HOME",
38
+ "TMPDIR",
39
+ "TMP",
40
+ "TEMP",
41
+ "LANG",
42
+ "LC_ALL",
43
+ "XDG_CONFIG_HOME",
44
+ "XDG_DATA_HOME",
45
+ "XDG_CACHE_HOME",
46
+ "PIFLOW_CODING_AGENT_DIR",
47
+ "PIFLOW_CODING_AGENT_SESSION_DIR",
48
+ "PI_PACKAGE_DIR",
49
+ "PI_OFFLINE",
50
+ ];
30
51
  const TURN_WORKSPACE_KINDS = new Set(["path", "changed-paths", "file-text", "json"]);
31
52
 
32
53
  export class VariantSetupError extends Error {
@@ -36,19 +57,20 @@ export class VariantSetupError extends Error {
36
57
  }
37
58
  }
38
59
 
39
- export async function runDescription({ group, variant, root, variantDirectory, fixture, signal }) {
60
+ export async function runDescription({ group, variant, root, variantDirectory, fixture, runtime, signal }) {
40
61
  if (group.input.turns !== undefined) {
41
- return runPersistentDescription({ group, variant, root, variantDirectory, fixture, signal });
62
+ return runPersistentDescription({ group, variant, root, variantDirectory, fixture, runtime, signal });
42
63
  }
43
- return runOneShotDescription({ group, variant, root, variantDirectory, fixture, signal });
64
+ return runOneShotDescription({ group, variant, root, variantDirectory, fixture, runtime, signal });
44
65
  }
45
66
 
46
- export async function runBody({ group, variant, root, variantDirectory, fixture, signal }) {
47
- const options = { group, variant, root, variantDirectory, fixture, signal };
67
+ export async function runBody({ group, variant, root, variantDirectory, fixture, runtime, signal }) {
68
+ const options = { group, variant, root, variantDirectory, fixture, runtime, signal };
48
69
  const subject = await prepareSubject(options, "rpc");
49
70
  const before = await fingerprintDirectory(subject.workspace);
50
71
  const declaredPrompts = group.input.prompt === undefined ? [...group.input.turns] : [group.input.prompt];
51
72
  const observation = await runRpcBodySession({
73
+ command: subject.host.command,
52
74
  args: subject.args,
53
75
  cwd: subject.workspace,
54
76
  eventsFile: subject.eventsFile,
@@ -61,6 +83,11 @@ export async function runBody({ group, variant, root, variantDirectory, fixture,
61
83
  ? (turn) => snapshotWorkspace({ workspace: subject.workspace, variantDirectory, turn, before })
62
84
  : null,
63
85
  });
86
+ const contextObservation = await readContextObservations(
87
+ subject.contextObservationPath,
88
+ subject.requiresContextObservation,
89
+ );
90
+ observation.contextObservationArtifact = contextObservation.artifact;
64
91
  await verifySubjectResources(subject, observation);
65
92
  const after = await fingerprintDirectory(subject.workspace);
66
93
 
@@ -132,14 +159,17 @@ function subjectRun({ group, variant }, subject, observation) {
132
159
  source: subject.environment.source,
133
160
  skills: subject.environment.skills,
134
161
  context: subject.environment.context,
162
+ runtime: subject.runtime,
135
163
  contextDelivery: subject.environment.contextDelivery,
136
164
  targetPath: subject.environment.targetPath,
137
165
  },
166
+ contextObservationArtifact: observation.contextObservationArtifact ?? null,
138
167
  response: observation.response,
139
168
  transcript: observation.transcript,
140
169
  usage: observation.usage,
141
170
  process: {
142
- command: "pi",
171
+ command: subject.host.command,
172
+ host: subject.host.name,
143
173
  args: subject.args,
144
174
  exitCode: observation.exitCode,
145
175
  signal: observation.signal,
@@ -156,6 +186,7 @@ function subjectRun({ group, variant }, subject, observation) {
156
186
  async function runOneShotDescription(options) {
157
187
  const subject = await prepareSubject(options, "json");
158
188
  const observation = await runPiProcess({
189
+ command: subject.host.command,
159
190
  args: subject.args,
160
191
  cwd: subject.workspace,
161
192
  eventsFile: subject.eventsFile,
@@ -163,6 +194,11 @@ async function runOneShotDescription(options) {
163
194
  signal: options.signal,
164
195
  environment: subject.processEnvironment,
165
196
  });
197
+ const contextObservation = await readContextObservations(
198
+ subject.contextObservationPath,
199
+ subject.requiresContextObservation,
200
+ );
201
+ observation.contextObservationArtifact = contextObservation.artifact;
166
202
  await verifySubjectResources(subject, observation);
167
203
  const successfulReadPaths = await canonicalReadPaths(observation.successfulReads, subject.workspace);
168
204
  const targetRead =
@@ -181,6 +217,7 @@ async function runOneShotDescription(options) {
181
217
  async function runPersistentDescription(options) {
182
218
  const subject = await prepareSubject(options, "rpc");
183
219
  const observation = await runRpcDescriptionSession({
220
+ command: subject.host.command,
184
221
  args: subject.args,
185
222
  cwd: subject.workspace,
186
223
  eventsFile: subject.eventsFile,
@@ -189,6 +226,11 @@ async function runPersistentDescription(options) {
189
226
  prompts: options.group.input.turns,
190
227
  environment: subject.processEnvironment,
191
228
  });
229
+ const contextObservation = await readContextObservations(
230
+ subject.contextObservationPath,
231
+ subject.requiresContextObservation,
232
+ );
233
+ observation.contextObservationArtifact = contextObservation.artifact;
192
234
  await verifySubjectResources(subject, observation);
193
235
 
194
236
  const turns = [];
@@ -222,52 +264,130 @@ async function runPersistentDescription(options) {
222
264
  });
223
265
  }
224
266
 
225
- async function prepareSubject({ group, variant, root, variantDirectory, fixture: fixtureSnapshot }, mode) {
267
+ async function prepareSubject({ group, variant, root, variantDirectory, fixture: fixtureSnapshot, runtime }, mode) {
226
268
  const workspace = path.join(variantDirectory, "workspace");
227
269
 
228
270
  let environment;
229
271
  let fixture;
272
+ let materializedRuntime;
230
273
  try {
231
274
  environment = await materializeEnvironment({
232
275
  environment: group.variants[variant],
233
276
  root,
234
277
  variantDirectory,
235
278
  });
279
+ materializedRuntime = await materializeRuntime({ runtime, variantDirectory });
236
280
  fixture = await materializeFixture({ fixture: fixtureSnapshot, workspace });
237
281
  } catch (error) {
238
282
  if (error instanceof VariantSetupError) throw error;
239
283
  throw new VariantSetupError(error instanceof Error ? error.message : String(error));
240
284
  }
241
285
 
242
- const args = piArguments(group, environment, mode);
286
+ const host = { name: materializedRuntime.host, command: HOST_COMMANDS[materializedRuntime.host] };
287
+ const contextObservationPath = path.join(variantDirectory, "context-observations.jsonl");
288
+ const requiresContextObservation = group.expectations.some((expectation) => expectation.kind === "context-text");
289
+ const sessionDirectory = path.join(variantDirectory, "session");
290
+ if (materializedRuntime.session) await mkdir(sessionDirectory, { recursive: true });
291
+ const args = piArguments(group, environment, materializedRuntime, mode, requiresContextObservation, sessionDirectory);
243
292
  const allowedRoots = [
244
293
  await realpath(workspace),
245
294
  ...environment.skills.map((skill) => skill.path),
246
295
  ...environment.context.map((entry) => entry.path),
296
+ ...materializedRuntime.extensions.flatMap((bundle) => bundle.resources.map((resource) => resource.path)),
247
297
  ];
248
298
  return {
249
299
  args,
250
300
  environment,
301
+ runtime: materializedRuntime,
302
+ host,
251
303
  fixture,
252
304
  workspace,
305
+ contextObservationPath,
306
+ requiresContextObservation,
307
+ sessionDirectory,
253
308
  eventsFile: path.join(variantDirectory, "events.jsonl"),
254
309
  stderrFile: path.join(variantDirectory, "stderr.log"),
255
310
  startedAt: new Date().toISOString(),
256
- processEnvironment: {
257
- ...process.env,
258
- PI_SKIP_VERSION_CHECK: "1",
259
- PI_TELEMETRY: "0",
260
- SKILL_EVAL_ALLOWED_ROOTS: JSON.stringify(allowedRoots),
261
- SKILL_EVAL_WRITABLE_ROOT: await realpath(workspace),
262
- SKILL_EVAL_ALLOWED_TOOLS: JSON.stringify(group.tools),
263
- SKILL_EVAL_CONTEXT_MANIFEST: environment.contextDelivery.manifestPath ?? "",
264
- },
311
+ processEnvironment: createProcessEnvironment({
312
+ runtime: materializedRuntime,
313
+ group,
314
+ workspace,
315
+ allowedRoots,
316
+ contextManifestPath: environment.contextDelivery.manifestPath,
317
+ contextObservationPath,
318
+ requiresContextObservation,
319
+ }),
320
+ };
321
+ }
322
+
323
+ function createProcessEnvironment({
324
+ runtime,
325
+ group,
326
+ workspace,
327
+ allowedRoots,
328
+ contextManifestPath,
329
+ contextObservationPath,
330
+ requiresContextObservation,
331
+ }) {
332
+ const processEnvironment = Object.fromEntries(
333
+ BASE_PROCESS_ENVIRONMENT_KEYS.flatMap((key) => (process.env[key] === undefined ? [] : [[key, process.env[key]]])),
334
+ );
335
+ Object.assign(processEnvironment, {
336
+ PWD: workspace,
337
+ ...runtime.environment.literal,
338
+ PI_SKIP_VERSION_CHECK: "1",
339
+ PI_TELEMETRY: "0",
340
+ SKILL_EVAL_ALLOWED_ROOTS: JSON.stringify(allowedRoots),
341
+ SKILL_EVAL_WRITABLE_ROOT: workspace,
342
+ SKILL_EVAL_ALLOWED_TOOLS: JSON.stringify(group.tools),
343
+ SKILL_EVAL_CONTEXT_MANIFEST: contextManifestPath ?? "",
344
+ SKILL_EVAL_HOST: runtime.host,
345
+ });
346
+ for (const key of runtime.environment.inherit) {
347
+ if (process.env[key] === undefined) {
348
+ throw new VariantSetupError(`inherited runtime environment variable is unavailable: ${key}`);
349
+ }
350
+ processEnvironment[key] = process.env[key];
351
+ }
352
+ if (requiresContextObservation) processEnvironment.SKILL_EVAL_CONTEXT_OBSERVATION_PATH = contextObservationPath;
353
+ return processEnvironment;
354
+ }
355
+
356
+ async function readContextObservations(file, required) {
357
+ const contents = await readFile(file, "utf8").catch((error) => {
358
+ if (error?.code === "ENOENT") {
359
+ if (required) throw new Error("context observer produced no evidence");
360
+ return "";
361
+ }
362
+ throw error;
363
+ });
364
+ const observations = [];
365
+ for (const line of contents.split("\n").filter(Boolean)) {
366
+ try {
367
+ observations.push(JSON.parse(line));
368
+ } catch (error) {
369
+ throw new Error(`context observation is malformed: ${error instanceof Error ? error.message : String(error)}`);
370
+ }
371
+ }
372
+ return {
373
+ observations,
374
+ artifact:
375
+ contents === ""
376
+ ? null
377
+ : {
378
+ path: file,
379
+ sha256: createHash("sha256").update(contents, "utf8").digest("hex"),
380
+ bytes: Buffer.byteLength(contents, "utf8"),
381
+ count: observations.length,
382
+ surfaces: [...new Set(observations.map((entry) => entry.surface).filter(Boolean))],
383
+ },
265
384
  };
266
385
  }
267
386
 
268
387
  async function verifySubjectResources(subject, observation) {
269
388
  try {
270
389
  await verifyEnvironment(subject.environment);
390
+ await verifyRuntime(subject.runtime);
271
391
  } catch (error) {
272
392
  observation.protocolErrors ??= [];
273
393
  observation.protocolErrors.push({
@@ -300,20 +420,15 @@ function subjectRunState(observation) {
300
420
  return "infrastructure-failed";
301
421
  }
302
422
 
303
- function piArguments(group, environment, mode) {
423
+ function piArguments(group, environment, runtime, mode, observeContext, sessionDirectory) {
304
424
  const args = ["--mode", mode];
305
425
  if (mode === "json") args.push("-p");
306
- args.push(
307
- "--no-session",
308
- "--no-extensions",
309
- "--extension",
310
- guardExtension,
311
- "--no-skills",
312
- "--no-prompt-templates",
313
- "--no-themes",
314
- "--no-context-files",
315
- "--no-approve",
316
- );
426
+ if (runtime.session) args.push("--session-dir", sessionDirectory);
427
+ else args.push("--no-session");
428
+ args.push("--no-extensions", "--extension", guardExtension);
429
+ for (const bundle of runtime.extensions) args.push("--extension", bundle.entry);
430
+ if (observeContext) args.push("--extension", observerExtension);
431
+ args.push("--no-skills", "--no-prompt-templates", "--no-themes", "--no-context-files", "--no-approve");
317
432
  if (group.tools.length === 0) args.push("--no-tools");
318
433
  else args.push("--tools", group.tools.join(","));
319
434
  for (const skill of environment.skills) args.push("--skill", skill.path);
@@ -323,8 +438,8 @@ function piArguments(group, environment, mode) {
323
438
  return args;
324
439
  }
325
440
 
326
- async function runPiProcess({ args, cwd, eventsFile, stderrFile, signal, environment }) {
327
- const child = spawn("pi", args, {
441
+ async function runPiProcess({ command, args, cwd, eventsFile, stderrFile, signal, environment }) {
442
+ const child = spawn(command, args, {
328
443
  cwd,
329
444
  detached: process.platform !== "win32",
330
445
  env: environment,
@@ -28,8 +28,8 @@ export async function runRpcBodySession(options) {
28
28
  }
29
29
 
30
30
  class RpcSession {
31
- constructor({ args, cwd, eventsFile, stderrFile, signal, environment, afterTurn = null }) {
32
- this.child = spawn("pi", args, {
31
+ constructor({ command = "pi", args, cwd, eventsFile, stderrFile, signal, environment, afterTurn = null }) {
32
+ this.child = spawn(command, args, {
33
33
  cwd,
34
34
  detached: process.platform !== "win32",
35
35
  env: environment,
@@ -79,17 +79,17 @@ class RpcSession {
79
79
  async run(prompts, targetPath = null) {
80
80
  let deliveredPrompts = prompts;
81
81
  /** @type {any} */
82
- let delivery = { kind: "natural-prompt", turn: 1, targetPath: null };
82
+ let delivery = { kind: "unavailable", turn: 1, targetPath };
83
83
  try {
84
84
  await this.configure();
85
85
  if (targetPath !== null) {
86
- delivery = { kind: "unavailable", turn: 1, targetPath };
87
86
  const command = await this.resolveSkillCommand(targetPath);
88
87
  deliveredPrompts = [`/${command} ${prompts[0]}`, ...prompts.slice(1)];
89
88
  delivery = { kind: "explicit-skill-command", turn: 1, targetPath, command };
90
89
  }
91
90
  for (const [index, prompt] of deliveredPrompts.entries()) {
92
91
  if (this.signal?.aborted || this.terminationReason !== null) break;
92
+ if (delivery.kind === "unavailable") delivery = { kind: "natural-prompt", turn: 1, targetPath: null };
93
93
  const turn = await this.runTurn(index + 1, prompt);
94
94
  this.turns.push(turn);
95
95
  if (this.afterTurn !== null) turn.workspace = await this.afterTurn(turn.turn);
@@ -5,11 +5,14 @@ import { loadDefinition, selectDefinition } from "./definitions.mjs";
5
5
  import { createInvocationId, fileIdentity, writeJson, writeText } from "./evidence.mjs";
6
6
  import { gradeDeterministic } from "./grade.mjs";
7
7
  import { runBody, runDescription, VariantSetupError } from "./pi.mjs";
8
- import { prepareFixture } from "./sandbox.mjs";
8
+ import { prepareFixture, prepareRuntime } from "./sandbox.mjs";
9
9
 
10
10
  const VARIANTS = ["baseline", "candidate"];
11
11
  const DESCRIPTION_TOOLS = new Set(["read"]);
12
12
  const BODY_TOOLS = new Set(["read", "write", "edit"]);
13
+ // Native tools must be separated from extension tools before CLI tool selection is built.
14
+ const NATIVE_TOOLS = new Set(["read", "bash", "powershell", "edit", "write", "grep", "find", "ls"]);
15
+ const DEFAULT_RUNTIME = { host: "pi", session: false, extensions: [], environment: { literal: {}, inherit: [] } };
13
16
 
14
17
  /**
15
18
  * @param {string} definitionFile
@@ -69,8 +72,18 @@ function assertSupportedSelection(selection) {
69
72
  throw new Error("command expectations are not supported");
70
73
  }
71
74
  const supportedTools = group.type === "body" ? BODY_TOOLS : DESCRIPTION_TOOLS;
72
- if (group.tools.some((tool) => !supportedTools.has(tool))) {
73
- throw new Error(`${group.type} execution currently supports only ${[...supportedTools].join(", ")} tools`);
75
+ const unsupportedNativeTools = group.tools.filter((tool) => NATIVE_TOOLS.has(tool) && !supportedTools.has(tool));
76
+ if (unsupportedNativeTools.length > 0) {
77
+ throw new Error(`${group.type} execution does not support native tools: ${unsupportedNativeTools.join(", ")}`);
78
+ }
79
+ const customTools = group.tools.filter((tool) => !NATIVE_TOOLS.has(tool) && !supportedTools.has(tool));
80
+ if (group.type === "description" && customTools.length > 0) {
81
+ throw new Error(`${group.type} execution does not support custom tools: ${customTools.join(", ")}`);
82
+ }
83
+ if (customTools.length > 0 && (group.runtime?.extensions?.length ?? 0) === 0) {
84
+ throw new Error(
85
+ `${group.type} execution requires declared extensions for custom tools: ${customTools.join(", ")}`,
86
+ );
74
87
  }
75
88
  }
76
89
  }
@@ -82,12 +95,18 @@ async function runGroup({ selected, root, resultDirectory, signal }) {
82
95
  await writeJson(path.join(groupDirectory, "definition.json"), group);
83
96
 
84
97
  let fixture = null;
85
- let fixtureError = null;
98
+ let runtime = null;
99
+ let resourceError = null;
86
100
  if (!signal?.aborted) {
87
101
  try {
88
102
  fixture = await prepareFixture({ declaredPath: group.fixture, root, groupDirectory });
103
+ runtime = await prepareRuntime({
104
+ runtime: group.runtime ?? DEFAULT_RUNTIME,
105
+ root,
106
+ groupDirectory,
107
+ });
89
108
  } catch (error) {
90
- fixtureError = error instanceof Error ? error.message : String(error);
109
+ resourceError = error instanceof Error ? error.message : String(error);
91
110
  }
92
111
  }
93
112
 
@@ -100,10 +119,10 @@ async function runGroup({ selected, root, resultDirectory, signal }) {
100
119
  runs[variant] = notSelectedRun(group, variant);
101
120
  } else if (signal?.aborted) {
102
121
  runs[variant] = cancelledRun(group, variant);
103
- } else if (fixtureError !== null) {
104
- runs[variant] = invalidRun(group, variant, fixtureError);
122
+ } else if (resourceError === null) {
123
+ runs[variant] = await executeVariant({ group, variant, root, variantDirectory, fixture, runtime, signal });
105
124
  } else {
106
- runs[variant] = await executeVariant({ group, variant, root, variantDirectory, fixture, signal });
125
+ runs[variant] = invalidRun(group, variant, resourceError);
107
126
  }
108
127
  const persisted = await persistRunOrFailure(variantDirectory, runs[variant]);
109
128
  runs[variant] = persisted.run;
@@ -120,7 +139,8 @@ async function runGroup({ selected, root, resultDirectory, signal }) {
120
139
  (variant) => runs[variant],
121
140
  );
122
141
  const selectedState = selectedGroupState(selectedRuns, persistedGrade.grade);
123
- const groupState = selectedState === "cancelled" ? selectedState : fixtureError === null ? selectedState : "invalid";
142
+ let groupState = selectedState;
143
+ if (selectedState !== "cancelled" && resourceError !== null) groupState = "invalid";
124
144
  const groupResult = {
125
145
  id: group.id,
126
146
  position: selected.position,
@@ -205,10 +225,10 @@ function errorMessage(error) {
205
225
  return error instanceof Error ? error.message : String(error);
206
226
  }
207
227
 
208
- async function executeVariant({ group, variant, root, variantDirectory, fixture, signal }) {
228
+ async function executeVariant({ group, variant, root, variantDirectory, fixture, runtime, signal }) {
209
229
  try {
210
230
  const run = group.type === "body" ? runBody : runDescription;
211
- return await run({ group, variant, root, variantDirectory, fixture, signal });
231
+ return await run({ group, variant, root, variantDirectory, fixture, runtime, signal });
212
232
  } catch (error) {
213
233
  if (error instanceof VariantSetupError) return invalidRun(group, variant, error.message);
214
234
  return infrastructureFailedRun(group, variant, error);
@@ -241,16 +261,18 @@ async function persistRun(variantDirectory, run) {
241
261
  await writeText(path.join(variantDirectory, "final.md"), response === "" ? "" : `${response}\n`);
242
262
  const runFile = path.join(variantDirectory, "run.json");
243
263
  const hasProcessEvidence = run.process !== undefined;
244
- await writeJson(runFile, {
264
+ const persisted = {
245
265
  ...run,
246
- ...(Array.isArray(run.turns) ? { turns: externalizeTurnTranscripts(run.turns) } : {}),
247
266
  transcript: "transcript.json",
248
267
  artifacts: {
249
268
  events: hasProcessEvidence ? "events.jsonl" : null,
250
269
  final: "final.md",
251
270
  stderr: hasProcessEvidence ? "stderr.log" : null,
271
+ context: run.contextObservationArtifact ? "context-observations.jsonl" : null,
252
272
  },
253
- });
273
+ };
274
+ if (Array.isArray(run.turns)) persisted.turns = externalizeTurnTranscripts(run.turns);
275
+ await writeJson(runFile, persisted);
254
276
  return runFile;
255
277
  }
256
278