@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,395 @@
1
+ // Container environments (optional): a configuration may run the subject's shell and the group's command graders
2
+ // in a digest-pinned image through the standard `docker` command line (Docker Desktop, Colima, OrbStack or Podman
3
+ // all provide it). Docker is looked for only when a group declares a container; an image is never pulled, and a
4
+ // missing CLI, daemon or image refuses the group with what to install or pull. A container environment is a shell
5
+ // isolation like any other: the shell tier's probes verify it before any subject runs.
6
+
7
+ import { execFile } from "node:child_process";
8
+ import { randomBytes } from "node:crypto";
9
+ import { mkdir, mkdtemp, realpath, rm, writeFile } from "node:fs/promises";
10
+ import path from "node:path";
11
+ import { promisify } from "node:util";
12
+
13
+ const run = promisify(execFile);
14
+ const IMAGE = /^[^\s@]+@sha256:[0-9a-f]{64}$/;
15
+ const DOCKER_TIMEOUT_MS = 30_000;
16
+ /** Resource limits for every container the evaluator starts. */
17
+ export const CONTAINER_LIMITS = Object.freeze({ memory: "4g", cpus: "2", pids: "1024" });
18
+ const KEEP_ALIVE = "trap 'exit 0' TERM INT; while :; do sleep 3600 & wait $!; done";
19
+ // Run as root in a subject's container after it starts: replaces the declared workdir (empty, or absent, in the image)
20
+ // with a link to the workspace's mount at its host path. The image's own references to the workdir, such as
21
+ // GIT_WORK_TREE=/testbed or a script that changes into it, then reach the workspace under the same canonical path the
22
+ // shell starts in. A second mount would not do: Git compares the working directory with its work tree by path, so from
23
+ // a subfolder of one mount it would resolve paths against the other's root.
24
+ const LINK_WORKDIR = [
25
+ 'if [ -d "$1" ] && [ ! -L "$1" ]; then',
26
+ ' rmdir "$1" 2>/dev/null || { echo "the image has files at $1, where the workspace goes; use an image whose workdir is empty" >&2; exit 1; }',
27
+ "fi",
28
+ 'mkdir -p "$(dirname "$1")" && ln -s "$2" "$1"',
29
+ ].join("\n");
30
+
31
+ function isPlainObject(value) {
32
+ return value !== null && typeof value === "object" && !Array.isArray(value);
33
+ }
34
+
35
+ /**
36
+ * Validates a configuration's `environment`: `{ kind: "container", image: "<name>@sha256:<digest>", workdir,
37
+ * network: "none" }`.
38
+ *
39
+ * @param {any} value
40
+ * @param {string} label
41
+ * @param {(code: string, message: string) => never} fail
42
+ */
43
+ export function validateContainerEnvironment(value, label, fail) {
44
+ if (!isPlainObject(value)) fail("invalid-definition", `${label} must be an object`);
45
+ for (const key of Object.keys(value)) {
46
+ if (!["kind", "image", "workdir", "network"].includes(key)) {
47
+ fail("invalid-definition", `${label} contains unsupported field ${key}`);
48
+ }
49
+ }
50
+ if (value.kind !== "container") fail("invalid-definition", `${label}.kind must be container`);
51
+ if (typeof value.image !== "string" || !IMAGE.test(value.image)) {
52
+ fail(
53
+ "invalid-definition",
54
+ `${label}.image must be pinned by digest, such as name@sha256:<64 hex characters>; a tag can change under the run`,
55
+ );
56
+ }
57
+ const workdir = value.workdir;
58
+ if (
59
+ typeof workdir !== "string" ||
60
+ !workdir.startsWith("/") ||
61
+ workdir === "/" ||
62
+ workdir.endsWith("/") ||
63
+ workdir
64
+ .split("/")
65
+ .slice(1)
66
+ .some((part) => part === "" || part === "." || part === "..")
67
+ ) {
68
+ fail("invalid-definition", `${label}.workdir must be an absolute path such as /testbed`);
69
+ }
70
+ if (value.network !== "none") fail("invalid-definition", `${label}.network must be "none"`);
71
+ }
72
+
73
+ /**
74
+ * The shell a container environment provides: bash runs in the container, which shows the workspace at its host
75
+ * path, where bash starts, and at workdir, a link to it; `workspace` is that guest name.
76
+ */
77
+ export function containerShell(environment) {
78
+ if (environment?.kind !== "container") return undefined;
79
+ const container = { image: environment.image, workdir: environment.workdir, network: environment.network };
80
+ return { isolation: "container", network: environment.network, workspace: environment.workdir, container };
81
+ }
82
+
83
+ /** The docker CLI's endpoint, so a subject's isolated shell (which has no ~/.docker) reaches the same daemon. */
84
+ export async function dockerHost() {
85
+ if (process.env.DOCKER_HOST) return process.env.DOCKER_HOST;
86
+ try {
87
+ const { stdout } = await run("docker", ["context", "inspect", "--format", "{{.Endpoints.docker.Host}}"], {
88
+ timeout: DOCKER_TIMEOUT_MS,
89
+ });
90
+ return stdout.trim() || null;
91
+ } catch {
92
+ return null;
93
+ }
94
+ }
95
+
96
+ const DOCKER_INSTALL = {
97
+ macos: "brew install colima docker && colima start (or install Docker Desktop or OrbStack)",
98
+ apt: "sudo apt-get install docker.io && sudo systemctl start docker (or install Docker Engine from docs.docker.com, or Podman with podman-docker)",
99
+ dnf: "sudo dnf install podman-docker (or Docker Engine from docs.docker.com, then sudo systemctl start docker)",
100
+ };
101
+ const DOCKER_START = {
102
+ macos: "colima start, or open Docker Desktop or OrbStack",
103
+ apt: "sudo systemctl start docker",
104
+ dnf: "sudo systemctl start docker (or podman machine start)",
105
+ };
106
+
107
+ /**
108
+ * What is missing for a container environment, in the shell tier's refusal shape: the docker CLI, its daemon, the
109
+ * image present locally (with its pull command and, when the registry reports it, its download size), and, given the
110
+ * folder workspaces will live under, that the engine shares that folder with its containers.
111
+ *
112
+ * @param {{ image: string, workdir: string }} container
113
+ * @param {string | null} manager "macos", "apt", "dnf" or null
114
+ * @param {string | null} [workspaceRoot] the folder the run's workspaces are created under
115
+ */
116
+ export async function missingContainerPrerequisites(container, manager, workspaceRoot = null) {
117
+ const reason = "the configuration runs the subject's shell and the command graders in a container";
118
+ try {
119
+ await run("docker", ["version", "--format", "{{.Server.Version}}"], { timeout: DOCKER_TIMEOUT_MS });
120
+ } catch (error) {
121
+ if (error?.code === "ENOENT") {
122
+ return [
123
+ {
124
+ name: "a docker command line",
125
+ need: "docker on PATH (Docker Desktop, Colima, OrbStack or Podman's docker)",
126
+ found: null,
127
+ reason,
128
+ install: manager === null ? null : DOCKER_INSTALL[manager],
129
+ download: null,
130
+ },
131
+ ];
132
+ }
133
+ return [
134
+ {
135
+ name: "a running container engine",
136
+ need: "a docker daemon that answers `docker version`",
137
+ found: firstLine(error?.stderr) ?? "no daemon answered",
138
+ reason,
139
+ install: manager === null ? null : `start it: ${DOCKER_START[manager]}`,
140
+ download: null,
141
+ },
142
+ ];
143
+ }
144
+ try {
145
+ await run("docker", ["image", "inspect", container.image], { timeout: DOCKER_TIMEOUT_MS });
146
+ } catch {
147
+ const size = await imageDownloadSize(container.image);
148
+ return [
149
+ {
150
+ name: `the image ${container.image}`,
151
+ need: "the image present locally (the evaluator never pulls one)",
152
+ found: null,
153
+ reason,
154
+ install: `docker pull ${container.image}`,
155
+ download: size === null ? null : `about ${formatBytes(size)} compressed, from docker manifest inspect`,
156
+ },
157
+ ];
158
+ }
159
+ const hidden = workspaceRoot === null ? null : await hiddenWorkspace(container, workspaceRoot);
160
+ if (hidden === null) return [];
161
+ return [
162
+ {
163
+ name: "a workspace folder the container engine can see",
164
+ need: `containers to see folders under ${workspaceRoot}`,
165
+ found: hidden,
166
+ reason,
167
+ install:
168
+ "a VM-based engine shares only some host folders (Colima, by default, only your home folder): run from a folder it shares (for check-tasks, set TMPDIR to one), pass --results there, or add the folder to the engine's mounts",
169
+ download: null,
170
+ },
171
+ ];
172
+ }
173
+
174
+ /**
175
+ * Whether a folder under `root` reaches containers. A VM-based engine (Colima, Docker Desktop, OrbStack, Podman's
176
+ * machine) mounts only the host folders it shares, and a workspace elsewhere appears empty inside the container, so
177
+ * graders and shells would run on nothing. Writes a marker in a fresh folder under `root` and reads it back through
178
+ * the image. Returns null when it arrives (or when the image has no `cat` to tell), else what went wrong.
179
+ */
180
+ async function hiddenWorkspace(container, root) {
181
+ await mkdir(root, { recursive: true });
182
+ const probe = await mkdtemp(path.join(root, ".skill-eval-share-"));
183
+ const marker = randomBytes(8).toString("hex");
184
+ try {
185
+ await writeFile(path.join(probe, "marker"), marker);
186
+ const { stdout } = await run(
187
+ "docker",
188
+ [
189
+ "run",
190
+ "--rm",
191
+ "--label",
192
+ "skill-eval=1",
193
+ "--network",
194
+ "none",
195
+ "--volume",
196
+ `${probe}:${container.workdir}:ro`,
197
+ "--workdir",
198
+ container.workdir,
199
+ "--entrypoint",
200
+ "cat",
201
+ container.image,
202
+ "marker",
203
+ ],
204
+ { timeout: DOCKER_TIMEOUT_MS },
205
+ );
206
+ if (stdout.trim() === marker) return null;
207
+ return `the container read something other than the marker written in ${probe}`;
208
+ } catch (error) {
209
+ const detail = firstLine(error?.stderr) ?? String(error?.message ?? error);
210
+ // An image without `cat` cannot answer the question; it is not evidence of a hidden folder.
211
+ if (/executable file not found|no such file or directory: unknown|exec: "cat"/i.test(detail)) return null;
212
+ return `a marker written in ${probe} did not reach the container: ${detail}`;
213
+ } finally {
214
+ await rm(probe, { recursive: true, force: true });
215
+ }
216
+ }
217
+
218
+ /** Compressed layer bytes for this machine's platform from `docker manifest inspect -v`, or null when unknown. */
219
+ async function imageDownloadSize(image) {
220
+ let parsed;
221
+ try {
222
+ const { stdout } = await run("docker", ["manifest", "inspect", "-v", image], { timeout: DOCKER_TIMEOUT_MS });
223
+ parsed = JSON.parse(stdout);
224
+ } catch {
225
+ return null;
226
+ }
227
+ const entries = Array.isArray(parsed) ? parsed : [parsed];
228
+ const architecture = process.arch === "arm64" ? "arm64" : "amd64";
229
+ const entry =
230
+ entries.find(
231
+ (candidate) =>
232
+ candidate?.Descriptor?.platform?.os === "linux" && candidate.Descriptor.platform.architecture === architecture,
233
+ ) ?? entries[0];
234
+ const layers = entry?.SchemaV2Manifest?.layers ?? entry?.OCIManifest?.layers;
235
+ if (!Array.isArray(layers) || layers.length === 0) return null;
236
+ return layers.reduce((total, layer) => total + (Number.isFinite(layer?.size) ? layer.size : 0), 0);
237
+ }
238
+
239
+ function formatBytes(bytes) {
240
+ return bytes >= 1024 * 1024
241
+ ? `${Math.round(bytes / (1024 * 1024))} MB`
242
+ : `${Math.max(1, Math.round(bytes / 1024))} KB`;
243
+ }
244
+
245
+ function firstLine(value) {
246
+ const line = String(value ?? "")
247
+ .split("\n")
248
+ .find((entry) => entry.trim() !== "");
249
+ return line?.trim() ?? null;
250
+ }
251
+
252
+ // What the docker client needs from the user's environment to reach its engine, and nothing else.
253
+ const DOCKER_CLIENT_KEYS = [
254
+ "PATH",
255
+ "HOME",
256
+ "USER",
257
+ "LANG",
258
+ "TMPDIR",
259
+ "XDG_RUNTIME_DIR",
260
+ "DOCKER_HOST",
261
+ "DOCKER_CONTEXT",
262
+ "DOCKER_CONFIG",
263
+ "DOCKER_CERT_PATH",
264
+ "DOCKER_TLS_VERIFY",
265
+ ];
266
+
267
+ /** The environment for a docker client the evaluator runs itself (a grader's `docker run`). */
268
+ export function dockerClientEnvironment() {
269
+ return Object.fromEntries(
270
+ DOCKER_CLIENT_KEYS.flatMap((key) => (process.env[key] === undefined ? [] : [[key, process.env[key]]])),
271
+ );
272
+ }
273
+
274
+ export function containerName(purpose) {
275
+ return `skill-eval-${purpose}-${randomBytes(6).toString("hex")}`;
276
+ }
277
+
278
+ /**
279
+ * The options every evaluator container shares: limits, the declared network, the workspace mounted read-write at
280
+ * `at` and used as the working directory, and each declared resource (a skill, context, Freeflow's package) read-only
281
+ * at its own host path, so a path the subject was given, such as `node <skill>/scripts/x.mjs`, resolves in the
282
+ * container exactly as it does for `read`.
283
+ */
284
+ function runOptions(container, { workspace, at, resources = [] }) {
285
+ const mounts = resources.flatMap((resource) => ["--volume", `${resource}:${resource}:ro`]);
286
+ return [
287
+ "--label",
288
+ "skill-eval=1",
289
+ "--network",
290
+ container.network,
291
+ "--memory",
292
+ CONTAINER_LIMITS.memory,
293
+ "--cpus",
294
+ CONTAINER_LIMITS.cpus,
295
+ "--pids-limit",
296
+ CONTAINER_LIMITS.pids,
297
+ "--volume",
298
+ `${workspace}:${at}`,
299
+ ...mounts,
300
+ "--workdir",
301
+ at,
302
+ ];
303
+ }
304
+
305
+ /**
306
+ * Creates and starts a long-lived container for one subject (or one probe run). The workspace is mounted at its own
307
+ * canonical host path, which is also the shell's working directory, so `pwd`, the working directory the host tells
308
+ * the model and the paths its file tools take all name one directory; the declared workdir (such as /testbed) is a
309
+ * link to it. Each of `resources` (absolute host directories) is mounted read-only at the same path. The handle's
310
+ * `workdir` is where bash and declared commands run. `remove()` force-removes the container and is safe to call more
311
+ * than once; callers call it in `finally`, so a cancelled run removes it too.
312
+ */
313
+ export async function startContainer({ container, workspace, resources = [], purpose = "subject" }) {
314
+ const name = containerName(purpose);
315
+ const host = await dockerHost();
316
+ // The host process (Pi, Claude Code) reports its working directory resolved, so the mount uses the same spelling.
317
+ const workdir = await realpath(workspace);
318
+ let removed = false;
319
+ const remove = async () => {
320
+ if (removed) return;
321
+ removed = true;
322
+ await run("docker", ["rm", "--force", name], { timeout: DOCKER_TIMEOUT_MS }).catch(() => {});
323
+ };
324
+ let step = "starting a container";
325
+ try {
326
+ await run(
327
+ "docker",
328
+ [
329
+ "create",
330
+ "--name",
331
+ name,
332
+ ...runOptions(container, { workspace: workdir, at: workdir, resources }),
333
+ "--entrypoint",
334
+ "sh",
335
+ container.image,
336
+ "-c",
337
+ KEEP_ALIVE,
338
+ ],
339
+ { timeout: DOCKER_TIMEOUT_MS },
340
+ );
341
+ await run("docker", ["start", name], { timeout: DOCKER_TIMEOUT_MS });
342
+ step = `linking ${container.workdir} to the workspace in a container`;
343
+ await run("docker", ["exec", "--user", "0", name, "sh", "-c", LINK_WORKDIR, "sh", container.workdir, workdir], {
344
+ timeout: DOCKER_TIMEOUT_MS,
345
+ });
346
+ } catch (error) {
347
+ await remove();
348
+ throw new Error(`${step} from ${container.image} failed: ${firstLine(error?.stderr) ?? error.message}`);
349
+ }
350
+ return { name, host, workdir, remove };
351
+ }
352
+
353
+ /** The value the guard reads to route bash into a running container. */
354
+ export function containerTarget(handle) {
355
+ return JSON.stringify({ name: handle.name, host: handle.host, workdir: handle.workdir });
356
+ }
357
+
358
+ /** The running container from `containerTarget`'s value, or null when there is none. */
359
+ export function parseContainerTarget(value) {
360
+ if (!value) return null;
361
+ try {
362
+ const parsed = JSON.parse(value);
363
+ return typeof parsed?.name === "string" && typeof parsed.workdir === "string" ? parsed : null;
364
+ } catch {
365
+ return null;
366
+ }
367
+ }
368
+
369
+ /** The docker client argv that runs a declared command's argv inside the running container, where bash runs. */
370
+ export function containerExecArgv(target, argv) {
371
+ const host = typeof target.host === "string" && target.host !== "" ? ["--host", target.host] : [];
372
+ return ["docker", ...host, "exec", "--workdir", target.workdir, target.name, ...argv];
373
+ }
374
+
375
+ /**
376
+ * The argv that runs a command grader in a fresh container on the graded copy, with the same limits and network.
377
+ * A grader is the author's, not the subject's, so it sees the copy at the declared workdir, where its argv expects
378
+ * it. The caller removes the container by name afterwards, so a timeout or cancellation leaves nothing running.
379
+ */
380
+ export function graderArgv({ container, copy, argv, name }) {
381
+ return [
382
+ "docker",
383
+ "run",
384
+ "--rm",
385
+ "--name",
386
+ name,
387
+ ...runOptions(container, { workspace: copy, at: container.workdir }),
388
+ container.image,
389
+ ...argv,
390
+ ];
391
+ }
392
+
393
+ export async function removeContainer(name) {
394
+ await run("docker", ["rm", "--force", name], { timeout: DOCKER_TIMEOUT_MS }).catch(() => {});
395
+ }