@hassangameryt/freeflow 0.7.2 → 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 (267) 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 +151 -136
  7. package/capabilities/cognitive-routing/SKILL.md +110 -96
  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 +40 -21
  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/cognitive-routing/event-store.js +280 -0
  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/projection.js +503 -0
  29. package/pi-extension/dist/cognitive-routing/provenance.js +81 -0
  30. package/pi-extension/dist/{cognitive-routing-v2 → cognitive-routing}/render.js +31 -10
  31. package/pi-extension/dist/cognitive-routing/runtime.js +396 -0
  32. package/pi-extension/dist/cognitive-routing/schemas.js +209 -0
  33. package/pi-extension/dist/cognitive-routing/session.js +221 -0
  34. package/pi-extension/dist/cognitive-routing/sources.js +291 -0
  35. package/pi-extension/dist/cognitive-routing/state.js +487 -0
  36. package/pi-extension/dist/cognitive-routing/status.js +80 -0
  37. package/pi-extension/dist/cognitive-routing/tools.js +60 -0
  38. package/pi-extension/dist/{cognitive-routing-v2 → cognitive-routing}/types.js +10 -12
  39. package/pi-extension/dist/compaction/carry.js +191 -0
  40. package/pi-extension/dist/compaction/config.js +22 -0
  41. package/pi-extension/dist/compaction/controller.js +471 -0
  42. package/pi-extension/dist/compaction/harness.js +77 -0
  43. package/pi-extension/dist/compaction/results.js +158 -0
  44. package/pi-extension/dist/compaction/thresholds.js +35 -0
  45. package/pi-extension/dist/compaction/tool.js +57 -0
  46. package/pi-extension/dist/host/branch.js +13 -0
  47. package/pi-extension/dist/host/canonical.js +12 -0
  48. package/pi-extension/dist/host/catalog.js +77 -0
  49. package/pi-extension/dist/host/config.js +374 -0
  50. package/pi-extension/dist/host/projection-tags.js +60 -0
  51. package/pi-extension/dist/host/prompts.js +82 -0
  52. package/pi-extension/dist/{session-sources → host}/read-only-session.js +112 -1
  53. package/pi-extension/dist/host/request-history.js +298 -0
  54. package/pi-extension/dist/host/runtime-state.js +189 -0
  55. package/pi-extension/dist/host/settings/freeflow-command.js +1570 -0
  56. package/pi-extension/dist/{settings/settings-tui.js → host/settings/settings-view.js} +186 -73
  57. package/pi-extension/dist/host/staging.js +28 -0
  58. package/pi-extension/dist/host/status.js +101 -0
  59. package/pi-extension/dist/index.js +441 -170
  60. package/pi-extension/dist/provider-support/cache/anchor.js +21 -0
  61. package/pi-extension/dist/provider-support/cache/anthropic.js +102 -0
  62. package/pi-extension/dist/provider-support/cache/health.js +73 -0
  63. package/pi-extension/dist/provider-support/cache/index.js +58 -0
  64. package/pi-extension/dist/provider-support/cache/keepalive.js +210 -0
  65. package/pi-extension/dist/provider-support/cache/monitor.js +17 -0
  66. package/pi-extension/dist/provider-support/cache/openai-responses.js +48 -0
  67. package/pi-extension/dist/provider-support/effort.js +13 -0
  68. package/pi-extension/dist/provider-support/index.js +45 -0
  69. package/pi-extension/dist/provider-support/openai/adapter.js +127 -0
  70. package/pi-extension/dist/provider-support/openai/history.js +127 -0
  71. package/pi-extension/dist/provider-support/openai/index.js +15 -0
  72. package/pi-extension/dist/provider-support/openai/session-state.js +105 -0
  73. package/pi-extension/dist/provider-support/routes.js +17 -0
  74. package/pi-extension/dist/tool-execution/apply-patch/grammar.js +23 -0
  75. package/pi-extension/dist/tool-execution/apply-patch/match.js +38 -0
  76. package/pi-extension/dist/tool-execution/apply-patch/parser.js +177 -0
  77. package/pi-extension/dist/tool-execution/apply-patch/plan.js +201 -0
  78. package/pi-extension/dist/tool-execution/apply-patch/tool.js +84 -0
  79. package/pi-extension/dist/tool-execution/background.js +238 -0
  80. package/pi-extension/dist/tool-execution/bash-guard.js +23 -0
  81. package/pi-extension/dist/tool-execution/config.js +22 -0
  82. package/pi-extension/dist/tool-execution/edit-messages.js +120 -0
  83. package/pi-extension/dist/tool-execution/file-state.js +138 -0
  84. package/pi-extension/dist/tool-execution/file-tracking.js +143 -0
  85. package/pi-extension/dist/tool-execution/prompt.js +54 -0
  86. package/pi-extension/dist/tool-execution/tools.js +16 -0
  87. package/pi-extension/freeflow/README.md +9 -0
  88. package/plugin.json +1 -1
  89. package/runtime/prompts/cognitive-routing.md +4 -4
  90. package/runtime/prompts/core.md +22 -75
  91. package/runtime/prompts/interaction-contract.md +15 -8
  92. package/runtime/prompts/tool-execution.md +38 -0
  93. package/runtime/prompts/working-method.md +108 -0
  94. package/skills/action-selection/SKILL.md +39 -47
  95. package/skills/action-selection/references/trajectory-stalls.md +2 -0
  96. package/skills/bypass/SKILL.md +2 -2
  97. package/skills/commit-work/SKILL.md +31 -83
  98. package/skills/commit-work/references/git-state-edges.md +1 -1
  99. package/skills/commit-work/references/simple-push.md +2 -0
  100. package/skills/commit-work/references/staging-decisions.md +1 -1
  101. package/skills/decision-gate/SKILL.md +31 -50
  102. package/skills/design-for-depth/SKILL.md +44 -53
  103. package/skills/design-for-depth/references/design-pressure-signals.md +46 -144
  104. package/skills/design-for-depth/references/implementation-decisions.md +4 -4
  105. package/skills/design-for-depth/references/interface-design-loop.md +5 -36
  106. package/skills/design-for-depth/references/module-and-dependency-design.md +2 -32
  107. package/skills/design-for-depth/references/state-and-failure-boundaries.md +2 -13
  108. package/skills/diagnose-failure/SKILL.md +39 -24
  109. package/skills/diagnose-failure/references/diagnostic-loop-catalog.md +2 -0
  110. package/skills/diagnose-failure/references/flaky-and-performance-diagnosis.md +2 -2
  111. package/skills/discuss/SKILL.md +37 -13
  112. package/skills/discuss/references/discussion-continuity.md +7 -27
  113. package/skills/evaluate-skill/SKILL.md +49 -80
  114. package/skills/evaluate-skill/references/evaluation-definition-schema.md +35 -5
  115. package/skills/evaluate-skill/references/evaluation-design.md +14 -14
  116. package/skills/evaluate-skill/references/execution-and-evidence.md +26 -5
  117. package/skills/evaluate-skill/references/review-and-revision.md +3 -1
  118. package/skills/evaluate-skill/scripts/lib/definitions.mjs +49 -8
  119. package/skills/evaluate-skill/scripts/lib/pi.mjs +14 -7
  120. package/skills/evaluate-skill/scripts/lib/review.mjs +196 -0
  121. package/skills/evaluate-skill/scripts/lib/runner.mjs +45 -8
  122. package/skills/evaluate-skill/scripts/lib/sandbox.mjs +2 -0
  123. package/skills/evaluate-skill/scripts/lib/trials.mjs +103 -0
  124. package/skills/evaluate-skill/scripts/lib/view.mjs +67 -1
  125. package/skills/evaluate-skill/scripts/pi-command-tool.mjs +144 -0
  126. package/skills/evaluate-skill/scripts/pi-guard.mjs +30 -13
  127. package/skills/evaluate-skill/scripts/skill-eval.mjs +114 -34
  128. package/skills/execute-work/SKILL.md +41 -9
  129. package/skills/execute-work/references/code-practices.md +2 -0
  130. package/skills/execute-work/references/execute-work-edges.md +11 -1
  131. package/skills/handoff/SKILL.md +1 -1
  132. package/skills/handoff/references/handoff-templates.md +1 -1
  133. package/skills/review-artifact/SKILL.md +42 -41
  134. package/skills/review-artifact/references/adjudicate-artifact-review.md +1 -1
  135. package/skills/review-artifact/references/independent-artifact-reviewer-contract.md +1 -1
  136. package/skills/review-work/SKILL.md +38 -32
  137. package/skills/review-work/references/adjudicate-work-review.md +1 -1
  138. package/skills/review-work/references/independent-work-reviewer-contract.md +1 -1
  139. package/skills/review-work/references/security-risk-lens.md +2 -4
  140. package/skills/setup-freeflow/SKILL.md +3 -1
  141. package/skills/setup-freeflow/references/activation-contract.md +7 -13
  142. package/skills/setup-freeflow/references/host-setup.md +3 -3
  143. package/skills/track-work/SKILL.md +113 -161
  144. package/skills/track-work/references/working-record-format.md +189 -350
  145. package/skills/track-work/scripts/lib/document.mjs +62 -6
  146. package/skills/track-work/scripts/lib/format.mjs +16 -1
  147. package/skills/track-work/scripts/lib/store.mjs +9 -8
  148. package/skills/track-work/scripts/lib/transitions.mjs +79 -4
  149. package/skills/track-work/scripts/working-record.mjs +34 -12
  150. package/skills/verify-work/SKILL.md +29 -20
  151. package/skills/verify-work/references/browser-runtime-evidence.md +1 -5
  152. package/skills/verify-work/references/integration-evidence.md +2 -0
  153. package/skills/verify-work/references/performance-evidence.md +3 -1
  154. package/skills/verify-work/references/test-design.md +3 -1
  155. package/skills/workflow/SKILL.md +65 -21
  156. package/skills/workflow/references/domain-skill-composition.md +5 -3
  157. package/skills/write-docs/SKILL.md +83 -0
  158. package/skills/write-docs/references/documentation-shapes.md +119 -0
  159. package/skills/write-docs/scripts/docs-check.mjs +139 -0
  160. package/skills/write-plan/SKILL.md +40 -40
  161. package/skills/write-plan/references/plan-shapes.md +1 -1
  162. package/skills/write-skill/SKILL.md +107 -147
  163. package/skills/write-skill/references/activation-boundaries.md +2 -0
  164. package/skills/write-skill/references/agent-first-instructions.md +22 -10
  165. package/skills/write-skill/references/progressive-disclosure.md +4 -2
  166. package/skills/write-skill/references/skill-dependency-graphs.md +2 -0
  167. package/skills/write-skill/scripts/lib/similarity.mjs +56 -0
  168. package/skills/write-skill/scripts/skill-author.mjs +18 -2
  169. package/skills/write-spec/SKILL.md +34 -41
  170. package/skills/write-spec/references/artifact-standards.md +1 -1
  171. package/skills/write-spec/references/decision-records.md +2 -0
  172. package/skills/write-spec/references/spec-shapes.md +1 -1
  173. package/capabilities/context-virtualization/SKILL.md +0 -103
  174. package/capabilities/conversation-history/SKILL.md +0 -53
  175. package/hooks/tests/check-host-adapters.test.mjs +0 -82
  176. package/hooks/tests/check-runtime-context-hook.sh +0 -212
  177. package/pi-extension/dist/cognitive-routing-v2/projection.js +0 -321
  178. package/pi-extension/dist/cognitive-routing-v2/runtime.js +0 -1585
  179. package/pi-extension/dist/cognitive-routing-v2/schemas.js +0 -107
  180. package/pi-extension/dist/cognitive-routing-v2/state.js +0 -612
  181. package/pi-extension/dist/cognitive-routing-v2/tools.js +0 -91
  182. package/pi-extension/dist/context-virtualization/commands.js +0 -89
  183. package/pi-extension/dist/context-virtualization/projector.js +0 -42
  184. package/pi-extension/dist/context-virtualization/runtime.js +0 -356
  185. package/pi-extension/dist/context-virtualization/state.js +0 -57
  186. package/pi-extension/dist/context-virtualization/types.js +0 -10
  187. package/pi-extension/dist/conversation-history/passages.js +0 -66
  188. package/pi-extension/dist/conversation-history/ranking.js +0 -105
  189. package/pi-extension/dist/conversation-history/runtime.js +0 -226
  190. package/pi-extension/dist/conversation-history/search.js +0 -56
  191. package/pi-extension/dist/conversation-history/types.js +0 -1
  192. package/pi-extension/dist/freeflow-context/resolver.js +0 -120
  193. package/pi-extension/dist/freeflow-context/runtime.js +0 -80
  194. package/pi-extension/dist/freeflow-context/source-projector.js +0 -95
  195. package/pi-extension/dist/freeflow-context/tool.js +0 -313
  196. package/pi-extension/dist/freeflow-context/types.js +0 -12
  197. package/pi-extension/dist/freeflow-context/visibility.js +0 -28
  198. package/pi-extension/dist/integrations/mcp-config.js +0 -29
  199. package/pi-extension/dist/runtime/runtime-context.js +0 -735
  200. package/pi-extension/dist/runtime/runtime-identity.js +0 -5
  201. package/pi-extension/dist/session-sources/events.js +0 -195
  202. package/pi-extension/dist/session-sources/sources.js +0 -233
  203. package/pi-extension/dist/settings/settings-ui.js +0 -1160
  204. package/pi-extension/dist/ui/text-component.js +0 -57
  205. package/pi-extension/src/cognitive-routing-v2/budget.ts +0 -42
  206. package/pi-extension/src/cognitive-routing-v2/config.ts +0 -143
  207. package/pi-extension/src/cognitive-routing-v2/projection.ts +0 -365
  208. package/pi-extension/src/cognitive-routing-v2/render.ts +0 -256
  209. package/pi-extension/src/cognitive-routing-v2/runtime.ts +0 -1634
  210. package/pi-extension/src/cognitive-routing-v2/schemas.ts +0 -109
  211. package/pi-extension/src/cognitive-routing-v2/state.ts +0 -622
  212. package/pi-extension/src/cognitive-routing-v2/tools.ts +0 -93
  213. package/pi-extension/src/cognitive-routing-v2/types.ts +0 -222
  214. package/pi-extension/src/context-virtualization/commands.ts +0 -111
  215. package/pi-extension/src/context-virtualization/projector.ts +0 -62
  216. package/pi-extension/src/context-virtualization/runtime.ts +0 -406
  217. package/pi-extension/src/context-virtualization/state.ts +0 -79
  218. package/pi-extension/src/context-virtualization/types.ts +0 -92
  219. package/pi-extension/src/conversation-history/passages.ts +0 -81
  220. package/pi-extension/src/conversation-history/ranking.ts +0 -155
  221. package/pi-extension/src/conversation-history/runtime.ts +0 -250
  222. package/pi-extension/src/conversation-history/search.ts +0 -79
  223. package/pi-extension/src/conversation-history/types.ts +0 -31
  224. package/pi-extension/src/freeflow-context/resolver.ts +0 -139
  225. package/pi-extension/src/freeflow-context/runtime.ts +0 -100
  226. package/pi-extension/src/freeflow-context/source-projector.ts +0 -106
  227. package/pi-extension/src/freeflow-context/tool.ts +0 -365
  228. package/pi-extension/src/freeflow-context/types.ts +0 -70
  229. package/pi-extension/src/freeflow-context/visibility.ts +0 -53
  230. package/pi-extension/src/index.ts +0 -368
  231. package/pi-extension/src/integrations/mcp-config.ts +0 -32
  232. package/pi-extension/src/runtime/runtime-context.ts +0 -838
  233. package/pi-extension/src/runtime/runtime-identity.ts +0 -8
  234. package/pi-extension/src/session-sources/events.ts +0 -208
  235. package/pi-extension/src/session-sources/read-only-session.ts +0 -170
  236. package/pi-extension/src/session-sources/sources.ts +0 -263
  237. package/pi-extension/src/settings/settings-tui.ts +0 -501
  238. package/pi-extension/src/settings/settings-ui.ts +0 -1422
  239. package/pi-extension/src/ui/text-component.ts +0 -66
  240. package/pi-extension/tests/cognitive-routing-v2/communication.test.js +0 -484
  241. package/pi-extension/tests/cognitive-routing-v2/compaction-input.test.js +0 -183
  242. package/pi-extension/tests/cognitive-routing-v2/native.test.js +0 -441
  243. package/pi-extension/tests/cognitive-routing-v2/performance.test.js +0 -87
  244. package/pi-extension/tests/cognitive-routing-v2/persistence.test.js +0 -245
  245. package/pi-extension/tests/cognitive-routing-v2/projection.test.js +0 -241
  246. package/pi-extension/tests/cognitive-routing-v2/recovery.test.js +0 -236
  247. package/pi-extension/tests/cognitive-routing-v2/render.test.js +0 -230
  248. package/pi-extension/tests/cognitive-routing-v2/runtime.test.js +0 -153
  249. package/pi-extension/tests/cognitive-routing-v2/stabilization.test.js +0 -521
  250. package/pi-extension/tests/cognitive-routing-v2/state.test.js +0 -275
  251. package/pi-extension/tests/context-virtualization/extension.test.js +0 -765
  252. package/pi-extension/tests/context-virtualization/projector.test.js +0 -110
  253. package/pi-extension/tests/context-virtualization/runtime.test.js +0 -216
  254. package/pi-extension/tests/conversation-history/runtime.test.js +0 -98
  255. package/pi-extension/tests/conversation-history/search.test.js +0 -97
  256. package/pi-extension/tests/fixtures/pi-host.js +0 -4
  257. package/pi-extension/tests/fixtures/routing-native.js +0 -188
  258. package/pi-extension/tests/integration/mode-free-core.test.js +0 -219
  259. package/pi-extension/tests/integration/pi-extension.test.js +0 -968
  260. package/pi-extension/tests/integration/prompt-architecture.test.js +0 -431
  261. package/pi-extension/tests/integration/unaffected-regressions.test.js +0 -478
  262. package/pi-extension/tests/settings/settings-tui.test.js +0 -243
  263. package/pi-extension/tsconfig.json +0 -16
  264. package/runtime/prompts/context-virtualization.md +0 -3
  265. package/runtime/prompts/conversation-history.md +0 -3
  266. package/skills/design-for-depth/references/software-design-philosophy.md +0 -202
  267. /package/pi-extension/dist/{cognitive-routing-v2 → cognitive-routing}/budget.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
+ }