@hassangameryt/freeflow 0.8.0 → 0.9.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 (290) hide show
  1. package/.agents/plugins/marketplace.json +2 -2
  2. package/.claude-plugin/marketplace.json +1 -1
  3. package/.claude-plugin/plugin.json +2 -2
  4. package/.codex-plugin/plugin.json +3 -3
  5. package/.cursor-plugin/plugin.json +1 -1
  6. package/README.md +96 -149
  7. package/capabilities/cognitive-routing/SKILL.md +21 -129
  8. package/capabilities/cognitive-routing/references/coordinator.md +163 -0
  9. package/capabilities/cognitive-routing/references/worker.md +38 -0
  10. package/capabilities/compaction/SKILL.md +3 -3
  11. package/capabilities/compaction/references/summary-format.md +26 -15
  12. package/capabilities/messaging/SKILL.md +49 -0
  13. package/capabilities/task-memory/SKILL.md +87 -0
  14. package/command-surface.json +6 -54
  15. package/gemini-extension.json +1 -1
  16. package/hooks/claude/hooks.json +13 -3
  17. package/hooks/codex/hooks.json +2 -1
  18. package/hooks/shared/runtime-context.mjs +69 -77
  19. package/package.json +25 -8
  20. package/pi-extension/dist/ask-user/ask.js +108 -0
  21. package/pi-extension/dist/ask-user/form.js +250 -0
  22. package/pi-extension/dist/ask-user/questions.js +66 -0
  23. package/pi-extension/dist/ask-user/tool.js +124 -0
  24. package/pi-extension/dist/cognitive-routing/assembler.js +35 -11
  25. package/pi-extension/dist/cognitive-routing/bootstrap.js +90 -0
  26. package/pi-extension/dist/cognitive-routing/config.js +3 -4
  27. package/pi-extension/dist/cognitive-routing/event-store.js +4 -1
  28. package/pi-extension/dist/cognitive-routing/gate.js +20 -5
  29. package/pi-extension/dist/cognitive-routing/handlers.js +15 -13
  30. package/pi-extension/dist/cognitive-routing/model-control.js +17 -8
  31. package/pi-extension/dist/cognitive-routing/projection.js +23 -6
  32. package/pi-extension/dist/cognitive-routing/provenance.js +3 -1
  33. package/pi-extension/dist/cognitive-routing/render.js +15 -15
  34. package/pi-extension/dist/cognitive-routing/runtime.js +4 -2
  35. package/pi-extension/dist/cognitive-routing/schemas.js +4 -4
  36. package/pi-extension/dist/cognitive-routing/session.js +3 -1
  37. package/pi-extension/dist/cognitive-routing/sources.js +29 -4
  38. package/pi-extension/dist/cognitive-routing/state.js +18 -8
  39. package/pi-extension/dist/cognitive-routing/status.js +4 -3
  40. package/pi-extension/dist/cognitive-routing/tools.js +29 -15
  41. package/pi-extension/dist/cognitive-routing/types.js +10 -1
  42. package/pi-extension/dist/compaction/carry.js +36 -22
  43. package/pi-extension/dist/compaction/config.js +4 -2
  44. package/pi-extension/dist/compaction/controller.js +52 -29
  45. package/pi-extension/dist/compaction/harness.js +19 -3
  46. package/pi-extension/dist/compaction/results.js +14 -13
  47. package/pi-extension/dist/compaction/tool.js +6 -6
  48. package/pi-extension/dist/host/catalog.js +20 -35
  49. package/pi-extension/dist/host/config-validation.js +233 -0
  50. package/pi-extension/dist/host/config.js +78 -91
  51. package/pi-extension/dist/host/inbound.js +142 -0
  52. package/pi-extension/dist/host/projection-tags.js +1 -1
  53. package/pi-extension/dist/host/prompts.js +24 -13
  54. package/pi-extension/dist/host/read-only-session.js +6 -1
  55. package/pi-extension/dist/host/request-history.js +7 -3
  56. package/pi-extension/dist/host/rows.js +25 -0
  57. package/pi-extension/dist/host/runtime-state.js +17 -2
  58. package/pi-extension/dist/host/section-keeper.js +85 -0
  59. package/pi-extension/dist/host/settings/freeflow-command.js +846 -93
  60. package/pi-extension/dist/host/settings/settings-view.js +27 -8
  61. package/pi-extension/dist/host/status.js +19 -5
  62. package/pi-extension/dist/index.js +638 -72
  63. package/pi-extension/dist/messaging/config.js +27 -0
  64. package/pi-extension/dist/messaging/messaging.js +398 -0
  65. package/pi-extension/dist/messaging/registry.js +57 -0
  66. package/pi-extension/dist/messaging/render.js +90 -0
  67. package/pi-extension/dist/messaging/tools.js +109 -0
  68. package/pi-extension/dist/messaging/transport.js +101 -0
  69. package/pi-extension/dist/provider-support/cache/keepalive.js +16 -4
  70. package/pi-extension/dist/task-memory/config.js +49 -0
  71. package/pi-extension/dist/task-memory/registry.js +119 -0
  72. package/pi-extension/dist/task-memory/render.js +107 -0
  73. package/pi-extension/dist/task-memory/runtime.js +994 -0
  74. package/pi-extension/dist/task-memory/tool.js +233 -0
  75. package/pi-extension/dist/tool-execution/apply-patch/tool.js +1 -1
  76. package/pi-extension/dist/tool-execution/background.js +220 -28
  77. package/pi-extension/dist/tool-execution/config.js +14 -3
  78. package/pi-extension/dist/tool-execution/file-tracking.js +24 -4
  79. package/pi-extension/dist/tool-execution/guidance-notices.js +124 -0
  80. package/pi-extension/dist/tool-execution/skill-reads.js +143 -0
  81. package/pi-extension/dist/tool-execution/tools.js +2 -2
  82. package/runtime/prompts/cognitive-routing.md +4 -4
  83. package/runtime/prompts/core.md +24 -14
  84. package/runtime/prompts/interaction-contract.md +7 -4
  85. package/runtime/prompts/tool-execution.md +3 -16
  86. package/runtime/prompts/working-method.md +16 -99
  87. package/skills/deliver-work/SKILL.md +76 -0
  88. package/skills/deliver-work/references/branch-closeout.md +134 -0
  89. package/skills/deliver-work/references/commit.md +83 -0
  90. package/skills/deliver-work/references/launch.md +94 -0
  91. package/skills/deliver-work/references/release.md +85 -0
  92. package/skills/deliver-work/scripts/commit-state.mjs +139 -0
  93. package/skills/deliver-work/scripts/delivery-state.mjs +107 -0
  94. package/skills/deliver-work/scripts/lib/git-state.mjs +178 -0
  95. package/skills/deliver-work/scripts/push-route.mjs +122 -0
  96. package/skills/deliver-work/scripts/release-preflight.mjs +177 -0
  97. package/skills/discuss-work/SKILL.md +148 -0
  98. package/skills/{discuss → discuss-work}/references/discussion-continuity.md +10 -27
  99. package/skills/discuss-work/references/learning-action.md +36 -0
  100. package/skills/discuss-work/scripts/choices.mjs +101 -0
  101. package/skills/discuss-work/scripts/lib/choices.mjs +159 -0
  102. package/skills/evaluate-behavior/SKILL.md +107 -0
  103. package/skills/evaluate-behavior/references/authoring.md +381 -0
  104. package/skills/evaluate-behavior/references/coding-tasks.md +145 -0
  105. package/skills/evaluate-behavior/references/conversations-pipelines-judges.md +177 -0
  106. package/skills/evaluate-behavior/references/design-and-measurement.md +273 -0
  107. package/skills/evaluate-behavior/references/running-and-evidence.md +245 -0
  108. package/skills/evaluate-behavior/scripts/claude-guard.mjs +76 -0
  109. package/skills/evaluate-behavior/scripts/guard-rules.mjs +309 -0
  110. package/skills/evaluate-behavior/scripts/lib/agreement.mjs +90 -0
  111. package/skills/evaluate-behavior/scripts/lib/arms.mjs +55 -0
  112. package/skills/evaluate-behavior/scripts/lib/budget.mjs +87 -0
  113. package/skills/evaluate-behavior/scripts/lib/build-source.mjs +287 -0
  114. package/skills/evaluate-behavior/scripts/lib/clear.mjs +74 -0
  115. package/skills/evaluate-behavior/scripts/lib/container.mjs +395 -0
  116. package/skills/evaluate-behavior/scripts/lib/decision.mjs +418 -0
  117. package/skills/evaluate-behavior/scripts/lib/definitions.mjs +1072 -0
  118. package/skills/evaluate-behavior/scripts/lib/freeflow.mjs +103 -0
  119. package/skills/evaluate-behavior/scripts/lib/grade-command.mjs +591 -0
  120. package/skills/evaluate-behavior/scripts/lib/grade-comparison.mjs +90 -0
  121. package/skills/{evaluate-skill → evaluate-behavior}/scripts/lib/grade-interaction.mjs +19 -18
  122. package/skills/{evaluate-skill → evaluate-behavior}/scripts/lib/grade-support.mjs +32 -5
  123. package/skills/{evaluate-skill → evaluate-behavior}/scripts/lib/grade-workspace.mjs +28 -12
  124. package/skills/evaluate-behavior/scripts/lib/grade.mjs +101 -0
  125. package/skills/evaluate-behavior/scripts/lib/hooks.mjs +98 -0
  126. package/skills/evaluate-behavior/scripts/lib/hosts/claude-code/confinement.mjs +196 -0
  127. package/skills/evaluate-behavior/scripts/lib/hosts/claude-code/constraints.mjs +111 -0
  128. package/skills/evaluate-behavior/scripts/lib/hosts/claude-code/index.mjs +48 -0
  129. package/skills/evaluate-behavior/scripts/lib/hosts/claude-code/isolation.mjs +74 -0
  130. package/skills/evaluate-behavior/scripts/lib/hosts/claude-code/launch.mjs +123 -0
  131. package/skills/evaluate-behavior/scripts/lib/hosts/claude-code/model-call.mjs +73 -0
  132. package/skills/evaluate-behavior/scripts/lib/hosts/claude-code/recorder.mjs +178 -0
  133. package/skills/evaluate-behavior/scripts/lib/hosts/claude-code/session.mjs +494 -0
  134. package/skills/evaluate-behavior/scripts/lib/hosts/claude-code/skills.mjs +81 -0
  135. package/skills/evaluate-behavior/scripts/lib/hosts/index.mjs +31 -0
  136. package/skills/evaluate-behavior/scripts/lib/hosts/names.mjs +3 -0
  137. package/skills/evaluate-behavior/scripts/lib/hosts/pi/confinement.mjs +173 -0
  138. package/skills/evaluate-behavior/scripts/lib/hosts/pi/host.mjs +148 -0
  139. package/skills/evaluate-behavior/scripts/lib/hosts/pi/index.mjs +70 -0
  140. package/skills/evaluate-behavior/scripts/lib/hosts/pi/isolation.mjs +81 -0
  141. package/skills/evaluate-behavior/scripts/lib/hosts/pi/model-call.mjs +79 -0
  142. package/skills/evaluate-behavior/scripts/lib/hosts/pi/process.mjs +241 -0
  143. package/skills/{evaluate-skill/scripts/lib → evaluate-behavior/scripts/lib/hosts/pi}/rpc.mjs +144 -26
  144. package/skills/evaluate-behavior/scripts/lib/identity.mjs +82 -0
  145. package/skills/evaluate-behavior/scripts/lib/judge.mjs +688 -0
  146. package/skills/evaluate-behavior/scripts/lib/metrics.mjs +332 -0
  147. package/skills/evaluate-behavior/scripts/lib/model-call.mjs +17 -0
  148. package/skills/evaluate-behavior/scripts/lib/participants.mjs +616 -0
  149. package/skills/{evaluate-skill → evaluate-behavior}/scripts/lib/process.mjs +22 -0
  150. package/skills/evaluate-behavior/scripts/lib/queue.mjs +54 -0
  151. package/skills/evaluate-behavior/scripts/lib/reference.mjs +343 -0
  152. package/skills/evaluate-behavior/scripts/lib/reuse.mjs +277 -0
  153. package/skills/evaluate-behavior/scripts/lib/review.mjs +933 -0
  154. package/skills/evaluate-behavior/scripts/lib/runner.mjs +732 -0
  155. package/skills/{evaluate-skill → evaluate-behavior}/scripts/lib/sandbox.mjs +119 -14
  156. package/skills/evaluate-behavior/scripts/lib/scan-shell.mjs +457 -0
  157. package/skills/evaluate-behavior/scripts/lib/scan.mjs +479 -0
  158. package/skills/evaluate-behavior/scripts/lib/seal.mjs +91 -0
  159. package/skills/evaluate-behavior/scripts/lib/shell.mjs +560 -0
  160. package/skills/evaluate-behavior/scripts/lib/source-override.mjs +64 -0
  161. package/skills/evaluate-behavior/scripts/lib/subject-file.mjs +88 -0
  162. package/skills/{evaluate-skill/scripts/lib/pi.mjs → evaluate-behavior/scripts/lib/subject.mjs} +235 -265
  163. package/skills/evaluate-behavior/scripts/lib/task-copy.mjs +124 -0
  164. package/skills/evaluate-behavior/scripts/lib/trials.mjs +524 -0
  165. package/skills/evaluate-behavior/scripts/lib/validity.mjs +68 -0
  166. package/skills/evaluate-behavior/scripts/lib/view-html.mjs +454 -0
  167. package/skills/{evaluate-skill → evaluate-behavior}/scripts/lib/view.mjs +159 -18
  168. package/skills/{evaluate-skill → evaluate-behavior}/scripts/pi-command-tool.mjs +21 -6
  169. package/skills/evaluate-behavior/scripts/pi-container-route.mjs +23 -0
  170. package/skills/evaluate-behavior/scripts/pi-guard.mjs +77 -0
  171. package/skills/evaluate-behavior/scripts/pi-login-refresh.mjs +31 -0
  172. package/skills/evaluate-behavior/scripts/skill-eval.mjs +586 -0
  173. package/skills/execute-work/SKILL.md +96 -104
  174. package/skills/execute-work/references/code-practices.md +27 -51
  175. package/skills/execute-work/references/design.md +271 -0
  176. package/skills/execute-work/references/diagnose.md +117 -0
  177. package/skills/execute-work/references/migration.md +108 -0
  178. package/skills/execute-work/references/simplify.md +65 -0
  179. package/skills/execute-work/scripts/self-review.mjs +422 -0
  180. package/skills/execute-work/scripts/touch-map.mjs +290 -0
  181. package/skills/review-work/SKILL.md +89 -65
  182. package/skills/review-work/references/adjudication.md +94 -0
  183. package/skills/review-work/references/artifact.md +198 -0
  184. package/skills/review-work/references/evals-and-judges.md +66 -0
  185. package/skills/review-work/references/work.md +177 -0
  186. package/skills/review-work/scripts/check-brief.mjs +128 -0
  187. package/skills/review-work/scripts/review-packet.mjs +233 -0
  188. package/skills/setup-freeflow/SKILL.md +68 -17
  189. package/skills/track-work/SKILL.md +52 -20
  190. package/skills/track-work/references/handoff.md +190 -0
  191. package/skills/track-work/references/projects.md +51 -0
  192. package/skills/track-work/references/working-record-format.md +36 -18
  193. package/skills/track-work/scripts/lib/document.mjs +17 -13
  194. package/skills/track-work/scripts/lib/format.mjs +2 -0
  195. package/skills/track-work/scripts/lib/recover.mjs +254 -0
  196. package/skills/track-work/scripts/lib/registry.mjs +656 -0
  197. package/skills/track-work/scripts/lib/search.mjs +40 -0
  198. package/skills/track-work/scripts/lib/store.mjs +46 -15
  199. package/skills/track-work/scripts/lib/views.mjs +50 -1
  200. package/skills/track-work/scripts/working-record.mjs +232 -13
  201. package/skills/verify-work/SKILL.md +45 -37
  202. package/skills/verify-work/references/feature-map.md +120 -0
  203. package/skills/verify-work/references/runtime-evidence.md +203 -0
  204. package/skills/verify-work/references/test-design.md +25 -6
  205. package/skills/verify-work/scripts/test-changes.mjs +246 -0
  206. package/skills/write-docs/SKILL.md +113 -25
  207. package/skills/write-docs/references/documentation-shapes.md +68 -3
  208. package/skills/write-docs/references/spec-and-plan-shapes.md +308 -0
  209. package/skills/write-docs/scripts/artifact-check.mjs +148 -0
  210. package/skills/write-docs/scripts/docs-check.mjs +96 -6
  211. package/skills/write-docs/scripts/plan-lint.mjs +123 -0
  212. package/skills/write-skill/SKILL.md +92 -52
  213. package/skills/write-skill/references/activation-and-wording.md +143 -0
  214. package/skills/write-skill/references/placement-and-dependencies.md +154 -0
  215. package/skills/write-skill/scripts/lib/compare.mjs +701 -0
  216. package/skills/write-skill/scripts/lib/skill-author.mjs +197 -3
  217. package/skills/write-skill/scripts/skill-author.mjs +51 -8
  218. package/capabilities/cognitive-routing/references/both-mode.md +0 -36
  219. package/capabilities/cognitive-routing/references/executor-mode.md +0 -48
  220. package/capabilities/cognitive-routing/references/helper-mode.md +0 -47
  221. package/plugin.json +0 -33
  222. package/skills/action-selection/SKILL.md +0 -92
  223. package/skills/action-selection/references/trajectory-stalls.md +0 -73
  224. package/skills/bypass/SKILL.md +0 -153
  225. package/skills/commit-work/SKILL.md +0 -93
  226. package/skills/commit-work/references/git-state-edges.md +0 -94
  227. package/skills/commit-work/references/simple-push.md +0 -94
  228. package/skills/commit-work/references/staging-decisions.md +0 -106
  229. package/skills/decision-gate/SKILL.md +0 -87
  230. package/skills/design-for-depth/SKILL.md +0 -108
  231. package/skills/design-for-depth/references/design-pressure-signals.md +0 -84
  232. package/skills/design-for-depth/references/implementation-decisions.md +0 -76
  233. package/skills/design-for-depth/references/interface-design-loop.md +0 -105
  234. package/skills/design-for-depth/references/module-and-dependency-design.md +0 -117
  235. package/skills/design-for-depth/references/state-and-failure-boundaries.md +0 -125
  236. package/skills/diagnose-failure/SKILL.md +0 -140
  237. package/skills/diagnose-failure/references/diagnostic-loop-catalog.md +0 -50
  238. package/skills/diagnose-failure/references/flaky-and-performance-diagnosis.md +0 -51
  239. package/skills/discuss/SKILL.md +0 -140
  240. package/skills/evaluate-skill/SKILL.md +0 -188
  241. package/skills/evaluate-skill/references/evaluation-definition-schema.md +0 -415
  242. package/skills/evaluate-skill/references/evaluation-design.md +0 -112
  243. package/skills/evaluate-skill/references/execution-and-evidence.md +0 -111
  244. package/skills/evaluate-skill/references/review-and-revision.md +0 -94
  245. package/skills/evaluate-skill/scripts/lib/definitions.mjs +0 -448
  246. package/skills/evaluate-skill/scripts/lib/grade-comparison.mjs +0 -60
  247. package/skills/evaluate-skill/scripts/lib/grade.mjs +0 -64
  248. package/skills/evaluate-skill/scripts/lib/review.mjs +0 -196
  249. package/skills/evaluate-skill/scripts/lib/runner.mjs +0 -405
  250. package/skills/evaluate-skill/scripts/lib/trials.mjs +0 -103
  251. package/skills/evaluate-skill/scripts/pi-guard.mjs +0 -181
  252. package/skills/evaluate-skill/scripts/skill-eval.mjs +0 -182
  253. package/skills/execute-work/references/execute-work-edges.md +0 -78
  254. package/skills/finish-branch/SKILL.md +0 -117
  255. package/skills/finish-branch/references/integration-options.md +0 -80
  256. package/skills/handoff/SKILL.md +0 -82
  257. package/skills/handoff/references/handoff-templates.md +0 -150
  258. package/skills/launch-work/SKILL.md +0 -116
  259. package/skills/launch-work/references/launch-readiness.md +0 -85
  260. package/skills/migration-work/SKILL.md +0 -115
  261. package/skills/migration-work/references/migration-lifecycle.md +0 -82
  262. package/skills/release-work/SKILL.md +0 -118
  263. package/skills/release-work/references/release-evidence.md +0 -78
  264. package/skills/review-artifact/SKILL.md +0 -143
  265. package/skills/review-artifact/references/adjudicate-artifact-review.md +0 -101
  266. package/skills/review-artifact/references/independent-artifact-reviewer-contract.md +0 -159
  267. package/skills/review-work/references/adjudicate-work-review.md +0 -97
  268. package/skills/review-work/references/independent-work-reviewer-contract.md +0 -139
  269. package/skills/review-work/references/security-risk-lens.md +0 -67
  270. package/skills/setup-freeflow/references/activation-contract.md +0 -103
  271. package/skills/setup-freeflow/references/host-setup.md +0 -67
  272. package/skills/simplify-code/SKILL.md +0 -85
  273. package/skills/simplify-code/references/simplification-patterns.md +0 -70
  274. package/skills/verify-work/references/browser-runtime-evidence.md +0 -51
  275. package/skills/verify-work/references/integration-evidence.md +0 -49
  276. package/skills/verify-work/references/performance-evidence.md +0 -59
  277. package/skills/workflow/SKILL.md +0 -171
  278. package/skills/workflow/references/domain-skill-composition.md +0 -45
  279. package/skills/write-plan/SKILL.md +0 -92
  280. package/skills/write-plan/references/plan-shapes.md +0 -156
  281. package/skills/write-skill/references/activation-boundaries.md +0 -106
  282. package/skills/write-skill/references/agent-first-instructions.md +0 -133
  283. package/skills/write-skill/references/progressive-disclosure.md +0 -129
  284. package/skills/write-skill/references/skill-dependency-graphs.md +0 -192
  285. package/skills/write-spec/SKILL.md +0 -85
  286. package/skills/write-spec/references/artifact-standards.md +0 -86
  287. package/skills/write-spec/references/decision-records.md +0 -80
  288. package/skills/write-spec/references/spec-shapes.md +0 -143
  289. /package/skills/{evaluate-skill → evaluate-behavior}/scripts/lib/evidence.mjs +0 -0
  290. /package/skills/{evaluate-skill → evaluate-behavior}/scripts/pi-observer.mjs +0 -0
@@ -0,0 +1,81 @@
1
+ // Declared skills on Claude Code. Its own skill loader would also find bundled and user skills, so the evaluator
2
+ // keeps it off and delivers declared skills the way Pi's evaluated subjects see them: a listing of each skill's
3
+ // name, description and SKILL.md location in the system prompt, and, for a body run's target, Pi's exact skill
4
+ // command expansion as the first prompt.
5
+
6
+ import { readFile } from "node:fs/promises";
7
+ import path from "node:path";
8
+
9
+ import { escapeXml } from "../../../guard-rules.mjs";
10
+
11
+ const FRONTMATTER = /^---\r?\n([\s\S]*?)\r?\n---(?:\r?\n|$)/;
12
+
13
+ function scalar(value) {
14
+ const trimmed = value.trim();
15
+ if (trimmed.startsWith('"')) return JSON.parse(trimmed);
16
+ if (trimmed.startsWith("'") && trimmed.endsWith("'")) return trimmed.slice(1, -1).replaceAll("''", "'");
17
+ return trimmed;
18
+ }
19
+
20
+ /**
21
+ * A declared skill's SKILL.md: its frontmatter `name`, `description` and `disable-model-invocation`, and its body.
22
+ * Only one-line scalars are read; a skill whose name or description this cannot read is refused, never guessed.
23
+ *
24
+ * @param {string} directory the materialized skill directory
25
+ */
26
+ export async function readSkill(directory) {
27
+ const filePath = path.join(directory, "SKILL.md");
28
+ const contents = await readFile(filePath, "utf8");
29
+ const match = FRONTMATTER.exec(contents);
30
+ if (match === null) throw new Error(`skill has no frontmatter: ${filePath}`);
31
+ const fields = {};
32
+ for (const line of match[1].split(/\r?\n/)) {
33
+ const field = /^([A-Za-z][\w-]*):(.*)$/.exec(line);
34
+ if (field !== null) fields[field[1]] = field[2];
35
+ }
36
+ const read = (key) => {
37
+ if (fields[key] === undefined) return undefined;
38
+ try {
39
+ return scalar(fields[key]);
40
+ } catch {
41
+ return undefined;
42
+ }
43
+ };
44
+ const name = read("name");
45
+ const description = read("description");
46
+ if (!name || !description || /^[>|]/.test(name) || /^[>|]/.test(description)) {
47
+ throw new Error(`skill needs a one-line name and description the Claude Code adapter can read: ${filePath}`);
48
+ }
49
+ return {
50
+ name,
51
+ description,
52
+ filePath,
53
+ baseDir: directory,
54
+ disableModelInvocation: read("disable-model-invocation") === "true",
55
+ body: contents.slice(match[0].length).trim(),
56
+ };
57
+ }
58
+
59
+ /** The `<available_skills>` listing for the system prompt, as Pi writes it; empty when no skill may be invoked. */
60
+ export function skillListing(skills) {
61
+ const listed = skills.filter((skill) => !skill.disableModelInvocation);
62
+ if (listed.length === 0) return "";
63
+ const lines = ["<available_skills>"];
64
+ for (const skill of listed) {
65
+ lines.push(
66
+ " <skill>",
67
+ ` <name>${escapeXml(skill.name)}</name>`,
68
+ ` <description>${escapeXml(skill.description)}</description>`,
69
+ ` <location>${escapeXml(skill.filePath)}</location>`,
70
+ " </skill>",
71
+ );
72
+ }
73
+ lines.push("</available_skills>");
74
+ return lines.join("\n");
75
+ }
76
+
77
+ /** The prompt Pi delivers for `/skill:<name> <prompt>`, so an explicit invocation reads the same on both hosts. */
78
+ export function skillCommandPrompt(skill, prompt) {
79
+ const block = `<skill name="${skill.name}" location="${skill.filePath}">\nReferences are relative to ${skill.baseDir}.\n\n${skill.body}\n</skill>`;
80
+ return prompt ? `${block}\n\n${prompt}` : block;
81
+ }
@@ -0,0 +1,31 @@
1
+ // Evaluation hosts. A host is the agent program that runs a subject's model and tools: Pi or Claude Code. The evaluator's
2
+ // core is host-neutral; each host's adapter implements the same interface:
3
+ //
4
+ // - name, label, command; version(environment): the installed host's version.
5
+ // - resolveExtensions(declared, { deferSources }): declared host extensions, found among the installed ones.
6
+ // - refreshLogins(providers): make every login a batch uses valid before it starts.
7
+ // - effectiveSettings(declared): the evaluator's host defaults with declared settings winning.
8
+ // - prepareIsolation(directory, model, { settings }) -> { environment, evidence, loginDirectory }: a private home,
9
+ // temp and host configuration for one process, with only the declared provider's login, and the directory that
10
+ // login lives in (the shell probes plant a marker there); removeCredentials(directory) after.
11
+ // - prepareLaunch(context) -> { args, environment }: one subject's arguments and the host's own variables.
12
+ // - runOneShot(options), runSession(options): run a subject and return the normalized observation (see subject.mjs).
13
+ // - modelCall(directory, prompt, options): one tool-less call for a judge, reviewer or simulated user.
14
+ // - probeShell(options): run the shell tier's confinement probes through the host (see shell.mjs).
15
+ //
16
+ // Names a definition may use are in names.mjs, which definitions.mjs reads without loading any adapter.
17
+
18
+ import { claudeCodeHost } from "./claude-code/index.mjs";
19
+ import { piHost } from "./pi/index.mjs";
20
+
21
+ const ADAPTERS = new Map([
22
+ [piHost.name, piHost],
23
+ [claudeCodeHost.name, claudeCodeHost],
24
+ ]);
25
+
26
+ /** The adapter for a host name; an unknown name is an error, never a fallback to Pi. */
27
+ export function hostAdapter(name) {
28
+ const adapter = ADAPTERS.get(name);
29
+ if (adapter === undefined) throw new Error(`unknown evaluation host: ${name}`);
30
+ return adapter;
31
+ }
@@ -0,0 +1,3 @@
1
+ // The hosts a definition may name in `host.name`. Each has an adapter in hosts/; definitions.mjs reads this list
2
+ // without loading the adapters.
3
+ export const EVALUATION_HOSTS = new Set(["pi", "claude-code"]);
@@ -0,0 +1,173 @@
1
+ // Pi's part of the shell tier's confinement probes (shell.mjs): the arm's own Pi, with its extensions and the
2
+ // evaluator's guard, runs every probe as a bash call from a local scripted model, and reports what each returned.
3
+
4
+ import { readFile, writeFile } from "node:fs/promises";
5
+ import http from "node:http";
6
+ import path from "node:path";
7
+
8
+ import { baseProcessEnvironment } from "../../process.mjs";
9
+ import { containerRouteExtensionPath, guardExtensionPath, runPiProcess } from "./process.mjs";
10
+
11
+ const PROBE_PROVIDER = "skill-eval-probe";
12
+ const PROBE_MODEL = "confinement";
13
+
14
+ /**
15
+ * Runs the probes through Pi in JSON mode. `guard` is the guard's environment for the probe workspace; `isolation`
16
+ * is the probe's prepared isolation, whose agent directory gets the scripted model.
17
+ *
18
+ * @returns {Promise<{observation: any, launchError: string | null, results: Map<string, {ran: boolean, isError: boolean, output: string}>}>}
19
+ */
20
+ export async function probeShell({ probes, workspace, isolation, extensions, guard, eventsFile, stderrFile, signal }) {
21
+ const model = await startModelStub(probes);
22
+ try {
23
+ const agent = path.join(isolation.evidence.root, "agent");
24
+ await writeFile(
25
+ path.join(agent, "models.json"),
26
+ `${JSON.stringify({ providers: { [PROBE_PROVIDER]: { baseUrl: model.baseUrl, api: "openai-completions", apiKey: "probe", models: [{ id: PROBE_MODEL }] } } }, null, 2)}\n`,
27
+ );
28
+ const environment = {
29
+ ...baseProcessEnvironment(),
30
+ PI_SKIP_VERSION_CHECK: "1",
31
+ PI_TELEMETRY: "0",
32
+ ...guard,
33
+ ...isolation.environment,
34
+ };
35
+ const args = ["--mode", "json", "-p", "--no-session", "--no-extensions", "--extension", guardExtensionPath()];
36
+ for (const extension of extensions) args.push("--extension", extension.path);
37
+ // A container shell's commands reach the container through the route extension, last, as in a subject.
38
+ if (guard.SKILL_EVAL_CONTAINER) args.push("--extension", containerRouteExtensionPath());
39
+ args.push(
40
+ "--no-skills",
41
+ "--no-prompt-templates",
42
+ "--no-themes",
43
+ "--no-context-files",
44
+ "--no-approve",
45
+ "--tools",
46
+ "bash",
47
+ "--model",
48
+ `${PROBE_PROVIDER}/${PROBE_MODEL}`,
49
+ "Run each confinement probe with bash, exactly as given.",
50
+ );
51
+ let observation = null;
52
+ let launchError = null;
53
+ try {
54
+ observation = await runPiProcess({
55
+ command: "pi",
56
+ args,
57
+ cwd: workspace,
58
+ eventsFile,
59
+ stderrFile,
60
+ signal,
61
+ environment,
62
+ });
63
+ } catch (error) {
64
+ launchError = error instanceof Error ? error.message : String(error);
65
+ }
66
+ // Whatever Pi reported before a launch error still counts.
67
+ return { observation, launchError, results: await toolResults(eventsFile) };
68
+ } finally {
69
+ await model.close();
70
+ }
71
+ }
72
+
73
+ /** Each probe's tool result from Pi's JSON events, by tool call ID. */
74
+ async function toolResults(eventsFile) {
75
+ const results = new Map();
76
+ const contents = await readFile(eventsFile, "utf8").catch(() => "");
77
+ for (const line of contents.split("\n")) {
78
+ if (line.trim() === "") continue;
79
+ let event;
80
+ try {
81
+ event = JSON.parse(line);
82
+ } catch {
83
+ continue;
84
+ }
85
+ if (event.type !== "tool_execution_end" || typeof event.toolCallId !== "string") continue;
86
+ const content = Array.isArray(event.result?.content) ? event.result.content : [];
87
+ results.set(event.toolCallId, {
88
+ ran: true,
89
+ isError: event.isError === true,
90
+ output: content
91
+ .filter((part) => part?.type === "text")
92
+ .map((part) => part.text)
93
+ .join(""),
94
+ });
95
+ }
96
+ return results;
97
+ }
98
+
99
+ /**
100
+ * A local OpenAI-compatible chat-completions endpoint that answers the first request with every probe as a bash
101
+ * tool call, and any request carrying tool results with a short final answer. No model is called.
102
+ */
103
+ function startModelStub(probes) {
104
+ return new Promise((resolve, reject) => {
105
+ const server = http.createServer((request, response) => {
106
+ let body = "";
107
+ request.setEncoding("utf8");
108
+ request.on("data", (chunk) => {
109
+ body += chunk;
110
+ });
111
+ request.on("end", () => {
112
+ let parsed = {};
113
+ try {
114
+ parsed = JSON.parse(body || "{}");
115
+ } catch {
116
+ // Answered as a first request.
117
+ }
118
+ const followUp = Array.isArray(parsed.messages) && parsed.messages.some((message) => message?.role === "tool");
119
+ const message = followUp
120
+ ? { role: "assistant", content: "Probes finished." }
121
+ : {
122
+ role: "assistant",
123
+ content: null,
124
+ tool_calls: probes.map((probe, index) => ({
125
+ index,
126
+ id: probe.id,
127
+ type: "function",
128
+ function: { name: "bash", arguments: JSON.stringify({ command: probe.command }) },
129
+ })),
130
+ };
131
+ const finish = followUp ? "stop" : "tool_calls";
132
+ const base = { id: "probe", created: 0, model: parsed.model ?? PROBE_MODEL };
133
+ const usage = { prompt_tokens: 0, completion_tokens: 0, total_tokens: 0 };
134
+ if (parsed.stream !== true) {
135
+ response.writeHead(200, { "content-type": "application/json" });
136
+ response.end(
137
+ JSON.stringify({
138
+ ...base,
139
+ object: "chat.completion",
140
+ choices: [
141
+ {
142
+ index: 0,
143
+ message: { ...message, tool_calls: message.tool_calls?.map(({ index: _index, ...call }) => call) },
144
+ finish_reason: finish,
145
+ },
146
+ ],
147
+ usage,
148
+ }),
149
+ );
150
+ return;
151
+ }
152
+ response.writeHead(200, { "content-type": "text/event-stream", "cache-control": "no-cache" });
153
+ const send = (chunk) =>
154
+ response.write(`data: ${JSON.stringify({ ...base, object: "chat.completion.chunk", ...chunk })}\n\n`);
155
+ send({ choices: [{ index: 0, delta: message, finish_reason: null }] });
156
+ send({ choices: [{ index: 0, delta: {}, finish_reason: finish }] });
157
+ send({ choices: [], usage });
158
+ response.end("data: [DONE]\n\n");
159
+ });
160
+ });
161
+ server.once("error", reject);
162
+ server.listen(0, "127.0.0.1", () => {
163
+ resolve({
164
+ baseUrl: `http://127.0.0.1:${server.address().port}/v1`,
165
+ close: () =>
166
+ new Promise((done) => {
167
+ server.closeAllConnections?.();
168
+ server.close(() => done());
169
+ }),
170
+ });
171
+ });
172
+ });
173
+ }
@@ -0,0 +1,148 @@
1
+ // Pi-specific knowledge the evaluator needs outside a subject process: where the user's Pi agent directory
2
+ // is, how installed Pi packages are found, and how a stored OAuth login is kept fresh. Kept in one module so a
3
+ // later host adapter has one place to grow from (D-007).
4
+
5
+ import { execFile } from "node:child_process";
6
+ import { createHash } from "node:crypto";
7
+ import { mkdir, readFile, rm, stat } from "node:fs/promises";
8
+ import os from "node:os";
9
+ import path from "node:path";
10
+ import { fileURLToPath } from "node:url";
11
+ import { promisify } from "node:util";
12
+
13
+ const run = promisify(execFile);
14
+ const REFRESH_HELPER = fileURLToPath(new URL("../../../pi-login-refresh.mjs", import.meta.url));
15
+ const LOCK_STALE_MS = 2 * 60 * 1000;
16
+ const LOCK_WAIT_MS = 90 * 1000;
17
+
18
+ /**
19
+ * Pi settings the evaluator uses unless a configuration's host.settings declares otherwise: automatic retry and
20
+ * compaction stay off, so a provider error or a compaction never happens silently inside a measured run.
21
+ */
22
+ export const EVALUATOR_PI_SETTINGS = Object.freeze({ retry: { enabled: false }, compaction: { enabled: false } });
23
+
24
+ /** The settings a subject actually runs with: the evaluator's defaults, with every declared setting winning. */
25
+ export function effectivePiSettings(declared = {}) {
26
+ const merge = (base, overlay) => {
27
+ const result = structuredClone(base);
28
+ for (const [key, value] of Object.entries(overlay)) {
29
+ const isObject = (entry) => entry !== null && typeof entry === "object" && !Array.isArray(entry);
30
+ result[key] = isObject(result[key]) && isObject(value) ? merge(result[key], value) : structuredClone(value);
31
+ }
32
+ return result;
33
+ };
34
+ return merge(EVALUATOR_PI_SETTINGS, declared);
35
+ }
36
+
37
+ /** The user's Pi agent directory: an explicit override for the evaluator, then Pi's own variable, then its default. */
38
+ export function piAgentDirectory() {
39
+ return (
40
+ process.env.SKILL_EVAL_CREDENTIALS_DIR ||
41
+ process.env.PI_CODING_AGENT_DIR ||
42
+ path.join(process.env.HOME || os.homedir(), ".pi", "agent")
43
+ );
44
+ }
45
+
46
+ export async function readJsonObject(file) {
47
+ try {
48
+ const value = JSON.parse(await readFile(file, "utf8"));
49
+ return value && typeof value === "object" && !Array.isArray(value) ? value : null;
50
+ } catch {
51
+ return null;
52
+ }
53
+ }
54
+
55
+ /**
56
+ * Finds each declared package extension among the user's installed Pi packages (`<agent dir>/npm/node_modules`),
57
+ * by exact name and version. A missing package or another version refuses the run. An extension built from source
58
+ * (`source` or `path`, see build-source.mjs) is built per group into its resources, so with `deferSources` it stays
59
+ * in its declared position as `{ pending: <declared entry> }`; without it, it is refused.
60
+ *
61
+ * @param {Array<any>} declared
62
+ * @param {{deferSources?: boolean}} [options]
63
+ */
64
+ export async function resolveHostExtensions(declared, { deferSources = false } = {}) {
65
+ const root = path.join(piAgentDirectory(), "npm", "node_modules");
66
+ const resolved = [];
67
+ for (const extension of declared) {
68
+ if (!Object.hasOwn(extension, "package")) {
69
+ if (!deferSources) throw new Error("host.extensions built from source are supported only in arm configurations");
70
+ resolved.push({ pending: extension });
71
+ continue;
72
+ }
73
+ const directory = path.join(root, ...extension.package.split("/"));
74
+ const manifest = await readJsonObject(path.join(directory, "package.json"));
75
+ if (manifest === null) {
76
+ throw new Error(`host.extensions: ${extension.package} is not installed in Pi (looked in ${root})`);
77
+ }
78
+ if (manifest.version !== extension.version) {
79
+ throw new Error(
80
+ `host.extensions: ${extension.package} is ${manifest.version} in Pi, but the definition declares ${extension.version}`,
81
+ );
82
+ }
83
+ resolved.push({ path: directory, package: `${extension.package}@${extension.version}` });
84
+ }
85
+ return resolved;
86
+ }
87
+
88
+ /** Minutes left on a provider's stored OAuth login, or null when the login is not OAuth or not stored. */
89
+ export async function loginMinutesLeft(provider) {
90
+ const login = (await readJsonObject(path.join(piAgentDirectory(), "auth.json")))?.[provider];
91
+ if (!login || login.type !== "oauth" || typeof login.expires !== "number") return null;
92
+ return Math.floor((login.expires - Date.now()) / 60000);
93
+ }
94
+
95
+ /**
96
+ * Makes a provider's stored OAuth login valid for at least `target` minutes when it has fewer than `threshold`
97
+ * left, by asking the installed Pi to refresh it in the user's own agent directory. One evaluator process
98
+ * refreshes at a time; the others wait and then reread. API-key and absent logins are left alone.
99
+ *
100
+ * @returns {Promise<number | null>} minutes left afterwards, or null when the login is not OAuth
101
+ */
102
+ export async function ensureLogin(provider, { threshold, target }) {
103
+ const before = await loginMinutesLeft(provider);
104
+ if (before === null || before >= threshold) return before;
105
+ const authPath = path.join(piAgentDirectory(), "auth.json");
106
+ await withLock(authPath, async () => {
107
+ const current = await loginMinutesLeft(provider);
108
+ if (current !== null && current >= threshold) return;
109
+ const helper = process.env.SKILL_EVAL_LOGIN_REFRESH || REFRESH_HELPER;
110
+ try {
111
+ await run(process.execPath, [helper, authPath, provider, String(target)], { timeout: 60000 });
112
+ } catch (error) {
113
+ const detail = error?.stderr?.trim() || (error instanceof Error ? error.message : String(error));
114
+ throw new Error(`refreshing the ${provider} login failed (log in again in Pi): ${detail}`);
115
+ }
116
+ });
117
+ const after = await loginMinutesLeft(provider);
118
+ if (after === null || after < threshold) {
119
+ throw new Error(`the ${provider} login has ${after ?? 0} minutes left after a refresh; log in again in Pi`);
120
+ }
121
+ return after;
122
+ }
123
+
124
+ async function withLock(authPath, work) {
125
+ const lock = path.join(
126
+ os.tmpdir(),
127
+ `skill-eval-login-${createHash("sha256").update(authPath).digest("hex").slice(0, 16)}.lock`,
128
+ );
129
+ await mkdir(path.dirname(lock), { recursive: true });
130
+ const deadline = Date.now() + LOCK_WAIT_MS;
131
+ for (;;) {
132
+ try {
133
+ await mkdir(lock);
134
+ break;
135
+ } catch (error) {
136
+ if (error?.code !== "EEXIST") throw error;
137
+ const info = await stat(lock).catch(() => null);
138
+ if (info && Date.now() - info.mtimeMs > LOCK_STALE_MS) await rm(lock, { recursive: true, force: true });
139
+ if (Date.now() > deadline) throw new Error(`timed out waiting for another login refresh (${lock})`);
140
+ await new Promise((resolve) => setTimeout(resolve, 200));
141
+ }
142
+ }
143
+ try {
144
+ return await work();
145
+ } finally {
146
+ await rm(lock, { recursive: true, force: true });
147
+ }
148
+ }
@@ -0,0 +1,70 @@
1
+ // The Pi host adapter: everything the evaluator does that depends on Pi, behind the interface in ../index.mjs.
2
+
3
+ import { installedHostVersion } from "../../identity.mjs";
4
+ import { baseProcessEnvironment } from "../../process.mjs";
5
+ import { probeShell } from "./confinement.mjs";
6
+ import { effectivePiSettings, ensureLogin, resolveHostExtensions } from "./host.mjs";
7
+ import { prepareIsolation, removeCopiedCredentials, TARGET_MINUTES } from "./isolation.mjs";
8
+ import { launchModel } from "./model-call.mjs";
9
+ import { piArguments, runPiProcess } from "./process.mjs";
10
+ import { runRpcBodySession } from "./rpc.mjs";
11
+
12
+ export const piHost = Object.freeze({
13
+ name: "pi",
14
+ label: "Pi",
15
+ command: "pi",
16
+
17
+ /** The installed Pi's version, as `pi --version` prints it. */
18
+ version: (environment = baseProcessEnvironment()) => installedHostVersion("pi", environment),
19
+
20
+ resolveExtensions: resolveHostExtensions,
21
+
22
+ /** Makes each provider's stored OAuth login valid for the target window, once, before a batch. */
23
+ async refreshLogins(providers) {
24
+ for (const provider of providers)
25
+ await ensureLogin(provider, { threshold: TARGET_MINUTES, target: TARGET_MINUTES });
26
+ },
27
+
28
+ effectiveSettings: effectivePiSettings,
29
+ prepareIsolation,
30
+ removeCredentials: removeCopiedCredentials,
31
+
32
+ /** Pi's arguments and its own environment switches for one subject. */
33
+ async prepareLaunch({
34
+ group,
35
+ variant,
36
+ environment,
37
+ runtime,
38
+ mode,
39
+ observeContext,
40
+ sessionDirectory,
41
+ model,
42
+ freeflowEntry,
43
+ hostExtensions,
44
+ }) {
45
+ return {
46
+ args: piArguments(
47
+ group,
48
+ environment,
49
+ runtime,
50
+ mode,
51
+ observeContext,
52
+ sessionDirectory,
53
+ model,
54
+ freeflowEntry,
55
+ hostExtensions,
56
+ group.variants[variant]?.shell?.container !== undefined,
57
+ ),
58
+ environment: { PI_SKIP_VERSION_CHECK: "1", PI_TELEMETRY: "0" },
59
+ };
60
+ },
61
+
62
+ /** One prompt in JSON mode; the prompt is the last argument. */
63
+ runOneShot: runPiProcess,
64
+
65
+ /** A persistent RPC session over the declared prompts, then any `nextPrompt` replies. */
66
+ runSession: runRpcBodySession,
67
+
68
+ modelCall: launchModel,
69
+ probeShell,
70
+ });
@@ -0,0 +1,81 @@
1
+ import { mkdir, rm, writeFile } from "node:fs/promises";
2
+ import path from "node:path";
3
+
4
+ import { ensureLogin, piAgentDirectory, readJsonObject } from "./host.mjs";
5
+
6
+ // A copied OAuth login must outlast the process using it: a refresh inside the copy would rotate the token
7
+ // there and leave the user's own login holding a revoked one. So the login is refreshed centrally, in the
8
+ // user's agent directory, whenever it has less than SUBJECT_MINUTES left, to at least TARGET_MINUTES.
9
+ export const SUBJECT_MINUTES = 30;
10
+ export const TARGET_MINUTES = 120;
11
+
12
+ /**
13
+ * Creates a private home, temp directory and Pi agent directory for one host process, and the environment
14
+ * that points the process at them. Only the declared provider's login is copied in, never another's, after
15
+ * a central refresh if it was close to expiring; the declared Pi settings become the agent directory's
16
+ * settings.json. The parent's HOME, TMPDIR, XDG directories and Pi settings are not visible.
17
+ *
18
+ * @param {string} directory where the isolation root is created
19
+ * @param {string | null} model the declared `provider/model`, or null
20
+ * @param {{settings?: Record<string, unknown>}} [options]
21
+ * @returns {Promise<{environment: Record<string, string>, evidence: any, loginDirectory: string}>}
22
+ */
23
+ export async function prepareIsolation(directory, model, { settings = {} } = {}) {
24
+ const root = path.join(directory, "isolation");
25
+ const home = path.join(root, "home");
26
+ const agent = path.join(root, "agent");
27
+ const tmp = path.join(root, "tmp");
28
+ for (const entry of [home, agent, tmp]) await mkdir(entry, { recursive: true });
29
+ if (Object.keys(settings).length > 0) {
30
+ await writeFile(path.join(agent, "settings.json"), `${JSON.stringify(settings, null, 2)}\n`);
31
+ }
32
+
33
+ const provider = typeof model === "string" && model.includes("/") ? model.split("/")[0] : null;
34
+ const source = piAgentDirectory();
35
+ const credentials = [];
36
+ if (provider !== null) {
37
+ // The caller reports a refresh failure as an invalid run, before any host process starts.
38
+ await ensureLogin(provider, { threshold: SUBJECT_MINUTES, target: TARGET_MINUTES });
39
+ const login = (await readJsonObject(path.join(source, "auth.json")))?.[provider];
40
+ if (login && typeof login === "object") {
41
+ await writeFile(path.join(agent, "auth.json"), `${JSON.stringify({ [provider]: login })}\n`, { mode: 0o600 });
42
+ credentials.push({ file: "auth.json", provider, type: typeof login.type === "string" ? login.type : null });
43
+ }
44
+ const models = await readJsonObject(path.join(source, "models.json"));
45
+ const custom = models?.providers?.[provider];
46
+ if (custom && typeof custom === "object") {
47
+ await writeFile(path.join(agent, "models.json"), `${JSON.stringify({ providers: { [provider]: custom } })}\n`, {
48
+ mode: 0o600,
49
+ });
50
+ credentials.push({ file: "models.json", provider, type: "provider" });
51
+ }
52
+ }
53
+
54
+ return {
55
+ environment: {
56
+ HOME: home,
57
+ TMPDIR: tmp,
58
+ TMP: tmp,
59
+ TEMP: tmp,
60
+ XDG_CONFIG_HOME: path.join(home, ".config"),
61
+ XDG_DATA_HOME: path.join(home, ".local", "share"),
62
+ XDG_CACHE_HOME: path.join(home, ".cache"),
63
+ PI_CODING_AGENT_DIR: agent,
64
+ },
65
+ evidence: { root, provider, credentials, settings },
66
+ // Where the host keeps its login: the confinement probe plants a marker here that the shell must not read.
67
+ loginDirectory: agent,
68
+ };
69
+ }
70
+
71
+ /**
72
+ * Deletes the copied login and model entries once the process has exited, so no copy of a credential
73
+ * stays in the run's evidence. Safe to call when nothing was copied.
74
+ *
75
+ * @param {string} directory the directory passed to prepareIsolation
76
+ */
77
+ export async function removeCopiedCredentials(directory) {
78
+ for (const file of ["auth.json", "models.json"]) {
79
+ await rm(path.join(directory, "isolation", "agent", file), { force: true });
80
+ }
81
+ }
@@ -0,0 +1,79 @@
1
+ // One model call through Pi that is not a subject: a judge, a reviewer, or a simulated user. It runs without tools,
2
+ // skills, context files or session, in its own isolated home and agent directory, with the provider's login copied in
3
+ // and removed afterward.
4
+
5
+ import { mkdir, rm } from "node:fs/promises";
6
+ import path from "node:path";
7
+
8
+ import { writeText } from "../../evidence.mjs";
9
+ import { baseProcessEnvironment } from "../../process.mjs";
10
+ import { effectivePiSettings } from "./host.mjs";
11
+ import { prepareIsolation, removeCopiedCredentials } from "./isolation.mjs";
12
+ import { runPiProcess } from "./process.mjs";
13
+
14
+ /**
15
+ * Starts one model call on a prompt in its own directory, which keeps the prompt, events, final response and stderr.
16
+ *
17
+ * @param {string} directory replaced on every call
18
+ * @param {string} prompt
19
+ * @param {{model: string, thinking: string | null, extensions: Array<{path: string}>, settings: object, signal?: AbortSignal, role?: string}} options
20
+ * @returns {Promise<{response: string | null, errors: string[], usage: any, isolation: any}>}
21
+ */
22
+ export async function launchModel(directory, prompt, options) {
23
+ const { model, thinking, extensions, settings, signal, role = "reviewer" } = options;
24
+ await rm(directory, { recursive: true, force: true });
25
+ await mkdir(directory, { recursive: true });
26
+ await writeText(path.join(directory, "prompt.md"), prompt);
27
+
28
+ const args = [
29
+ "--mode",
30
+ "json",
31
+ "-p",
32
+ "--no-session",
33
+ "--no-extensions",
34
+ ...extensions.flatMap((extension) => ["--extension", extension.path]),
35
+ "--no-skills",
36
+ "--no-prompt-templates",
37
+ "--no-themes",
38
+ "--no-context-files",
39
+ "--no-approve",
40
+ "--no-tools",
41
+ "--model",
42
+ model,
43
+ ];
44
+ if (thinking !== null) args.push("--thinking", thinking);
45
+ args.push(prompt);
46
+ let isolation;
47
+ try {
48
+ // The evaluator's Pi defaults (retry and compaction off) apply here as for subjects; declared settings win.
49
+ isolation = await prepareIsolation(directory, model, { settings: effectivePiSettings(settings) });
50
+ } catch (error) {
51
+ const message = error instanceof Error ? error.message : String(error);
52
+ return { response: null, errors: [message], usage: null, isolation: null };
53
+ }
54
+ const observation = await runPiProcess({
55
+ command: "pi",
56
+ args,
57
+ cwd: directory,
58
+ eventsFile: path.join(directory, "events.jsonl"),
59
+ stderrFile: path.join(directory, "stderr.log"),
60
+ signal,
61
+ environment: {
62
+ ...baseProcessEnvironment(),
63
+ PWD: directory,
64
+ PI_SKIP_VERSION_CHECK: "1",
65
+ PI_TELEMETRY: "0",
66
+ ...isolation.environment,
67
+ },
68
+ });
69
+ await removeCopiedCredentials(directory);
70
+ await writeText(path.join(directory, "final.md"), `${observation.response}\n`);
71
+
72
+ const errors = [];
73
+ if (observation.exitCode !== 0 || !observation.settled || observation.assistantError !== null) {
74
+ errors.push(
75
+ `${role} did not complete: exit=${observation.exitCode} settled=${observation.settled} error=${observation.assistantError}`,
76
+ );
77
+ }
78
+ return { response: observation.response, errors, usage: observation.usage, isolation: isolation.evidence };
79
+ }