@hassangameryt/freeflow 0.2.0 → 0.4.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 (312) hide show
  1. package/.agents/plugins/marketplace.json +1 -1
  2. package/.claude-plugin/marketplace.json +4 -4
  3. package/{plugins/freeflow/.claude-plugin → .claude-plugin}/plugin.json +5 -2
  4. package/{plugins/freeflow/.codex-plugin → .codex-plugin}/plugin.json +8 -9
  5. package/README.md +194 -127
  6. package/assets/feedback-loop.png +0 -0
  7. package/assets/freeflow-plugin-icon.png +0 -0
  8. package/assets/freeflow_workflow_map.png +0 -0
  9. package/assets/workflow.png +0 -0
  10. package/command-surface.json +129 -0
  11. package/hooks/freeflow-runtime-context.mjs +419 -0
  12. package/hooks/tests/check-runtime-context-hook.sh +223 -0
  13. package/package.json +41 -22
  14. package/pi-extension/dist/index.js +269 -0
  15. package/pi-extension/dist/integrations/mcp-config.js +29 -0
  16. package/pi-extension/dist/output-router/host-producer-identification.js +232 -0
  17. package/pi-extension/dist/output-router/native-safety-net.js +216 -0
  18. package/pi-extension/dist/output-router/observed-tool-routing.js +122 -0
  19. package/pi-extension/dist/output-router/renderers.js +1023 -0
  20. package/pi-extension/dist/output-router/router-tools.js +554 -0
  21. package/pi-extension/dist/output-router/schemas.js +383 -0
  22. package/pi-extension/dist/output-router/status.js +744 -0
  23. package/pi-extension/dist/output-router/utils.js +502 -0
  24. package/pi-extension/dist/runtime/runtime-context.js +798 -0
  25. package/pi-extension/dist/settings/settings-tui.js +314 -0
  26. package/pi-extension/dist/settings/settings-ui.js +1487 -0
  27. package/pi-extension/freeflow/index.js +1 -0
  28. package/pi-extension/src/index.ts +295 -0
  29. package/pi-extension/src/integrations/mcp-config.ts +32 -0
  30. package/pi-extension/src/output-router/host-producer-identification.ts +256 -0
  31. package/pi-extension/src/output-router/native-safety-net.ts +248 -0
  32. package/pi-extension/src/output-router/observed-tool-routing.ts +134 -0
  33. package/pi-extension/src/output-router/renderers.ts +1108 -0
  34. package/pi-extension/src/output-router/router-tools.ts +579 -0
  35. package/pi-extension/src/output-router/schemas.ts +387 -0
  36. package/pi-extension/src/output-router/status.ts +791 -0
  37. package/pi-extension/src/output-router/utils.ts +554 -0
  38. package/pi-extension/src/runtime/runtime-context.ts +895 -0
  39. package/pi-extension/src/settings/settings-tui.ts +408 -0
  40. package/pi-extension/src/settings/settings-ui.ts +1730 -0
  41. package/pi-extension/tests/integration/pi-extension.test.js +4018 -0
  42. package/pi-extension/tests/output-router/observed-producers.test.js +132 -0
  43. package/pi-extension/tests/output-router/search.test.js +722 -0
  44. package/pi-extension/tests/settings/settings-tui.test.js +243 -0
  45. package/pi-extension/tsconfig.json +16 -0
  46. package/{plugins/freeflow/router/dist → router/dist/benchmarks}/benchmark-harness.d.ts +16 -13
  47. package/router/dist/benchmarks/benchmark-harness.js +92 -0
  48. package/router/dist/benchmarks/benchmarks.d.ts +109 -0
  49. package/router/dist/benchmarks/benchmarks.js +979 -0
  50. package/router/dist/benchmarks/codex-qa-benchmarks.d.ts +95 -0
  51. package/router/dist/benchmarks/codex-qa-benchmarks.js +500 -0
  52. package/router/dist/benchmarks/command-benchmarks.d.ts +143 -0
  53. package/router/dist/benchmarks/command-benchmarks.js +698 -0
  54. package/router/dist/benchmarks/context-mode-normalized-benchmarks.d.ts +82 -0
  55. package/router/dist/benchmarks/context-mode-normalized-benchmarks.js +773 -0
  56. package/router/dist/benchmarks/context-mode-real-deep-benchmark.d.ts +132 -0
  57. package/router/dist/benchmarks/context-mode-real-deep-benchmark.js +1380 -0
  58. package/router/dist/benchmarks/context-saving-baseline.d.ts +60 -0
  59. package/router/dist/benchmarks/context-saving-baseline.js +470 -0
  60. package/router/dist/benchmarks/index-benchmarks.d.ts +103 -0
  61. package/router/dist/benchmarks/index-benchmarks.js +862 -0
  62. package/router/dist/benchmarks/storage-policy-benchmarks.d.ts +101 -0
  63. package/router/dist/benchmarks/storage-policy-benchmarks.js +618 -0
  64. package/router/dist/benchmarks/vault-index-benchmarks.d.ts +56 -0
  65. package/router/dist/benchmarks/vault-index-benchmarks.js +322 -0
  66. package/router/dist/config/config.d.ts +103 -0
  67. package/router/dist/config/config.js +632 -0
  68. package/{plugins/freeflow/router/dist → router/dist/config}/router-contract.d.ts +2 -2
  69. package/router/dist/config/router-contract.js +54 -0
  70. package/{plugins/freeflow/router/dist → router/dist/config}/schema.d.ts +19 -10
  71. package/router/dist/config/schema.js +648 -0
  72. package/router/dist/config/types.d.ts +573 -0
  73. package/router/dist/config/types.js +41 -0
  74. package/router/dist/evidence/bounded-evidence.d.ts +40 -0
  75. package/router/dist/evidence/bounded-evidence.js +323 -0
  76. package/router/dist/evidence/evidence-range-selector.d.ts +24 -0
  77. package/router/dist/evidence/evidence-range-selector.js +232 -0
  78. package/router/dist/evidence/evidence-search.d.ts +27 -0
  79. package/router/dist/evidence/evidence-search.js +490 -0
  80. package/{plugins/freeflow/router/dist → router/dist/evidence}/evidence.d.ts +18 -18
  81. package/router/dist/evidence/evidence.js +200 -0
  82. package/router/dist/evidence/failure-contracts.d.ts +37 -0
  83. package/router/dist/evidence/failure-contracts.js +166 -0
  84. package/router/dist/evidence/line-ranges.d.ts +24 -0
  85. package/router/dist/evidence/line-ranges.js +22 -0
  86. package/router/dist/experiments/local-index.d.ts +64 -0
  87. package/router/dist/experiments/local-index.js +581 -0
  88. package/router/dist/index.d.ts +30 -0
  89. package/router/dist/index.js +32 -0
  90. package/router/dist/local/local-traversal.d.ts +18 -0
  91. package/router/dist/local/local-traversal.js +334 -0
  92. package/router/dist/processing/engine.d.ts +132 -0
  93. package/router/dist/processing/engine.js +533 -0
  94. package/router/dist/processing/reducers.d.ts +280 -0
  95. package/router/dist/processing/reducers.js +1673 -0
  96. package/router/dist/processing/renderers.d.ts +31 -0
  97. package/router/dist/processing/renderers.js +195 -0
  98. package/router/dist/processing/scripts.d.ts +91 -0
  99. package/router/dist/processing/scripts.js +386 -0
  100. package/{plugins/freeflow/router/dist → router/dist/repo}/repo-traversal.d.ts +9 -9
  101. package/router/dist/repo/repo-traversal.js +245 -0
  102. package/router/dist/routing/observed-reducers.d.ts +41 -0
  103. package/router/dist/routing/observed-reducers.js +661 -0
  104. package/router/dist/routing/observed-routing.d.ts +26 -0
  105. package/router/dist/routing/observed-routing.js +378 -0
  106. package/router/dist/routing/parsers.d.ts +16 -0
  107. package/router/dist/routing/parsers.js +521 -0
  108. package/router/dist/routing/run-filters.d.ts +60 -0
  109. package/router/dist/routing/run-filters.js +238 -0
  110. package/router/dist/routing/run-reducers.d.ts +49 -0
  111. package/router/dist/routing/run-reducers.js +122 -0
  112. package/router/dist/sandbox/adapter-roots.d.ts +46 -0
  113. package/router/dist/sandbox/adapter-roots.js +66 -0
  114. package/router/dist/sandbox/eryx-python-adapter.d.ts +13 -0
  115. package/router/dist/sandbox/eryx-python-adapter.js +868 -0
  116. package/router/dist/sandbox/jq-wasm-adapter.d.ts +9 -0
  117. package/router/dist/sandbox/jq-wasm-adapter.js +429 -0
  118. package/router/dist/sandbox/quickjs-wasi-adapter.d.ts +13 -0
  119. package/router/dist/sandbox/quickjs-wasi-adapter.js +384 -0
  120. package/router/dist/sandbox/script-sandbox.d.ts +134 -0
  121. package/router/dist/sandbox/script-sandbox.js +337 -0
  122. package/router/dist/setup/script-transform-adapters.d.ts +32 -0
  123. package/router/dist/setup/script-transform-adapters.js +278 -0
  124. package/router/dist/tools/batch.d.ts +33 -0
  125. package/router/dist/tools/batch.js +586 -0
  126. package/router/dist/tools/run.d.ts +62 -0
  127. package/router/dist/tools/run.js +1304 -0
  128. package/router/dist/tools/search.d.ts +66 -0
  129. package/router/dist/tools/search.js +1364 -0
  130. package/router/dist/transform/engine.d.ts +168 -0
  131. package/router/dist/transform/engine.js +2072 -0
  132. package/router/dist/vault/vault-index.d.ts +125 -0
  133. package/router/dist/vault/vault-index.js +416 -0
  134. package/router/dist/vault/vault.d.ts +138 -0
  135. package/router/dist/vault/vault.js +630 -0
  136. package/runtime/interaction-contract.md +16 -0
  137. package/skills/bypass/SKILL.md +82 -0
  138. package/skills/commit-work/SKILL.md +106 -0
  139. package/skills/commit-work/references/staging-decisions.md +73 -0
  140. package/skills/decision-gate/SKILL.md +48 -0
  141. package/skills/design-for-depth/SKILL.md +106 -0
  142. package/skills/design-for-depth/references/design-pressure-signals.md +314 -0
  143. package/skills/design-for-depth/references/interface-design-loop.md +156 -0
  144. package/skills/design-for-depth/references/software-design-philosophy.md +248 -0
  145. package/skills/diagnose-failure/SKILL.md +105 -0
  146. package/skills/diagnose-failure/references/diagnostic-loop-catalog.md +36 -0
  147. package/skills/diagnose-failure/references/flaky-and-performance.md +43 -0
  148. package/skills/discuss/SKILL.md +121 -0
  149. package/skills/discuss/references/checkpoints.md +66 -0
  150. package/skills/evaluate-skill/SKILL.md +105 -0
  151. package/skills/evaluate-skill/references/definition-schema.md +345 -0
  152. package/skills/evaluate-skill/references/evaluation-design.md +86 -0
  153. package/skills/evaluate-skill/references/execution-and-evidence.md +85 -0
  154. package/skills/evaluate-skill/references/review-and-revision.md +51 -0
  155. package/skills/evaluate-skill/scripts/lib/definitions.mjs +296 -0
  156. package/skills/evaluate-skill/scripts/lib/evidence.mjs +46 -0
  157. package/skills/evaluate-skill/scripts/lib/grade-comparison.mjs +60 -0
  158. package/skills/evaluate-skill/scripts/lib/grade-interaction.mjs +148 -0
  159. package/skills/evaluate-skill/scripts/lib/grade-support.mjs +126 -0
  160. package/skills/evaluate-skill/scripts/lib/grade-workspace.mjs +192 -0
  161. package/skills/evaluate-skill/scripts/lib/grade.mjs +56 -0
  162. package/skills/evaluate-skill/scripts/lib/pi.mjs +489 -0
  163. package/skills/evaluate-skill/scripts/lib/process.mjs +79 -0
  164. package/skills/evaluate-skill/scripts/lib/rpc.mjs +444 -0
  165. package/skills/evaluate-skill/scripts/lib/runner.mjs +346 -0
  166. package/skills/evaluate-skill/scripts/lib/sandbox.mjs +409 -0
  167. package/skills/evaluate-skill/scripts/lib/view.mjs +337 -0
  168. package/skills/evaluate-skill/scripts/pi-guard.mjs +164 -0
  169. package/skills/evaluate-skill/scripts/skill-eval.mjs +101 -0
  170. package/skills/execute-work/SKILL.md +99 -0
  171. package/skills/execute-work/references/code-practices.md +78 -0
  172. package/skills/execute-work/references/execution-loop.md +77 -0
  173. package/skills/finish-branch/SKILL.md +117 -0
  174. package/skills/finish-branch/references/integration-options.md +80 -0
  175. package/skills/handoff/SKILL.md +92 -0
  176. package/skills/handoff/references/templates.md +141 -0
  177. package/skills/launch-work/SKILL.md +116 -0
  178. package/skills/launch-work/references/launch-readiness.md +85 -0
  179. package/skills/migration-work/SKILL.md +115 -0
  180. package/skills/migration-work/references/migration-lifecycle.md +82 -0
  181. package/skills/mode-contract/SKILL.md +82 -0
  182. package/skills/output-router/SKILL.md +206 -0
  183. package/skills/output-router/references/safety-policy.md +40 -0
  184. package/skills/release-work/SKILL.md +118 -0
  185. package/skills/release-work/references/release-evidence.md +78 -0
  186. package/skills/review-artifact/SKILL.md +126 -0
  187. package/skills/review-artifact/references/reviewer-prompt.md +148 -0
  188. package/skills/review-work/SKILL.md +126 -0
  189. package/skills/review-work/references/reviewer-prompt.md +134 -0
  190. package/skills/review-work/references/security-risk-lens.md +67 -0
  191. package/skills/setup-freeflow/SKILL.md +98 -0
  192. package/skills/setup-freeflow/references/activation-contract.md +120 -0
  193. package/skills/setup-freeflow/references/host-setup.md +72 -0
  194. package/skills/setup-freeflow/references/output-router-setup.md +110 -0
  195. package/skills/simplify-code/SKILL.md +80 -0
  196. package/skills/simplify-code/references/simplification-patterns.md +70 -0
  197. package/skills/tdd/SKILL.md +96 -0
  198. package/skills/tdd/references/test-design.md +96 -0
  199. package/skills/track-work/SKILL.md +84 -0
  200. package/skills/track-work/references/working-record-schema.md +378 -0
  201. package/skills/verify-work/SKILL.md +85 -0
  202. package/skills/verify-work/references/browser-runtime-evidence.md +55 -0
  203. package/skills/verify-work/references/integration-evidence.md +51 -0
  204. package/skills/verify-work/references/performance-evidence.md +57 -0
  205. package/skills/workflow/SKILL.md +91 -0
  206. package/skills/workflow/references/domain-skill-composition.md +57 -0
  207. package/skills/workflow/references/workflow-loop.md +92 -0
  208. package/skills/write-plan/SKILL.md +123 -0
  209. package/skills/write-plan/references/plan-shapes.md +156 -0
  210. package/skills/write-skill/SKILL.md +119 -0
  211. package/skills/write-skill/references/activation-boundaries.md +62 -0
  212. package/skills/write-skill/references/agent-first-instructions.md +87 -0
  213. package/skills/write-skill/references/progressive-disclosure.md +63 -0
  214. package/skills/write-skill/scripts/lib/skill-author.mjs +653 -0
  215. package/skills/write-skill/scripts/skill-author.mjs +104 -0
  216. package/skills/write-spec/SKILL.md +104 -0
  217. package/skills/write-spec/references/artifact-standards.md +86 -0
  218. package/skills/write-spec/references/decision-records.md +77 -0
  219. package/skills/write-spec/references/spec-shapes.md +128 -0
  220. package/plugins/freeflow/command-surface.json +0 -77
  221. package/plugins/freeflow/docs/README.md +0 -40
  222. package/plugins/freeflow/docs/adr/0001-three-modes.md +0 -27
  223. package/plugins/freeflow/docs/adr/0002-evals-before-hooks.md +0 -17
  224. package/plugins/freeflow/docs/adr/0003-release-boundary.md +0 -21
  225. package/plugins/freeflow/docs/adr/0004-research-replaces-shallow-discovery-skills.md +0 -26
  226. package/plugins/freeflow/docs/adr/README.md +0 -8
  227. package/plugins/freeflow/docs/architecture.md +0 -86
  228. package/plugins/freeflow/docs/output-router.md +0 -212
  229. package/plugins/freeflow/docs/release-evidence.md +0 -69
  230. package/plugins/freeflow/docs/skills.md +0 -43
  231. package/plugins/freeflow/docs/workflow.md +0 -57
  232. package/plugins/freeflow/hooks/freeflow-runtime-context.mjs +0 -257
  233. package/plugins/freeflow/pi-extension/index.js +0 -1176
  234. package/plugins/freeflow/router/dist/benchmark-harness.js +0 -92
  235. package/plugins/freeflow/router/dist/benchmarks.d.ts +0 -102
  236. package/plugins/freeflow/router/dist/benchmarks.js +0 -880
  237. package/plugins/freeflow/router/dist/bounded-evidence.d.ts +0 -40
  238. package/plugins/freeflow/router/dist/bounded-evidence.js +0 -310
  239. package/plugins/freeflow/router/dist/codex-qa-benchmarks.d.ts +0 -91
  240. package/plugins/freeflow/router/dist/codex-qa-benchmarks.js +0 -461
  241. package/plugins/freeflow/router/dist/command-benchmarks.d.ts +0 -128
  242. package/plugins/freeflow/router/dist/command-benchmarks.js +0 -652
  243. package/plugins/freeflow/router/dist/config.d.ts +0 -22
  244. package/plugins/freeflow/router/dist/config.js +0 -114
  245. package/plugins/freeflow/router/dist/evidence-range-selector.d.ts +0 -22
  246. package/plugins/freeflow/router/dist/evidence-range-selector.js +0 -180
  247. package/plugins/freeflow/router/dist/evidence-search.d.ts +0 -25
  248. package/plugins/freeflow/router/dist/evidence-search.js +0 -450
  249. package/plugins/freeflow/router/dist/evidence.js +0 -196
  250. package/plugins/freeflow/router/dist/experimental-local-index.d.ts +0 -1
  251. package/plugins/freeflow/router/dist/experimental-local-index.js +0 -1
  252. package/plugins/freeflow/router/dist/experiments/local-index.d.ts +0 -58
  253. package/plugins/freeflow/router/dist/experiments/local-index.js +0 -562
  254. package/plugins/freeflow/router/dist/index-benchmarks.d.ts +0 -84
  255. package/plugins/freeflow/router/dist/index-benchmarks.js +0 -458
  256. package/plugins/freeflow/router/dist/index.d.ts +0 -12
  257. package/plugins/freeflow/router/dist/index.js +0 -12
  258. package/plugins/freeflow/router/dist/line-ranges.d.ts +0 -22
  259. package/plugins/freeflow/router/dist/line-ranges.js +0 -22
  260. package/plugins/freeflow/router/dist/parsers.d.ts +0 -16
  261. package/plugins/freeflow/router/dist/parsers.js +0 -473
  262. package/plugins/freeflow/router/dist/repo-traversal.js +0 -242
  263. package/plugins/freeflow/router/dist/retrieve.d.ts +0 -33
  264. package/plugins/freeflow/router/dist/retrieve.js +0 -855
  265. package/plugins/freeflow/router/dist/router-contract.js +0 -54
  266. package/plugins/freeflow/router/dist/run.d.ts +0 -26
  267. package/plugins/freeflow/router/dist/run.js +0 -356
  268. package/plugins/freeflow/router/dist/schema.js +0 -354
  269. package/plugins/freeflow/router/dist/types.d.ts +0 -234
  270. package/plugins/freeflow/router/dist/types.js +0 -9
  271. package/plugins/freeflow/router/dist/vault.d.ts +0 -80
  272. package/plugins/freeflow/router/dist/vault.js +0 -484
  273. package/plugins/freeflow/skills/bypass/SKILL.md +0 -68
  274. package/plugins/freeflow/skills/commit-work/SKILL.md +0 -98
  275. package/plugins/freeflow/skills/commit-work/references/staging-decisions.md +0 -51
  276. package/plugins/freeflow/skills/diagnose-failure/SKILL.md +0 -95
  277. package/plugins/freeflow/skills/diagnose-failure/references/feedback-loop-catalog.md +0 -25
  278. package/plugins/freeflow/skills/diagnose-failure/references/flaky-and-performance.md +0 -31
  279. package/plugins/freeflow/skills/evaluate-skill/SKILL.md +0 -96
  280. package/plugins/freeflow/skills/evaluate-skill/references/eval-patterns.md +0 -41
  281. package/plugins/freeflow/skills/evaluate-skill/references/grading-priority.md +0 -33
  282. package/plugins/freeflow/skills/execute-plan/SKILL.md +0 -132
  283. package/plugins/freeflow/skills/execute-plan/references/execution-map.md +0 -122
  284. package/plugins/freeflow/skills/handoff/SKILL.md +0 -66
  285. package/plugins/freeflow/skills/handoff/references/templates.md +0 -64
  286. package/plugins/freeflow/skills/interview-gate/SKILL.md +0 -90
  287. package/plugins/freeflow/skills/mode-contract/SKILL.md +0 -79
  288. package/plugins/freeflow/skills/output-router/SKILL.md +0 -69
  289. package/plugins/freeflow/skills/output-router/references/safety-policy.md +0 -33
  290. package/plugins/freeflow/skills/research/ARTIFACT-DESTINATIONS.md +0 -58
  291. package/plugins/freeflow/skills/research/CHECKPOINTS.md +0 -74
  292. package/plugins/freeflow/skills/research/LANGUAGE.md +0 -61
  293. package/plugins/freeflow/skills/research/SKILL.md +0 -128
  294. package/plugins/freeflow/skills/review-artifact/SKILL.md +0 -115
  295. package/plugins/freeflow/skills/review-artifact/references/reviewer-prompt.md +0 -56
  296. package/plugins/freeflow/skills/review-work/SKILL.md +0 -96
  297. package/plugins/freeflow/skills/review-work/references/reviewer-prompt.md +0 -97
  298. package/plugins/freeflow/skills/setup-freeflow/SKILL.md +0 -94
  299. package/plugins/freeflow/skills/setup-freeflow/references/activation-contract.md +0 -99
  300. package/plugins/freeflow/skills/setup-freeflow/references/host-setup.md +0 -35
  301. package/plugins/freeflow/skills/setup-freeflow/references/output-router-setup.md +0 -52
  302. package/plugins/freeflow/skills/verify-work/SKILL.md +0 -62
  303. package/plugins/freeflow/skills/workflow/SKILL.md +0 -95
  304. package/plugins/freeflow/skills/workflow/references/workflow-map.md +0 -102
  305. package/plugins/freeflow/skills/write-plan/SKILL.md +0 -88
  306. package/plugins/freeflow/skills/write-plan/references/plan-shapes.md +0 -70
  307. package/plugins/freeflow/skills/write-skill/SKILL.md +0 -88
  308. package/plugins/freeflow/skills/write-spec/SKILL.md +0 -90
  309. package/plugins/freeflow/skills/write-spec/references/artifact-standards.md +0 -67
  310. package/plugins/freeflow/skills/write-spec/references/spec-shapes.md +0 -69
  311. /package/{plugins/freeflow/assets → assets}/freeflow-plugin-icon-transparent.png +0 -0
  312. /package/{plugins/freeflow/hooks → hooks}/hooks.json +0 -0
@@ -0,0 +1,409 @@
1
+ import { spawn } from "node:child_process";
2
+ import { createHash } from "node:crypto";
3
+ import {
4
+ chmod,
5
+ cp,
6
+ copyFile,
7
+ lstat,
8
+ mkdir,
9
+ readFile,
10
+ readdir,
11
+ readlink,
12
+ realpath,
13
+ rm,
14
+ symlink,
15
+ writeFile,
16
+ } from "node:fs/promises";
17
+ import path from "node:path";
18
+
19
+ export async function prepareFixture({ declaredPath, root, groupDirectory }) {
20
+ if (declaredPath === null) return null;
21
+
22
+ const canonicalRoot = await realpath(root);
23
+ const sourcePath = await realpath(path.resolve(root, declaredPath)).catch(() => null);
24
+ if (sourcePath === null || !isContained(canonicalRoot, sourcePath)) {
25
+ throw new Error(`declared fixture is missing or escapes the definition root: ${declaredPath}`);
26
+ }
27
+ const sourceStat = await lstat(sourcePath);
28
+ if (!sourceStat.isDirectory()) throw new Error(`declared fixture is not a directory: ${declaredPath}`);
29
+
30
+ const files = await fingerprintDirectory(sourcePath);
31
+ const snapshotPath = path.join(groupDirectory, "resources", "fixture");
32
+ await mkdir(path.dirname(snapshotPath), { recursive: true });
33
+ await cp(sourcePath, snapshotPath, { recursive: true, dereference: false, errorOnExist: true, force: false });
34
+ const snapshotFiles = await fingerprintDirectory(await realpath(snapshotPath));
35
+ const sourceFilesAfter = await fingerprintDirectory(sourcePath);
36
+ if (
37
+ JSON.stringify(snapshotFiles) !== JSON.stringify(files) ||
38
+ JSON.stringify(sourceFilesAfter) !== JSON.stringify(files)
39
+ ) {
40
+ throw new Error(`declared fixture changed while it was being snapshotted: ${declaredPath}`);
41
+ }
42
+
43
+ return {
44
+ declaredPath,
45
+ sourcePath,
46
+ snapshotPath: await realpath(snapshotPath),
47
+ files,
48
+ };
49
+ }
50
+
51
+ export async function materializeFixture({ fixture, workspace }) {
52
+ if (fixture === null) {
53
+ await mkdir(workspace, { recursive: true });
54
+ return null;
55
+ }
56
+
57
+ await cp(fixture.snapshotPath, workspace, {
58
+ recursive: true,
59
+ dereference: false,
60
+ errorOnExist: true,
61
+ force: false,
62
+ });
63
+ const materializedPath = await realpath(workspace);
64
+ const materializedFiles = await fingerprintDirectory(materializedPath);
65
+ if (JSON.stringify(materializedFiles) !== JSON.stringify(fixture.files)) {
66
+ throw new Error(`declared fixture changed while it was being copied: ${fixture.declaredPath}`);
67
+ }
68
+
69
+ return { ...fixture, materializedPath };
70
+ }
71
+
72
+ export async function materializeEnvironment({ environment, root, variantDirectory }) {
73
+ const canonicalRoot = await realpath(root);
74
+ let source;
75
+ let snapshotResource;
76
+ if (environment.source.kind === "working-tree") {
77
+ source = { kind: "working-tree", root: canonicalRoot };
78
+ snapshotResource = (options) => snapshotWorkingTreeResource({ canonicalRoot, ...options });
79
+ } else {
80
+ const git = await prepareGitSource(canonicalRoot, environment.source.ref);
81
+ source = { kind: "git", ref: environment.source.ref, commit: git.commit };
82
+ snapshotResource = (options) => snapshotGitResource({ ...git, ...options });
83
+ }
84
+
85
+ const skillsRoot = path.join(variantDirectory, "resources", "skills");
86
+ const contextRoot = path.join(variantDirectory, "resources", "context");
87
+ await mkdir(skillsRoot, { recursive: true });
88
+ await mkdir(contextRoot, { recursive: true });
89
+
90
+ const skills = [];
91
+ for (const [index, declaredPath] of environment.skills.entries()) {
92
+ const skill = await snapshotResource({
93
+ declaredPath,
94
+ destination: path.join(skillsRoot, String(index)),
95
+ requireDirectory: true,
96
+ label: "skill",
97
+ });
98
+ const skillFile = await realpath(path.join(skill.path, "SKILL.md")).catch(() => null);
99
+ if (skillFile === null || !isContained(skill.path, skillFile)) {
100
+ throw new Error(`declared skill has no contained SKILL.md: ${declaredPath}`);
101
+ }
102
+ skills.push(skill);
103
+ }
104
+
105
+ const context = [];
106
+ const deliveredEntries = [];
107
+ for (const [index, declaredPath] of environment.context.entries()) {
108
+ const entry = await snapshotResource({
109
+ declaredPath,
110
+ destination: path.join(contextRoot, String(index)),
111
+ requireDirectory: false,
112
+ label: "context",
113
+ });
114
+ const deliveredFiles = await contextFiles(entry.path, entry.files);
115
+ context.push(entry);
116
+ deliveredEntries.push({ declaredPath, files: deliveredFiles });
117
+ }
118
+
119
+ const manifestPath = path.join(variantDirectory, "resources", "context-delivery.json");
120
+ const manifest = { schema_version: 1, entries: deliveredEntries };
121
+ const manifestContents = `${JSON.stringify(manifest, null, 2)}\n`;
122
+ await writeFile(manifestPath, manifestContents, "utf8");
123
+
124
+ return {
125
+ source,
126
+ skills,
127
+ context,
128
+ contextDelivery:
129
+ context.length === 0
130
+ ? { kind: "none", manifestPath: null, sha256: null }
131
+ : { kind: "system-prompt", manifestPath, sha256: sha256(manifestContents) },
132
+ targetPath:
133
+ environment.target === null ? null : await realpath(path.join(skills[environment.target].path, "SKILL.md")),
134
+ };
135
+ }
136
+
137
+ export async function verifyEnvironment(environment) {
138
+ for (const [label, resources] of [
139
+ ["skill", environment.skills],
140
+ ["context", environment.context],
141
+ ]) {
142
+ for (const resource of resources) {
143
+ const current = await fingerprintDirectory(resource.path);
144
+ if (JSON.stringify(current) !== JSON.stringify(resource.files)) {
145
+ throw new Error(`declared ${label} changed during subject execution: ${resource.declaredPath}`);
146
+ }
147
+ }
148
+ }
149
+ if (environment.contextDelivery.kind === "system-prompt") {
150
+ const contents = await readFile(environment.contextDelivery.manifestPath);
151
+ if (sha256(contents) !== environment.contextDelivery.sha256) {
152
+ throw new Error("declared context delivery manifest changed during subject execution");
153
+ }
154
+ }
155
+ }
156
+
157
+ export async function snapshotWorkspace({ workspace, variantDirectory, turn, before }) {
158
+ const snapshotPath = path.join(variantDirectory, "turns", String(turn), "workspace");
159
+ await rm(snapshotPath, { recursive: true, force: true });
160
+ await mkdir(path.dirname(snapshotPath), { recursive: true });
161
+
162
+ const sourceBefore = await fingerprintDirectory(workspace);
163
+ await cp(workspace, snapshotPath, { recursive: true, force: false, errorOnExist: true, verbatimSymlinks: true });
164
+ const [sourceAfter, files] = await Promise.all([fingerprintDirectory(workspace), fingerprintDirectory(snapshotPath)]);
165
+ if (
166
+ JSON.stringify(sourceBefore) !== JSON.stringify(sourceAfter) ||
167
+ JSON.stringify(sourceAfter) !== JSON.stringify(files)
168
+ ) {
169
+ throw new Error(`workspace changed while capturing turn ${turn}`);
170
+ }
171
+ return { path: snapshotPath, files, changes: workspaceChanges(before, files) };
172
+ }
173
+
174
+ export async function fingerprintDirectory(root) {
175
+ const canonicalRoot = await realpath(root);
176
+ const files = [];
177
+ await walk(canonicalRoot, canonicalRoot, files);
178
+ return files.sort((left, right) => comparePaths(left.path, right.path));
179
+ }
180
+
181
+ export function workspaceChanges(before, after) {
182
+ const beforeEntries = before.filter((entry) => entry.type !== "directory");
183
+ const afterEntries = after.filter((entry) => entry.type !== "directory");
184
+ const beforeByPath = new Map(beforeEntries.map((file) => [file.path, file.sha256]));
185
+ const afterByPath = new Map(afterEntries.map((file) => [file.path, file.sha256]));
186
+ return {
187
+ created: afterEntries.filter((file) => !beforeByPath.has(file.path)).map((file) => file.path),
188
+ modified: afterEntries
189
+ .filter((file) => beforeByPath.has(file.path) && beforeByPath.get(file.path) !== file.sha256)
190
+ .map((file) => file.path),
191
+ deleted: beforeEntries.filter((file) => !afterByPath.has(file.path)).map((file) => file.path),
192
+ };
193
+ }
194
+
195
+ async function snapshotWorkingTreeResource({ canonicalRoot, declaredPath, destination, requireDirectory, label }) {
196
+ const sourcePath = await realpath(path.resolve(canonicalRoot, declaredPath)).catch(() => null);
197
+ if (sourcePath === null || !isContained(canonicalRoot, sourcePath)) {
198
+ throw new Error(`declared ${label} is missing or escapes the definition root: ${declaredPath}`);
199
+ }
200
+ const sourceStat = await lstat(sourcePath);
201
+ if (requireDirectory && !sourceStat.isDirectory()) {
202
+ throw new Error(`declared ${label} is not a directory: ${declaredPath}`);
203
+ }
204
+ if (!sourceStat.isDirectory() && !sourceStat.isFile()) {
205
+ throw new Error(`declared ${label} is not a file or directory: ${declaredPath}`);
206
+ }
207
+
208
+ const sourceFiles = sourceStat.isDirectory()
209
+ ? await fingerprintDirectory(sourcePath)
210
+ : [{ path: path.basename(sourcePath), type: "file", sha256: sha256(await readFile(sourcePath)) }];
211
+ if (sourceStat.isDirectory()) {
212
+ await mkdir(path.dirname(destination), { recursive: true });
213
+ await cp(sourcePath, destination, { recursive: true, dereference: false, errorOnExist: true, force: false });
214
+ } else {
215
+ await mkdir(destination, { recursive: true });
216
+ await copyFile(sourcePath, path.join(destination, path.basename(sourcePath)));
217
+ }
218
+ const snapshotPath = await realpath(destination);
219
+ const files = await fingerprintDirectory(snapshotPath);
220
+ const sourceFilesAfter = sourceStat.isDirectory()
221
+ ? await fingerprintDirectory(sourcePath)
222
+ : [{ path: path.basename(sourcePath), type: "file", sha256: sha256(await readFile(sourcePath)) }];
223
+ if (
224
+ JSON.stringify(files) !== JSON.stringify(sourceFiles) ||
225
+ JSON.stringify(sourceFilesAfter) !== JSON.stringify(sourceFiles)
226
+ ) {
227
+ throw new Error(`declared ${label} changed while it was being snapshotted: ${declaredPath}`);
228
+ }
229
+ return { declaredPath, sourcePath, path: snapshotPath, files };
230
+ }
231
+
232
+ async function prepareGitSource(canonicalRoot, ref) {
233
+ const repositoryOutput = await runGit(canonicalRoot, ["rev-parse", "--show-toplevel"]);
234
+ const repositoryRoot = await realpath(repositoryOutput.toString("utf8").trimEnd());
235
+ if (!isContained(repositoryRoot, canonicalRoot)) {
236
+ throw new Error("definition root is not contained by its Git repository");
237
+ }
238
+ const commitOutput = await runGit(canonicalRoot, ["rev-parse", "--verify", "--end-of-options", `${ref}^{commit}`]);
239
+ const commit = commitOutput.toString("utf8").trim();
240
+ if (!/^[a-f0-9]{40,64}$/.test(commit)) throw new Error(`Git ref did not resolve to one commit: ${ref}`);
241
+ const rootPrefix = path.relative(repositoryRoot, canonicalRoot).split(path.sep).join("/");
242
+ return { repositoryRoot, rootPrefix, commit };
243
+ }
244
+
245
+ async function snapshotGitResource({
246
+ repositoryRoot,
247
+ rootPrefix,
248
+ commit,
249
+ declaredPath,
250
+ destination,
251
+ requireDirectory,
252
+ label,
253
+ }) {
254
+ const gitPath = [rootPrefix, declaredPath.split(path.sep).join("/")].filter(Boolean).join("/");
255
+ const selected = await gitTreeEntries(repositoryRoot, commit, gitPath, false);
256
+ const exact = selected.find((entry) => entry.path === gitPath);
257
+ if (!exact) throw new Error(`declared ${label} is missing from Git commit ${commit}: ${declaredPath}`);
258
+ if (requireDirectory && exact.type !== "tree") {
259
+ throw new Error(`declared ${label} is not a directory in Git commit ${commit}: ${declaredPath}`);
260
+ }
261
+ if (exact.type !== "tree" && exact.type !== "blob") {
262
+ throw new Error(`declared ${label} has unsupported Git type ${exact.type}: ${declaredPath}`);
263
+ }
264
+
265
+ await mkdir(destination, { recursive: true });
266
+ if (exact.type === "tree") {
267
+ const entries = await gitTreeEntries(repositoryRoot, commit, gitPath, true);
268
+ for (const entry of entries) {
269
+ if (entry.type !== "blob" || !entry.path.startsWith(`${gitPath}/`)) {
270
+ throw new Error(`declared ${label} contains an unsupported Git entry: ${entry.path}`);
271
+ }
272
+ await materializeGitBlob(repositoryRoot, entry, entry.path.slice(gitPath.length + 1), destination);
273
+ }
274
+ } else {
275
+ await materializeGitBlob(repositoryRoot, exact, path.posix.basename(gitPath), destination);
276
+ }
277
+
278
+ const snapshotPath = await realpath(destination);
279
+ const files = await fingerprintDirectory(snapshotPath);
280
+ return {
281
+ declaredPath,
282
+ sourcePath: `${commit}:${declaredPath}`,
283
+ path: snapshotPath,
284
+ files,
285
+ };
286
+ }
287
+
288
+ async function gitTreeEntries(repositoryRoot, commit, gitPath, recursive) {
289
+ const args = ["ls-tree", "-z"];
290
+ if (recursive) args.push("-r");
291
+ args.push("--full-tree", commit, "--", `:(literal)${gitPath}`);
292
+ const output = await runGit(repositoryRoot, args);
293
+ const entries = [];
294
+ for (const frame of output.toString("utf8").split("\0")) {
295
+ if (frame === "") continue;
296
+ const separator = frame.indexOf("\t");
297
+ const metadata = separator === -1 ? [] : frame.slice(0, separator).split(" ");
298
+ if (metadata.length !== 3) throw new Error("Git tree output was malformed");
299
+ entries.push({ mode: metadata[0], type: metadata[1], object: metadata[2], path: frame.slice(separator + 1) });
300
+ }
301
+ return entries;
302
+ }
303
+
304
+ async function materializeGitBlob(repositoryRoot, entry, relativePath, destination) {
305
+ if (
306
+ relativePath === "" ||
307
+ path.posix.isAbsolute(relativePath) ||
308
+ relativePath.split("/").some((segment) => segment === "" || segment === "." || segment === "..")
309
+ ) {
310
+ throw new Error(`Git entry escapes its declared resource: ${relativePath}`);
311
+ }
312
+ const outputPath = path.join(destination, ...relativePath.split("/"));
313
+ await mkdir(path.dirname(outputPath), { recursive: true });
314
+ const contents = await runGit(repositoryRoot, ["cat-file", "blob", entry.object]);
315
+ if (entry.mode === "120000") {
316
+ const target = new TextDecoder("utf-8", { fatal: true }).decode(contents);
317
+ await symlink(target, outputPath);
318
+ return;
319
+ }
320
+ if (entry.mode !== "100644" && entry.mode !== "100755") {
321
+ throw new Error(`unsupported Git blob mode ${entry.mode}: ${entry.path}`);
322
+ }
323
+ await writeFile(outputPath, contents);
324
+ if (entry.mode === "100755") await chmod(outputPath, 0o755);
325
+ }
326
+
327
+ async function runGit(cwd, args) {
328
+ return new Promise((resolve, reject) => {
329
+ const child = spawn("git", ["-C", cwd, ...args], { shell: false, stdio: ["ignore", "pipe", "pipe"] });
330
+ const stdout = [];
331
+ const stderr = [];
332
+ child.stdout.on("data", (chunk) => stdout.push(chunk));
333
+ child.stderr.on("data", (chunk) => stderr.push(chunk));
334
+ child.once("error", reject);
335
+ child.once("close", (code, signal) => {
336
+ if (code === 0) resolve(Buffer.concat(stdout));
337
+ else {
338
+ reject(
339
+ new Error(
340
+ `Git command failed (${signal ?? code}): ${Buffer.concat(stderr).toString("utf8").trim() || args[0]}`,
341
+ ),
342
+ );
343
+ }
344
+ });
345
+ });
346
+ }
347
+
348
+ async function contextFiles(root, fingerprints) {
349
+ const decoder = new TextDecoder("utf-8", { fatal: true });
350
+ const files = [];
351
+ for (const fingerprint of fingerprints) {
352
+ if (fingerprint.type === "directory") continue;
353
+ const file = path.join(root, fingerprint.path);
354
+ const resolved = await realpath(file).catch(() => null);
355
+ const resolvedStat = resolved === null ? null : await lstat(resolved);
356
+ if (resolved === null || !isContained(root, resolved) || !resolvedStat.isFile()) {
357
+ throw new Error(`declared context entry is not a contained file: ${fingerprint.path}`);
358
+ }
359
+ const contents = await readFile(resolved);
360
+ let text;
361
+ try {
362
+ text = decoder.decode(contents);
363
+ } catch {
364
+ throw new Error(`declared context file is not valid UTF-8 text: ${fingerprint.path}`);
365
+ }
366
+ files.push({ path: fingerprint.path, sha256: sha256(text), content: text });
367
+ }
368
+ return files;
369
+ }
370
+
371
+ async function walk(root, directory, files) {
372
+ const entries = await readdir(directory, { withFileTypes: true });
373
+ entries.sort((left, right) => comparePaths(left.name, right.name));
374
+ for (const entry of entries) {
375
+ const entryPath = path.join(directory, entry.name);
376
+ const relativePath = path.relative(root, entryPath);
377
+ const entryStat = await lstat(entryPath);
378
+ if (entryStat.isSymbolicLink()) {
379
+ const target = await readlink(entryPath);
380
+ const canonicalTarget = await realpath(entryPath).catch(() => null);
381
+ if (path.isAbsolute(target) || canonicalTarget === null || !isContained(root, canonicalTarget)) {
382
+ throw new Error(`resource symlink escapes its declared root: ${relativePath}`);
383
+ }
384
+ files.push({ path: relativePath, type: "symlink", sha256: sha256(`symlink\0${target}`) });
385
+ } else if (entryStat.isDirectory()) {
386
+ files.push({ path: relativePath, type: "directory", sha256: sha256("directory") });
387
+ await walk(root, entryPath, files);
388
+ } else if (entryStat.isFile()) {
389
+ files.push({ path: relativePath, type: "file", sha256: sha256(await readFile(entryPath)) });
390
+ } else {
391
+ throw new Error(`unsupported resource entry: ${relativePath}`);
392
+ }
393
+ }
394
+ }
395
+
396
+ function sha256(value) {
397
+ return createHash("sha256").update(value).digest("hex");
398
+ }
399
+
400
+ function comparePaths(left, right) {
401
+ if (left < right) return -1;
402
+ if (left > right) return 1;
403
+ return 0;
404
+ }
405
+
406
+ function isContained(root, target) {
407
+ const relative = path.relative(root, target);
408
+ return relative === "" || (!relative.startsWith(`..${path.sep}`) && relative !== ".." && !path.isAbsolute(relative));
409
+ }