@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,722 @@
1
+ import assert from "node:assert/strict";
2
+ import { mkdir, mkdtemp, rm, writeFile } from "node:fs/promises";
3
+ import { tmpdir } from "node:os";
4
+ import { join } from "node:path";
5
+ import test from "node:test";
6
+
7
+ import freeflow from "../../dist/index.js";
8
+ import { createVault, readOutputText, storeCommandOutput } from "../../../router/dist/index.js";
9
+
10
+ function registerMockPi() {
11
+ const tools = new Map();
12
+ const commands = new Map();
13
+ const handlers = new Map();
14
+ const pi = {
15
+ registerTool(tool) {
16
+ tools.set(tool.name, tool);
17
+ },
18
+ registerCommand(name, command) {
19
+ commands.set(name, command);
20
+ },
21
+ on(name, handler) {
22
+ handlers.set(name, handler);
23
+ },
24
+ appendEntry() {},
25
+ async sendUserMessage() {},
26
+ async exec() {
27
+ throw new Error("exec should not be called in pi extension transform tests");
28
+ },
29
+ };
30
+ freeflow(pi);
31
+ return { tools, commands, handlers };
32
+ }
33
+
34
+ function mockCtx(cwd = process.cwd(), sessionId = "pi-extension-search-test") {
35
+ return {
36
+ cwd,
37
+ ui: {
38
+ notify() {},
39
+ setStatus() {},
40
+ },
41
+ sessionManager: {
42
+ getSessionId: () => sessionId,
43
+ },
44
+ };
45
+ }
46
+
47
+ const testTheme = {
48
+ fg(_color, text) {
49
+ return text;
50
+ },
51
+ bold(text) {
52
+ return text;
53
+ },
54
+ };
55
+
56
+ function renderText(component, width = 120) {
57
+ return component.render(width).join("\n");
58
+ }
59
+
60
+ function schemaMatches(schema, value) {
61
+ return schemaErrors(schema, value).length === 0;
62
+ }
63
+
64
+ function schemaErrors(schema, value, path = "$") {
65
+ if (!schema) {
66
+ return [];
67
+ }
68
+
69
+ const errors = [];
70
+ if (schema.oneOf) {
71
+ const matches = schema.oneOf.filter((candidate) => schemaMatches(candidate, value));
72
+ if (matches.length !== 1) {
73
+ errors.push(`${path} should match exactly one schema, matched ${matches.length}`);
74
+ }
75
+ }
76
+ if (schema.anyOf) {
77
+ const matches = schema.anyOf.filter((candidate) => schemaMatches(candidate, value));
78
+ if (matches.length === 0) {
79
+ errors.push(`${path} should match at least one schema`);
80
+ }
81
+ }
82
+ if (schema.allOf) {
83
+ for (const candidate of schema.allOf) {
84
+ errors.push(...schemaErrors(candidate, value, path));
85
+ }
86
+ }
87
+ if (schema.not && schemaMatches(schema.not, value)) {
88
+ errors.push(`${path} should not match forbidden schema`);
89
+ }
90
+
91
+ if (schema.type) {
92
+ const validType =
93
+ schema.type === "object"
94
+ ? value !== null && typeof value === "object" && !Array.isArray(value)
95
+ : schema.type === "array"
96
+ ? Array.isArray(value)
97
+ : schema.type === "integer"
98
+ ? Number.isInteger(value)
99
+ : schema.type === "number"
100
+ ? typeof value === "number"
101
+ : typeof value === schema.type;
102
+ if (!validType) {
103
+ errors.push(`${path} should be ${schema.type}`);
104
+ return errors;
105
+ }
106
+ }
107
+
108
+ if (Object.prototype.hasOwnProperty.call(schema, "const") && value !== schema.const) {
109
+ errors.push(`${path} should equal ${schema.const}`);
110
+ }
111
+ if (schema.enum && !schema.enum.includes(value)) {
112
+ errors.push(`${path} should be one of ${schema.enum.join(", ")}`);
113
+ }
114
+ if (typeof value === "string") {
115
+ if (schema.minLength !== undefined && value.length < schema.minLength) {
116
+ errors.push(`${path} should have length >= ${schema.minLength}`);
117
+ }
118
+ if (schema.pattern && !new RegExp(schema.pattern).test(value)) {
119
+ errors.push(`${path} should match ${schema.pattern}`);
120
+ }
121
+ }
122
+ if (typeof value === "number") {
123
+ if (schema.minimum !== undefined && value < schema.minimum) {
124
+ errors.push(`${path} should be >= ${schema.minimum}`);
125
+ }
126
+ if (schema.maximum !== undefined && value > schema.maximum) {
127
+ errors.push(`${path} should be <= ${schema.maximum}`);
128
+ }
129
+ }
130
+
131
+ if (Array.isArray(value)) {
132
+ if (schema.minItems !== undefined && value.length < schema.minItems) {
133
+ errors.push(`${path} should have at least ${schema.minItems} item(s)`);
134
+ }
135
+ if (schema.items) {
136
+ value.forEach((item, index) => {
137
+ errors.push(...schemaErrors(schema.items, item, `${path}[${index}]`));
138
+ });
139
+ }
140
+ }
141
+
142
+ if (value !== null && typeof value === "object" && !Array.isArray(value)) {
143
+ for (const key of schema.required ?? []) {
144
+ if (!Object.prototype.hasOwnProperty.call(value, key)) {
145
+ errors.push(`${path}.${key} is required`);
146
+ }
147
+ }
148
+ if (schema.additionalProperties === false && schema.properties) {
149
+ for (const key of Object.keys(value)) {
150
+ if (!Object.prototype.hasOwnProperty.call(schema.properties, key)) {
151
+ errors.push(`${path}.${key} is not allowed`);
152
+ }
153
+ }
154
+ }
155
+ for (const [key, propertySchema] of Object.entries(schema.properties ?? {})) {
156
+ if (Object.prototype.hasOwnProperty.call(value, key)) {
157
+ errors.push(...schemaErrors(propertySchema, value[key], `${path}.${key}`));
158
+ }
159
+ }
160
+ }
161
+
162
+ return errors;
163
+ }
164
+
165
+ function assertSchemaAccepts(schema, value) {
166
+ assert.deepEqual(schemaErrors(schema, value), []);
167
+ }
168
+
169
+ function assertSchemaRejects(schema, value) {
170
+ assert.equal(schemaMatches(schema, value), false, `schema unexpectedly accepted ${JSON.stringify(value)}`);
171
+ }
172
+
173
+ function collectSchemaKeys(schema, keys, path = "$", found = []) {
174
+ if (!schema || typeof schema !== "object") {
175
+ return found;
176
+ }
177
+ for (const key of keys) {
178
+ if (Object.prototype.hasOwnProperty.call(schema, key)) {
179
+ found.push(`${path}.${key}`);
180
+ }
181
+ }
182
+ for (const [key, value] of Object.entries(schema)) {
183
+ if (value && typeof value === "object") {
184
+ collectSchemaKeys(value, keys, `${path}.${key}`, found);
185
+ }
186
+ }
187
+ return found;
188
+ }
189
+
190
+ test("Pi extension registers public freeflow_search with deterministic operation schema", () => {
191
+ const { tools } = registerMockPi();
192
+ const search = tools.get("freeflow_search");
193
+
194
+ assert.ok(search, "freeflow_search should be registered");
195
+ assert.equal(tools.has("freeflow_script_transform"), false);
196
+ assert.deepEqual(search.parameters.required, ["action"]);
197
+ assert.deepEqual(search.parameters.properties.source.properties.kind.enum, ["repo", "local", "vault"]);
198
+ assert.match(search.description, /Search|transform/i);
199
+ assert.match(search.promptGuidelines.join("\n"), /action=transform/i);
200
+ assert.deepEqual(collectSchemaKeys(search.parameters, ["oneOf", "anyOf", "allOf", "not", "const"]), []);
201
+
202
+ const operationKinds = search.parameters.properties.operation.properties.kind.enum;
203
+ assert.deepEqual(operationKinds, [
204
+ "regexFilter",
205
+ "countMatches",
206
+ "jsonExtract",
207
+ "groupByRegex",
208
+ "dedupe",
209
+ "topN",
210
+ "extractUrls",
211
+ "extractCitations",
212
+ "lineStats",
213
+ "sizeStats",
214
+ "script",
215
+ ]);
216
+ });
217
+
218
+ test("Pi extension freeflow_search schema stays Pi-compatible while rejecting invalid primitive shapes", () => {
219
+ const { tools } = registerMockPi();
220
+ const search = tools.get("freeflow_search");
221
+ const schema = search.parameters;
222
+ const source = { kind: "vault", outputId: "ffout_source" };
223
+
224
+ assertSchemaAccepts(schema, {
225
+ action: "query",
226
+ source: { kind: "local", root: "/absolute/local/docs" },
227
+ query: "docs marker",
228
+ });
229
+ assertSchemaAccepts(schema, {
230
+ action: "retrieve",
231
+ source: { kind: "local", root: "/absolute/local/docs", path: "README.md" },
232
+ lineRange: { start: 1, end: 3 },
233
+ });
234
+ assertSchemaAccepts(schema, { action: "transform", source, operation: { kind: "jsonExtract", pointer: "" } });
235
+ assertSchemaAccepts(schema, {
236
+ action: "transform",
237
+ source,
238
+ operation: { kind: "jsonExtract", pointer: "/suite/failures/0/message" },
239
+ });
240
+ assertSchemaAccepts(schema, {
241
+ action: "transform",
242
+ source,
243
+ operation: { kind: "jsonExtract", pointer: "/escaped~0tilde/~1slash" },
244
+ });
245
+ assertSchemaAccepts(schema, { action: "transform", source, operation: { kind: "jsonExtract", path: "$" } });
246
+ assertSchemaAccepts(schema, {
247
+ action: "transform",
248
+ source,
249
+ operation: { kind: "jsonExtract", path: "$.suite.failures[0]" },
250
+ });
251
+ assertSchemaAccepts(schema, {
252
+ action: "transform",
253
+ source,
254
+ operation: { kind: "jsonExtract", path: '$["quoted.key"]' },
255
+ });
256
+ assertSchemaAccepts(schema, { action: "transform", source, operation: { kind: "topN", limit: 10 } });
257
+ assertSchemaAccepts(schema, {
258
+ action: "transform",
259
+ source,
260
+ operation: { kind: "topN", limit: 10, pattern: "duration=(\\d+)", flags: "im", group: "1", sort: "numeric" },
261
+ });
262
+ assertSchemaAccepts(schema, {
263
+ action: "transform",
264
+ sources: [{ kind: "vault", outputId: "ffout_source", stream: "combined", alias: "test_log" }],
265
+ operation: { kind: "script", language: "python", code: "write_text('ok')" },
266
+ limits: { timeoutMs: 1000, maxInputBytes: 2048, maxOutputBytes: 4096 },
267
+ });
268
+
269
+ assertSchemaRejects(schema, {
270
+ action: "transform",
271
+ source: { kind: "file", outputId: "ffout_source" },
272
+ operation: { kind: "lineStats" },
273
+ });
274
+ assertSchemaRejects(schema, {
275
+ action: "transform",
276
+ source,
277
+ operation: { kind: "jsonExtract", pointer: "not/a/pointer" },
278
+ });
279
+ assertSchemaRejects(schema, {
280
+ action: "transform",
281
+ source,
282
+ operation: { kind: "jsonExtract", pointer: "/bad~2escape" },
283
+ });
284
+ assertSchemaRejects(schema, {
285
+ action: "transform",
286
+ source,
287
+ operation: { kind: "jsonExtract", pointer: "/dangling~" },
288
+ });
289
+ assertSchemaRejects(schema, { action: "transform", source, operation: { kind: "jsonExtract", path: "$." } });
290
+ assertSchemaRejects(schema, {
291
+ action: "transform",
292
+ source,
293
+ operation: { kind: "jsonExtract", path: "$['singleQuoted']" },
294
+ });
295
+ assertSchemaRejects(schema, { action: "transform", source, operation: { kind: "jsonExtract", path: "$[01]" } });
296
+ assertSchemaRejects(schema, {
297
+ action: "transform",
298
+ source,
299
+ operation: { kind: "jsonExtract", path: '$["unterminated]' },
300
+ });
301
+ assertSchemaRejects(schema, {
302
+ action: "transform",
303
+ source,
304
+ operation: { kind: "jsonExtract", path: '$["bad\\xescape"]' },
305
+ });
306
+ assertSchemaRejects(schema, { action: "transform", source, operation: { kind: "notReal" } });
307
+ assertSchemaRejects(schema, { action: "transform", source, operation: { kind: "topN", limit: 10, group: 1 } });
308
+ assertSchemaRejects(schema, {
309
+ action: "transform",
310
+ source,
311
+ operation: { kind: "regexFilter", pattern: "FAIL", flags: "ii" },
312
+ });
313
+ assertSchemaRejects(schema, {
314
+ action: "transform",
315
+ source,
316
+ operation: { kind: "regexFilter", pattern: "FAIL", flags: "y" },
317
+ });
318
+ assertSchemaRejects(schema, {
319
+ action: "transform",
320
+ source,
321
+ operation: { kind: "regexFilter", pattern: "FAIL", contextLines: 21 },
322
+ });
323
+ assertSchemaRejects(schema, {
324
+ action: "transform",
325
+ source,
326
+ operation: { kind: "regexFilter", pattern: "FAIL", maxMatches: 1001 },
327
+ });
328
+ assertSchemaRejects(schema, {
329
+ action: "transform",
330
+ source,
331
+ operation: { kind: "groupByRegex", pattern: "kind=(\\w+)", maxGroups: 1001 },
332
+ });
333
+ assertSchemaRejects(schema, {
334
+ action: "transform",
335
+ source,
336
+ operation: { kind: "groupByRegex", pattern: "kind=(\\w+)", maxLinesPerGroup: 1001 },
337
+ });
338
+ assertSchemaRejects(schema, { action: "transform", source, operation: { kind: "dedupe", maxLines: 10001 } });
339
+ assertSchemaRejects(schema, { action: "transform", source, operation: { kind: "topN", limit: 1001 } });
340
+ assertSchemaRejects(schema, { action: "transform", source, operation: { kind: "extractUrls", maxMatches: 1001 } });
341
+ assertSchemaRejects(schema, {
342
+ action: "transform",
343
+ source,
344
+ operation: { kind: "extractCitations", maxMatches: 1001 },
345
+ });
346
+ assertSchemaRejects(schema, {
347
+ action: "transform",
348
+ sources: [{ kind: "vault", outputId: "ffout_source", alias: "1bad" }],
349
+ operation: { kind: "script", language: "python", code: "ok" },
350
+ });
351
+ assertSchemaRejects(schema, {
352
+ action: "transform",
353
+ sources: [{ kind: "vault", outputId: "ffout_source", alias: "ok" }],
354
+ operation: { kind: "script", language: "ruby", code: "ok" },
355
+ });
356
+ assertSchemaRejects(schema, {
357
+ action: "transform",
358
+ sources: [{ kind: "vault", outputId: "ffout_source", alias: "ok" }],
359
+ operation: { kind: "script", language: "python", code: "" },
360
+ });
361
+ assertSchemaRejects(schema, {
362
+ action: "transform",
363
+ sources: [{ kind: "vault", outputId: "ffout_source", alias: "ok" }],
364
+ operation: { kind: "script", language: "python", code: "ok" },
365
+ limits: { timeoutMs: 0 },
366
+ });
367
+ });
368
+
369
+ test("Pi extension public freeflow_search returns structured disabled result for script transform by default", async () => {
370
+ const cwd = await mkdtemp(join(tmpdir(), "freeflow-pi-transform-script-disabled-"));
371
+ try {
372
+ await mkdir(join(cwd, ".freeflow"));
373
+ await writeFile(
374
+ join(cwd, ".freeflow/config.json"),
375
+ JSON.stringify({ defaultMode: "workflow", outputRouter: { enabled: true } }),
376
+ "utf8",
377
+ );
378
+
379
+ const { tools } = registerMockPi();
380
+ const search = tools.get("freeflow_search");
381
+ const result = await search.execute(
382
+ "search-transform-script-disabled",
383
+ {
384
+ action: "transform",
385
+ sources: [{ kind: "vault", outputId: "ffout_missing", alias: "missing" }],
386
+ operation: { kind: "script", language: "python", code: "print('RAW_SCRIPT_SENTINEL')" },
387
+ },
388
+ undefined,
389
+ undefined,
390
+ mockCtx(cwd, "pi-extension-search-script-disabled-test"),
391
+ );
392
+
393
+ assert.equal(result.details.result.failure.kind, "script_transform_disabled");
394
+ assert.equal(result.details.result.transformExecution.status, "unavailable");
395
+ assert.equal(result.details.result.persistence.recoverability, "none");
396
+ assert.doesNotMatch(JSON.stringify(result.details.result), /RAW_SCRIPT_SENTINEL/);
397
+ } finally {
398
+ await rm(cwd, { recursive: true, force: true });
399
+ }
400
+ });
401
+
402
+ test("Pi extension public freeflow_search returns structured failures for operation-specific validation", async () => {
403
+ const cwd = await mkdtemp(join(tmpdir(), "freeflow-pi-transform-invalid-"));
404
+ const sessionId = "pi-extension-search-invalid-test";
405
+ try {
406
+ const vaultRoot = join(cwd, "vault");
407
+ await mkdir(join(cwd, ".freeflow"));
408
+ await writeFile(
409
+ join(cwd, ".freeflow/config.json"),
410
+ JSON.stringify({ defaultMode: "workflow", outputRouter: { enabled: true, vaultRoot } }),
411
+ "utf8",
412
+ );
413
+
414
+ const vault = createVault({ root: vaultRoot });
415
+ const source = await storeCommandOutput(vault, {
416
+ sessionId,
417
+ command: "printf json",
418
+ stdout: '{"suite":{"failed":1}}',
419
+ stderr: "",
420
+ executionStatus: "success",
421
+ exitCode: 0,
422
+ createdAt: "2026-06-22T00:13:00.000Z",
423
+ });
424
+
425
+ const { tools } = registerMockPi();
426
+ const search = tools.get("freeflow_search");
427
+ const missingSelector = await search.execute(
428
+ "search-transform-invalid-json",
429
+ {
430
+ action: "transform",
431
+ source: { kind: "vault", outputId: source.outputId, stream: "stdout" },
432
+ operation: { kind: "jsonExtract" },
433
+ },
434
+ undefined,
435
+ undefined,
436
+ mockCtx(cwd, sessionId),
437
+ );
438
+
439
+ assert.equal(missingSelector.details.result.failure.kind, "transform_validation_failure");
440
+ assert.equal(missingSelector.details.result.transformExecution.status, "rejected");
441
+ assert.match(missingSelector.details.result.failure.message, /exactly one JSON selector/);
442
+
443
+ const topNGroupWithoutPattern = await search.execute(
444
+ "search-transform-invalid-topn",
445
+ {
446
+ action: "transform",
447
+ source: { kind: "vault", outputId: source.outputId, stream: "stdout" },
448
+ operation: { kind: "topN", limit: 10, group: "1" },
449
+ },
450
+ undefined,
451
+ undefined,
452
+ mockCtx(cwd, sessionId),
453
+ );
454
+
455
+ assert.equal(topNGroupWithoutPattern.details.result.failure.kind, "transform_validation_failure");
456
+ assert.equal(topNGroupWithoutPattern.details.result.transformExecution.status, "rejected");
457
+ assert.match(topNGroupWithoutPattern.details.result.failure.message, /topN group requires a pattern/);
458
+ } finally {
459
+ await rm(cwd, { recursive: true, force: true });
460
+ }
461
+ });
462
+
463
+ test("Pi extension public freeflow_search action=transform processes repo files", async () => {
464
+ const cwd = await mkdtemp(join(tmpdir(), "freeflow-pi-search-transform-repo-"));
465
+ try {
466
+ await mkdir(join(cwd, ".freeflow"));
467
+ await writeFile(
468
+ join(cwd, ".freeflow/config.json"),
469
+ JSON.stringify({ defaultMode: "workflow", outputRouter: { enabled: true } }),
470
+ "utf8",
471
+ );
472
+ await writeFile(
473
+ join(cwd, "test-output.txt"),
474
+ [
475
+ " RUN v2.1.8 /repo",
476
+ " ✗ src/components/UserList.test.tsx (4 tests) 234ms",
477
+ " Test Files 1 failed | 2 passed (3)",
478
+ " Tests 1 failed | 7 passed (8)",
479
+ ].join("\n"),
480
+ "utf8",
481
+ );
482
+
483
+ const { tools } = registerMockPi();
484
+ const search = tools.get("freeflow_search");
485
+ const result = await search.execute(
486
+ "search-transform-repo",
487
+ {
488
+ action: "transform",
489
+ source: { kind: "repo", path: "test-output.txt" },
490
+ goal: "test output summary",
491
+ },
492
+ undefined,
493
+ undefined,
494
+ mockCtx(cwd, "pi-extension-search-transform-repo-test"),
495
+ );
496
+
497
+ const visibleText = result.content[0].text;
498
+ const routed = result.details.result;
499
+ assert.match(visibleText, /freeflow_search\|ok\|transform/);
500
+ assert.match(visibleText, /1 failed/);
501
+ assert.equal(routed.status, "ok");
502
+ assert.equal(routed.implementation, "processing-engine-skeleton-v1");
503
+ assert.equal(routed.source.displayPath, "test-output.txt");
504
+ assert.equal(routed.reducer.status, "selected");
505
+ assert.equal(routed.reducer.selected.name, "test-output");
506
+ } finally {
507
+ await rm(cwd, { recursive: true, force: true });
508
+ }
509
+ });
510
+
511
+ test("Pi extension public freeflow_search action=transform processes explicit local files", async () => {
512
+ const cwd = await mkdtemp(join(tmpdir(), "freeflow-pi-search-transform-local-cwd-"));
513
+ const localRoot = await mkdtemp(join(tmpdir(), "freeflow-pi-search-transform-local-source-"));
514
+ try {
515
+ await mkdir(join(cwd, ".freeflow"));
516
+ await writeFile(
517
+ join(cwd, ".freeflow/config.json"),
518
+ JSON.stringify({ defaultMode: "workflow", outputRouter: { enabled: true } }),
519
+ "utf8",
520
+ );
521
+ await writeFile(
522
+ join(localRoot, "test-output.txt"),
523
+ [
524
+ " RUN v2.1.8 /external-docs",
525
+ " ✗ docs/LocalDocs.test.ts (1 test) 42ms",
526
+ " Test Files 1 failed | 0 passed (1)",
527
+ " Tests 1 failed | 0 passed (1)",
528
+ ].join("\n"),
529
+ "utf8",
530
+ );
531
+
532
+ const { tools } = registerMockPi();
533
+ const search = tools.get("freeflow_search");
534
+ const result = await search.execute(
535
+ "search-transform-local",
536
+ {
537
+ action: "transform",
538
+ source: { kind: "local", root: localRoot, path: "test-output.txt" },
539
+ goal: "test output summary",
540
+ },
541
+ undefined,
542
+ undefined,
543
+ mockCtx(cwd, "pi-extension-search-transform-local-test"),
544
+ );
545
+
546
+ const visibleText = result.content[0].text;
547
+ const routed = result.details.result;
548
+ assert.match(visibleText, /freeflow_search\|ok\|transform\|local-file:test-output\.txt/);
549
+ assert.match(visibleText, /1 failed/);
550
+ assert.equal(routed.status, "ok");
551
+ assert.equal(routed.implementation, "processing-engine-skeleton-v1");
552
+ assert.equal(routed.source.kind, "local-file");
553
+ assert.equal(routed.source.ref.kind, "local");
554
+ assert.equal(routed.source.displayPath, "test-output.txt");
555
+ assert.equal(routed.reducer.status, "selected");
556
+ assert.equal(routed.reducer.selected.name, "test-output");
557
+ } finally {
558
+ await rm(cwd, { recursive: true, force: true });
559
+ await rm(localRoot, { recursive: true, force: true });
560
+ }
561
+ });
562
+
563
+ test("Pi extension public freeflow_search executes against vaulted output", async () => {
564
+ const cwd = await mkdtemp(join(tmpdir(), "freeflow-pi-transform-"));
565
+ const sessionId = "pi-extension-search-execute-test";
566
+ try {
567
+ const vaultRoot = join(cwd, "vault");
568
+ await mkdir(join(cwd, ".freeflow"));
569
+ await writeFile(
570
+ join(cwd, ".freeflow/config.json"),
571
+ JSON.stringify({ defaultMode: "workflow", outputRouter: { enabled: true, vaultRoot } }),
572
+ "utf8",
573
+ );
574
+
575
+ const vault = createVault({ root: vaultRoot });
576
+ const source = await storeCommandOutput(vault, {
577
+ sessionId,
578
+ command: "npm test",
579
+ stdout: ["PASS first", "FAIL target", "FAIL second"].join("\n"),
580
+ stderr: "",
581
+ executionStatus: "failed",
582
+ exitCode: 1,
583
+ createdAt: "2026-06-22T00:12:00.000Z",
584
+ });
585
+
586
+ const { tools } = registerMockPi();
587
+ const search = tools.get("freeflow_search");
588
+ const result = await search.execute(
589
+ "search-transform-regex",
590
+ {
591
+ action: "transform",
592
+ source: { kind: "vault", outputId: source.outputId, stream: "stdout" },
593
+ operation: { kind: "regexFilter", pattern: "FAIL", contextLines: 0, maxMatches: 10 },
594
+ preserve: "important",
595
+ },
596
+ undefined,
597
+ undefined,
598
+ mockCtx(cwd, sessionId),
599
+ );
600
+
601
+ const visibleText = result.content[0].text;
602
+ const routed = result.details.result;
603
+ assert.match(visibleText, /freeflow_search\|routed/);
604
+ assert.match(visibleText, /regexFilter/);
605
+ assert.doesNotMatch(visibleText, /^\s*\{/);
606
+ assert.ok(Buffer.byteLength(visibleText, "utf8") < Buffer.byteLength(JSON.stringify(routed, null, 2), "utf8"));
607
+ assert.equal(routed.toolStatus, "ok");
608
+ assert.equal(routed.routing.route, "transform");
609
+ assert.equal(routed.routing.status, "routed");
610
+ assert.equal(routed.producer.name, "regexFilter");
611
+ assert.equal(routed.source.outputId, source.outputId);
612
+ assert.equal(routed.persistence.recoverability, "exact");
613
+ assert.deepEqual(routed.lineage.sourceOutputIds, [source.outputId]);
614
+ assert.ok(routed.outputId.startsWith("ffout_"));
615
+
616
+ const raw = await readOutputText(vault, sessionId, routed.outputId, "raw");
617
+ assert.match(raw, /# freeflow_search action=transform regexFilter/);
618
+ assert.match(raw, /matches: 2/);
619
+ assert.match(raw, /2\| FAIL target/);
620
+ assert.match(raw, /3\| FAIL second/);
621
+ } finally {
622
+ await rm(cwd, { recursive: true, force: true });
623
+ }
624
+ });
625
+
626
+ test("Pi extension freeflow_search renders source, operation, lineage, routing, persistence, evidence, and recovery", () => {
627
+ const { tools } = registerMockPi();
628
+ const search = tools.get("freeflow_search");
629
+
630
+ const call = renderText(
631
+ search.renderCall(
632
+ {
633
+ action: "transform",
634
+ source: { kind: "vault", outputId: "ffout_source123", stream: "stdout" },
635
+ operation: { kind: "topN", pattern: "duration=(\\d+)", group: 1, sort: "numeric", order: "desc", limit: 2 },
636
+ preserve: "important",
637
+ },
638
+ testTheme,
639
+ ),
640
+ );
641
+ assert.match(call, /freeflow_search transform/);
642
+ assert.match(call, /vault ffout_source123:stdout/);
643
+ assert.doesNotMatch(call, /raw json/);
644
+
645
+ const toolResult = {
646
+ content: [{ type: "text", text: "raw json should not be the visible UI" }],
647
+ details: {
648
+ result: {
649
+ toolStatus: "ok",
650
+ decisionId: "ffdec_transform_test",
651
+ preserve: "important",
652
+ outputId: "ffout_transform123",
653
+ recordId: "ffrec_transform123",
654
+ action: "transform",
655
+ source: { kind: "vault", outputId: "ffout_source123", stream: "stdout" },
656
+ operation: { kind: "topN", pattern: "duration=(\\d+)", group: 1, sort: "numeric", order: "desc", limit: 2 },
657
+ producer: { kind: "transform", name: "topN" },
658
+ persistence: { status: "vaulted", recoverability: "exact", recoveryOutputId: "ffout_transform123" },
659
+ lineage: {
660
+ sourceOutputIds: ["ffout_source123"],
661
+ sourceRecordIds: ["ffrec_source123"],
662
+ operation: "topN",
663
+ operationHash: "sha256_abcdef",
664
+ },
665
+ routing: {
666
+ status: "routed",
667
+ route: "transform",
668
+ reason: "Transformed output was vaulted and returned within routing caps.",
669
+ },
670
+ summary: "Transformed topN from vaulted stdout output: returned 2 of 3 matched line(s).",
671
+ evidence: [
672
+ {
673
+ id: "ev_transform",
674
+ source: { kind: "vault", outputId: "ffout_transform123", stream: "raw" },
675
+ path: "ffout_transform123:raw",
676
+ lines: "1-8",
677
+ excerpt: "# freeflow_search topN\n2| score=200 | duration=200 slow",
678
+ why: "Transformed exact topN output from source ffout_source123:stdout within routing caps; source lineage is preserved.",
679
+ window: "exact",
680
+ expandable: true,
681
+ },
682
+ ],
683
+ recovery: {
684
+ how: "Use freeflow_search with source.kind=vault and outputId=ffout_transform123 to recover exact transformed content.",
685
+ outputId: "ffout_transform123",
686
+ },
687
+ },
688
+ },
689
+ };
690
+
691
+ const collapsed = renderText(search.renderResult(toolResult, { expanded: false }, testTheme));
692
+ assert.match(collapsed, /freeflow_search topN/);
693
+ assert.match(collapsed, /routing: routed/);
694
+ assert.match(collapsed, /persistence vaulted\/exact/);
695
+ assert.match(collapsed, /transformed output recoverable from vault/);
696
+ assert.doesNotMatch(collapsed, /raw json/);
697
+
698
+ const expanded = renderText(search.renderResult(toolResult, { expanded: true }, testTheme));
699
+ assert.match(expanded, /toolStatus: ok/);
700
+ assert.match(expanded, /routing\.status: routed/);
701
+ assert.match(expanded, /persistence: vaulted \/ exact/);
702
+ assert.match(expanded, /Storage/);
703
+ assert.match(expanded, /decisionId: ffdec_transform_test/);
704
+ assert.match(expanded, /recordId: ffrec_transform123/);
705
+ assert.match(expanded, /Source/);
706
+ assert.match(expanded, /ffout_source123:stdout/);
707
+ assert.match(expanded, /Operation/);
708
+ assert.match(expanded, /"kind":"topN"/);
709
+ assert.match(expanded, /Lineage/);
710
+ assert.match(expanded, /sourceOutputIds: ffout_source123/);
711
+ assert.match(expanded, /Evidence/);
712
+ assert.match(expanded, /evidenceId: ev_transform/);
713
+ assert.match(expanded, /source: vault ffout_transform123:raw/);
714
+ assert.match(expanded, /expandable: true/);
715
+ assert.match(expanded, /duration=200 slow/);
716
+ assert.match(expanded, /Recovery/);
717
+ assert.match(expanded, /ffout_transform123/);
718
+ assert.match(
719
+ expanded,
720
+ /exact search: action=retrieve source.kind=vault lineRange=1-8 stream=raw outputId=ffout_transform123/,
721
+ );
722
+ });