@hassangameryt/freeflow 0.7.3 → 0.8.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 (276) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/.codex-plugin/plugin.json +1 -1
  4. package/.cursor-plugin/plugin.json +1 -1
  5. package/NOTICE +14 -0
  6. package/README.md +26 -31
  7. package/capabilities/cognitive-routing/SKILL.md +106 -161
  8. package/capabilities/cognitive-routing/references/both-mode.md +36 -0
  9. package/capabilities/cognitive-routing/references/executor-mode.md +48 -0
  10. package/capabilities/cognitive-routing/references/helper-mode.md +47 -0
  11. package/capabilities/compaction/SKILL.md +49 -0
  12. package/capabilities/compaction/references/summary-format.md +239 -0
  13. package/command-surface.json +4 -0
  14. package/gemini-extension.json +1 -1
  15. package/hooks/shared/runtime-context.mjs +28 -30
  16. package/licenses/Apache-2.0.txt +55 -0
  17. package/package.json +22 -9
  18. package/pi-extension/README.md +41 -0
  19. package/pi-extension/dist/cognitive-routing/assembler.js +393 -0
  20. package/pi-extension/dist/{cognitive-routing-v2 → cognitive-routing}/config.js +5 -2
  21. package/pi-extension/dist/cognitive-routing/economics.js +45 -0
  22. package/pi-extension/dist/cognitive-routing/event-schema.js +388 -0
  23. package/pi-extension/dist/{session-sources/events.js → cognitive-routing/event-store.js} +103 -40
  24. package/pi-extension/dist/cognitive-routing/gate.js +241 -0
  25. package/pi-extension/dist/cognitive-routing/handlers.js +899 -0
  26. package/pi-extension/dist/cognitive-routing/handoffs.js +350 -0
  27. package/pi-extension/dist/cognitive-routing/model-control.js +467 -0
  28. package/pi-extension/dist/{cognitive-routing-v2 → cognitive-routing}/projection.js +229 -56
  29. package/pi-extension/dist/cognitive-routing/provenance.js +81 -0
  30. package/pi-extension/dist/cognitive-routing/runtime.js +396 -0
  31. package/pi-extension/dist/{cognitive-routing-v2 → cognitive-routing}/schemas.js +43 -7
  32. package/pi-extension/dist/cognitive-routing/session.js +221 -0
  33. package/pi-extension/dist/{session-sources → cognitive-routing}/sources.js +99 -85
  34. package/pi-extension/dist/{cognitive-routing-v2 → cognitive-routing}/state.js +37 -378
  35. package/pi-extension/dist/cognitive-routing/status.js +80 -0
  36. package/pi-extension/dist/{cognitive-routing-v2 → cognitive-routing}/tools.js +12 -12
  37. package/pi-extension/dist/{cognitive-routing-v2 → cognitive-routing}/types.js +5 -11
  38. package/pi-extension/dist/compaction/carry.js +191 -0
  39. package/pi-extension/dist/compaction/config.js +22 -0
  40. package/pi-extension/dist/compaction/controller.js +471 -0
  41. package/pi-extension/dist/compaction/harness.js +77 -0
  42. package/pi-extension/dist/compaction/results.js +158 -0
  43. package/pi-extension/dist/compaction/thresholds.js +35 -0
  44. package/pi-extension/dist/compaction/tool.js +57 -0
  45. package/pi-extension/dist/host/branch.js +13 -0
  46. package/pi-extension/dist/host/canonical.js +12 -0
  47. package/pi-extension/dist/host/catalog.js +77 -0
  48. package/pi-extension/dist/host/config.js +374 -0
  49. package/pi-extension/dist/host/projection-tags.js +60 -0
  50. package/pi-extension/dist/host/prompts.js +82 -0
  51. package/pi-extension/dist/{session-sources → host}/read-only-session.js +112 -1
  52. package/pi-extension/dist/{runtime → host}/request-history.js +121 -39
  53. package/pi-extension/dist/host/runtime-state.js +189 -0
  54. package/pi-extension/dist/{settings/settings-ui.js → host/settings/freeflow-command.js} +620 -410
  55. package/pi-extension/dist/{settings/settings-tui.js → host/settings/settings-view.js} +186 -73
  56. package/pi-extension/dist/host/staging.js +28 -0
  57. package/pi-extension/dist/host/status.js +101 -0
  58. package/pi-extension/dist/index.js +423 -173
  59. package/pi-extension/dist/provider-support/cache/anchor.js +21 -0
  60. package/pi-extension/dist/provider-support/cache/anthropic.js +102 -0
  61. package/pi-extension/dist/provider-support/cache/health.js +73 -0
  62. package/pi-extension/dist/provider-support/cache/index.js +58 -0
  63. package/pi-extension/dist/provider-support/cache/keepalive.js +210 -0
  64. package/pi-extension/dist/provider-support/cache/monitor.js +17 -0
  65. package/pi-extension/dist/provider-support/cache/openai-responses.js +48 -0
  66. package/pi-extension/dist/provider-support/effort.js +13 -0
  67. package/pi-extension/dist/provider-support/index.js +44 -3
  68. package/pi-extension/dist/provider-support/{astra → openai}/adapter.js +46 -29
  69. package/pi-extension/dist/provider-support/{astra → openai}/history.js +10 -8
  70. package/pi-extension/dist/provider-support/{astra → openai}/index.js +8 -4
  71. package/pi-extension/dist/provider-support/{astra → openai}/session-state.js +29 -38
  72. package/pi-extension/dist/provider-support/routes.js +17 -0
  73. package/pi-extension/dist/tool-execution/apply-patch/grammar.js +23 -0
  74. package/pi-extension/dist/tool-execution/apply-patch/match.js +38 -0
  75. package/pi-extension/dist/tool-execution/apply-patch/parser.js +177 -0
  76. package/pi-extension/dist/tool-execution/apply-patch/plan.js +201 -0
  77. package/pi-extension/dist/tool-execution/apply-patch/tool.js +84 -0
  78. package/pi-extension/dist/tool-execution/background.js +238 -0
  79. package/pi-extension/dist/tool-execution/bash-guard.js +23 -0
  80. package/pi-extension/dist/tool-execution/config.js +22 -0
  81. package/pi-extension/dist/tool-execution/edit-messages.js +120 -0
  82. package/pi-extension/dist/tool-execution/file-state.js +138 -0
  83. package/pi-extension/dist/tool-execution/file-tracking.js +143 -0
  84. package/pi-extension/dist/tool-execution/prompt.js +54 -0
  85. package/pi-extension/dist/tool-execution/tools.js +16 -0
  86. package/pi-extension/freeflow/README.md +9 -0
  87. package/plugin.json +1 -1
  88. package/runtime/prompts/cognitive-routing.md +4 -4
  89. package/runtime/prompts/core.md +22 -75
  90. package/runtime/prompts/interaction-contract.md +15 -8
  91. package/runtime/prompts/tool-execution.md +38 -0
  92. package/runtime/prompts/working-method.md +108 -0
  93. package/skills/action-selection/SKILL.md +39 -47
  94. package/skills/action-selection/references/trajectory-stalls.md +2 -0
  95. package/skills/bypass/SKILL.md +2 -2
  96. package/skills/commit-work/SKILL.md +31 -83
  97. package/skills/commit-work/references/git-state-edges.md +1 -1
  98. package/skills/commit-work/references/simple-push.md +2 -0
  99. package/skills/commit-work/references/staging-decisions.md +1 -1
  100. package/skills/decision-gate/SKILL.md +31 -50
  101. package/skills/design-for-depth/SKILL.md +44 -53
  102. package/skills/design-for-depth/references/design-pressure-signals.md +46 -144
  103. package/skills/design-for-depth/references/implementation-decisions.md +1 -1
  104. package/skills/design-for-depth/references/interface-design-loop.md +5 -36
  105. package/skills/design-for-depth/references/module-and-dependency-design.md +2 -32
  106. package/skills/design-for-depth/references/state-and-failure-boundaries.md +2 -13
  107. package/skills/diagnose-failure/SKILL.md +39 -24
  108. package/skills/diagnose-failure/references/diagnostic-loop-catalog.md +2 -0
  109. package/skills/diagnose-failure/references/flaky-and-performance-diagnosis.md +2 -2
  110. package/skills/discuss/SKILL.md +35 -11
  111. package/skills/discuss/references/discussion-continuity.md +7 -27
  112. package/skills/evaluate-skill/SKILL.md +49 -80
  113. package/skills/evaluate-skill/references/evaluation-definition-schema.md +35 -5
  114. package/skills/evaluate-skill/references/evaluation-design.md +14 -14
  115. package/skills/evaluate-skill/references/execution-and-evidence.md +26 -5
  116. package/skills/evaluate-skill/references/review-and-revision.md +3 -1
  117. package/skills/evaluate-skill/scripts/lib/definitions.mjs +49 -8
  118. package/skills/evaluate-skill/scripts/lib/pi.mjs +14 -7
  119. package/skills/evaluate-skill/scripts/lib/review.mjs +196 -0
  120. package/skills/evaluate-skill/scripts/lib/runner.mjs +45 -8
  121. package/skills/evaluate-skill/scripts/lib/sandbox.mjs +2 -0
  122. package/skills/evaluate-skill/scripts/lib/trials.mjs +103 -0
  123. package/skills/evaluate-skill/scripts/lib/view.mjs +67 -1
  124. package/skills/evaluate-skill/scripts/pi-command-tool.mjs +144 -0
  125. package/skills/evaluate-skill/scripts/pi-guard.mjs +30 -13
  126. package/skills/evaluate-skill/scripts/skill-eval.mjs +114 -34
  127. package/skills/execute-work/SKILL.md +41 -9
  128. package/skills/execute-work/references/code-practices.md +2 -0
  129. package/skills/execute-work/references/execute-work-edges.md +11 -1
  130. package/skills/handoff/SKILL.md +1 -1
  131. package/skills/handoff/references/handoff-templates.md +1 -1
  132. package/skills/review-artifact/SKILL.md +42 -41
  133. package/skills/review-artifact/references/adjudicate-artifact-review.md +1 -1
  134. package/skills/review-artifact/references/independent-artifact-reviewer-contract.md +1 -1
  135. package/skills/review-work/SKILL.md +38 -32
  136. package/skills/review-work/references/adjudicate-work-review.md +1 -1
  137. package/skills/review-work/references/independent-work-reviewer-contract.md +1 -1
  138. package/skills/review-work/references/security-risk-lens.md +2 -4
  139. package/skills/setup-freeflow/SKILL.md +3 -1
  140. package/skills/setup-freeflow/references/activation-contract.md +7 -13
  141. package/skills/setup-freeflow/references/host-setup.md +3 -3
  142. package/skills/track-work/SKILL.md +113 -161
  143. package/skills/track-work/references/working-record-format.md +189 -350
  144. package/skills/track-work/scripts/lib/document.mjs +62 -6
  145. package/skills/track-work/scripts/lib/format.mjs +16 -1
  146. package/skills/track-work/scripts/lib/store.mjs +9 -8
  147. package/skills/track-work/scripts/lib/transitions.mjs +79 -4
  148. package/skills/track-work/scripts/working-record.mjs +34 -12
  149. package/skills/verify-work/SKILL.md +29 -20
  150. package/skills/verify-work/references/browser-runtime-evidence.md +1 -5
  151. package/skills/verify-work/references/integration-evidence.md +2 -0
  152. package/skills/verify-work/references/performance-evidence.md +3 -1
  153. package/skills/verify-work/references/test-design.md +3 -1
  154. package/skills/workflow/SKILL.md +65 -21
  155. package/skills/workflow/references/domain-skill-composition.md +5 -3
  156. package/skills/write-docs/SKILL.md +83 -0
  157. package/skills/write-docs/references/documentation-shapes.md +119 -0
  158. package/skills/write-docs/scripts/docs-check.mjs +139 -0
  159. package/skills/write-plan/SKILL.md +40 -40
  160. package/skills/write-plan/references/plan-shapes.md +1 -1
  161. package/skills/write-skill/SKILL.md +107 -147
  162. package/skills/write-skill/references/activation-boundaries.md +2 -0
  163. package/skills/write-skill/references/agent-first-instructions.md +22 -10
  164. package/skills/write-skill/references/progressive-disclosure.md +4 -2
  165. package/skills/write-skill/references/skill-dependency-graphs.md +2 -0
  166. package/skills/write-skill/scripts/lib/similarity.mjs +56 -0
  167. package/skills/write-skill/scripts/skill-author.mjs +18 -2
  168. package/skills/write-spec/SKILL.md +34 -41
  169. package/skills/write-spec/references/artifact-standards.md +1 -1
  170. package/skills/write-spec/references/decision-records.md +2 -0
  171. package/skills/write-spec/references/spec-shapes.md +1 -1
  172. package/capabilities/context-virtualization/SKILL.md +0 -103
  173. package/capabilities/conversation-history/SKILL.md +0 -53
  174. package/hooks/tests/check-host-adapters.test.mjs +0 -82
  175. package/hooks/tests/check-runtime-context-hook.sh +0 -212
  176. package/pi-extension/dist/cognitive-routing-v2/runtime.js +0 -2181
  177. package/pi-extension/dist/context-virtualization/commands.js +0 -89
  178. package/pi-extension/dist/context-virtualization/projector.js +0 -42
  179. package/pi-extension/dist/context-virtualization/runtime.js +0 -378
  180. package/pi-extension/dist/context-virtualization/state.js +0 -57
  181. package/pi-extension/dist/context-virtualization/types.js +0 -10
  182. package/pi-extension/dist/conversation-history/passages.js +0 -66
  183. package/pi-extension/dist/conversation-history/ranking.js +0 -105
  184. package/pi-extension/dist/conversation-history/runtime.js +0 -226
  185. package/pi-extension/dist/conversation-history/search.js +0 -56
  186. package/pi-extension/dist/conversation-history/types.js +0 -1
  187. package/pi-extension/dist/freeflow-context/resolver.js +0 -120
  188. package/pi-extension/dist/freeflow-context/runtime.js +0 -80
  189. package/pi-extension/dist/freeflow-context/source-projector.js +0 -95
  190. package/pi-extension/dist/freeflow-context/tool.js +0 -314
  191. package/pi-extension/dist/freeflow-context/types.js +0 -12
  192. package/pi-extension/dist/freeflow-context/visibility.js +0 -28
  193. package/pi-extension/dist/integrations/mcp-config.js +0 -29
  194. package/pi-extension/dist/runtime/runtime-context.js +0 -754
  195. package/pi-extension/dist/runtime/runtime-identity.js +0 -5
  196. package/pi-extension/dist/session-sources/provenance.js +0 -32
  197. package/pi-extension/dist/ui/text-component.js +0 -57
  198. package/pi-extension/src/cognitive-routing-v2/budget.ts +0 -42
  199. package/pi-extension/src/cognitive-routing-v2/config.ts +0 -173
  200. package/pi-extension/src/cognitive-routing-v2/projection.ts +0 -375
  201. package/pi-extension/src/cognitive-routing-v2/render.ts +0 -277
  202. package/pi-extension/src/cognitive-routing-v2/runtime.ts +0 -2241
  203. package/pi-extension/src/cognitive-routing-v2/schemas.ts +0 -180
  204. package/pi-extension/src/cognitive-routing-v2/state.ts +0 -839
  205. package/pi-extension/src/cognitive-routing-v2/tools.ts +0 -62
  206. package/pi-extension/src/cognitive-routing-v2/types.ts +0 -254
  207. package/pi-extension/src/context-virtualization/commands.ts +0 -111
  208. package/pi-extension/src/context-virtualization/projector.ts +0 -62
  209. package/pi-extension/src/context-virtualization/runtime.ts +0 -428
  210. package/pi-extension/src/context-virtualization/state.ts +0 -79
  211. package/pi-extension/src/context-virtualization/types.ts +0 -92
  212. package/pi-extension/src/conversation-history/passages.ts +0 -81
  213. package/pi-extension/src/conversation-history/ranking.ts +0 -155
  214. package/pi-extension/src/conversation-history/runtime.ts +0 -250
  215. package/pi-extension/src/conversation-history/search.ts +0 -79
  216. package/pi-extension/src/conversation-history/types.ts +0 -31
  217. package/pi-extension/src/freeflow-context/resolver.ts +0 -139
  218. package/pi-extension/src/freeflow-context/runtime.ts +0 -100
  219. package/pi-extension/src/freeflow-context/source-projector.ts +0 -106
  220. package/pi-extension/src/freeflow-context/tool.ts +0 -366
  221. package/pi-extension/src/freeflow-context/types.ts +0 -70
  222. package/pi-extension/src/freeflow-context/visibility.ts +0 -53
  223. package/pi-extension/src/index.ts +0 -390
  224. package/pi-extension/src/integrations/mcp-config.ts +0 -32
  225. package/pi-extension/src/provider-support/astra/adapter.ts +0 -108
  226. package/pi-extension/src/provider-support/astra/history.ts +0 -145
  227. package/pi-extension/src/provider-support/astra/index.ts +0 -12
  228. package/pi-extension/src/provider-support/astra/session-state.ts +0 -113
  229. package/pi-extension/src/provider-support/index.ts +0 -5
  230. package/pi-extension/src/runtime/request-history.ts +0 -243
  231. package/pi-extension/src/runtime/runtime-context.ts +0 -860
  232. package/pi-extension/src/runtime/runtime-identity.ts +0 -8
  233. package/pi-extension/src/session-sources/events.ts +0 -230
  234. package/pi-extension/src/session-sources/provenance.ts +0 -39
  235. package/pi-extension/src/session-sources/read-only-session.ts +0 -170
  236. package/pi-extension/src/session-sources/sources.ts +0 -334
  237. package/pi-extension/src/settings/settings-tui.ts +0 -501
  238. package/pi-extension/src/settings/settings-ui.ts +0 -1652
  239. package/pi-extension/src/ui/text-component.ts +0 -66
  240. package/pi-extension/tests/cache-reuse/history.test.js +0 -117
  241. package/pi-extension/tests/cache-reuse/native.test.js +0 -91
  242. package/pi-extension/tests/cache-reuse/settings.test.js +0 -211
  243. package/pi-extension/tests/cognitive-routing-v2/communication.test.js +0 -577
  244. package/pi-extension/tests/cognitive-routing-v2/compaction-input.test.js +0 -183
  245. package/pi-extension/tests/cognitive-routing-v2/delegation.test.js +0 -557
  246. package/pi-extension/tests/cognitive-routing-v2/native.test.js +0 -474
  247. package/pi-extension/tests/cognitive-routing-v2/performance.test.js +0 -87
  248. package/pi-extension/tests/cognitive-routing-v2/persistence.test.js +0 -300
  249. package/pi-extension/tests/cognitive-routing-v2/projection.test.js +0 -241
  250. package/pi-extension/tests/cognitive-routing-v2/recovery.test.js +0 -1034
  251. package/pi-extension/tests/cognitive-routing-v2/render.test.js +0 -244
  252. package/pi-extension/tests/cognitive-routing-v2/runtime.test.js +0 -423
  253. package/pi-extension/tests/cognitive-routing-v2/stabilization.test.js +0 -521
  254. package/pi-extension/tests/cognitive-routing-v2/state.test.js +0 -586
  255. package/pi-extension/tests/context-virtualization/extension.test.js +0 -775
  256. package/pi-extension/tests/context-virtualization/projector.test.js +0 -110
  257. package/pi-extension/tests/context-virtualization/runtime.test.js +0 -230
  258. package/pi-extension/tests/conversation-history/runtime.test.js +0 -98
  259. package/pi-extension/tests/conversation-history/search.test.js +0 -97
  260. package/pi-extension/tests/fixtures/pi-host.js +0 -4
  261. package/pi-extension/tests/fixtures/routing-native.js +0 -191
  262. package/pi-extension/tests/integration/mode-free-core.test.js +0 -220
  263. package/pi-extension/tests/integration/pi-extension.test.js +0 -1447
  264. package/pi-extension/tests/integration/prompt-architecture.test.js +0 -447
  265. package/pi-extension/tests/integration/unaffected-regressions.test.js +0 -478
  266. package/pi-extension/tests/provider-support/astra-boundaries.test.js +0 -75
  267. package/pi-extension/tests/provider-support/astra.test.js +0 -306
  268. package/pi-extension/tests/provider-support/cli.test.js +0 -92
  269. package/pi-extension/tests/provider-support/native.test.js +0 -183
  270. package/pi-extension/tests/settings/settings-tui.test.js +0 -243
  271. package/pi-extension/tsconfig.json +0 -16
  272. package/runtime/prompts/context-virtualization.md +0 -3
  273. package/runtime/prompts/conversation-history.md +0 -3
  274. package/skills/design-for-depth/references/software-design-philosophy.md +0 -202
  275. /package/pi-extension/dist/{cognitive-routing-v2 → cognitive-routing}/budget.js +0 -0
  276. /package/pi-extension/dist/{cognitive-routing-v2 → cognitive-routing}/render.js +0 -0
@@ -0,0 +1,196 @@
1
+ import { randomInt } from "node:crypto";
2
+ import { mkdir, readFile, rm } from "node:fs/promises";
3
+ import path from "node:path";
4
+
5
+ import { sha256, writeJson, writeText } from "./evidence.mjs";
6
+ import { baseProcessEnvironment, runPiProcess } from "./pi.mjs";
7
+ import { resolveResult } from "./view.mjs";
8
+
9
+ const ANSWERS = new Set(["yes", "no", "unclear"]);
10
+ const RESPONSE_LIMIT = 8000;
11
+ const ARGUMENT_LIMIT = 300;
12
+
13
+ /**
14
+ * Answers each group's review questions with a separate model that sees two unlabeled runs.
15
+ * The result is advisory: it is written beside, never into, canonical run and grade evidence.
16
+ *
17
+ * @param {string} target
18
+ * @param {{group: string | null}} selectors
19
+ * @param {{root?: string, signal?: AbortSignal, model: string, thinking?: string | null}} options
20
+ */
21
+ export async function reviewResult(target, selectors, { root = process.cwd(), signal, model, thinking = null }) {
22
+ const resultDirectory = await resolveResult(target, root);
23
+ const summary = JSON.parse(await readFile(path.join(resultDirectory, "summary.json"), "utf8"));
24
+ const groups = selectGroups(summary, selectors.group);
25
+ const outcomes = [];
26
+ for (const group of groups) {
27
+ if (signal?.aborted) break;
28
+ outcomes.push(
29
+ await reviewGroup(path.join(resultDirectory, "groups", group.id), group.id, { model, thinking, signal }),
30
+ );
31
+ }
32
+ return { resultDirectory, groups: outcomes };
33
+ }
34
+
35
+ function selectGroups(summary, selector) {
36
+ const groups = Array.isArray(summary.groups) ? summary.groups : [];
37
+ if (selector === null) return groups;
38
+ if (summary.definitionKind === "group") throw new Error("--group cannot be used with a direct group result");
39
+ const position = /^\d+$/.test(selector) ? Number(selector) : null;
40
+ const selected = groups.filter((group) => (position === null ? group.id === selector : group.position === position));
41
+ if (selected.length === 0) throw new Error(`Unknown group selector: ${selector}`);
42
+ return selected;
43
+ }
44
+
45
+ async function reviewGroup(groupDirectory, id, { model, thinking, signal }) {
46
+ const definition = JSON.parse(await readFile(path.join(groupDirectory, "definition.json"), "utf8"));
47
+ const questions = Array.isArray(definition.review_questions) ? definition.review_questions : [];
48
+ if (questions.length === 0) return { id, state: "skipped", reason: "no review questions" };
49
+ const runs = {};
50
+ for (const variant of ["baseline", "candidate"]) {
51
+ runs[variant] = JSON.parse(await readFile(path.join(groupDirectory, variant, "run.json"), "utf8"));
52
+ if (runs[variant].state !== "complete")
53
+ return { id, state: "skipped", reason: `${variant} run is ${runs[variant].state}` };
54
+ }
55
+
56
+ const labels = randomInt(2) === 0 ? { A: "baseline", B: "candidate" } : { A: "candidate", B: "baseline" };
57
+ const prompt = reviewPrompt(definition, questions, { A: runs[labels.A], B: runs[labels.B] });
58
+ const reviewDirectory = path.join(groupDirectory, "review");
59
+ await rm(reviewDirectory, { recursive: true, force: true });
60
+ await mkdir(reviewDirectory, { recursive: true });
61
+ await writeText(path.join(reviewDirectory, "prompt.md"), prompt);
62
+
63
+ const args = [
64
+ "--mode",
65
+ "json",
66
+ "-p",
67
+ "--no-session",
68
+ "--no-extensions",
69
+ "--no-skills",
70
+ "--no-prompt-templates",
71
+ "--no-themes",
72
+ "--no-context-files",
73
+ "--no-approve",
74
+ "--no-tools",
75
+ "--model",
76
+ model,
77
+ ];
78
+ if (thinking !== null) args.push("--thinking", thinking);
79
+ args.push(prompt);
80
+ const observation = await runPiProcess({
81
+ command: "pi",
82
+ args,
83
+ cwd: reviewDirectory,
84
+ eventsFile: path.join(reviewDirectory, "events.jsonl"),
85
+ stderrFile: path.join(reviewDirectory, "stderr.log"),
86
+ signal,
87
+ environment: { ...baseProcessEnvironment(), PWD: reviewDirectory, PI_SKIP_VERSION_CHECK: "1", PI_TELEMETRY: "0" },
88
+ });
89
+ await writeText(path.join(reviewDirectory, "final.md"), `${observation.response}\n`);
90
+
91
+ const errors = [];
92
+ if (observation.exitCode !== 0 || !observation.settled || observation.assistantError !== null) {
93
+ errors.push(
94
+ `reviewer did not complete: exit=${observation.exitCode} settled=${observation.settled} error=${observation.assistantError}`,
95
+ );
96
+ }
97
+ const answers = errors.length === 0 ? parseAnswers(observation.response, questions, labels, errors) : [];
98
+ const state = errors.length === 0 ? "complete" : "review-error";
99
+ await writeJson(path.join(groupDirectory, "semantic-grade.json"), {
100
+ schema_version: 1,
101
+ kind: "advisory-review",
102
+ advisory: true,
103
+ state,
104
+ model,
105
+ thinking,
106
+ labels,
107
+ prompt: { file: "review/prompt.md", sha256: sha256(prompt) },
108
+ answers,
109
+ errors,
110
+ usage: observation.usage,
111
+ artifacts: { events: "review/events.jsonl", final: "review/final.md", stderr: "review/stderr.log" },
112
+ completedAt: new Date().toISOString(),
113
+ });
114
+ return { id, state, artifact: "semantic-grade.json" };
115
+ }
116
+
117
+ function reviewPrompt(definition, questions, runs) {
118
+ const task =
119
+ typeof definition.input?.prompt === "string" ? [definition.input.prompt] : (definition.input?.turns ?? []);
120
+ const lines = [
121
+ "You are reviewing two anonymous runs, Run A and Run B, of the same agent task.",
122
+ "Judge only the evidence below. Answer every question for both runs with yes, no, or unclear.",
123
+ "",
124
+ "Task turns:",
125
+ ...task.map((turn, index) => `${index + 1}. ${turn}`),
126
+ "",
127
+ "Questions:",
128
+ ...questions.map((question, index) => `${index + 1}. ${question}`),
129
+ ];
130
+ for (const label of ["A", "B"]) lines.push("", `## Run ${label}`, ...runEvidence(runs[label]));
131
+ lines.push(
132
+ "",
133
+ "Reply with only JSON in this shape:",
134
+ '{"answers":[{"question":1,"A":"yes|no|unclear","B":"yes|no|unclear","rationale":"one sentence"}]}',
135
+ );
136
+ return lines.join("\n");
137
+ }
138
+
139
+ function runEvidence(run) {
140
+ const turns = Array.isArray(run.turns)
141
+ ? run.turns
142
+ : [{ turn: 1, response: run.response, toolActivity: run.toolActivity }];
143
+ const lines = [];
144
+ for (const turn of turns) {
145
+ lines.push(`Turn ${turn.turn ?? 1} response:`, truncate(String(turn.response ?? ""), RESPONSE_LIMIT));
146
+ for (const call of turn.toolActivity ?? []) {
147
+ const status = call.completed ? (call.isError ? "failed" : "succeeded") : "incomplete";
148
+ lines.push(`- tool ${call.toolName} ${status}: ${truncate(JSON.stringify(call.args ?? null), ARGUMENT_LIMIT)}`);
149
+ }
150
+ }
151
+ const changes = run.effects?.changes;
152
+ if (changes) {
153
+ for (const kind of ["created", "modified", "deleted"]) {
154
+ lines.push(`Files ${kind}: ${(changes[kind] ?? []).join(", ") || "none"}`);
155
+ }
156
+ }
157
+ return lines;
158
+ }
159
+
160
+ function truncate(value, limit) {
161
+ return value.length <= limit ? value : `${value.slice(0, limit)}… [truncated ${value.length - limit} characters]`;
162
+ }
163
+
164
+ function parseAnswers(response, questions, labels, errors) {
165
+ const text = response
166
+ .trim()
167
+ .replace(/^```(?:json)?\s*/i, "")
168
+ .replace(/\s*```$/, "");
169
+ let parsed;
170
+ try {
171
+ parsed = JSON.parse(text);
172
+ } catch {
173
+ errors.push("reviewer response is not JSON");
174
+ return [];
175
+ }
176
+ const answers = Array.isArray(parsed?.answers) ? parsed.answers : null;
177
+ if (answers === null || answers.length !== questions.length) {
178
+ errors.push(`reviewer must answer exactly ${questions.length} question(s)`);
179
+ return [];
180
+ }
181
+ const mapped = [];
182
+ for (const [index, question] of questions.entries()) {
183
+ const answer = answers.find((entry) => entry?.question === index + 1);
184
+ if (!answer || !ANSWERS.has(answer.A) || !ANSWERS.has(answer.B) || typeof answer.rationale !== "string") {
185
+ errors.push(`reviewer answer for question ${index + 1} is missing or invalid`);
186
+ continue;
187
+ }
188
+ mapped.push({
189
+ question,
190
+ [labels.A]: answer.A,
191
+ [labels.B]: answer.B,
192
+ rationale: answer.rationale,
193
+ });
194
+ }
195
+ return errors.length === 0 ? mapped : [];
196
+ }
@@ -1,7 +1,7 @@
1
1
  import { mkdir, realpath } from "node:fs/promises";
2
2
  import path from "node:path";
3
3
 
4
- import { loadDefinition, selectDefinition } from "./definitions.mjs";
4
+ import { COMMAND_TOOL, 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";
@@ -12,14 +12,24 @@ const DESCRIPTION_TOOLS = new Set(["read"]);
12
12
  const BODY_TOOLS = new Set(["read", "write", "edit"]);
13
13
  // Native tools must be separated from extension tools before CLI tool selection is built.
14
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: [] } };
15
+ const DEFAULT_RUNTIME = {
16
+ host: "pi",
17
+ session: false,
18
+ prompt: "isolated",
19
+ extensions: [],
20
+ environment: { literal: {}, inherit: [] },
21
+ };
16
22
 
17
23
  /**
18
24
  * @param {string} definitionFile
19
25
  * @param {any} selectors
20
- * @param {{root?: string, signal?: AbortSignal}} options
26
+ * @param {{root?: string, signal?: AbortSignal, budget?: {maxCost: number, spent: number, exhausted: boolean} | null}} options
21
27
  */
22
- export async function runEvaluation(definitionFile, selectors = {}, { root = process.cwd(), signal } = {}) {
28
+ export async function runEvaluation(
29
+ definitionFile,
30
+ selectors = {},
31
+ { root = process.cwd(), signal, budget = null } = {},
32
+ ) {
23
33
  const definitionPath = await realpath(path.resolve(root, definitionFile));
24
34
  const definition = await loadDefinition(definitionPath, { root });
25
35
  const selection = selectDefinition(definition, selectors);
@@ -45,9 +55,27 @@ export async function runEvaluation(definitionFile, selectors = {}, { root = pro
45
55
  startedAt: new Date().toISOString(),
46
56
  });
47
57
 
58
+ // Budget exhaustion cancels only queued subjects; a subject already running completes and is charged.
59
+ const controller = new AbortController();
60
+ const onParentAbort = () => controller.abort();
61
+ if (signal?.aborted) controller.abort();
62
+ else signal?.addEventListener("abort", onParentAbort, { once: true });
63
+ const charge = (run) => {
64
+ if (budget === null) return;
65
+ budget.spent += run.usage?.cost?.total ?? 0;
66
+ if (budget.spent >= budget.maxCost) {
67
+ budget.exhausted = true;
68
+ controller.abort();
69
+ }
70
+ };
71
+
48
72
  const groups = [];
49
- for (const selected of selection.groups) {
50
- groups.push(await runGroup({ selected, root, resultDirectory, signal }));
73
+ try {
74
+ for (const selected of selection.groups) {
75
+ groups.push(await runGroup({ selected, root, resultDirectory, signal: controller.signal, charge }));
76
+ }
77
+ } finally {
78
+ signal?.removeEventListener("abort", onParentAbort);
51
79
  }
52
80
  const state = batchState(groups);
53
81
  const summary = {
@@ -58,6 +86,7 @@ export async function runEvaluation(definitionFile, selectors = {}, { root = pro
58
86
  groups,
59
87
  completedAt: new Date().toISOString(),
60
88
  };
89
+ if (budget !== null) summary.budget = budgetEvidence(budget);
61
90
  await writeJson(path.join(resultDirectory, "summary.json"), summary);
62
91
  return { id: invocationId, path: resultDirectory, state, summary };
63
92
  }
@@ -76,7 +105,10 @@ function assertSupportedSelection(selection) {
76
105
  if (unsupportedNativeTools.length > 0) {
77
106
  throw new Error(`${group.type} execution does not support native tools: ${unsupportedNativeTools.join(", ")}`);
78
107
  }
79
- const customTools = group.tools.filter((tool) => !NATIVE_TOOLS.has(tool) && !supportedTools.has(tool));
108
+ // The evaluator supplies run_command itself when the definition declares commands.
109
+ const customTools = group.tools.filter(
110
+ (tool) => !NATIVE_TOOLS.has(tool) && !supportedTools.has(tool) && tool !== COMMAND_TOOL,
111
+ );
80
112
  if (group.type === "description" && customTools.length > 0) {
81
113
  throw new Error(`${group.type} execution does not support custom tools: ${customTools.join(", ")}`);
82
114
  }
@@ -88,7 +120,11 @@ function assertSupportedSelection(selection) {
88
120
  }
89
121
  }
90
122
 
91
- async function runGroup({ selected, root, resultDirectory, signal }) {
123
+ export function budgetEvidence(budget) {
124
+ return { maxCost: budget.maxCost, spent: Number(budget.spent.toFixed(10)), exhausted: budget.exhausted };
125
+ }
126
+
127
+ async function runGroup({ selected, root, resultDirectory, signal, charge }) {
92
128
  const { group } = selected;
93
129
  const groupDirectory = path.join(resultDirectory, "groups", group.id);
94
130
  await mkdir(groupDirectory, { recursive: true });
@@ -121,6 +157,7 @@ async function runGroup({ selected, root, resultDirectory, signal }) {
121
157
  runs[variant] = cancelledRun(group, variant);
122
158
  } else if (resourceError === null) {
123
159
  runs[variant] = await executeVariant({ group, variant, root, variantDirectory, fixture, runtime, signal });
160
+ charge(runs[variant]);
124
161
  } else {
125
162
  runs[variant] = invalidRun(group, variant, resourceError);
126
163
  }
@@ -110,6 +110,7 @@ export async function prepareRuntime({ runtime, root, groupDirectory }) {
110
110
  return {
111
111
  host: runtime.host,
112
112
  session: runtime.session,
113
+ prompt: runtime.prompt ?? "isolated",
113
114
  environment: { ...runtime.environment },
114
115
  extensions: bundles,
115
116
  };
@@ -145,6 +146,7 @@ export async function materializeRuntime({ runtime, variantDirectory }) {
145
146
  return {
146
147
  host: runtime.host,
147
148
  session: runtime.session,
149
+ prompt: runtime.prompt ?? "isolated",
148
150
  environment: { ...runtime.environment },
149
151
  extensions: bundles,
150
152
  };
@@ -0,0 +1,103 @@
1
+ import { mkdir, readFile } from "node:fs/promises";
2
+ import path from "node:path";
3
+
4
+ import { createInvocationId, writeJson } from "./evidence.mjs";
5
+ import { budgetEvidence, runEvaluation } from "./runner.mjs";
6
+
7
+ const CHECK_STATES = ["pass", "fail", "unavailable"];
8
+
9
+ /**
10
+ * Runs the selection as independent complete invocations and records an aggregate beside them.
11
+ * Each trial remains an ordinary result; the aggregate only counts their deterministic grades.
12
+ *
13
+ * @param {string} definitionFile
14
+ * @param {any} selectors
15
+ * @param {{root?: string, signal?: AbortSignal, trials: number, maxCost: number | null, onTrial?: (result: any) => void}} options
16
+ */
17
+ export async function runTrials(definitionFile, selectors, { root = process.cwd(), signal, trials, maxCost, onTrial }) {
18
+ const budget = maxCost === null ? null : { maxCost, spent: 0, exhausted: false };
19
+ const results = [];
20
+ for (let trial = 0; trial < trials; trial += 1) {
21
+ if (signal?.aborted || budget?.exhausted) break;
22
+ const result = await runEvaluation(definitionFile, selectors, { root, signal, budget });
23
+ results.push(result);
24
+ onTrial?.(result);
25
+ }
26
+
27
+ const id = createInvocationId();
28
+ const directory = path.join(root, ".skill-eval", "runs", id);
29
+ await mkdir(directory, { recursive: true });
30
+ const aggregate = {
31
+ schema_version: 1,
32
+ kind: "aggregate",
33
+ id,
34
+ state: aggregateState(results, trials),
35
+ requestedTrials: trials,
36
+ trials: results.map((result) => ({ id: result.id, state: result.state })),
37
+ groups: await aggregateGroups(results),
38
+ completedAt: new Date().toISOString(),
39
+ };
40
+ if (budget !== null) aggregate.budget = budgetEvidence(budget);
41
+ await writeJson(path.join(directory, "aggregate.json"), aggregate);
42
+ return { id, path: directory, state: aggregate.state, aggregate };
43
+ }
44
+
45
+ function aggregateState(results, requested) {
46
+ if (results.length < requested || results.some((result) => result.state === "cancelled")) return "cancelled";
47
+ if (results.every((result) => result.state === "complete")) return "complete";
48
+ return "partially-complete";
49
+ }
50
+
51
+ async function aggregateGroups(results) {
52
+ /** @type {Map<string, any>} */
53
+ const groups = new Map();
54
+ for (const result of results) {
55
+ for (const group of result.summary.groups) {
56
+ const entry = groups.get(group.id) ?? { id: group.id, trials: 0, comparisons: new Map(), checks: new Map() };
57
+ groups.set(group.id, entry);
58
+ const gradeFile = path.join(
59
+ result.path,
60
+ "groups",
61
+ group.id,
62
+ group.artifacts?.grade ?? "deterministic-grade.json",
63
+ );
64
+ const grade = JSON.parse(await readFile(gradeFile, "utf8"));
65
+ entry.trials += 1;
66
+ for (const check of grade.checks ?? []) {
67
+ const counts = entry.checks.get(check.id) ?? {
68
+ id: check.id,
69
+ variant: check.variant,
70
+ kind: check.kind,
71
+ ...zeroCounts(),
72
+ };
73
+ entry.checks.set(check.id, counts);
74
+ if (CHECK_STATES.includes(check.state)) counts[check.state] += 1;
75
+ }
76
+ for (const comparison of grade.comparisons ?? []) {
77
+ const counts = entry.comparisons.get(comparison.id) ?? {
78
+ id: comparison.id,
79
+ kind: comparison.kind,
80
+ baseline: zeroCounts(),
81
+ candidate: zeroCounts(),
82
+ transitions: {},
83
+ };
84
+ entry.comparisons.set(comparison.id, counts);
85
+ for (const variant of ["baseline", "candidate"]) {
86
+ const state = comparison[variant]?.state;
87
+ if (CHECK_STATES.includes(state)) counts[variant][state] += 1;
88
+ }
89
+ counts.transitions[comparison.transition] = (counts.transitions[comparison.transition] ?? 0) + 1;
90
+ }
91
+ }
92
+ }
93
+ return [...groups.values()].map((entry) => ({
94
+ id: entry.id,
95
+ trials: entry.trials,
96
+ comparisons: [...entry.comparisons.values()],
97
+ checks: [...entry.checks.values()],
98
+ }));
99
+ }
100
+
101
+ function zeroCounts() {
102
+ return { pass: 0, fail: 0, unavailable: 0 };
103
+ }
@@ -22,6 +22,13 @@ class ViewError extends Error {
22
22
 
23
23
  export async function renderResult(target, selectors = DEFAULT_SELECTORS, { root = process.cwd() } = {}) {
24
24
  const resultDirectory = await resolveResult(target, root);
25
+ const aggregateFile = path.join(resultDirectory, "aggregate.json");
26
+ if (await stat(aggregateFile).catch(() => null)) {
27
+ if (selectors.group !== null || selectors.variant !== null) {
28
+ throw new ViewError("selectors are not supported for an aggregate; view an individual trial instead");
29
+ }
30
+ return renderAggregate(resultDirectory, await readJson(aggregateFile, "aggregate"));
31
+ }
25
32
  const summary = await readJson(path.join(resultDirectory, "summary.json"), "result summary");
26
33
  let groups = Array.isArray(summary.groups) ? summary.groups : [];
27
34
  if (summary.definitionKind === "group" && selectors.group !== null) {
@@ -56,6 +63,7 @@ async function renderGroup(resultDirectory, group, selectedVariant) {
56
63
 
57
64
  appendSharedEvidence(lines, definition);
58
65
  appendGradeEvidence(lines, grade, group, definition, selectedVariant);
66
+ await appendAdvisoryReview(lines, groupDirectory, selectedVariant);
59
67
  for (const variant of variants) {
60
68
  await appendVariantEvidence(lines, resultDirectory, groupDirectory, group.id, variant);
61
69
  }
@@ -135,6 +143,64 @@ function appendGradeEvidence(lines, grade, group, definition, selectedVariant) {
135
143
  }
136
144
  }
137
145
 
146
+ // Advisory answers stay visually separate from deterministic grades; they are not canonical evidence.
147
+ async function appendAdvisoryReview(lines, groupDirectory, selectedVariant) {
148
+ const file = path.join(groupDirectory, "semantic-grade.json");
149
+ if (!(await stat(file).catch(() => null))) return;
150
+ const review = await readJson(file, "advisory review");
151
+ lines.push(`Advisory review [${review.state}] model=${review.model} (not canonical evidence)`);
152
+ const variants = selectedVariant === null ? VARIANTS : [selectedVariant];
153
+ for (const [index, answer] of (review.answers ?? []).entries()) {
154
+ lines.push(
155
+ compactRow("review", [
156
+ `Q${index + 1}`,
157
+ ...variants.map((variant) => `${variant}=${answer[variant]}`),
158
+ answer.rationale,
159
+ ]),
160
+ );
161
+ }
162
+ for (const error of review.errors ?? []) lines.push(compactRow("error", ["review", error]));
163
+ }
164
+
165
+ function renderAggregate(resultDirectory, aggregate) {
166
+ const lines = [
167
+ `Aggregate ${aggregate.id} [${aggregate.state}] trials=${aggregate.trials.length}/${aggregate.requestedTrials}`,
168
+ `Path: ${escapeCell(resultDirectory)}`,
169
+ ];
170
+ if (aggregate.budget) {
171
+ lines.push(
172
+ compactRow("budget", [
173
+ `max-cost=${formatNumber(aggregate.budget.maxCost)}`,
174
+ `spent=${formatNumber(aggregate.budget.spent)}`,
175
+ `exhausted=${aggregate.budget.exhausted}`,
176
+ ]),
177
+ );
178
+ }
179
+ for (const trial of aggregate.trials) lines.push(compactRow("trial", [trial.id, trial.state]));
180
+ for (const group of aggregate.groups) {
181
+ lines.push("", `Group ${group.id} trials=${group.trials}`);
182
+ for (const comparison of group.comparisons) {
183
+ lines.push(
184
+ compactRow("comparison", [
185
+ comparison.id,
186
+ comparison.kind,
187
+ `baseline=${passRate(comparison.baseline)}`,
188
+ `candidate=${passRate(comparison.candidate)}`,
189
+ ...Object.entries(comparison.transitions).map(([transition, count]) => `${transition}=${count}`),
190
+ ]),
191
+ );
192
+ }
193
+ for (const check of group.checks) {
194
+ lines.push(compactRow("check", [check.id, check.variant, check.kind, `pass=${passRate(check)}`]));
195
+ }
196
+ }
197
+ return `${lines.join("\n")}\n`;
198
+ }
199
+
200
+ function passRate(counts) {
201
+ return `${counts.pass}/${counts.pass + counts.fail + counts.unavailable}`;
202
+ }
203
+
138
204
  function selectedGradeErrors(errors, definition, selectedVariant) {
139
205
  if (!Array.isArray(errors)) return [];
140
206
  if (selectedVariant === null) return errors;
@@ -185,7 +251,7 @@ async function appendVariantEvidence(lines, resultDirectory, groupDirectory, gro
185
251
  );
186
252
  }
187
253
 
188
- async function resolveResult(target, root) {
254
+ export async function resolveResult(target, root) {
189
255
  const explicit = path.resolve(root, target);
190
256
  const stored = path.join(root, ".skill-eval", "runs", target);
191
257
  const isPath = path.isAbsolute(target) || target.includes("/") || target.includes("\\") || target.startsWith(".");
@@ -0,0 +1,144 @@
1
+ import { spawn } from "node:child_process";
2
+
3
+ import { COMMAND_TOOL } from "./lib/definitions.mjs";
4
+
5
+ const DEFAULT_TIMEOUT_MS = 120 * 1000;
6
+ const OUTPUT_LIMIT_BYTES = 32 * 1024;
7
+ const TERMINATION_GRACE_MS = 2000;
8
+
9
+ function parseCommands(value) {
10
+ const parsed = JSON.parse(value ?? "[]");
11
+ if (!Array.isArray(parsed) || parsed.length === 0) throw new Error("SKILL_EVAL_COMMANDS must declare commands");
12
+ return parsed;
13
+ }
14
+
15
+ function commandEnvironment() {
16
+ return Object.fromEntries(Object.entries(process.env).filter(([key]) => !key.startsWith("SKILL_EVAL_")));
17
+ }
18
+
19
+ function captureStream(stream) {
20
+ const chunks = [];
21
+ const capture = { bytes: 0, kept: 0, truncated: false };
22
+ stream.on("data", (chunk) => {
23
+ capture.bytes += chunk.length;
24
+ const room = OUTPUT_LIMIT_BYTES - capture.kept;
25
+ if (room <= 0) {
26
+ capture.truncated = true;
27
+ return;
28
+ }
29
+ const kept = chunk.length > room ? chunk.subarray(0, room) : chunk;
30
+ if (kept.length < chunk.length) capture.truncated = true;
31
+ chunks.push(kept);
32
+ capture.kept += kept.length;
33
+ });
34
+ capture.text = () => Buffer.concat(chunks).toString("utf8");
35
+ return capture;
36
+ }
37
+
38
+ function signalGroup(child, signal) {
39
+ try {
40
+ if (process.platform === "win32") child.kill(signal);
41
+ else process.kill(-child.pid, signal);
42
+ } catch {
43
+ // The process group already exited.
44
+ }
45
+ }
46
+
47
+ // Runs one author-declared argv in the workspace. A nonzero exit or timeout is ordinary evidence, not a tool error.
48
+ function runCommand(command, cwd, signal) {
49
+ const timeoutMs = command.timeout_ms ?? DEFAULT_TIMEOUT_MS;
50
+ return new Promise((resolve, reject) => {
51
+ const child = spawn(command.argv[0], command.argv.slice(1), {
52
+ cwd,
53
+ env: commandEnvironment(),
54
+ shell: false,
55
+ detached: process.platform !== "win32",
56
+ stdio: ["ignore", "pipe", "pipe"],
57
+ });
58
+ const stdout = captureStream(child.stdout);
59
+ const stderr = captureStream(child.stderr);
60
+ let timedOut = false;
61
+ let forced;
62
+ const stop = () => {
63
+ signalGroup(child, "SIGTERM");
64
+ forced = setTimeout(() => signalGroup(child, "SIGKILL"), TERMINATION_GRACE_MS);
65
+ };
66
+ const timer = setTimeout(() => {
67
+ timedOut = true;
68
+ stop();
69
+ }, timeoutMs);
70
+ const onAbort = () => stop();
71
+ signal?.addEventListener("abort", onAbort, { once: true });
72
+ child.once("error", (error) => {
73
+ clearTimeout(timer);
74
+ clearTimeout(forced);
75
+ signal?.removeEventListener("abort", onAbort);
76
+ reject(error);
77
+ });
78
+ child.once("close", (exitCode, exitSignal) => {
79
+ clearTimeout(timer);
80
+ clearTimeout(forced);
81
+ signal?.removeEventListener("abort", onAbort);
82
+ resolve({ exitCode, signal: exitSignal, timedOut, timeoutMs, stdout, stderr });
83
+ });
84
+ });
85
+ }
86
+
87
+ function renderOutput(label, capture) {
88
+ const note = capture.truncated ? ` (truncated: showing ${capture.kept} of ${capture.bytes} bytes)` : "";
89
+ return `--- ${label}${note} ---\n${capture.text()}`;
90
+ }
91
+
92
+ export default function registerCommandTool(pi) {
93
+ const commands = parseCommands(process.env.SKILL_EVAL_COMMANDS);
94
+ const workspace = process.env.SKILL_EVAL_WRITABLE_ROOT;
95
+ if (!workspace) throw new Error("SKILL_EVAL_WRITABLE_ROOT is required");
96
+ const byId = new Map(commands.map((command) => [command.id, command]));
97
+
98
+ pi.registerTool({
99
+ name: COMMAND_TOOL,
100
+ label: "Run command",
101
+ description: `Run one declared command in the workspace. Available commands: ${commands
102
+ .map((command) => `${command.id} (${command.argv.join(" ")})`)
103
+ .join("; ")}.`,
104
+ parameters: {
105
+ type: "object",
106
+ additionalProperties: false,
107
+ required: ["command"],
108
+ properties: {
109
+ command: { type: "string", enum: commands.map((command) => command.id), description: "Declared command ID" },
110
+ },
111
+ },
112
+ executionMode: "sequential",
113
+ async execute(_toolCallId, input, signal) {
114
+ const command = byId.get(input?.command);
115
+ if (!command) throw new Error(`Command is not declared for this evaluation: ${String(input?.command)}`);
116
+ const result = await runCommand(command, workspace, signal);
117
+ const status = result.timedOut
118
+ ? `timed out after ${result.timeoutMs} ms`
119
+ : `exit code: ${result.exitCode ?? `signal ${result.signal}`}`;
120
+ return {
121
+ content: [
122
+ {
123
+ type: "text",
124
+ text: [
125
+ `$ ${command.argv.join(" ")}`,
126
+ status,
127
+ renderOutput("stdout", result.stdout),
128
+ renderOutput("stderr", result.stderr),
129
+ ].join("\n"),
130
+ },
131
+ ],
132
+ details: {
133
+ id: command.id,
134
+ argv: command.argv,
135
+ exitCode: result.exitCode,
136
+ signal: result.signal,
137
+ timedOut: result.timedOut,
138
+ stdout: { bytes: result.stdout.bytes, truncated: result.stdout.truncated },
139
+ stderr: { bytes: result.stderr.bytes, truncated: result.stderr.truncated },
140
+ },
141
+ };
142
+ },
143
+ });
144
+ }