@hassangameryt/freeflow 0.5.0 → 0.7.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 (301) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +2 -2
  3. package/.codex-plugin/plugin.json +4 -5
  4. package/README.md +141 -163
  5. package/capabilities/cognitive-routing/SKILL.md +387 -0
  6. package/capabilities/context-virtualization/SKILL.md +103 -0
  7. package/capabilities/conversation-history/SKILL.md +53 -0
  8. package/command-surface.json +6 -37
  9. package/hooks/freeflow-runtime-context.mjs +50 -541
  10. package/hooks/hooks.json +1 -26
  11. package/hooks/tests/check-runtime-context-hook.sh +114 -308
  12. package/package.json +27 -17
  13. package/pi-extension/dist/cognitive-routing/commands.js +96 -0
  14. package/pi-extension/dist/cognitive-routing/config.js +232 -0
  15. package/pi-extension/dist/cognitive-routing/controller.js +1292 -0
  16. package/pi-extension/dist/cognitive-routing/history.js +451 -0
  17. package/pi-extension/dist/cognitive-routing/host.js +188 -0
  18. package/pi-extension/dist/cognitive-routing/pi-routing-session.js +414 -0
  19. package/pi-extension/dist/cognitive-routing/pi-session-control.js +320 -0
  20. package/pi-extension/dist/cognitive-routing/runtime.js +43 -0
  21. package/pi-extension/dist/cognitive-routing/tool.js +212 -0
  22. package/pi-extension/dist/cognitive-routing/types.js +6 -0
  23. package/pi-extension/dist/context-virtualization/commands.js +89 -0
  24. package/pi-extension/dist/context-virtualization/projector.js +42 -0
  25. package/pi-extension/dist/context-virtualization/runtime.js +356 -0
  26. package/pi-extension/dist/context-virtualization/state.js +57 -0
  27. package/pi-extension/dist/context-virtualization/types.js +10 -0
  28. package/pi-extension/dist/conversation-history/passages.js +66 -0
  29. package/pi-extension/dist/conversation-history/ranking.js +105 -0
  30. package/pi-extension/dist/conversation-history/runtime.js +226 -0
  31. package/pi-extension/dist/conversation-history/search.js +56 -0
  32. package/pi-extension/dist/conversation-history/types.js +1 -0
  33. package/pi-extension/dist/freeflow-context/resolver.js +120 -0
  34. package/pi-extension/dist/freeflow-context/runtime.js +80 -0
  35. package/pi-extension/dist/freeflow-context/source-projector.js +95 -0
  36. package/pi-extension/dist/freeflow-context/tool.js +308 -0
  37. package/pi-extension/dist/freeflow-context/types.js +12 -0
  38. package/pi-extension/dist/freeflow-context/visibility.js +28 -0
  39. package/pi-extension/dist/index.js +564 -129
  40. package/pi-extension/dist/runtime/runtime-context.js +357 -470
  41. package/pi-extension/dist/runtime/runtime-identity.js +5 -0
  42. package/pi-extension/dist/settings/settings-tui.js +78 -0
  43. package/pi-extension/dist/settings/settings-ui.js +572 -826
  44. package/pi-extension/dist/ui/text-component.js +57 -0
  45. package/pi-extension/src/cognitive-routing/commands.ts +129 -0
  46. package/pi-extension/src/cognitive-routing/config.ts +303 -0
  47. package/pi-extension/src/cognitive-routing/controller.ts +1628 -0
  48. package/pi-extension/src/cognitive-routing/history.ts +650 -0
  49. package/pi-extension/src/cognitive-routing/host.ts +266 -0
  50. package/pi-extension/src/cognitive-routing/pi-routing-session.ts +538 -0
  51. package/pi-extension/src/cognitive-routing/pi-session-control.ts +444 -0
  52. package/pi-extension/src/cognitive-routing/runtime.ts +57 -0
  53. package/pi-extension/src/cognitive-routing/tool.ts +273 -0
  54. package/pi-extension/src/cognitive-routing/types.ts +89 -0
  55. package/pi-extension/src/context-virtualization/commands.ts +111 -0
  56. package/pi-extension/src/context-virtualization/projector.ts +62 -0
  57. package/pi-extension/src/context-virtualization/runtime.ts +406 -0
  58. package/pi-extension/src/context-virtualization/state.ts +79 -0
  59. package/pi-extension/src/context-virtualization/types.ts +92 -0
  60. package/pi-extension/src/conversation-history/passages.ts +81 -0
  61. package/pi-extension/src/conversation-history/ranking.ts +155 -0
  62. package/pi-extension/src/conversation-history/runtime.ts +250 -0
  63. package/pi-extension/src/conversation-history/search.ts +79 -0
  64. package/pi-extension/src/conversation-history/types.ts +31 -0
  65. package/pi-extension/src/freeflow-context/resolver.ts +139 -0
  66. package/pi-extension/src/freeflow-context/runtime.ts +100 -0
  67. package/pi-extension/src/freeflow-context/source-projector.ts +106 -0
  68. package/pi-extension/src/freeflow-context/tool.ts +360 -0
  69. package/pi-extension/src/freeflow-context/types.ts +70 -0
  70. package/pi-extension/src/freeflow-context/visibility.ts +53 -0
  71. package/pi-extension/src/index.ts +597 -135
  72. package/pi-extension/src/runtime/runtime-context.ts +373 -491
  73. package/pi-extension/src/runtime/runtime-identity.ts +8 -0
  74. package/pi-extension/src/settings/settings-tui.ts +93 -0
  75. package/pi-extension/src/settings/settings-ui.ts +679 -910
  76. package/pi-extension/src/ui/text-component.ts +66 -0
  77. package/pi-extension/tests/cognitive-routing/commands.test.js +134 -0
  78. package/pi-extension/tests/cognitive-routing/controller.test.js +1128 -0
  79. package/pi-extension/tests/cognitive-routing/extension-lifecycle.test.js +1142 -0
  80. package/pi-extension/tests/cognitive-routing/history.test.js +274 -0
  81. package/pi-extension/tests/cognitive-routing/host-fixture.js +4 -0
  82. package/pi-extension/tests/cognitive-routing/host-identity.test.js +63 -0
  83. package/pi-extension/tests/cognitive-routing/pi-normal-lifecycle.test.js +434 -0
  84. package/pi-extension/tests/cognitive-routing/pi-session-control.test.js +174 -0
  85. package/pi-extension/tests/cognitive-routing/pi-session-history.test.js +91 -0
  86. package/pi-extension/tests/cognitive-routing/profile-resolution.test.js +279 -0
  87. package/pi-extension/tests/cognitive-routing/tool.test.js +230 -0
  88. package/pi-extension/tests/context-virtualization/extension.test.js +749 -0
  89. package/pi-extension/tests/context-virtualization/projector.test.js +110 -0
  90. package/pi-extension/tests/context-virtualization/runtime.test.js +216 -0
  91. package/pi-extension/tests/conversation-history/runtime.test.js +98 -0
  92. package/pi-extension/tests/conversation-history/search.test.js +97 -0
  93. package/pi-extension/tests/integration/mode-free-core.test.js +218 -0
  94. package/pi-extension/tests/integration/pi-extension.test.js +532 -3701
  95. package/pi-extension/tests/integration/prompt-architecture.test.js +350 -0
  96. package/pi-extension/tests/integration/unaffected-regressions.test.js +411 -0
  97. package/runtime/prompts/cognitive-routing.md +10 -0
  98. package/runtime/prompts/context-virtualization.md +3 -0
  99. package/runtime/prompts/conversation-history.md +3 -0
  100. package/runtime/prompts/core.md +84 -0
  101. package/runtime/prompts/interaction-contract.md +11 -0
  102. package/skills/action-selection/SKILL.md +140 -0
  103. package/skills/action-selection/references/trajectory-stalls.md +71 -0
  104. package/skills/bypass/SKILL.md +109 -38
  105. package/skills/commit-work/SKILL.md +94 -55
  106. package/skills/commit-work/references/git-state-edges.md +94 -0
  107. package/skills/commit-work/references/simple-push.md +92 -0
  108. package/skills/commit-work/references/staging-decisions.md +79 -46
  109. package/skills/decision-gate/SKILL.md +80 -22
  110. package/skills/design-for-depth/SKILL.md +206 -60
  111. package/skills/design-for-depth/references/design-pressure-signals.md +108 -240
  112. package/skills/design-for-depth/references/interface-design-loop.md +77 -97
  113. package/skills/design-for-depth/references/module-and-dependency-design.md +147 -0
  114. package/skills/design-for-depth/references/software-design-philosophy.md +128 -174
  115. package/skills/design-for-depth/references/state-and-failure-boundaries.md +136 -0
  116. package/skills/diagnose-failure/SKILL.md +200 -48
  117. package/skills/diagnose-failure/references/diagnostic-loop-catalog.md +18 -6
  118. package/skills/diagnose-failure/references/{flaky-and-performance.md → flaky-and-performance-diagnosis.md} +13 -5
  119. package/skills/discuss/SKILL.md +119 -65
  120. package/skills/discuss/references/discussion-continuity.md +123 -0
  121. package/skills/evaluate-skill/SKILL.md +166 -52
  122. package/skills/evaluate-skill/references/{definition-schema.md → evaluation-definition-schema.md} +43 -3
  123. package/skills/evaluate-skill/references/evaluation-design.md +56 -30
  124. package/skills/evaluate-skill/references/execution-and-evidence.md +8 -3
  125. package/skills/evaluate-skill/references/review-and-revision.md +62 -21
  126. package/skills/evaluate-skill/scripts/lib/definitions.mjs +114 -3
  127. package/skills/evaluate-skill/scripts/lib/grade-interaction.mjs +156 -1
  128. package/skills/evaluate-skill/scripts/lib/grade.mjs +9 -1
  129. package/skills/evaluate-skill/scripts/lib/pi.mjs +147 -32
  130. package/skills/evaluate-skill/scripts/lib/rpc.mjs +4 -4
  131. package/skills/evaluate-skill/scripts/lib/runner.mjs +36 -14
  132. package/skills/evaluate-skill/scripts/lib/sandbox.mjs +92 -0
  133. package/skills/evaluate-skill/scripts/pi-guard.mjs +1 -1
  134. package/skills/evaluate-skill/scripts/pi-observer.mjs +66 -0
  135. package/skills/evaluate-skill/scripts/skill-eval.mjs +6 -5
  136. package/skills/execute-work/SKILL.md +99 -57
  137. package/skills/execute-work/references/code-practices.md +4 -4
  138. package/skills/execute-work/references/execute-work-edges.md +62 -0
  139. package/skills/finish-branch/SKILL.md +2 -2
  140. package/skills/finish-branch/references/integration-options.md +1 -1
  141. package/skills/handoff/SKILL.md +3 -3
  142. package/skills/launch-work/SKILL.md +1 -1
  143. package/skills/launch-work/references/launch-readiness.md +2 -2
  144. package/skills/migration-work/SKILL.md +2 -2
  145. package/skills/migration-work/references/migration-lifecycle.md +1 -1
  146. package/skills/release-work/SKILL.md +1 -1
  147. package/skills/release-work/references/release-evidence.md +1 -1
  148. package/skills/review-artifact/SKILL.md +129 -66
  149. package/skills/review-artifact/references/adjudicate-artifact-review.md +101 -0
  150. package/skills/review-artifact/references/independent-artifact-reviewer-contract.md +159 -0
  151. package/skills/review-work/SKILL.md +113 -68
  152. package/skills/review-work/references/adjudicate-work-review.md +97 -0
  153. package/skills/review-work/references/independent-work-reviewer-contract.md +139 -0
  154. package/skills/review-work/references/security-risk-lens.md +18 -16
  155. package/skills/setup-freeflow/SKILL.md +16 -17
  156. package/skills/setup-freeflow/references/activation-contract.md +24 -30
  157. package/skills/setup-freeflow/references/host-setup.md +6 -7
  158. package/skills/simplify-code/SKILL.md +97 -39
  159. package/skills/simplify-code/references/simplification-patterns.md +1 -1
  160. package/skills/tdd/SKILL.md +100 -53
  161. package/skills/tdd/references/test-design.md +3 -3
  162. package/skills/track-work/SKILL.md +195 -58
  163. package/skills/track-work/references/working-record-format.md +515 -0
  164. package/skills/track-work/scripts/lib/document.mjs +713 -0
  165. package/skills/track-work/scripts/lib/format.mjs +166 -0
  166. package/skills/track-work/scripts/lib/store.mjs +233 -0
  167. package/skills/track-work/scripts/lib/transitions.mjs +790 -0
  168. package/skills/track-work/scripts/lib/views.mjs +40 -0
  169. package/skills/track-work/scripts/working-record.mjs +316 -0
  170. package/skills/verify-work/SKILL.md +57 -19
  171. package/skills/verify-work/references/browser-runtime-evidence.md +2 -2
  172. package/skills/verify-work/references/integration-evidence.md +1 -7
  173. package/skills/verify-work/references/performance-evidence.md +2 -2
  174. package/skills/workflow/SKILL.md +137 -55
  175. package/skills/workflow/references/domain-skill-composition.md +26 -40
  176. package/skills/write-plan/SKILL.md +1 -1
  177. package/skills/write-skill/SKILL.md +218 -61
  178. package/skills/write-skill/references/activation-boundaries.md +63 -21
  179. package/skills/write-skill/references/agent-first-instructions.md +61 -27
  180. package/skills/write-skill/references/progressive-disclosure.md +97 -33
  181. package/skills/write-skill/references/skill-dependency-graphs.md +190 -0
  182. package/capabilities/output-router/SKILL.md +0 -206
  183. package/capabilities/output-router/references/safety-policy.md +0 -40
  184. package/capabilities/output-router/references/setup.md +0 -110
  185. package/pi-extension/dist/output-router/host-producer-identification.js +0 -232
  186. package/pi-extension/dist/output-router/native-safety-net.js +0 -216
  187. package/pi-extension/dist/output-router/observed-tool-routing.js +0 -122
  188. package/pi-extension/dist/output-router/renderers.js +0 -1023
  189. package/pi-extension/dist/output-router/router-tools.js +0 -554
  190. package/pi-extension/dist/output-router/schemas.js +0 -383
  191. package/pi-extension/dist/output-router/status.js +0 -744
  192. package/pi-extension/dist/output-router/utils.js +0 -502
  193. package/pi-extension/src/output-router/host-producer-identification.ts +0 -256
  194. package/pi-extension/src/output-router/native-safety-net.ts +0 -248
  195. package/pi-extension/src/output-router/observed-tool-routing.ts +0 -134
  196. package/pi-extension/src/output-router/renderers.ts +0 -1108
  197. package/pi-extension/src/output-router/router-tools.ts +0 -579
  198. package/pi-extension/src/output-router/schemas.ts +0 -387
  199. package/pi-extension/src/output-router/status.ts +0 -791
  200. package/pi-extension/src/output-router/utils.ts +0 -554
  201. package/pi-extension/tests/output-router/observed-producers.test.js +0 -132
  202. package/pi-extension/tests/output-router/search.test.js +0 -722
  203. package/router/dist/benchmarks/benchmark-harness.d.ts +0 -35
  204. package/router/dist/benchmarks/benchmark-harness.js +0 -92
  205. package/router/dist/benchmarks/benchmarks.d.ts +0 -109
  206. package/router/dist/benchmarks/benchmarks.js +0 -979
  207. package/router/dist/benchmarks/codex-qa-benchmarks.d.ts +0 -95
  208. package/router/dist/benchmarks/codex-qa-benchmarks.js +0 -500
  209. package/router/dist/benchmarks/command-benchmarks.d.ts +0 -143
  210. package/router/dist/benchmarks/command-benchmarks.js +0 -698
  211. package/router/dist/benchmarks/context-mode-normalized-benchmarks.d.ts +0 -82
  212. package/router/dist/benchmarks/context-mode-normalized-benchmarks.js +0 -773
  213. package/router/dist/benchmarks/context-mode-real-deep-benchmark.d.ts +0 -132
  214. package/router/dist/benchmarks/context-mode-real-deep-benchmark.js +0 -1380
  215. package/router/dist/benchmarks/context-saving-baseline.d.ts +0 -60
  216. package/router/dist/benchmarks/context-saving-baseline.js +0 -470
  217. package/router/dist/benchmarks/index-benchmarks.d.ts +0 -103
  218. package/router/dist/benchmarks/index-benchmarks.js +0 -862
  219. package/router/dist/benchmarks/storage-policy-benchmarks.d.ts +0 -101
  220. package/router/dist/benchmarks/storage-policy-benchmarks.js +0 -618
  221. package/router/dist/benchmarks/vault-index-benchmarks.d.ts +0 -56
  222. package/router/dist/benchmarks/vault-index-benchmarks.js +0 -322
  223. package/router/dist/config/config.d.ts +0 -103
  224. package/router/dist/config/config.js +0 -632
  225. package/router/dist/config/router-contract.d.ts +0 -10
  226. package/router/dist/config/router-contract.js +0 -54
  227. package/router/dist/config/schema.d.ts +0 -27
  228. package/router/dist/config/schema.js +0 -648
  229. package/router/dist/config/types.d.ts +0 -573
  230. package/router/dist/config/types.js +0 -41
  231. package/router/dist/evidence/bounded-evidence.d.ts +0 -40
  232. package/router/dist/evidence/bounded-evidence.js +0 -323
  233. package/router/dist/evidence/evidence-range-selector.d.ts +0 -24
  234. package/router/dist/evidence/evidence-range-selector.js +0 -232
  235. package/router/dist/evidence/evidence-search.d.ts +0 -27
  236. package/router/dist/evidence/evidence-search.js +0 -490
  237. package/router/dist/evidence/evidence.d.ts +0 -38
  238. package/router/dist/evidence/evidence.js +0 -200
  239. package/router/dist/evidence/failure-contracts.d.ts +0 -37
  240. package/router/dist/evidence/failure-contracts.js +0 -166
  241. package/router/dist/evidence/line-ranges.d.ts +0 -24
  242. package/router/dist/evidence/line-ranges.js +0 -22
  243. package/router/dist/experiments/local-index.d.ts +0 -64
  244. package/router/dist/experiments/local-index.js +0 -581
  245. package/router/dist/index.d.ts +0 -30
  246. package/router/dist/index.js +0 -32
  247. package/router/dist/local/local-traversal.d.ts +0 -18
  248. package/router/dist/local/local-traversal.js +0 -334
  249. package/router/dist/processing/engine.d.ts +0 -132
  250. package/router/dist/processing/engine.js +0 -533
  251. package/router/dist/processing/reducers.d.ts +0 -280
  252. package/router/dist/processing/reducers.js +0 -1673
  253. package/router/dist/processing/renderers.d.ts +0 -31
  254. package/router/dist/processing/renderers.js +0 -195
  255. package/router/dist/processing/scripts.d.ts +0 -91
  256. package/router/dist/processing/scripts.js +0 -386
  257. package/router/dist/repo/repo-traversal.d.ts +0 -18
  258. package/router/dist/repo/repo-traversal.js +0 -245
  259. package/router/dist/routing/observed-reducers.d.ts +0 -41
  260. package/router/dist/routing/observed-reducers.js +0 -661
  261. package/router/dist/routing/observed-routing.d.ts +0 -26
  262. package/router/dist/routing/observed-routing.js +0 -378
  263. package/router/dist/routing/parsers.d.ts +0 -16
  264. package/router/dist/routing/parsers.js +0 -521
  265. package/router/dist/routing/run-filters.d.ts +0 -60
  266. package/router/dist/routing/run-filters.js +0 -238
  267. package/router/dist/routing/run-reducers.d.ts +0 -49
  268. package/router/dist/routing/run-reducers.js +0 -122
  269. package/router/dist/sandbox/adapter-roots.d.ts +0 -46
  270. package/router/dist/sandbox/adapter-roots.js +0 -66
  271. package/router/dist/sandbox/eryx-python-adapter.d.ts +0 -13
  272. package/router/dist/sandbox/eryx-python-adapter.js +0 -868
  273. package/router/dist/sandbox/jq-wasm-adapter.d.ts +0 -9
  274. package/router/dist/sandbox/jq-wasm-adapter.js +0 -429
  275. package/router/dist/sandbox/quickjs-wasi-adapter.d.ts +0 -13
  276. package/router/dist/sandbox/quickjs-wasi-adapter.js +0 -384
  277. package/router/dist/sandbox/script-sandbox.d.ts +0 -134
  278. package/router/dist/sandbox/script-sandbox.js +0 -337
  279. package/router/dist/setup/script-transform-adapters.d.ts +0 -32
  280. package/router/dist/setup/script-transform-adapters.js +0 -278
  281. package/router/dist/tools/batch.d.ts +0 -33
  282. package/router/dist/tools/batch.js +0 -586
  283. package/router/dist/tools/run.d.ts +0 -62
  284. package/router/dist/tools/run.js +0 -1304
  285. package/router/dist/tools/search.d.ts +0 -66
  286. package/router/dist/tools/search.js +0 -1364
  287. package/router/dist/transform/engine.d.ts +0 -168
  288. package/router/dist/transform/engine.js +0 -2072
  289. package/router/dist/vault/vault-index.d.ts +0 -125
  290. package/router/dist/vault/vault-index.js +0 -416
  291. package/router/dist/vault/vault.d.ts +0 -138
  292. package/router/dist/vault/vault.js +0 -630
  293. package/runtime/interaction-contract.md +0 -16
  294. package/skills/discuss/references/checkpoints.md +0 -66
  295. package/skills/execute-work/references/execution-loop.md +0 -77
  296. package/skills/mode-contract/SKILL.md +0 -104
  297. package/skills/review-artifact/references/reviewer-prompt.md +0 -148
  298. package/skills/review-work/references/reviewer-prompt.md +0 -134
  299. package/skills/track-work/references/working-record-schema.md +0 -378
  300. package/skills/workflow/references/workflow-loop.md +0 -92
  301. /package/skills/handoff/references/{templates.md → handoff-templates.md} +0 -0
@@ -10,45 +10,49 @@ import freeflowExtension from "../../dist/index.js";
10
10
  import {
11
11
  readCapabilityState,
12
12
  readFreeflowConfigLayers,
13
- readModeState,
14
- readOutputRouterConfig,
15
13
  resetSessionOverrides,
16
- restoreModeOverride,
14
+ restoreSessionOverrides,
15
+ setFreeflowStatus,
17
16
  setSessionCoreOverride,
18
- setSessionMode,
19
17
  } from "../../dist/runtime/runtime-context.js";
20
- import { createVault, storeTextOutput } from "../../../router/dist/index.js";
18
+ import { PIFLOW_HOST } from "../cognitive-routing/host-fixture.js";
21
19
 
22
20
  const execFileAsync = promisify(execFile);
23
21
 
24
- function loadExtension() {
22
+ function loadExtension(extension = freeflowExtension, host = PIFLOW_HOST, runtimeApi = {}) {
25
23
  const handlers = new Map();
26
24
  const tools = [];
27
25
  const commands = [];
26
+ const shortcuts = [];
28
27
  const entries = [];
29
28
  const sentMessages = [];
29
+ const sentMessageOptions = [];
30
30
  let activeToolNames;
31
31
  const pi = {
32
+ host,
32
33
  registerTool(tool) {
33
- tools.push(tool);
34
+ const index = tools.findIndex((existing) => existing.name === tool.name);
35
+ if (index >= 0) tools[index] = tool;
36
+ else tools.push(tool);
34
37
  },
35
38
  registerCommand(name, definition) {
36
39
  commands.push({ name, definition });
37
40
  },
41
+ registerShortcut(shortcut, definition) {
42
+ shortcuts.push({ shortcut, definition });
43
+ },
38
44
  on(event, handler) {
39
45
  handlers.set(event, handler);
40
46
  },
41
47
  appendEntry(customType, data) {
42
48
  entries.push({ customType, data });
43
49
  },
44
- sendUserMessage(message) {
50
+ sendUserMessage(message, options) {
45
51
  sentMessages.push(message);
52
+ sentMessageOptions.push(options);
46
53
  },
47
54
  getAllTools() {
48
- return tools.map((tool) => ({
49
- name: tool.name,
50
- sourceInfo: { source: "extension" },
51
- }));
55
+ return tools.map((tool) => ({ name: tool.name, sourceInfo: { source: "extension" } }));
52
56
  },
53
57
  getActiveTools() {
54
58
  return activeToolNames ?? tools.map((tool) => tool.name);
@@ -57,15 +61,18 @@ function loadExtension() {
57
61
  activeToolNames = [...names];
58
62
  },
59
63
  };
64
+ Object.assign(pi, runtimeApi);
60
65
 
61
- freeflowExtension(pi);
66
+ extension(pi);
62
67
  return {
63
68
  pi,
64
69
  handlers,
65
70
  tools,
66
71
  commands,
72
+ shortcuts,
67
73
  entries,
68
74
  sentMessages,
75
+ sentMessageOptions,
69
76
  activeToolNames: () => activeToolNames ?? tools.map((tool) => tool.name),
70
77
  };
71
78
  }
@@ -116,3905 +123,729 @@ const testTheme = {
116
123
  },
117
124
  };
118
125
 
126
+ const readyFreeflowContext = {
127
+ corePrompt: "core",
128
+ interactionContractPrompt: "interaction contract",
129
+ };
130
+
119
131
  function renderText(component, width = 120) {
120
132
  return component.render(width).join("\n");
121
133
  }
122
134
 
123
- test("Pi registers capability commands and no public capture tool", () => {
124
- const { commands, tools } = loadExtension();
135
+ async function configuredRepo(config = {}) {
136
+ const cwd = await mkdtemp(join(tmpdir(), "freeflow-pi-integration-"));
137
+ await mkdir(join(cwd, ".freeflow"));
138
+ await writeFile(join(cwd, ".freeflow/config.json"), JSON.stringify(config, null, 2), "utf8");
139
+ return cwd;
140
+ }
141
+
142
+ function lastRuntimeState(messages) {
143
+ return messages.findLast((message) => message.customType === "freeflow-runtime-state");
144
+ }
145
+
146
+ test("keeps Runtime State before the latest user message during context refreshes", async () => {
147
+ const cwd = await configuredRepo();
148
+ try {
149
+ const { handlers } = loadExtension();
150
+ const ctx = context(cwd);
151
+ await handlers.get("session_start")({ type: "session_start", reason: "startup" }, ctx);
152
+
153
+ const conversation = [
154
+ { role: "user", content: "release 0.6.1" },
155
+ { role: "assistant", content: "delegating" },
156
+ { role: "toolResult", toolName: "freeflow_switch_profile", content: [] },
157
+ ];
158
+ const first = await handlers.get("context")({ messages: conversation }, ctx);
159
+ const firstUserIndex = first.messages.findIndex((message) => message.role === "user");
160
+ const firstRuntimeIndex = first.messages.findIndex((message) => message.customType === "freeflow-runtime-state");
161
+ assert.equal(firstRuntimeIndex, firstUserIndex - 1);
162
+ assert.equal(first.messages.at(-1).role, "toolResult");
163
+
164
+ await handlers.get("session_compact")({ type: "session_compact", reason: "threshold" }, ctx);
165
+ const refreshed = await handlers.get("context")({ messages: first.messages }, ctx);
166
+ const refreshedUserIndex = refreshed.messages.findIndex((message) => message.role === "user");
167
+ const refreshedRuntimeIndex = refreshed.messages.findIndex(
168
+ (message) => message.customType === "freeflow-runtime-state",
169
+ );
170
+ assert.equal(refreshedRuntimeIndex, refreshedUserIndex - 1);
171
+ assert.equal(refreshed.messages.at(-1).role, "toolResult");
172
+
173
+ const interrupted = await handlers.get("context")(
174
+ {
175
+ messages: [...refreshed.messages, { role: "user", content: "stop" }],
176
+ },
177
+ ctx,
178
+ );
179
+ const latestUserIndex = interrupted.messages.findLastIndex((message) => message.role === "user");
180
+ const latestRuntimeIndex = interrupted.messages.findIndex(
181
+ (message) => message.customType === "freeflow-runtime-state",
182
+ );
183
+ assert.equal(latestRuntimeIndex, latestUserIndex - 1);
184
+ assert.equal(interrupted.messages.at(-1).content, "stop");
185
+ } finally {
186
+ await rm(cwd, { recursive: true, force: true });
187
+ }
188
+ });
189
+
190
+ test("Pi registers the remaining Freeflow commands without mode controls or retired router tools", () => {
191
+ const { commands, shortcuts, tools } = loadExtension();
125
192
  const commandNames = commands.map((command) => command.name);
126
193
  const toolNames = tools.map((tool) => tool.name);
194
+ const freeflowCommand = commands.find((command) => command.name === "freeflow");
127
195
 
128
196
  assert.ok(commandNames.includes("freeflow"));
129
- assert.ok(commandNames.includes("output-router"));
130
- for (const command of ["discuss", "discover", "track-work", "execute-work", "execute-plan"]) {
131
- assert.ok(commandNames.includes(command));
132
- }
197
+ assert.ok(!commandNames.includes("output-router"));
198
+ for (const command of ["discuss", "track-work", "execute-work"]) assert.ok(commandNames.includes(command));
199
+ assert.ok(!commandNames.includes("discover"));
200
+ assert.ok(!commandNames.includes("execute-plan"));
133
201
  assert.ok(!commandNames.includes("workflow"));
134
- assert.ok(toolNames.includes("freeflow_status"));
135
- assert.ok(toolNames.includes("freeflow_search"));
136
- assert.ok(toolNames.includes("freeflow_run"));
137
- assert.ok(toolNames.includes("freeflow_batch"));
138
- assert.ok(!toolNames.includes("freeflow_retrieve"));
139
- assert.ok(!toolNames.includes("freeflow_search action=transform"));
140
- assert.ok(!toolNames.includes("freeflow_capture"));
202
+ assert.deepEqual(
203
+ shortcuts.map(({ shortcut }) => shortcut),
204
+ ["ctrl+shift+r", "ctrl+shift+a"],
205
+ );
206
+ assert.ok(tools.some((tool) => tool.name === "freeflow_context"));
207
+ assert.ok(tools.some((tool) => tool.name === "freeflow_switch_profile"));
208
+ assert.ok(freeflowCommand);
209
+ assert.ok(!freeflowCommand.definition.getArgumentCompletions("").some((item) => item.value === "mode"));
210
+ assert.deepEqual(freeflowCommand.definition.getArgumentCompletions("mode "), []);
211
+ assert.ok(
212
+ !toolNames.some((name) => ["freeflow_status", "freeflow_search", "freeflow_run", "freeflow_batch"].includes(name)),
213
+ );
214
+ });
215
+
216
+ test("unsupported Pi hosts keep Cognitive Routing unavailable while exposing its configuration", async () => {
217
+ const cwd = await configuredRepo({
218
+ cognitiveRouting: {
219
+ enabled: true,
220
+ profiles: {
221
+ standard: { provider: "test", model: "standard", thinkingLevel: "high" },
222
+ reasoning: { provider: "test", model: "reasoning", thinkingLevel: "max" },
223
+ },
224
+ },
225
+ });
226
+ try {
227
+ const { commands, handlers, shortcuts, tools } = loadExtension(freeflowExtension, null);
228
+ const freeflowCommand = commands.find((command) => command.name === "freeflow");
229
+ assert.ok(freeflowCommand);
230
+ assert.deepEqual(shortcuts, []);
231
+ assert.ok(!tools.some((tool) => tool.name === "freeflow_switch_profile"));
232
+ assert.ok(!freeflowCommand.definition.getArgumentCompletions("").some((item) => item.value === "profile"));
233
+ const capabilityState = await readCapabilityState(cwd, undefined, undefined);
234
+ assert.equal(capabilityState.cognitiveRouting.enabled, true);
235
+ assert.equal(capabilityState.cognitiveRouting.effective, false);
236
+ assert.equal(capabilityState.cognitiveRouting.blockingReason.code, "host_unsupported");
237
+
238
+ const ctx = context(cwd);
239
+ await handlers.get("session_start")({ type: "session_start" }, ctx);
240
+ assert.match(ctx.statuses.at(-1).value, /cognitive blocked · host_unsupported/);
241
+ const before = await handlers.get("before_agent_start")({ systemPrompt: "base" }, ctx);
242
+ assert.doesNotMatch(before.systemPrompt, /Cognitive Routing/);
243
+ } finally {
244
+ await rm(cwd, { recursive: true, force: true });
245
+ }
246
+ });
247
+
248
+ test("normal Pi settings expose active Cognitive Routing configuration", async () => {
249
+ const cwd = await configuredRepo({
250
+ cognitiveRouting: {
251
+ enabled: true,
252
+ profiles: {
253
+ standard: { provider: "test", model: "model-a", thinkingLevel: "low" },
254
+ reasoning: { provider: "test", model: "model-b", thinkingLevel: "high" },
255
+ },
256
+ },
257
+ });
258
+ try {
259
+ const { commands } = loadExtension(freeflowExtension, null, {
260
+ appendEntry() {},
261
+ async setModel() {
262
+ return true;
263
+ },
264
+ setThinkingLevel() {},
265
+ });
266
+ const freeflowCommand = commands.find((command) => command.name === "freeflow");
267
+ assert.ok(freeflowCommand);
268
+ const settingsCtx = context(cwd);
269
+ settingsCtx.mode = "tui";
270
+ settingsCtx.hasUI = true;
271
+ settingsCtx.isIdle = () => true;
272
+ settingsCtx.modelRegistry = cognitiveRoutingModelRegistry();
273
+ settingsCtx.ui.custom = async (factory) => {
274
+ const component = factory({ requestRender() {} }, testTheme, {}, () => {});
275
+ const rootText = renderText(component);
276
+ assert.match(rootText, /Cognitive Routing\s+enabled \(5\) active/);
277
+ assert.doesNotMatch(rootText, /PiFlow only/);
278
+ return undefined;
279
+ };
280
+
281
+ await freeflowCommand.definition.handler("settings", settingsCtx);
282
+ } finally {
283
+ await rm(cwd, { recursive: true, force: true });
284
+ }
285
+ });
286
+
287
+ test("Pi statusline reports only dynamic Cognitive Routing and context state", () => {
288
+ const ctx = context();
289
+ setFreeflowStatus(
290
+ ctx,
291
+ {
292
+ configured: true,
293
+ enabled: true,
294
+ configSources: { enabled: "builtin" },
295
+ contextVirtualization: { effective: true },
296
+ conversationHistory: { effective: false },
297
+ cognitiveRouting: { enabled: true, effective: true, blockingReason: null },
298
+ },
299
+ { effective: true, activeProfile: "reasoning", controlMode: "automatic" },
300
+ readyFreeflowContext,
301
+ );
302
+ assert.equal(ctx.statuses.at(-1).value, "freeflow: reasoning · automatic · context");
303
+ assert.doesNotMatch(ctx.statuses.at(-1).value, /interaction|workflow|mode|skills/i);
304
+ });
305
+
306
+ test("Pi statusline shows the configured Cognitive Routing profile while activation is pending", () => {
307
+ const ctx = context();
308
+ setFreeflowStatus(
309
+ ctx,
310
+ {
311
+ configured: true,
312
+ enabled: true,
313
+ configSources: { enabled: "builtin" },
314
+ contextVirtualization: { effective: false },
315
+ conversationHistory: { effective: false },
316
+ cognitiveRouting: {
317
+ enabled: true,
318
+ effective: true,
319
+ sessionStart: { profile: "reasoning" },
320
+ blockingReason: null,
321
+ },
322
+ },
323
+ undefined,
324
+ readyFreeflowContext,
325
+ { cognitiveRoutingStartupPending: true },
326
+ );
327
+ assert.equal(ctx.statuses.at(-1).value, "freeflow: reasoning · pending");
328
+ });
329
+
330
+ test("Pi statusline keeps an inactive Cognitive Routing runtime blocked", () => {
331
+ const ctx = context();
332
+ setFreeflowStatus(
333
+ ctx,
334
+ {
335
+ configured: true,
336
+ enabled: true,
337
+ configSources: { enabled: "builtin" },
338
+ contextVirtualization: { effective: false },
339
+ conversationHistory: { effective: false },
340
+ cognitiveRouting: { enabled: true, effective: true, blockingReason: null },
341
+ },
342
+ { effective: false, activeProfile: "standard", controlMode: "automatic" },
343
+ readyFreeflowContext,
344
+ { cognitiveRoutingStartupPending: true },
345
+ );
346
+ assert.equal(ctx.statuses.at(-1).value, "freeflow: cognitive blocked · runtime_inactive");
347
+ });
348
+
349
+ test("PiFlow empty configured sessions show a pending profile before first prompt", async () => {
350
+ const cwd = await configuredRepo({
351
+ cognitiveRouting: {
352
+ enabled: true,
353
+ profiles: {
354
+ standard: { provider: "test", model: "model-a", thinkingLevel: "low" },
355
+ reasoning: { provider: "test", model: "model-b", thinkingLevel: "high" },
356
+ },
357
+ },
358
+ });
359
+ try {
360
+ const { handlers } = loadExtension();
361
+ const ctx = context(cwd);
362
+ ctx.modelRegistry = cognitiveRoutingModelRegistry();
363
+ await handlers.get("session_start")({ type: "session_start", reason: "startup" }, ctx);
364
+ assert.equal(ctx.statuses.at(-1).value, "freeflow: reasoning · automatic");
365
+ } finally {
366
+ await rm(cwd, { recursive: true, force: true });
367
+ }
141
368
  });
142
369
 
143
370
  test("Pi exposes bypass scope argument completions", () => {
144
371
  const { commands } = loadExtension();
145
372
  const bypassCommand = commands.find((command) => command.name === "bypass");
146
373
  assert.ok(bypassCommand);
147
-
148
374
  assert.deepEqual(bypassCommand.definition.getArgumentCompletions(""), [
149
375
  { value: "next", label: "next", description: "Skip one optional step" },
150
376
  { value: "task", label: "task", description: "Reduce optional pressure for the current task" },
151
377
  ]);
152
- assert.deepEqual(bypassCommand.definition.getArgumentCompletions("t"), [
153
- { value: "task", label: "task", description: "Reduce optional pressure for the current task" },
154
- ]);
155
- assert.deepEqual(bypassCommand.definition.getArgumentCompletions("unknown"), []);
156
378
  });
157
379
 
158
- test("Pi describes Freeflow and Output Router argument completions", () => {
380
+ test("Pi describes the mode-free Freeflow argument surface", () => {
159
381
  const { commands } = loadExtension();
160
382
  const freeflowCommand = commands.find((command) => command.name === "freeflow");
161
- const outputRouterCommand = commands.find((command) => command.name === "output-router");
162
383
  assert.ok(freeflowCommand);
163
- assert.ok(outputRouterCommand);
164
-
165
384
  assert.deepEqual(freeflowCommand.definition.getArgumentCompletions(""), [
166
385
  { value: "settings", label: "settings", description: "Open personal override settings" },
167
386
  { value: "status", label: "status", description: "Show effective Freeflow state" },
168
- { value: "mode", label: "mode", description: "Select a temporary session mode" },
387
+ { value: "context", label: "context", description: "Inspect Freeflow Context" },
388
+ { value: "profile", label: "profile", description: "Hold or release Cognitive Routing profile control" },
169
389
  { value: "enable", label: "enable", description: "Enable Freeflow for this repository" },
170
390
  { value: "disable", label: "disable", description: "Disable Freeflow for this repository" },
171
391
  ]);
172
- assert.deepEqual(freeflowCommand.definition.getArgumentCompletions("settings "), [
173
- { value: "settings session", label: "session", description: "Override Freeflow for this Pi session" },
174
- { value: "settings local", label: "local", description: "Edit personal overrides for this repository" },
175
- { value: "settings repo", label: "repo", description: "Edit shared repository settings" },
176
- ]);
177
- assert.deepEqual(freeflowCommand.definition.getArgumentCompletions("mode "), [
178
- { value: "mode conversation", label: "conversation", description: "Read-only discussion and inspection" },
179
- { value: "mode workflow", label: "workflow", description: "Adaptive workflow for consequential work" },
180
- {
181
- value: "mode strict-workflow",
182
- label: "strict-workflow",
183
- description: "Stronger pressure at high-risk boundaries",
184
- },
185
- {
186
- value: "mode reset",
187
- label: "reset",
188
- description: "Clear the session override and use the configured default",
189
- },
190
- ]);
191
- assert.deepEqual(outputRouterCommand.definition.getArgumentCompletions(""), [
192
- { value: "settings", label: "settings", description: "Open repository Output Router settings" },
193
- { value: "status", label: "status", description: "Show effective Output Router state" },
194
- { value: "enable", label: "enable", description: "Enable Output Router for this repository" },
195
- { value: "disable", label: "disable", description: "Disable Output Router for this repository" },
392
+ assert.deepEqual(freeflowCommand.definition.getArgumentCompletions("mode "), []);
393
+ assert.deepEqual(freeflowCommand.definition.getArgumentCompletions("context "), [
394
+ { value: "context status", label: "status", description: "Show Freeflow Context state" },
395
+ { value: "context list", label: "list", description: "List archived context projections" },
396
+ { value: "context restore", label: "restore", description: "Restore one or more context references" },
397
+ { value: "context reset all", label: "reset all", description: "Reset projection decisions on the active branch" },
196
398
  ]);
197
399
  });
198
400
 
199
- test("Pi exposes canonical model skills and maps published command aliases", async () => {
200
- const cwd = await mkdtemp(join(tmpdir(), "freeflow-pi-canonical-skills-"));
401
+ test("Pi exposes 25 base skills without a mode skill or compatibility aliases", async () => {
402
+ const cwd = await configuredRepo();
201
403
  try {
202
- await mkdir(join(cwd, ".freeflow"));
203
- await writeFile(join(cwd, ".freeflow/config.json"), JSON.stringify({ defaultMode: "workflow" }, null, 2), "utf8");
204
-
205
- const { handlers, commands, sentMessages } = loadExtension();
404
+ const { handlers, commands, sentMessages, sentMessageOptions } = loadExtension();
206
405
  const resources = await handlers.get("resources_discover")({ cwd }, context(cwd));
207
406
  const skillNames = resources.skillPaths.map((path) => {
208
- const match = path.match(/[\\/]skills[\\/]([^\\/]+)[\\/]SKILL\.md$/);
407
+ const match = path.match(/[\\/]skills[\\/]([^\\/]+)\/SKILL\.md$/);
209
408
  assert.ok(match, `unexpected skill path: ${path}`);
210
409
  return match[1];
211
410
  });
212
411
  assert.equal(skillNames.length, 25);
213
- for (const skill of ["discuss", "execute-work", "track-work"]) {
214
- assert.ok(skillNames.includes(skill));
215
- }
216
- assert.ok(!skillNames.includes("output-router"));
412
+ assert.ok(skillNames.includes("workflow"));
413
+ assert.ok(!skillNames.includes("mode-contract"));
217
414
  assert.ok(!skillNames.includes("discover"));
218
415
  assert.ok(!skillNames.includes("execute-plan"));
219
- await Promise.all(resources.skillPaths.map((path) => readFile(path, "utf8")));
220
416
 
221
417
  for (const [commandName, expectedSkill] of [
418
+ ["action-selection", "action-selection"],
222
419
  ["discuss", "discuss"],
223
- ["discover", "discuss"],
224
420
  ["track-work", "track-work"],
225
421
  ["execute-work", "execute-work"],
226
- ["execute-plan", "execute-work"],
227
422
  ]) {
228
423
  const command = commands.find((candidate) => candidate.name === commandName);
229
424
  assert.ok(command);
230
425
  await command.definition.handler(undefined, context(cwd));
231
426
  assert.equal(sentMessages.at(-1), `/skill:${expectedSkill}`);
427
+ assert.deepEqual(sentMessageOptions.at(-1), { expandPromptTemplates: true });
232
428
  }
233
429
  } finally {
234
430
  await rm(cwd, { recursive: true, force: true });
235
431
  }
236
432
  });
237
433
 
238
- test("Pi keeps Freeflow inactive until setup config exists", async () => {
239
- const cwd = await mkdtemp(join(tmpdir(), "freeflow-pi-missing-setup-"));
434
+ test("Pi expands setup-freeflow when dispatching its unconfigured skill command", async () => {
435
+ const cwd = await mkdtemp(join(tmpdir(), "freeflow-pi-setup-dispatch-"));
240
436
  try {
241
- const { handlers, activeToolNames } = loadExtension();
242
- const resourcesDiscover = handlers.get("resources_discover");
243
- const beforeAgentStart = handlers.get("before_agent_start");
244
- assert.ok(resourcesDiscover);
245
- assert.ok(beforeAgentStart);
437
+ const { commands, sentMessages, sentMessageOptions } = loadExtension();
438
+ const setup = commands.find((command) => command.name === "setup-freeflow");
439
+ assert.ok(setup);
246
440
 
247
- const resources = await resourcesDiscover({ cwd }, context(cwd));
248
- assert.equal(resources.skillPaths.length, 1);
249
- assert.match(resources.skillPaths[0], /setup-freeflow\/SKILL\.md$/);
441
+ await setup.definition.handler(undefined, context(cwd));
250
442
 
251
- const result = await beforeAgentStart({ systemPrompt: "base prompt" }, context(cwd));
252
- assert.equal(result.systemPrompt, "base prompt");
253
- assert.ok(!activeToolNames().includes("freeflow_status"));
254
- assert.ok(!activeToolNames().includes("freeflow_search"));
255
- assert.ok(!activeToolNames().includes("freeflow_run"));
256
- assert.ok(!activeToolNames().includes("freeflow_batch"));
443
+ assert.equal(sentMessages.at(-1), "/skill:setup-freeflow");
444
+ assert.deepEqual(sentMessageOptions.at(-1), { expandPromptTemplates: true });
257
445
  } finally {
258
446
  await rm(cwd, { recursive: true, force: true });
259
447
  }
260
448
  });
261
449
 
262
- test("Pi treats invalid Freeflow setup config as inactive", async () => {
263
- const cwd = await mkdtemp(join(tmpdir(), "freeflow-pi-invalid-setup-"));
450
+ test("Pi keeps Freeflow inactive until repository activation exists", async () => {
451
+ const cwd = await mkdtemp(join(tmpdir(), "freeflow-pi-missing-setup-"));
264
452
  try {
265
- await mkdir(join(cwd, ".freeflow"));
266
- await writeFile(
267
- join(cwd, ".freeflow/config.json"),
268
- JSON.stringify(
269
- {
270
- defaultMode: "workflow",
271
- enabled: "false",
272
- interactionContract: "on",
273
- skills: { enabled: "no" },
274
- outputRouter: { enabled: true },
275
- },
276
- null,
277
- 2,
278
- ),
279
- "utf8",
280
- );
281
-
282
- const { handlers, commands, activeToolNames } = loadExtension();
453
+ const { handlers } = loadExtension();
283
454
  const resources = await handlers.get("resources_discover")({ cwd }, context(cwd));
284
455
  assert.equal(resources.skillPaths.length, 1);
285
456
  assert.match(resources.skillPaths[0], /setup-freeflow\/SKILL\.md$/);
286
-
287
457
  const result = await handlers.get("before_agent_start")({ systemPrompt: "base prompt" }, context(cwd));
288
458
  assert.equal(result.systemPrompt, "base prompt");
289
- assert.ok(!activeToolNames().includes("freeflow_status"));
290
- assert.ok(!activeToolNames().includes("freeflow_search"));
291
-
292
- const freeflowCommand = commands.find((command) => command.name === "freeflow");
293
- const statusCtx = context(cwd);
294
- await freeflowCommand.definition.handler("status", statusCtx);
295
- assert.match(statusCtx.notifications.at(-1).message, /invalid config/);
296
459
  } finally {
297
460
  await rm(cwd, { recursive: true, force: true });
298
461
  }
299
462
  });
300
463
 
301
- test("Pi rejects unsupported nested skills keys in repository and local config", async () => {
302
- for (const configFile of ["config.json", "local.json"]) {
303
- const cwd = await mkdtemp(join(tmpdir(), `freeflow-pi-unsupported-skills-${configFile}-`));
464
+ test("Pi rejects obsolete modes and removed core toggles in configuration", async () => {
465
+ for (const config of [{ defaultMode: "workflow" }, { interactionContract: false }, { skills: { enabled: false } }]) {
466
+ const cwd = await configuredRepo(config);
304
467
  try {
305
- await mkdir(join(cwd, ".freeflow"));
306
- if (configFile === "local.json") {
307
- await writeFile(join(cwd, ".freeflow/config.json"), JSON.stringify({ defaultMode: "workflow" }), "utf8");
308
- }
309
- await writeFile(join(cwd, `.freeflow/${configFile}`), JSON.stringify({ skills: { enable: false } }), "utf8");
310
-
311
468
  const layers = await readFreeflowConfigLayers(cwd);
312
- const invalidLayer = configFile === "config.json" ? layers.repository : layers.local;
313
- assert.equal(invalidLayer.valid, false, `${configFile} should reject unknown nested skills keys`);
314
469
  assert.equal(layers.configured, false);
315
- assert.equal(layers.parseError, "unsupported skills config key: enable");
470
+ assert.equal(layers.repository.valid, false);
471
+ assert.match(layers.parseError, /unsupported top-level config key/);
316
472
  } finally {
317
473
  await rm(cwd, { recursive: true, force: true });
318
474
  }
319
475
  }
320
476
  });
321
477
 
322
- test("Pi layers local core overrides over repository defaults with source evidence", async () => {
323
- const cwd = await mkdtemp(join(tmpdir(), "freeflow-pi-layered-core-"));
478
+ test("Pi treats invalid configuration as inactive", async () => {
479
+ const cwd = await configuredRepo({ enabled: "false" });
480
+ try {
481
+ const { handlers, commands } = loadExtension();
482
+ const resources = await handlers.get("resources_discover")({ cwd }, context(cwd));
483
+ assert.equal(resources.skillPaths.length, 1);
484
+ assert.match(resources.skillPaths[0], /setup-freeflow\/SKILL\.md$/);
485
+ const result = await handlers.get("before_agent_start")({ systemPrompt: "base prompt" }, context(cwd));
486
+ assert.equal(result.systemPrompt, "base prompt");
487
+ const freeflowCommand = commands.find((command) => command.name === "freeflow");
488
+ const statusCtx = context(cwd);
489
+ await freeflowCommand.definition.handler("status", statusCtx);
490
+ assert.match(statusCtx.notifications.at(-1).message, /invalid config/);
491
+ } finally {
492
+ await rm(cwd, { recursive: true, force: true });
493
+ }
494
+ });
495
+
496
+ test("Pi resolves only the remaining layered core values", async () => {
497
+ const cwd = await configuredRepo();
324
498
  try {
325
- await mkdir(join(cwd, ".freeflow"));
326
- await writeFile(
327
- join(cwd, ".freeflow/config.json"),
328
- JSON.stringify(
329
- {
330
- enabled: false,
331
- interactionContract: false,
332
- skills: { enabled: true },
333
- defaultMode: "workflow",
334
- },
335
- null,
336
- 2,
337
- ),
338
- "utf8",
339
- );
340
499
  await writeFile(
341
500
  join(cwd, ".freeflow/local.json"),
342
- JSON.stringify(
343
- {
344
- enabled: true,
345
- skills: { enabled: false },
346
- defaultMode: "strict-workflow",
347
- processing: { unsafeUnsandboxed: { enabled: true } },
348
- },
349
- null,
350
- 2,
351
- ),
501
+ JSON.stringify({ enabled: false, contextVirtualization: true }, null, 2),
352
502
  "utf8",
353
503
  );
354
-
355
504
  const layers = await readFreeflowConfigLayers(cwd);
356
- assert.equal(layers.configured, true);
357
- assert.equal(layers.repositoryConfigured, true);
358
505
  assert.deepEqual(layers.coreConfig, {
359
- enabled: true,
360
- interactionContract: false,
361
- skills: { enabled: false },
362
- defaultMode: "strict-workflow",
506
+ enabled: false,
507
+ contextVirtualization: true,
508
+ conversationHistory: false,
363
509
  });
364
510
  assert.deepEqual(layers.sources, {
365
511
  enabled: "local",
366
- interactionContract: "repository",
367
- skillsEnabled: "local",
368
- defaultMode: "local",
512
+ contextVirtualization: "local",
513
+ conversationHistory: "builtin",
369
514
  });
370
- assert.equal(layers.local.parsed.processing.unsafeUnsandboxed.enabled, true);
371
- const routerConfig = await readOutputRouterConfig(cwd);
372
- assert.equal(routerConfig.localConfig.processing.unsafeUnsandboxed.enabled, true);
373
-
374
- const capabilityState = await readCapabilityState(cwd);
375
- assert.equal(capabilityState.enabled, true);
376
- assert.equal(capabilityState.interactionContract.effective, false);
377
- assert.equal(capabilityState.skills.effective, false);
378
- assert.deepEqual(capabilityState.configSources, layers.sources);
379
-
380
- const modeState = await readModeState(cwd);
381
- assert.equal(modeState.repositoryDefaultMode, "workflow");
382
- assert.equal(modeState.repositoryDefaultModeSource, "repository");
383
- assert.equal(modeState.personalDefaultMode, "strict-workflow");
384
- assert.equal(modeState.defaultMode, "strict-workflow");
385
- assert.equal(modeState.defaultModeSource, "local");
386
- assert.equal(modeState.sessionMode, null);
387
- assert.equal(modeState.resolvedMode, "strict-workflow");
388
- assert.equal(modeState.active, false);
389
- assert.equal(modeState.effectiveMode, null);
390
- } finally {
391
- await rm(cwd, { recursive: true, force: true });
392
- }
393
- });
394
-
395
- test("Pi layers branch-aware session core overrides above configured values", async () => {
396
- const cwd = await mkdtemp(join(tmpdir(), "freeflow-pi-session-core-"));
397
- try {
398
- await mkdir(join(cwd, ".freeflow"));
399
- const configPath = join(cwd, ".freeflow/config.json");
400
- const configText = JSON.stringify({ defaultMode: "workflow" }, null, 2);
401
- await writeFile(configPath, configText, "utf8");
402
-
403
- const { pi, entries } = loadExtension();
404
- const ctx = context(cwd);
405
-
406
- let result = await setSessionCoreOverride("enabled", false, ctx, pi);
407
- assert.equal(result.changed, true);
408
- let capabilityState = await readCapabilityState(cwd);
409
- assert.equal(capabilityState.enabled, false);
410
- assert.equal(capabilityState.configSources.enabled, "session");
411
- assert.deepEqual(capabilityState.sessionOverrides, { enabled: false });
412
- assert.deepEqual(entries.at(-1), {
413
- customType: "freeflow-session-overrides",
414
- data: { overrides: { enabled: false } },
415
- });
416
-
417
- result = await setSessionCoreOverride("enabled", false, ctx, pi);
418
- assert.equal(result.changed, false);
419
- assert.equal(entries.length, 1);
420
-
421
- await setSessionCoreOverride("enabled", null, ctx, pi);
422
- await setSessionCoreOverride("interactionContract", false, ctx, pi);
423
- await setSessionCoreOverride("skillsEnabled", false, ctx, pi);
424
- capabilityState = await readCapabilityState(cwd);
425
- assert.equal(capabilityState.enabled, true);
426
- assert.equal(capabilityState.interactionContract.effective, false);
427
- assert.equal(capabilityState.skills.effective, false);
428
- assert.equal((await readModeState(cwd)).effectiveMode, null);
429
-
430
- await setSessionMode("conversation", ctx, pi);
431
- const reset = await resetSessionOverrides(ctx, pi);
432
- assert.equal(reset.changed, true);
433
- assert.equal(reset.reloadRequired, true);
434
- capabilityState = await readCapabilityState(cwd);
435
- assert.deepEqual(capabilityState.sessionOverrides, {});
436
- assert.equal(capabilityState.interactionContract.effective, true);
437
- assert.equal(capabilityState.skills.effective, true);
438
- assert.equal((await readModeState(cwd)).sessionMode, null);
439
- assert.equal(await readFile(configPath, "utf8"), configText);
515
+ const state = await readCapabilityState(cwd, undefined, PIFLOW_HOST);
516
+ assert.equal(state.enabled, false);
517
+ assert.equal("skills" in state, false);
518
+ assert.equal("interactionContract" in state, false);
440
519
  } finally {
441
- restoreModeOverride(context(cwd));
442
520
  await rm(cwd, { recursive: true, force: true });
443
521
  }
444
522
  });
445
523
 
446
- test("Pi session enablement can override configured off but cannot bypass activation", async () => {
524
+ test("Pi session enablement cannot bypass repository activation", async () => {
447
525
  const cwd = await mkdtemp(join(tmpdir(), "freeflow-pi-session-activation-"));
526
+ await mkdir(join(cwd, ".freeflow"));
527
+ const { pi } = loadExtension();
528
+ const ctx = context(cwd);
448
529
  try {
449
- await mkdir(join(cwd, ".freeflow"));
450
- const { pi } = loadExtension();
451
- const ctx = context(cwd);
452
530
  await setSessionCoreOverride("enabled", true, ctx, pi);
531
+ let state = await readCapabilityState(cwd, undefined, PIFLOW_HOST);
532
+ assert.equal(state.configured, false);
533
+ assert.equal(state.enabled, false);
453
534
 
454
- let capabilityState = await readCapabilityState(cwd);
455
- assert.equal(capabilityState.configured, false);
456
- assert.equal(capabilityState.enabled, false);
457
-
458
- await writeFile(join(cwd, ".freeflow/config.json"), "{ invalid", "utf8");
459
- capabilityState = await readCapabilityState(cwd);
460
- assert.equal(capabilityState.configured, false);
461
- assert.equal(capabilityState.enabled, false);
462
-
463
- await writeFile(
464
- join(cwd, ".freeflow/config.json"),
465
- JSON.stringify({ enabled: false, defaultMode: "workflow" }),
466
- "utf8",
467
- );
468
- capabilityState = await readCapabilityState(cwd);
469
- assert.equal(capabilityState.configured, true);
470
- assert.equal(capabilityState.enabled, true);
471
- assert.equal(capabilityState.configSources.enabled, "session");
535
+ await writeFile(join(cwd, ".freeflow/config.json"), JSON.stringify({ enabled: false }), "utf8");
536
+ state = await readCapabilityState(cwd, undefined, PIFLOW_HOST);
537
+ assert.equal(state.configured, true);
538
+ assert.equal(state.enabled, true);
539
+ assert.equal(state.configSources.enabled, "session");
472
540
  } finally {
473
- restoreModeOverride(context(cwd));
541
+ await resetSessionOverrides(ctx, pi);
474
542
  await rm(cwd, { recursive: true, force: true });
475
543
  }
476
544
  });
477
545
 
478
- test("Pi restores session core overrides from the active branch", async () => {
479
- const cwd = await mkdtemp(join(tmpdir(), "freeflow-pi-session-core-branch-"));
546
+ test("Pi restores remaining session overrides from the active branch and ignores old mode entries", async () => {
547
+ const cwd = await configuredRepo();
480
548
  try {
481
- await mkdir(join(cwd, ".freeflow"));
482
- await writeFile(join(cwd, ".freeflow/config.json"), JSON.stringify({ defaultMode: "workflow" }), "utf8");
483
549
  const activeBranchEntries = [
550
+ { type: "custom", customType: "freeflow-mode", data: { currentMode: "conversation" } },
484
551
  {
485
552
  type: "custom",
486
553
  customType: "freeflow-session-overrides",
487
- data: { overrides: { interactionContract: false } },
488
- },
489
- ];
490
- const allEntries = [
491
- ...activeBranchEntries,
492
- {
493
- type: "custom",
494
- customType: "freeflow-session-overrides",
495
- data: { overrides: { enabled: false } },
496
- },
497
- ];
498
- const { handlers } = loadExtension();
499
- await handlers.get("session_start")({ reason: "resume" }, context(cwd, allEntries, activeBranchEntries));
500
-
501
- let capabilityState = await readCapabilityState(cwd);
502
- assert.equal(capabilityState.enabled, true);
503
- assert.equal(capabilityState.interactionContract.effective, false);
504
- assert.deepEqual(capabilityState.sessionOverrides, { interactionContract: false });
505
-
506
- const switchedBranchEntries = [
507
- {
508
- type: "custom",
509
- customType: "freeflow-session-overrides",
510
- data: { overrides: { enabled: false } },
554
+ data: { overrides: { enabled: false, contextVirtualization: true } },
511
555
  },
512
556
  ];
513
- await handlers.get("session_tree")({}, context(cwd, allEntries, switchedBranchEntries));
514
- capabilityState = await readCapabilityState(cwd);
515
- assert.equal(capabilityState.enabled, false);
516
- assert.deepEqual(capabilityState.sessionOverrides, { enabled: false });
557
+ restoreSessionOverrides(context(cwd, activeBranchEntries, activeBranchEntries));
558
+ const state = await readCapabilityState(cwd, undefined, PIFLOW_HOST);
559
+ assert.equal(state.enabled, false);
560
+ assert.deepEqual(state.sessionOverrides, { enabled: false, contextVirtualization: true });
561
+ assert.equal("currentMode" in state, false);
517
562
  } finally {
518
- restoreModeOverride(context(cwd));
563
+ await resetSessionOverrides(context(cwd), loadExtension().pi);
519
564
  await rm(cwd, { recursive: true, force: true });
520
565
  }
521
566
  });
522
567
 
523
- test("Pi compact mode context distinguishes layered defaults, session state, and effective mode", async () => {
524
- const cwd = await mkdtemp(join(tmpdir(), "freeflow-pi-layered-mode-context-"));
568
+ test("Pi fails closed when an existing local override is invalid", async () => {
569
+ const cwd = await configuredRepo();
525
570
  try {
526
- await mkdir(join(cwd, ".freeflow"));
527
- await writeFile(join(cwd, ".freeflow/config.json"), JSON.stringify({ defaultMode: "workflow" }, null, 2), "utf8");
528
- await writeFile(
529
- join(cwd, ".freeflow/local.json"),
530
- JSON.stringify({ defaultMode: "strict-workflow" }, null, 2),
531
- "utf8",
532
- );
533
-
534
- const { handlers, commands } = loadExtension();
535
- const beforeAgentStart = handlers.get("before_agent_start");
536
- const freeflowCommand = commands.find((command) => command.name === "freeflow");
537
- assert.ok(beforeAgentStart);
538
- assert.ok(freeflowCommand);
539
-
540
- let result = await beforeAgentStart({ systemPrompt: "base prompt" }, context(cwd));
541
- assert.match(result.systemPrompt, /Repository default mode: `workflow` \(repository\)/);
542
- assert.match(result.systemPrompt, /Personal default override: `strict-workflow`/);
543
- assert.match(result.systemPrompt, /Configured default mode: `strict-workflow` \(personal override\)/);
544
- assert.match(result.systemPrompt, /Session mode override: `none`/);
545
- assert.match(result.systemPrompt, /Resolved mode: `strict-workflow`/);
546
- assert.match(result.systemPrompt, /Effective Freeflow mode: `strict-workflow`/);
547
- assert.match(result.systemPrompt, /direct skill calls do not change mode/);
548
- assert.match(result.systemPrompt, /## Strict Workflow Overlay/);
549
- assert.match(result.systemPrompt, /security, privacy, billing, data loss, migrations, public interfaces/);
550
- assert.doesNotMatch(result.systemPrompt, /## Conversation Mode Boundary/);
551
-
552
- await freeflowCommand.definition.handler("mode conversation", context(cwd));
553
- result = await beforeAgentStart({ systemPrompt: "base prompt" }, context(cwd));
554
- assert.match(result.systemPrompt, /Session mode override: `conversation`/);
555
- assert.match(result.systemPrompt, /Resolved mode: `conversation`/);
556
- assert.match(result.systemPrompt, /Effective Freeflow mode: `conversation`/);
557
- assert.match(result.systemPrompt, /## Conversation Mode Boundary/);
558
- assert.match(result.systemPrompt, /Do not call write, edit, or mutating tools/);
559
- assert.match(result.systemPrompt, /an execution skill does not override this boundary/);
560
- assert.doesNotMatch(result.systemPrompt, /## Strict Workflow Overlay/);
561
-
562
- await freeflowCommand.definition.handler("mode reset", context(cwd));
571
+ await writeFile(join(cwd, ".freeflow/local.json"), "{ invalid\n", "utf8");
572
+ const layers = await readFreeflowConfigLayers(cwd);
573
+ assert.equal(layers.repositoryConfigured, true);
574
+ assert.equal(layers.configured, false);
575
+ assert.equal(layers.local.valid, false);
576
+ const state = await readCapabilityState(cwd, undefined, PIFLOW_HOST);
577
+ assert.equal(state.configured, false);
578
+ assert.equal(state.enabled, false);
579
+ const { handlers } = loadExtension();
580
+ const resources = await handlers.get("resources_discover")({ cwd }, context(cwd));
581
+ assert.equal(resources.skillPaths.length, 1);
582
+ assert.match(resources.skillPaths[0], /setup-freeflow\/SKILL\.md$/);
563
583
  } finally {
564
584
  await rm(cwd, { recursive: true, force: true });
565
585
  }
566
586
  });
567
587
 
568
- test("Pi restores layered session mode on resume and preserves it through compaction", async () => {
569
- const cwd = await mkdtemp(join(tmpdir(), "freeflow-pi-mode-lifecycle-"));
588
+ test("Pi local enablement overrides the repository master switch", async () => {
589
+ const cwd = await configuredRepo({ enabled: true });
570
590
  try {
571
- await mkdir(join(cwd, ".freeflow"));
572
- await writeFile(join(cwd, ".freeflow/config.json"), JSON.stringify({ defaultMode: "workflow" }, null, 2), "utf8");
573
- const localPath = join(cwd, ".freeflow/local.json");
574
- await writeFile(localPath, JSON.stringify({ defaultMode: "strict-workflow" }, null, 2), "utf8");
575
-
576
- const sessionEntries = [
577
- {
578
- type: "custom",
579
- customType: "freeflow-mode",
580
- data: { currentMode: "conversation" },
581
- },
582
- ];
583
- const { handlers } = loadExtension();
584
- const sessionStart = handlers.get("session_start");
585
- const sessionCompact = handlers.get("session_compact");
586
- const beforeAgentStart = handlers.get("before_agent_start");
587
- assert.ok(sessionStart);
588
- assert.ok(sessionCompact);
589
- assert.ok(beforeAgentStart);
590
-
591
- const ctx = context(cwd, sessionEntries);
592
- await sessionStart({ reason: "resume" }, ctx);
593
- let modeState = await readModeState(cwd);
594
- assert.equal(modeState.repositoryDefaultMode, "workflow");
595
- assert.equal(modeState.personalDefaultMode, "strict-workflow");
596
- assert.equal(modeState.sessionMode, "conversation");
597
- assert.equal(modeState.resolvedMode, "conversation");
598
- assert.equal(modeState.effectiveMode, "conversation");
599
-
600
- await sessionCompact({ reason: "manual" }, ctx);
601
- let result = await beforeAgentStart({ prompt: "continue", systemPrompt: "base prompt" }, ctx);
602
- assert.match(result.systemPrompt, /Session mode override: `conversation`/);
603
- assert.match(result.systemPrompt, /Resolved mode: `conversation`/);
604
- assert.match(result.systemPrompt, /Effective Freeflow mode: `conversation`/);
605
-
606
- await writeFile(
607
- localPath,
608
- JSON.stringify(
609
- {
610
- defaultMode: "strict-workflow",
611
- skills: { enabled: false },
612
- },
613
- null,
614
- 2,
615
- ),
616
- "utf8",
617
- );
618
- await sessionCompact({ reason: "manual" }, ctx);
619
- modeState = await readModeState(cwd);
620
- assert.equal(modeState.sessionMode, "conversation");
621
- assert.equal(modeState.resolvedMode, "conversation");
622
- assert.equal(modeState.active, false);
623
- assert.equal(modeState.effectiveMode, null);
624
- result = await beforeAgentStart({ prompt: "continue", systemPrompt: "base prompt" }, ctx);
625
- assert.match(result.systemPrompt, /Session mode override: `conversation`/);
626
- assert.match(result.systemPrompt, /Resolved mode: `conversation` \(inactive because Skills are disabled\)/);
627
- assert.match(result.systemPrompt, /Effective Freeflow mode: `none`/);
628
-
629
- sessionEntries.push({
630
- type: "custom",
631
- customType: "freeflow-mode",
632
- data: { currentMode: null },
633
- });
634
- await sessionStart({ reason: "resume" }, ctx);
635
- modeState = await readModeState(cwd);
636
- assert.equal(modeState.sessionMode, null);
637
- assert.equal(modeState.resolvedMode, "strict-workflow");
638
- assert.equal(modeState.active, false);
639
- assert.equal(modeState.effectiveMode, null);
591
+ await writeFile(join(cwd, ".freeflow/local.json"), JSON.stringify({ enabled: false }, null, 2), "utf8");
592
+ const state = await readCapabilityState(cwd, undefined, PIFLOW_HOST);
593
+ assert.equal(state.configured, true);
594
+ assert.equal(state.enabled, false);
595
+ assert.equal(state.configSources.enabled, "local");
640
596
  } finally {
641
597
  await rm(cwd, { recursive: true, force: true });
642
598
  }
643
599
  });
644
600
 
645
- test("Pi restores session mode from the active branch on resume and tree navigation", async () => {
646
- const cwd = await mkdtemp(join(tmpdir(), "freeflow-pi-mode-active-branch-"));
601
+ test("Pi master Freeflow toggle suppresses core prompts, skills, and capabilities", async () => {
602
+ const cwd = await configuredRepo({ enabled: false });
647
603
  try {
648
- await mkdir(join(cwd, ".freeflow"));
649
- await writeFile(join(cwd, ".freeflow/config.json"), JSON.stringify({ defaultMode: "workflow" }, null, 2), "utf8");
650
-
651
- const activeBranchEntries = [
652
- {
653
- type: "custom",
654
- customType: "freeflow-mode",
655
- data: { currentMode: "conversation" },
656
- },
657
- ];
658
- const allSessionEntries = [
659
- ...activeBranchEntries,
660
- {
661
- type: "custom",
662
- customType: "freeflow-mode",
663
- data: { currentMode: "strict-workflow" },
664
- },
665
- ];
666
- const { handlers } = loadExtension();
667
- const sessionStart = handlers.get("session_start");
668
- const sessionTree = handlers.get("session_tree");
669
- assert.ok(sessionStart);
670
- assert.ok(sessionTree);
671
-
672
- const ctx = context(cwd, allSessionEntries, activeBranchEntries);
673
- await sessionStart({ reason: "resume" }, ctx);
674
-
675
- let modeState = await readModeState(cwd);
676
- assert.equal(modeState.sessionMode, "conversation");
677
- assert.equal(modeState.effectiveMode, "conversation");
678
-
679
- activeBranchEntries.splice(0, activeBranchEntries.length, allSessionEntries.at(-1));
680
- await sessionTree({ newLeafId: "strict-branch" }, ctx);
681
-
682
- modeState = await readModeState(cwd);
683
- assert.equal(modeState.sessionMode, "strict-workflow");
684
- assert.equal(modeState.effectiveMode, "strict-workflow");
604
+ const { handlers, activeToolNames } = loadExtension();
605
+ const ctx = context(cwd);
606
+ const resources = await handlers.get("resources_discover")({ cwd }, ctx);
607
+ assert.deepEqual(resources.skillPaths, []);
608
+ const result = await handlers.get("before_agent_start")({ systemPrompt: "base prompt" }, ctx);
609
+ assert.equal(result.systemPrompt, "base prompt");
610
+ const providerContext = await handlers.get("context")({ messages: [] }, ctx);
611
+ assert.match(lastRuntimeState(providerContext.messages).content, /Freeflow: inactive/);
612
+ assert.doesNotMatch(
613
+ lastRuntimeState(providerContext.messages).content,
614
+ /Default mode|Active mode|Interaction Contract|Skills/,
615
+ );
616
+ assert.ok(!activeToolNames().includes("freeflow_context"));
685
617
  } finally {
686
- restoreModeOverride(context(cwd));
687
618
  await rm(cwd, { recursive: true, force: true });
688
619
  }
689
620
  });
690
621
 
691
- test("Pi ordinary prompts and direct skill calls do not change mode state", async () => {
692
- const cwd = await mkdtemp(join(tmpdir(), "freeflow-pi-mode-boundaries-"));
622
+ test("Pi settings expose no mode, Skills, or Interaction Contract controls", async () => {
623
+ const cwd = await configuredRepo({ enabled: false });
693
624
  try {
694
- await mkdir(join(cwd, ".freeflow"));
695
- await writeFile(join(cwd, ".freeflow/config.json"), JSON.stringify({ defaultMode: "workflow" }, null, 2), "utf8");
696
- await writeFile(
697
- join(cwd, ".freeflow/local.json"),
698
- JSON.stringify({ defaultMode: "strict-workflow" }, null, 2),
699
- "utf8",
700
- );
701
-
702
- const { handlers, commands, entries, sentMessages } = loadExtension();
703
- const sessionStart = handlers.get("session_start");
704
- const beforeAgentStart = handlers.get("before_agent_start");
625
+ const { commands } = loadExtension();
705
626
  const freeflowCommand = commands.find((command) => command.name === "freeflow");
706
- const commitWorkCommand = commands.find((command) => command.name === "commit-work");
707
- assert.ok(sessionStart);
708
- assert.ok(beforeAgentStart);
709
627
  assert.ok(freeflowCommand);
710
- assert.ok(commitWorkCommand);
711
-
712
- await sessionStart({ reason: "startup" }, context(cwd));
713
- let result = await beforeAgentStart(
714
- {
715
- prompt: "Switch to conversation mode and implement this change.",
716
- systemPrompt: "base prompt",
717
- },
718
- context(cwd),
719
- );
720
- let modeState = await readModeState(cwd);
721
- assert.equal(modeState.sessionMode, null);
722
- assert.equal(modeState.resolvedMode, "strict-workflow");
723
- assert.equal(modeState.effectiveMode, "strict-workflow");
724
- assert.match(result.systemPrompt, /Task type, risk classification, and direct skill calls do not change mode/);
725
- assert.deepEqual(entries, []);
726
-
727
- await freeflowCommand.definition.handler("mode conversation", context(cwd));
728
- assert.equal(entries.length, 1);
729
- await commitWorkCommand.definition.handler("create a checkpoint", context(cwd));
730
- assert.deepEqual(sentMessages, ["/skill:commit-work\n\ncreate a checkpoint"]);
731
- assert.equal(entries.length, 1);
732
-
733
- modeState = await readModeState(cwd);
734
- assert.equal(modeState.sessionMode, "conversation");
735
- assert.equal(modeState.resolvedMode, "conversation");
736
- assert.equal(modeState.effectiveMode, "conversation");
737
- result = await beforeAgentStart({ prompt: "/commit-work", systemPrompt: "base prompt" }, context(cwd));
738
- assert.match(result.systemPrompt, /Session mode override: `conversation`/);
739
- assert.match(result.systemPrompt, /Effective Freeflow mode: `conversation`/);
628
+ const ctx = context(cwd);
629
+ ctx.ui.custom = async (factory) => {
630
+ let result;
631
+ const component = factory({ requestRender() {} }, testTheme, {}, (value) => {
632
+ result = value;
633
+ });
634
+ const text = renderText(component);
635
+ assert.match(text, /Freeflow Settings/);
636
+ assert.match(text, /Freeflow Context/);
637
+ assert.doesNotMatch(text, /Interaction Contract|Skills|Session mode|Default mode|Mode/);
638
+ component.handleInput("\u001b");
639
+ return result;
640
+ };
641
+ await freeflowCommand.definition.handler("settings", ctx);
642
+ assert.equal(ctx.reloads.length, 0);
740
643
  } finally {
741
- restoreModeOverride(context(cwd));
742
644
  await rm(cwd, { recursive: true, force: true });
743
645
  }
744
646
  });
745
647
 
746
- test("Pi layered core config inherits omitted values and built-in defaults", async () => {
747
- const cwd = await mkdtemp(join(tmpdir(), "freeflow-pi-layered-inherit-"));
648
+ test("Pi enable and disable commands mutate only the master switch", async () => {
649
+ const cwd = await configuredRepo();
748
650
  try {
749
- await mkdir(join(cwd, ".freeflow"));
750
- await writeFile(join(cwd, ".freeflow/config.json"), JSON.stringify({}, null, 2), "utf8");
751
- await writeFile(join(cwd, ".freeflow/local.json"), JSON.stringify({ interactionContract: false }, null, 2), "utf8");
651
+ const { commands } = loadExtension();
652
+ const freeflowCommand = commands.find((command) => command.name === "freeflow");
653
+ assert.ok(freeflowCommand);
654
+ const disableCtx = context(cwd);
655
+ await freeflowCommand.definition.handler("disable", disableCtx);
656
+ assert.deepEqual(JSON.parse(await readFile(join(cwd, ".freeflow/config.json"), "utf8")), { enabled: false });
657
+ assert.equal(disableCtx.statuses.at(-1).value, "freeflow: off");
658
+ assert.equal(disableCtx.reloads.length, 1);
752
659
 
753
- const layers = await readFreeflowConfigLayers(cwd);
754
- assert.deepEqual(layers.coreConfig, {
755
- enabled: true,
756
- interactionContract: false,
757
- skills: { enabled: true },
758
- defaultMode: "workflow",
759
- });
760
- assert.deepEqual(layers.sources, {
761
- enabled: "builtin",
762
- interactionContract: "local",
763
- skillsEnabled: "builtin",
764
- defaultMode: "builtin",
765
- });
660
+ const enableCtx = context(cwd);
661
+ await freeflowCommand.definition.handler("enable", enableCtx);
662
+ assert.deepEqual(JSON.parse(await readFile(join(cwd, ".freeflow/config.json"), "utf8")), {});
663
+ assert.equal(enableCtx.reloads.length, 1);
766
664
  } finally {
767
665
  await rm(cwd, { recursive: true, force: true });
768
666
  }
769
667
  });
770
668
 
771
- test("Pi requires repository activation even when local overrides exist", async () => {
772
- const cwd = await mkdtemp(join(tmpdir(), "freeflow-pi-local-only-"));
669
+ test("Pi statusline uses one umbrella context label for either enabled context feature", async () => {
670
+ const cwd = await configuredRepo({ contextVirtualization: true, conversationHistory: true });
773
671
  try {
774
- await mkdir(join(cwd, ".freeflow"));
775
- await writeFile(
776
- join(cwd, ".freeflow/local.json"),
777
- JSON.stringify({ enabled: true, defaultMode: "strict-workflow" }, null, 2),
778
- "utf8",
779
- );
780
-
781
- const layers = await readFreeflowConfigLayers(cwd);
782
- assert.equal(layers.repositoryConfigured, false);
783
- assert.equal(layers.configured, false);
784
- const capabilityState = await readCapabilityState(cwd);
785
- assert.equal(capabilityState.enabled, false);
786
- assert.equal(capabilityState.skills.effective, false);
787
-
788
672
  const { handlers } = loadExtension();
789
- const resources = await handlers.get("resources_discover")({ cwd }, context(cwd));
790
- assert.equal(resources.skillPaths.length, 1);
791
- assert.match(resources.skillPaths[0], /setup-freeflow\/SKILL\.md$/);
792
- } finally {
793
- await rm(cwd, { recursive: true, force: true });
794
- }
795
- });
796
-
797
- test("Pi fails closed when an existing local override is invalid", async () => {
798
- const cwd = await mkdtemp(join(tmpdir(), "freeflow-pi-invalid-local-"));
799
- try {
800
- await mkdir(join(cwd, ".freeflow"));
801
- await writeFile(join(cwd, ".freeflow/config.json"), JSON.stringify({ defaultMode: "workflow" }, null, 2), "utf8");
802
- await writeFile(join(cwd, ".freeflow/local.json"), "{ invalid\n", "utf8");
803
-
804
- const layers = await readFreeflowConfigLayers(cwd);
805
- assert.equal(layers.repositoryConfigured, true);
806
- assert.equal(layers.configured, false);
807
- assert.equal(layers.local.exists, true);
808
- assert.equal(layers.local.valid, false);
809
- assert.equal(layers.blockingConfigPath, layers.local.path);
810
- assert.match(layers.parseError, /JSON/);
811
-
812
- const capabilityState = await readCapabilityState(cwd);
813
- assert.equal(capabilityState.configured, false);
814
- assert.equal(capabilityState.enabled, false);
815
- assert.equal(capabilityState.interactionContract.effective, false);
816
- assert.equal(capabilityState.skills.effective, false);
817
- assert.equal(capabilityState.localConfigValid, false);
818
- assert.match(capabilityState.localConfigParseError, /JSON/);
819
-
820
- const { handlers, activeToolNames } = loadExtension();
821
- const resources = await handlers.get("resources_discover")({ cwd }, context(cwd));
822
- assert.equal(resources.skillPaths.length, 1);
823
- assert.match(resources.skillPaths[0], /setup-freeflow\/SKILL\.md$/);
824
- const result = await handlers.get("before_agent_start")({ systemPrompt: "base prompt" }, context(cwd));
825
- assert.equal(result.systemPrompt, "base prompt");
826
- assert.ok(!activeToolNames().includes("freeflow_status"));
827
- } finally {
828
- await rm(cwd, { recursive: true, force: true });
829
- }
830
- });
831
-
832
- test("Pi fails closed when a local core override has an invalid type", async () => {
833
- const cwd = await mkdtemp(join(tmpdir(), "freeflow-pi-invalid-local-shape-"));
834
- try {
835
- await mkdir(join(cwd, ".freeflow"));
836
- await writeFile(join(cwd, ".freeflow/config.json"), JSON.stringify({ defaultMode: "workflow" }, null, 2), "utf8");
837
- await writeFile(join(cwd, ".freeflow/local.json"), JSON.stringify({ skills: { enabled: "no" } }, null, 2), "utf8");
838
-
839
- const layers = await readFreeflowConfigLayers(cwd);
840
- assert.equal(layers.configured, false);
841
- assert.equal(layers.local.valid, false);
842
- assert.equal(layers.parseError, "skills.enabled must be a boolean");
843
- const state = await readCapabilityState(cwd);
844
- assert.equal(state.enabled, false);
845
- assert.equal(state.skills.effective, false);
846
- } finally {
847
- await rm(cwd, { recursive: true, force: true });
848
- }
849
- });
850
-
851
- test("Pi local enabled false overrides an enabled repository master switch", async () => {
852
- const cwd = await mkdtemp(join(tmpdir(), "freeflow-pi-local-disabled-"));
853
- try {
854
- await mkdir(join(cwd, ".freeflow"));
855
- await writeFile(
856
- join(cwd, ".freeflow/config.json"),
857
- JSON.stringify(
858
- {
859
- enabled: true,
860
- defaultMode: "workflow",
861
- outputRouter: { enabled: true },
862
- },
863
- null,
864
- 2,
865
- ),
866
- "utf8",
867
- );
868
- await writeFile(join(cwd, ".freeflow/local.json"), JSON.stringify({ enabled: false }, null, 2), "utf8");
869
-
870
- const state = await readCapabilityState(cwd);
871
- assert.equal(state.configured, true);
872
- assert.equal(state.enabled, false);
873
- assert.equal(state.configSources.enabled, "local");
874
- assert.equal(state.outputRouter.enabled, false);
875
- } finally {
876
- await rm(cwd, { recursive: true, force: true });
877
- }
878
- });
879
-
880
- test("Pi master Freeflow toggle disables skills, capabilities, and routing", async () => {
881
- const cwd = await mkdtemp(join(tmpdir(), "freeflow-pi-master-disabled-"));
882
- try {
883
- await mkdir(join(cwd, ".freeflow"));
884
- await writeFile(
885
- join(cwd, ".freeflow/config.json"),
886
- JSON.stringify(
887
- {
888
- enabled: false,
889
- defaultMode: "workflow",
890
- skills: { enabled: true },
891
- outputRouter: { enabled: true, postToolRouting: "safety-net" },
892
- },
893
- null,
894
- 2,
895
- ),
896
- "utf8",
897
- );
898
-
899
- const { handlers, activeToolNames } = loadExtension();
900
- const resources = await handlers.get("resources_discover")({ cwd }, context(cwd));
901
- assert.deepEqual(resources.skillPaths, []);
902
-
903
- const result = await handlers.get("before_agent_start")({ systemPrompt: "base prompt" }, context(cwd));
904
- assert.match(result.systemPrompt, /# Freeflow Disabled/);
905
- assert.doesNotMatch(result.systemPrompt, /# Freeflow Interaction Contract/);
906
- assert.doesNotMatch(result.systemPrompt, /# Freeflow Runtime Kernel/);
907
- assert.doesNotMatch(result.systemPrompt, /## Loaded Workflow Skill/);
908
- assert.doesNotMatch(result.systemPrompt, /## Loaded Output Router Skill/);
909
- assert.ok(!activeToolNames().includes("freeflow_status"));
910
- assert.ok(!activeToolNames().includes("freeflow_search"));
911
-
912
- const routed = await handlers.get("tool_result")(
913
- {
914
- toolName: "read",
915
- input: { path: "large.txt" },
916
- content: [{ type: "text", text: "line 1\nline 2" }],
917
- isError: false,
918
- },
919
- context(cwd),
920
- );
921
- assert.equal(routed, undefined);
922
- } finally {
923
- await rm(cwd, { recursive: true, force: true });
924
- }
925
- });
926
-
927
- test("Pi skills toggle suppresses workflow skills while allowing enabled router tools", async () => {
928
- const cwd = await mkdtemp(join(tmpdir(), "freeflow-pi-skills-disabled-router-on-"));
929
- try {
930
- await mkdir(join(cwd, ".freeflow"));
931
- await writeFile(
932
- join(cwd, ".freeflow/config.json"),
933
- JSON.stringify(
934
- {
935
- defaultMode: "workflow",
936
- skills: { enabled: false },
937
- outputRouter: { enabled: true },
938
- },
939
- null,
940
- 2,
941
- ),
942
- "utf8",
943
- );
944
-
945
- const { handlers, activeToolNames } = loadExtension();
946
- const resources = await handlers.get("resources_discover")({ cwd }, context(cwd));
947
- assert.deepEqual(resources.skillPaths, []);
948
-
949
673
  const ctx = context(cwd);
950
- const result = await handlers.get("before_agent_start")({ systemPrompt: "base prompt" }, ctx);
951
- assert.equal(result.message, undefined);
952
- assert.match(result.systemPrompt, /# Freeflow Interaction Contract/);
953
- assert.doesNotMatch(result.systemPrompt, /# Freeflow Runtime Kernel/);
954
- assert.doesNotMatch(result.systemPrompt, /## Loaded Workflow Skill/);
955
- assert.doesNotMatch(result.systemPrompt, /## Loaded Decision Gate Skill/);
956
- assert.doesNotMatch(result.systemPrompt, /## Discovery-light/);
957
- assert.match(result.systemPrompt, /Skills: disabled/);
958
- assert.match(result.systemPrompt, /Repository default mode: `workflow` \(repository\)/);
959
- assert.match(result.systemPrompt, /Personal default override: `none`/);
960
- assert.match(result.systemPrompt, /Configured default mode: `workflow` \(repository default\)/);
961
- assert.match(result.systemPrompt, /Resolved mode: `workflow` \(inactive because Skills are disabled\)/);
962
- assert.match(result.systemPrompt, /Effective Freeflow mode: `none`/);
963
- assert.match(result.systemPrompt, /## Loaded Output Router Skill/);
964
- assert.equal(ctx.statuses.at(-1).value, "freeflow: interaction · router");
965
- assert.ok(activeToolNames().includes("freeflow_status"));
966
- assert.ok(activeToolNames().includes("freeflow_search"));
967
- assert.ok(activeToolNames().includes("freeflow_run"));
968
- assert.ok(activeToolNames().includes("freeflow_batch"));
674
+ await handlers.get("session_start")({ reason: "startup" }, ctx);
675
+ assert.equal(ctx.statuses.at(-1).value, "freeflow: context");
969
676
  } finally {
970
677
  await rm(cwd, { recursive: true, force: true });
971
678
  }
972
679
  });
973
680
 
974
- test("Pi interaction contract can be disabled independently while Workflow stays enabled", async () => {
975
- const cwd = await mkdtemp(join(tmpdir(), "freeflow-pi-interaction-disabled-skills-on-"));
681
+ test("Pi settings refuse to overwrite an invalid local override", async () => {
682
+ const cwd = await configuredRepo();
976
683
  try {
977
- await mkdir(join(cwd, ".freeflow"));
978
- await writeFile(
979
- join(cwd, ".freeflow/config.json"),
980
- JSON.stringify({ defaultMode: "workflow", interactionContract: false }, null, 2),
981
- "utf8",
982
- );
983
- const { handlers } = loadExtension();
984
- const result = await handlers.get("before_agent_start")(
985
- { prompt: "hello", systemPrompt: "base prompt" },
986
- context(cwd),
987
- );
988
- assert.doesNotMatch(result.systemPrompt, /# Freeflow Interaction Contract/);
989
- assert.doesNotMatch(result.systemPrompt, /# Freeflow Runtime Kernel/);
990
- assert.equal(result.message.customType, "freeflow-workflow-bootstrap");
991
- assert.match(result.message.content, /# Workflow/);
684
+ const localPath = join(cwd, ".freeflow/local.json");
685
+ await writeFile(localPath, "{ invalid\n", "utf8");
686
+ const { commands } = loadExtension();
687
+ const freeflowCommand = commands.find((command) => command.name === "freeflow");
688
+ assert.ok(freeflowCommand);
689
+ const ctx = context(cwd);
690
+ ctx.ui.custom = async () => assert.fail("settings UI must not open for invalid local config");
691
+ await freeflowCommand.definition.handler("settings", ctx);
692
+ assert.equal(await readFile(localPath, "utf8"), "{ invalid\n");
693
+ assert.match(ctx.notifications.at(-1).message, /local\.json is invalid; repair or remove it/);
992
694
  } finally {
993
695
  await rm(cwd, { recursive: true, force: true });
994
696
  }
995
697
  });
996
698
 
997
- // Regression for the "everything disabled except Freeflow control plane" state.
998
- // The model should know Freeflow exists and can be reconfigured, but should not receive workflow/router/delegation behavior.
999
- // `defaultMode` is dormant while Skills are off.
1000
- test("Pi all-disabled capability state injects only Freeflow control-plane status", async () => {
1001
- const cwd = await mkdtemp(join(tmpdir(), "freeflow-pi-all-capabilities-disabled-"));
699
+ test("Pi personal settings refuse to write a tracked local override", async () => {
700
+ const cwd = await configuredRepo();
1002
701
  try {
1003
- await mkdir(join(cwd, ".freeflow"));
1004
- await writeFile(
1005
- join(cwd, ".freeflow/config.json"),
1006
- JSON.stringify(
1007
- {
1008
- defaultMode: "workflow",
1009
- interactionContract: false,
1010
- skills: { enabled: false },
1011
- },
1012
- null,
1013
- 2,
1014
- ),
1015
- "utf8",
1016
- );
1017
-
1018
- const { handlers, commands, activeToolNames } = loadExtension();
702
+ await writeFile(join(cwd, ".freeflow/local.json"), "{}\n", "utf8");
703
+ await execFileAsync("git", ["init", "-q", cwd]);
704
+ await execFileAsync("git", ["-C", cwd, "add", ".freeflow/local.json"]);
705
+ const { commands } = loadExtension();
706
+ const freeflowCommand = commands.find((command) => command.name === "freeflow");
707
+ assert.ok(freeflowCommand);
1019
708
  const ctx = context(cwd);
1020
- const result = await handlers.get("before_agent_start")({ systemPrompt: "base prompt" }, ctx);
1021
-
1022
- assert.match(result.systemPrompt, /# Freeflow Control Plane/);
709
+ ctx.ui.custom = async (factory) => {
710
+ let result;
711
+ const component = factory({ requestRender() {} }, testTheme, {}, (value) => {
712
+ result = value;
713
+ });
714
+ component.handleInput("\r");
715
+ component.handleInput("\u001b[B");
716
+ component.handleInput("\r");
717
+ component.handleInput("\u001b");
718
+ return result;
719
+ };
720
+ await freeflowCommand.definition.handler("settings", ctx);
721
+ assert.deepEqual(JSON.parse(await readFile(join(cwd, ".freeflow/local.json"), "utf8")), {});
1023
722
  assert.match(
1024
- result.systemPrompt,
1025
- /Freeflow is enabled for this repo, but no model-facing capabilities are enabled/,
723
+ ctx.notifications.map((notification) => notification.message).join("\n"),
724
+ /local\.json is tracked by git/,
725
+ );
726
+ assert.equal(ctx.reloads.length, 0);
727
+ assert.doesNotMatch(
728
+ ctx.notifications.map((notification) => notification.message).join("\n"),
729
+ /personal overrides saved/,
1026
730
  );
1027
- assert.match(result.systemPrompt, /Repository default mode: `workflow` \(repository\)/);
1028
- assert.match(result.systemPrompt, /Resolved mode: `workflow` \(inactive because Skills are disabled\)/);
1029
- assert.match(result.systemPrompt, /Effective Freeflow mode: `none`/);
1030
- assert.doesNotMatch(result.systemPrompt, /# Freeflow Runtime Context/);
1031
- assert.doesNotMatch(result.systemPrompt, /# Freeflow Interaction Contract/);
1032
- assert.doesNotMatch(result.systemPrompt, /# Freeflow Runtime Kernel/);
1033
- assert.doesNotMatch(result.systemPrompt, /## Loaded Mode Contract Skill/);
1034
- assert.doesNotMatch(result.systemPrompt, /## Loaded Workflow Skill/);
1035
- assert.doesNotMatch(result.systemPrompt, /## Loaded Decision Gate Skill/);
1036
- assert.doesNotMatch(result.systemPrompt, /## Loaded Output Router Skill/);
1037
- assert.equal(ctx.statuses.at(-1).value, "freeflow: idle");
1038
- assert.ok(activeToolNames().includes("freeflow_status"));
1039
- assert.ok(!activeToolNames().includes("freeflow_search"));
1040
-
1041
- const freeflowCommand = commands.find((command) => command.name === "freeflow");
1042
- assert.ok(freeflowCommand);
1043
- const modeCtx = context(cwd);
1044
- await freeflowCommand.definition.handler("mode strict-workflow", modeCtx);
1045
- assert.match(modeCtx.notifications.at(-1).message, /Freeflow modes are inactive because Skills are disabled/);
1046
- assert.match(modeCtx.notifications.at(-1).message, /Configured default mode: workflow \(repository default\)/);
1047
731
  } finally {
1048
732
  await rm(cwd, { recursive: true, force: true });
1049
733
  }
1050
734
  });
1051
735
 
1052
- test("Pi /freeflow mode is the only mode command and opens a dedicated selector", async () => {
1053
- const cwd = await mkdtemp(join(tmpdir(), "freeflow-pi-mode-command-"));
736
+ test("Pi Cognitive Routing settings preserve complete presets", async () => {
737
+ const cwd = await configuredRepo({
738
+ cognitiveRouting: {
739
+ enabled: true,
740
+ profiles: {
741
+ standard: { provider: "test", model: "model-a", thinkingLevel: "low" },
742
+ reasoning: { provider: "test", model: "model-b", thinkingLevel: "high" },
743
+ },
744
+ },
745
+ });
1054
746
  try {
1055
- await mkdir(join(cwd, ".freeflow"));
1056
- const configPath = join(cwd, ".freeflow/config.json");
1057
- const configText = JSON.stringify({ defaultMode: "workflow" }, null, 2);
1058
- await writeFile(configPath, configText, "utf8");
1059
-
1060
- const { commands, entries } = loadExtension();
747
+ const { commands } = loadExtension();
1061
748
  const freeflowCommand = commands.find((command) => command.name === "freeflow");
1062
749
  assert.ok(freeflowCommand);
1063
- assert.ok(!commands.some((command) => command.name === "workflow"));
1064
-
1065
- const alreadyDefaultCtx = context(cwd);
1066
- await freeflowCommand.definition.handler("mode workflow", alreadyDefaultCtx);
1067
- assert.equal(entries.length, 0);
1068
- assert.match(
1069
- alreadyDefaultCtx.notifications.at(-1).message,
1070
- /already in workflow mode from the configured default.*No session override was created/,
1071
- );
1072
-
1073
- const modeCtx = context(cwd);
1074
- modeCtx.ui.custom = async (factory) => {
750
+ const settingsCtx = context(cwd);
751
+ settingsCtx.isIdle = () => true;
752
+ settingsCtx.modelRegistry = cognitiveRoutingModelRegistry();
753
+ settingsCtx.ui.custom = async (factory) => {
1075
754
  let result;
1076
755
  const component = factory({ requestRender() {} }, testTheme, {}, (value) => {
1077
756
  result = value;
1078
757
  });
1079
- const text = renderText(component);
1080
- assert.match(text, /Freeflow Mode/);
1081
- assert.match(text, /Use configured default\s+workflow from repository/);
1082
- assert.match(text, /conversation\s+Discussion and read-only inspection/);
1083
- assert.match(text, /strict-workflow\s+Stronger decision and evidence pressure for high-risk work/);
1084
-
1085
758
  component.handleInput("\u001b[B");
759
+ component.handleInput("\r");
1086
760
  component.handleInput("\u001b[B");
761
+ component.handleInput("\r");
1087
762
  component.handleInput("\u001b[B");
1088
763
  component.handleInput("\r");
764
+ for (let index = 0; index < 4; index++) component.handleInput("\u001b[B");
765
+ component.handleInput("\r");
766
+ component.handleInput("\r");
767
+ await component.waitForWrites();
1089
768
  return result;
1090
769
  };
1091
-
1092
- await freeflowCommand.definition.handler("mode", modeCtx);
1093
- assert.deepEqual(entries.at(-1), {
1094
- customType: "freeflow-mode",
1095
- data: { currentMode: "strict-workflow" },
1096
- });
1097
- assert.equal(modeCtx.statuses.length, 1);
1098
- assert.equal(modeCtx.statuses.at(-1).value, "freeflow: interaction · strict-workflow (session)");
1099
- assert.match(modeCtx.notifications.at(-1).message, /Stored in Pi session history/);
1100
- assert.match(
1101
- modeCtx.notifications.at(-1).message,
1102
- /\.freeflow\/local\.json and \.freeflow\/config\.json were not changed/,
1103
- );
1104
- assert.equal(await readFile(configPath, "utf8"), configText);
1105
- assert.equal(modeCtx.reloads.length, 0);
1106
-
1107
- const alreadySessionCtx = context(cwd);
1108
- await freeflowCommand.definition.handler("mode strict-workflow", alreadySessionCtx);
1109
- assert.equal(entries.length, 1);
1110
- assert.match(alreadySessionCtx.notifications.at(-1).message, /already in strict-workflow mode for this Pi session/);
1111
-
1112
- const resetCtx = context(cwd);
1113
- await freeflowCommand.definition.handler("mode reset", resetCtx);
1114
- assert.deepEqual(entries.at(-1), {
1115
- customType: "freeflow-mode",
1116
- data: { currentMode: null },
770
+ await freeflowCommand.definition.handler("settings repo", settingsCtx);
771
+ const saved = JSON.parse(await readFile(join(cwd, ".freeflow/config.json"), "utf8"));
772
+ assert.deepEqual(saved.cognitiveRouting.profiles.standard, {
773
+ provider: "test",
774
+ model: "model-b",
775
+ thinkingLevel: "max",
1117
776
  });
1118
- assert.equal(resetCtx.statuses.at(-1).value, "freeflow: interaction · workflow");
1119
- assert.match(resetCtx.notifications.at(-1).message, /reset to configured default: workflow \(repository default\)/);
1120
- assert.match(resetCtx.notifications.at(-1).message, /Session override cleared/);
1121
- assert.match(
1122
- resetCtx.notifications.at(-1).message,
1123
- /\.freeflow\/local\.json and \.freeflow\/config\.json were not changed/,
1124
- );
1125
- assert.equal(entries.length, 2);
1126
-
1127
- const alreadyResetCtx = context(cwd);
1128
- await freeflowCommand.definition.handler("mode reset", alreadyResetCtx);
1129
- assert.equal(entries.length, 2);
1130
- assert.match(alreadyResetCtx.notifications.at(-1).message, /already using the configured default: workflow/);
1131
-
1132
- const nonTuiCtx = context(cwd);
1133
- await freeflowCommand.definition.handler("mode", nonTuiCtx);
1134
- assert.match(
1135
- nonTuiCtx.notifications.at(-1).message,
1136
- /Freeflow mode is workflow \(configured default workflow \(repository default\)\)/,
1137
- );
1138
- assert.doesNotMatch(nonTuiCtx.notifications.at(-1).message, /requires Pi TUI/);
777
+ assert.equal(settingsCtx.reloads.length, 1);
1139
778
  } finally {
1140
779
  await rm(cwd, { recursive: true, force: true });
1141
780
  }
1142
781
  });
1143
782
 
1144
- test("Pi selector commands explain the TUI requirement in RPC mode without mutation", async () => {
1145
- const cwd = await mkdtemp(join(tmpdir(), "freeflow-pi-rpc-selector-guidance-"));
783
+ test("Pi filters persisted Workflow and Cognitive Routing bootstrap entries without creating them", async () => {
784
+ const cwd = await configuredRepo();
1146
785
  try {
1147
- await mkdir(join(cwd, ".freeflow"));
1148
- const configPath = join(cwd, ".freeflow/config.json");
1149
- const configText = JSON.stringify({ defaultMode: "workflow" }, null, 2);
1150
- await writeFile(configPath, configText, "utf8");
1151
-
1152
- const { commands } = loadExtension();
1153
- const freeflowCommand = commands.find((command) => command.name === "freeflow");
1154
- assert.ok(freeflowCommand);
1155
-
1156
- for (const args of ["settings", "mode"]) {
1157
- const ctx = context(cwd);
1158
- ctx.mode = "rpc";
1159
- ctx.hasUI = true;
1160
- let customCalls = 0;
1161
- ctx.ui.custom = async () => {
1162
- customCalls += 1;
1163
- };
1164
-
1165
- await freeflowCommand.definition.handler(args, ctx);
1166
-
1167
- assert.equal(customCalls, 0);
1168
- assert.match(ctx.notifications.at(-1).message, /requires? Pi TUI mode/);
1169
- }
1170
-
1171
- assert.equal(await readFile(configPath, "utf8"), configText);
1172
- assert.equal((await readModeState(cwd)).sessionMode, null);
1173
-
1174
- const explicitModeCtx = context(cwd);
1175
- explicitModeCtx.mode = "rpc";
1176
- explicitModeCtx.hasUI = true;
1177
- await freeflowCommand.definition.handler("mode conversation", explicitModeCtx);
1178
- assert.equal((await readModeState(cwd)).sessionMode, "conversation");
786
+ const { handlers } = loadExtension();
787
+ const userMessage = { role: "user", content: "hello", timestamp: Date.now() };
788
+ const messages = [
789
+ { role: "custom", customType: "freeflow-workflow-bootstrap", content: "old workflow", display: false },
790
+ { role: "custom", customType: "freeflow-cognitive-routing-bootstrap", content: "old routing", display: false },
791
+ userMessage,
792
+ ];
793
+ const result = await handlers.get("context")({ messages }, context(cwd));
794
+ assert.deepEqual(
795
+ result.messages.filter((message) => message.customType !== "freeflow-runtime-state"),
796
+ [userMessage],
797
+ );
798
+ const runtimeStateIndex = result.messages.findIndex((message) => message.customType === "freeflow-runtime-state");
799
+ assert.equal(runtimeStateIndex, result.messages.findIndex((message) => message.role === "user") - 1);
1179
800
  } finally {
1180
- restoreModeOverride(context(cwd));
1181
801
  await rm(cwd, { recursive: true, force: true });
1182
802
  }
1183
803
  });
1184
804
 
1185
- test("Pi selector commands fail explicitly when notifications are unavailable", async () => {
1186
- const cwd = await mkdtemp(join(tmpdir(), "freeflow-pi-print-selector-guidance-"));
805
+ test("Pi preserves the host prompt when a mandatory prompt file is missing", async () => {
806
+ const root = await mkdtemp(join(tmpdir(), "freeflow-pi-missing-prompt-"));
807
+ const cwd = await configuredRepo();
1187
808
  try {
1188
- await mkdir(join(cwd, ".freeflow"));
1189
- const configPath = join(cwd, ".freeflow/config.json");
1190
- const configText = JSON.stringify({ defaultMode: "workflow" }, null, 2);
1191
- await writeFile(configPath, configText, "utf8");
1192
-
1193
- const { commands } = loadExtension();
1194
- const freeflowCommand = commands.find((command) => command.name === "freeflow");
1195
- assert.ok(freeflowCommand);
1196
-
1197
- for (const mode of ["json", "print"]) {
1198
- for (const args of ["settings", "mode"]) {
1199
- const ctx = context(cwd);
1200
- ctx.mode = mode;
1201
- ctx.hasUI = false;
1202
- ctx.ui.custom = async () => undefined;
1203
- await assert.rejects(freeflowCommand.definition.handler(args, ctx), /requires? Pi TUI mode/);
1204
- }
1205
- }
1206
-
1207
- assert.equal(await readFile(configPath, "utf8"), configText);
1208
- assert.equal((await readModeState(cwd)).sessionMode, null);
809
+ await mkdir(join(root, "pi-extension"), { recursive: true });
810
+ await execFileAsync("cp", ["-R", join(process.cwd(), "pi-extension", "dist"), join(root, "pi-extension")]);
811
+ await execFileAsync("cp", ["-R", join(process.cwd(), "runtime"), root]);
812
+ await execFileAsync("ln", ["-s", join(process.cwd(), "node_modules"), join(root, "node_modules")]);
813
+ await execFileAsync("rm", [join(root, "runtime", "prompts", "interaction-contract.md")]);
814
+ const extension = (
815
+ await import(`${new URL(`file://${join(root, "pi-extension", "dist", "index.js")}`).href}?missing=${Date.now()}`)
816
+ ).default;
817
+ const { handlers } = loadExtension(extension);
818
+ const before = await handlers.get("before_agent_start")({ systemPrompt: "base prompt" }, context(cwd));
819
+ assert.equal(before.systemPrompt, "base prompt");
820
+ const resources = await handlers.get("resources_discover")({ cwd }, context(cwd));
821
+ assert.deepEqual(resources.skillPaths, []);
1209
822
  } finally {
1210
- restoreModeOverride(context(cwd));
1211
823
  await rm(cwd, { recursive: true, force: true });
824
+ await rm(root, { recursive: true, force: true });
1212
825
  }
1213
826
  });
1214
827
 
1215
- test("Pi non-TUI selector guidance precedes inactive and invalid configuration state", async () => {
1216
- const cases = [
1217
- {
1218
- name: "disabled",
1219
- args: "mode",
1220
- repository: JSON.stringify({ enabled: false, defaultMode: "workflow" }),
828
+ function cognitiveRoutingModelRegistry() {
829
+ const models = [
830
+ { provider: "test", id: "model-a", name: "Model A", reasoning: true },
831
+ { provider: "test", id: "model-b", name: "Model B", reasoning: true },
832
+ ];
833
+ const supported = new Map([
834
+ ["model-a", new Set(["off", "low", "medium", "high"])],
835
+ ["model-b", new Set(["off", "low", "medium", "high", "max"])],
836
+ ]);
837
+ return {
838
+ getAvailable() {
839
+ return models;
1221
840
  },
1222
- {
1223
- name: "skills-off",
1224
- args: "mode",
1225
- repository: JSON.stringify({ defaultMode: "workflow", skills: { enabled: false } }),
841
+ find(provider, id) {
842
+ return models.find((model) => model.provider === provider && model.id === id);
1226
843
  },
1227
- {
1228
- name: "invalid-repository",
1229
- args: "settings",
1230
- repository: "{ invalid\n",
844
+ async getApiKeyAndHeaders() {
845
+ return { ok: true };
1231
846
  },
1232
- {
1233
- name: "invalid-local",
1234
- args: "settings",
1235
- repository: JSON.stringify({ defaultMode: "workflow" }),
1236
- local: "{ invalid\n",
847
+ clampThinkingLevel(model, level) {
848
+ return supported.get(model.id)?.has(level) ? level : "off";
1237
849
  },
1238
- ];
1239
-
1240
- for (const [index, testCase] of cases.entries()) {
1241
- const cwd = await mkdtemp(join(tmpdir(), `freeflow-pi-non-tui-${testCase.name}-`));
1242
- try {
1243
- await mkdir(join(cwd, ".freeflow"));
1244
- const configPath = join(cwd, ".freeflow/config.json");
1245
- const localPath = join(cwd, ".freeflow/local.json");
1246
- await writeFile(configPath, testCase.repository, "utf8");
1247
- if (testCase.local) await writeFile(localPath, testCase.local, "utf8");
1248
-
1249
- const { commands } = loadExtension();
1250
- const freeflowCommand = commands.find((command) => command.name === "freeflow");
1251
- assert.ok(freeflowCommand);
1252
-
1253
- const rpcCtx = context(cwd);
1254
- rpcCtx.mode = "rpc";
1255
- rpcCtx.hasUI = true;
1256
- let customCalls = 0;
1257
- rpcCtx.ui.custom = async () => {
1258
- customCalls += 1;
1259
- };
1260
- await freeflowCommand.definition.handler(testCase.args, rpcCtx);
1261
- assert.equal(customCalls, 0);
1262
- assert.match(rpcCtx.notifications.at(-1).message, /requires? Pi TUI mode/);
1263
-
1264
- const nonUiCtx = context(cwd);
1265
- nonUiCtx.mode = index % 2 === 0 ? "json" : "print";
1266
- nonUiCtx.hasUI = false;
1267
- nonUiCtx.ui.custom = async () => undefined;
1268
- await assert.rejects(freeflowCommand.definition.handler(testCase.args, nonUiCtx), /requires? Pi TUI mode/);
1269
-
1270
- assert.equal(await readFile(configPath, "utf8"), testCase.repository);
1271
- if (testCase.local) assert.equal(await readFile(localPath, "utf8"), testCase.local);
1272
- assert.equal((await readModeState(cwd)).sessionMode, null);
1273
- } finally {
1274
- restoreModeOverride(context(cwd));
1275
- await rm(cwd, { recursive: true, force: true });
1276
- }
1277
- }
1278
- });
1279
-
1280
- test("Pi statusline reports only effective Freeflow runtime state", async () => {
1281
- const cwd = await mkdtemp(join(tmpdir(), "freeflow-pi-statusline-state-"));
1282
- try {
1283
- await mkdir(join(cwd, ".freeflow"));
1284
- const configPath = join(cwd, ".freeflow/config.json");
1285
- const { pi, handlers, commands } = loadExtension();
1286
- const beforeAgentStart = handlers.get("before_agent_start");
1287
- const sessionStart = handlers.get("session_start");
1288
- const freeflowCommand = commands.find((command) => command.name === "freeflow");
1289
- assert.ok(beforeAgentStart);
1290
- assert.ok(sessionStart);
1291
- assert.ok(freeflowCommand);
1292
-
1293
- const statusFor = async (config) => {
1294
- if (config === null) {
1295
- await rm(configPath, { force: true });
1296
- } else if (typeof config === "string") {
1297
- await writeFile(configPath, config, "utf8");
1298
- } else {
1299
- await writeFile(configPath, JSON.stringify(config, null, 2), "utf8");
1300
- }
1301
- const ctx = context(cwd);
1302
- await sessionStart({ reason: "reload" }, ctx);
1303
- await beforeAgentStart({ systemPrompt: "base prompt" }, ctx);
1304
- return ctx.statuses.at(-1).value;
1305
- };
1306
-
1307
- assert.equal(await statusFor(null), "freeflow: setup needed");
1308
- assert.equal(await statusFor("{ invalid"), "freeflow: config error");
1309
- assert.equal(await statusFor({ enabled: false, defaultMode: "workflow" }), "freeflow: off");
1310
- assert.equal(await statusFor({ defaultMode: "workflow", skills: { enabled: false } }), "freeflow: interaction");
1311
- assert.equal(
1312
- await statusFor({
1313
- defaultMode: "workflow",
1314
- skills: { enabled: false },
1315
- outputRouter: { enabled: true },
1316
- }),
1317
- "freeflow: interaction · router",
1318
- );
1319
- assert.equal(
1320
- await statusFor({
1321
- defaultMode: "workflow",
1322
- outputRouter: { enabled: true },
1323
- }),
1324
- "freeflow: interaction · workflow · router",
1325
- );
1326
-
1327
- const masterOverrideCtx = context(cwd);
1328
- await setSessionCoreOverride("enabled", false, masterOverrideCtx, pi);
1329
- await beforeAgentStart({ systemPrompt: "base prompt" }, masterOverrideCtx);
1330
- assert.equal(masterOverrideCtx.statuses.at(-1).value, "freeflow: off (session)");
1331
- await resetSessionOverrides(masterOverrideCtx, pi);
1332
-
1333
- const interactionOverrideCtx = context(cwd);
1334
- await setSessionCoreOverride("interactionContract", false, interactionOverrideCtx, pi);
1335
- const interactionRuntime = await beforeAgentStart({ systemPrompt: "base prompt" }, interactionOverrideCtx);
1336
- assert.match(interactionRuntime.systemPrompt, /Interaction contract: disabled \(session override\)/);
1337
- assert.equal(
1338
- interactionOverrideCtx.statuses.at(-1).value,
1339
- "freeflow: interaction off (session) · workflow · router",
1340
- );
1341
- await resetSessionOverrides(interactionOverrideCtx, pi);
1342
-
1343
- const skillsOverrideCtx = context(cwd);
1344
- await setSessionCoreOverride("skillsEnabled", false, skillsOverrideCtx, pi);
1345
- await beforeAgentStart({ systemPrompt: "base prompt" }, skillsOverrideCtx);
1346
- assert.equal(skillsOverrideCtx.statuses.at(-1).value, "freeflow: interaction · skills off (session) · router");
1347
- await resetSessionOverrides(skillsOverrideCtx, pi);
1348
-
1349
- const modeCtx = context(cwd);
1350
- await freeflowCommand.definition.handler("mode conversation", modeCtx);
1351
- assert.equal(modeCtx.statuses.at(-1).value, "freeflow: interaction · conversation (session) · router");
1352
- await freeflowCommand.definition.handler("mode reset", context(cwd));
1353
- } finally {
1354
- await rm(cwd, { recursive: true, force: true });
1355
- }
1356
- });
1357
-
1358
- test("Pi /freeflow command toggles master switch and blocks inactive settings rows", async () => {
1359
- const cwd = await mkdtemp(join(tmpdir(), "freeflow-pi-freeflow-settings-"));
1360
- try {
1361
- await mkdir(join(cwd, ".freeflow"));
1362
- await writeFile(
1363
- join(cwd, ".freeflow/config.json"),
1364
- JSON.stringify(
1365
- {
1366
- enabled: false,
1367
- defaultMode: "workflow",
1368
- outputRouter: { enabled: true },
1369
- },
1370
- null,
1371
- 2,
1372
- ),
1373
- "utf8",
1374
- );
1375
-
1376
- const { commands } = loadExtension();
1377
- const freeflowCommand = commands.find((command) => command.name === "freeflow");
1378
- assert.ok(freeflowCommand);
1379
-
1380
- const settingsCtx = context(cwd);
1381
- settingsCtx.ui.custom = async (factory, options) => {
1382
- assert.equal(options, undefined);
1383
- let result;
1384
- const dimTheme = {
1385
- ...testTheme,
1386
- fg(color, text) {
1387
- return color === "dim" ? `[dim]${text}[/dim]` : text;
1388
- },
1389
- };
1390
- const component = factory({ requestRender() {} }, dimTheme, {}, (value) => {
1391
- result = value;
1392
- });
1393
- const rootText = renderText(component);
1394
- assert.match(rootText, /^─+/);
1395
- assert.match(rootText, /Freeflow Settings/);
1396
- assert.match(rootText, /\[dim\]Output Router\[\/dim\]/);
1397
- assert.match(rootText, /enabled \(22\) \(repository\) · inactive/);
1398
- assert.match(rootText, /\[dim\]Interaction Contract/);
1399
- assert.match(rootText, /\[dim\]Skills/);
1400
- assert.match(rootText, /\[dim\].*Output Router/);
1401
- assert.doesNotMatch(rootText, /Native safety net/);
1402
- component.handleInput("\u001b[B"); // Interaction Contract row is inactive while Freeflow is off.
1403
- component.handleInput("\r");
1404
- component.handleInput("\u001b");
1405
- return result;
1406
- };
1407
-
1408
- await freeflowCommand.definition.handler("settings", settingsCtx);
1409
- const afterInactiveEdit = JSON.parse(await readFile(join(cwd, ".freeflow/config.json"), "utf8"));
1410
- assert.deepEqual(afterInactiveEdit, {
1411
- enabled: false,
1412
- defaultMode: "workflow",
1413
- outputRouter: { enabled: true },
1414
- });
1415
- assert.equal(settingsCtx.reloads.length, 0);
1416
-
1417
- const enableCtx = context(cwd);
1418
- await freeflowCommand.definition.handler("enable", enableCtx);
1419
- const afterEnable = JSON.parse(await readFile(join(cwd, ".freeflow/config.json"), "utf8"));
1420
- assert.equal(afterEnable.enabled, undefined);
1421
- assert.equal(afterEnable.defaultMode, "workflow");
1422
- assert.equal(afterEnable.outputRouter.enabled, true);
1423
- assert.equal(enableCtx.reloads.length, 1);
1424
- } finally {
1425
- await rm(cwd, { recursive: true, force: true });
1426
- }
1427
- });
1428
-
1429
- test("Pi /freeflow disable applies live gates before reload completes", async () => {
1430
- const cwd = await mkdtemp(join(tmpdir(), "freeflow-pi-disable-live-gates-"));
1431
- try {
1432
- await mkdir(join(cwd, ".freeflow"));
1433
- await writeFile(
1434
- join(cwd, ".freeflow/config.json"),
1435
- JSON.stringify(
1436
- {
1437
- defaultMode: "workflow",
1438
- outputRouter: { enabled: true },
1439
- },
1440
- null,
1441
- 2,
1442
- ),
1443
- "utf8",
1444
- );
1445
-
1446
- const { commands, activeToolNames } = loadExtension();
1447
- const freeflowCommand = commands.find((command) => command.name === "freeflow");
1448
- assert.ok(freeflowCommand);
1449
- assert.ok(activeToolNames().includes("freeflow_search"));
1450
-
1451
- const ctx = context(cwd);
1452
- await freeflowCommand.definition.handler("disable", ctx);
1453
-
1454
- assert.equal(ctx.reloads.length, 1);
1455
- assert.ok(!activeToolNames().includes("freeflow_status"));
1456
- assert.ok(!activeToolNames().includes("freeflow_search"));
1457
- assert.ok(!activeToolNames().includes("freeflow_run"));
1458
- assert.ok(!activeToolNames().includes("freeflow_batch"));
1459
- } finally {
1460
- await rm(cwd, { recursive: true, force: true });
1461
- }
1462
- });
1463
-
1464
- test("Pi /freeflow settings marks default mode dormant when Skills are disabled", async () => {
1465
- const cwd = await mkdtemp(join(tmpdir(), "freeflow-pi-settings-skills-off-mode-dormant-"));
1466
- try {
1467
- await mkdir(join(cwd, ".freeflow"));
1468
- await writeFile(
1469
- join(cwd, ".freeflow/config.json"),
1470
- JSON.stringify({ defaultMode: "workflow", skills: { enabled: false } }, null, 2),
1471
- "utf8",
1472
- );
1473
-
1474
- const { commands } = loadExtension();
1475
- const freeflowCommand = commands.find((command) => command.name === "freeflow");
1476
- assert.ok(freeflowCommand);
1477
-
1478
- const settingsCtx = context(cwd);
1479
- settingsCtx.ui.custom = async (factory) => {
1480
- let result;
1481
- const component = factory({ requestRender() {} }, testTheme, {}, (value) => {
1482
- result = value;
1483
- });
1484
- const rootText = renderText(component);
1485
- assert.match(rootText, /Skills\s+disabled \(repository\)/);
1486
- assert.match(rootText, /Session mode\s+default \(workflow · repository · inactive\)/);
1487
- assert.match(rootText, /Default mode\s+workflow \(repository · inactive\)/);
1488
-
1489
- component.handleInput("\u001b[B");
1490
- component.handleInput("\u001b[B");
1491
- component.handleInput("\u001b[B");
1492
- component.handleInput("\u001b[B");
1493
- component.handleInput("\r");
1494
- const choiceText = renderText(component);
1495
- assert.match(choiceText, /Freeflow Settings · Personal overrides › Default mode/);
1496
- assert.match(choiceText, /conversation\s+Discussion and read-only inspection/);
1497
- assert.match(choiceText, /workflow\s+Adaptive workflow for consequential work/);
1498
- assert.match(choiceText, /strict-workflow\s+Stronger decision and evidence pressure for high-risk work/);
1499
- component.handleInput("\u001b[B");
1500
- component.handleInput("\u001b[B");
1501
- component.handleInput("\u001b[B");
1502
- component.handleInput("\r");
1503
- component.handleInput("\u001b");
1504
- return result;
1505
- };
1506
-
1507
- await freeflowCommand.definition.handler("settings", settingsCtx);
1508
- const repositoryAfter = JSON.parse(await readFile(join(cwd, ".freeflow/config.json"), "utf8"));
1509
- const localAfter = JSON.parse(await readFile(join(cwd, ".freeflow/local.json"), "utf8"));
1510
- assert.equal(repositoryAfter.defaultMode, "workflow");
1511
- assert.equal(repositoryAfter.skills.enabled, false);
1512
- assert.equal(localAfter.defaultMode, "strict-workflow");
1513
- assert.equal(settingsCtx.reloads.length, 1);
1514
- } finally {
1515
- await rm(cwd, { recursive: true, force: true });
1516
- }
1517
- });
1518
-
1519
- test("Pi /freeflow settings toggles the interaction contract independently", async () => {
1520
- const cwd = await mkdtemp(join(tmpdir(), "freeflow-pi-settings-interaction-contract-"));
1521
- try {
1522
- await mkdir(join(cwd, ".freeflow"));
1523
- const configPath = join(cwd, ".freeflow/config.json");
1524
- await writeFile(configPath, JSON.stringify({ defaultMode: "workflow" }, null, 2), "utf8");
1525
- await execFileAsync("git", ["init", "-q", cwd]);
1526
- const { commands, handlers } = loadExtension();
1527
- const freeflowCommand = commands.find((command) => command.name === "freeflow");
1528
- assert.ok(freeflowCommand);
1529
- const settingsCtx = context(cwd);
1530
- settingsCtx.ui.custom = async (factory) => {
1531
- let result;
1532
- const component = factory({ requestRender() {} }, testTheme, {}, (value) => {
1533
- result = value;
1534
- });
1535
- assert.match(renderText(component), /Interaction Contract\s+enabled \(builtin\)/);
1536
- component.handleInput("\u001b[B");
1537
- component.handleInput("\r");
1538
- component.handleInput("\u001b[B");
1539
- component.handleInput("\u001b[B");
1540
- component.handleInput("\r");
1541
- component.handleInput("\u001b");
1542
- return result;
1543
- };
1544
- await freeflowCommand.definition.handler("settings", settingsCtx);
1545
- const after = JSON.parse(await readFile(configPath, "utf8"));
1546
- const localAfter = JSON.parse(await readFile(join(cwd, ".freeflow/local.json"), "utf8"));
1547
- assert.equal(after.interactionContract, undefined);
1548
- assert.equal(after.skills, undefined);
1549
- assert.equal(localAfter.interactionContract, false);
1550
- await execFileAsync("git", ["-C", cwd, "check-ignore", "-q", "--", ".freeflow/local.json"]);
1551
- assert.equal(settingsCtx.reloads.length, 1);
1552
- const runtime = await handlers.get("before_agent_start")(
1553
- { prompt: "hello", systemPrompt: "base prompt" },
1554
- context(cwd),
1555
- );
1556
- assert.doesNotMatch(runtime.systemPrompt, /# Freeflow Interaction Contract/);
1557
- assert.equal(runtime.message.customType, "freeflow-workflow-bootstrap");
1558
- const enableCtx = context(cwd);
1559
- enableCtx.ui.custom = async (factory) => {
1560
- let result;
1561
- const component = factory({ requestRender() {} }, testTheme, {}, (value) => {
1562
- result = value;
1563
- });
1564
- assert.match(renderText(component), /Interaction Contract\s+disabled \(local\)/);
1565
- component.handleInput("\u001b[B");
1566
- component.handleInput("\r");
1567
- component.handleInput("\u001b[B");
1568
- component.handleInput("\r");
1569
- component.handleInput("\u001b");
1570
- return result;
1571
- };
1572
- await freeflowCommand.definition.handler("settings", enableCtx);
1573
- const reenabled = JSON.parse(await readFile(configPath, "utf8"));
1574
- assert.equal(reenabled.interactionContract, undefined);
1575
- await assert.rejects(readFile(join(cwd, ".freeflow/local.json"), "utf8"), (error) => error?.code === "ENOENT");
1576
- assert.equal(enableCtx.reloads.length, 1);
1577
- } finally {
1578
- await rm(cwd, { recursive: true, force: true });
1579
- }
1580
- });
1581
-
1582
- test("Pi session settings override Freeflow without changing config", async () => {
1583
- const cwd = await mkdtemp(join(tmpdir(), "freeflow-pi-session-settings-"));
1584
- try {
1585
- await mkdir(join(cwd, ".freeflow"));
1586
- const configPath = join(cwd, ".freeflow/config.json");
1587
- const configText = JSON.stringify({ defaultMode: "workflow" }, null, 2);
1588
- await writeFile(configPath, configText, "utf8");
1589
-
1590
- const { commands, entries } = loadExtension();
1591
- const freeflowCommand = commands.find((command) => command.name === "freeflow");
1592
- assert.ok(freeflowCommand);
1593
- const settingsCtx = context(cwd);
1594
- settingsCtx.ui.custom = async (factory) => {
1595
- let result;
1596
- const component = factory({ requestRender() {} }, testTheme, {}, (value) => {
1597
- result = value;
1598
- });
1599
- const rootText = renderText(component);
1600
- assert.match(rootText, /Freeflow Settings · Session overrides/);
1601
- assert.match(rootText, /Freeflow\s+enabled \(builtin\)/);
1602
- assert.match(rootText, /Interaction Contract\s+enabled \(builtin\)/);
1603
- assert.match(rootText, /Skills\s+enabled \(builtin\)/);
1604
- assert.match(rootText, /Mode\s+default \(workflow · repository\)/);
1605
- assert.match(rootText, /Reset session overrides\s+available/);
1606
-
1607
- component.handleInput("\r");
1608
- const choices = renderText(component);
1609
- assert.match(choices, /Inherit configured value/);
1610
- assert.match(choices, /Enabled for this session/);
1611
- assert.match(choices, /Disabled for this session/);
1612
- component.handleInput("\u001b[B");
1613
- component.handleInput("\u001b[B");
1614
- component.handleInput("\r");
1615
- component.handleInput("\u001b");
1616
- return result;
1617
- };
1618
-
1619
- await freeflowCommand.definition.handler("settings session", settingsCtx);
1620
- const capabilityState = await readCapabilityState(cwd);
1621
- assert.equal(capabilityState.enabled, false);
1622
- assert.equal(capabilityState.configSources.enabled, "session");
1623
- assert.equal(settingsCtx.reloads.length, 1);
1624
- assert.deepEqual(entries.at(-1), {
1625
- customType: "freeflow-session-overrides",
1626
- data: { overrides: { enabled: false } },
1627
- });
1628
- assert.equal(await readFile(configPath, "utf8"), configText);
1629
- await assert.rejects(readFile(join(cwd, ".freeflow/local.json"), "utf8"), (error) => error?.code === "ENOENT");
1630
-
1631
- const resetCtx = context(cwd);
1632
- resetCtx.ui.custom = async (factory) => {
1633
- let result;
1634
- const component = factory({ requestRender() {} }, testTheme, {}, (value) => {
1635
- result = value;
1636
- });
1637
- const rootText = renderText(component);
1638
- assert.match(rootText, /Freeflow\s+disabled \(session\)/);
1639
- assert.match(rootText, /Interaction Contract\s+enabled \(builtin\) · inactive/);
1640
- for (let index = 0; index < 4; index += 1) component.handleInput("\u001b[B");
1641
- component.handleInput("\r");
1642
- assert.match(renderText(component), /Reset all session overrides/);
1643
- component.handleInput("\r");
1644
- component.handleInput("\u001b");
1645
- return result;
1646
- };
1647
- await freeflowCommand.definition.handler("settings session", resetCtx);
1648
- assert.equal((await readCapabilityState(cwd)).enabled, true);
1649
- assert.equal(resetCtx.reloads.length, 1);
1650
- assert.deepEqual(entries.at(-1), {
1651
- customType: "freeflow-session-overrides",
1652
- data: { overrides: {} },
1653
- });
1654
- assert.equal(await readFile(configPath, "utf8"), configText);
1655
- } finally {
1656
- restoreModeOverride(context(cwd));
1657
- await rm(cwd, { recursive: true, force: true });
1658
- }
1659
- });
1660
-
1661
- test("Pi settings refuse to overwrite an invalid local override", async () => {
1662
- const cwd = await mkdtemp(join(tmpdir(), "freeflow-pi-invalid-local-settings-"));
1663
- try {
1664
- await mkdir(join(cwd, ".freeflow"));
1665
- await writeFile(join(cwd, ".freeflow/config.json"), JSON.stringify({ defaultMode: "workflow" }, null, 2), "utf8");
1666
- const localPath = join(cwd, ".freeflow/local.json");
1667
- await writeFile(localPath, "{ invalid\n", "utf8");
1668
-
1669
- const { commands } = loadExtension();
1670
- const freeflowCommand = commands.find((command) => command.name === "freeflow");
1671
- assert.ok(freeflowCommand);
1672
- const ctx = context(cwd);
1673
- ctx.ui.custom = async () => {
1674
- assert.fail("settings UI must not open for invalid local config");
1675
- };
1676
-
1677
- await freeflowCommand.definition.handler("settings", ctx);
1678
- assert.equal(await readFile(localPath, "utf8"), "{ invalid\n");
1679
- assert.match(ctx.notifications.at(-1).message, /local\.json is invalid; repair or remove it/);
1680
- assert.equal(ctx.reloads.length, 0);
1681
- } finally {
1682
- await rm(cwd, { recursive: true, force: true });
1683
- }
1684
- });
1685
-
1686
- test("Pi personal settings refuse to write a tracked local override", async () => {
1687
- const cwd = await mkdtemp(join(tmpdir(), "freeflow-pi-tracked-local-settings-"));
1688
- try {
1689
- await mkdir(join(cwd, ".freeflow"));
1690
- await writeFile(join(cwd, ".freeflow/config.json"), JSON.stringify({ defaultMode: "workflow" }, null, 2), "utf8");
1691
- await writeFile(join(cwd, ".freeflow/local.json"), "{}\n", "utf8");
1692
- await execFileAsync("git", ["init", "-q", cwd]);
1693
- await execFileAsync("git", ["-C", cwd, "add", ".freeflow/local.json"]);
1694
-
1695
- const { commands } = loadExtension();
1696
- const freeflowCommand = commands.find((command) => command.name === "freeflow");
1697
- assert.ok(freeflowCommand);
1698
- const ctx = context(cwd);
1699
- ctx.ui.custom = async (factory) => {
1700
- let result;
1701
- const component = factory({ requestRender() {} }, testTheme, {}, (value) => {
1702
- result = value;
1703
- });
1704
- component.handleInput("\u001b[B");
1705
- component.handleInput("\r");
1706
- component.handleInput("\u001b[B");
1707
- component.handleInput("\u001b[B");
1708
- component.handleInput("\r");
1709
- component.handleInput("\u001b");
1710
- return result;
1711
- };
1712
-
1713
- await freeflowCommand.definition.handler("settings", ctx);
1714
- assert.deepEqual(JSON.parse(await readFile(join(cwd, ".freeflow/local.json"), "utf8")), {});
1715
- assert.match(ctx.notifications.at(-1).message, /local\.json is tracked by git/);
1716
- assert.equal(ctx.reloads.length, 0);
1717
- } finally {
1718
- await rm(cwd, { recursive: true, force: true });
1719
- }
1720
- });
1721
-
1722
- test("Pi settings do not report save or reload success after a mixed write failure", async () => {
1723
- const cwd = await mkdtemp(join(tmpdir(), "freeflow-pi-mixed-settings-write-"));
1724
- try {
1725
- await mkdir(join(cwd, ".freeflow"));
1726
- await writeFile(join(cwd, ".freeflow/config.json"), JSON.stringify({ defaultMode: "workflow" }), "utf8");
1727
- await writeFile(join(cwd, ".freeflow/local.json"), "{}\n", "utf8");
1728
- await execFileAsync("git", ["init", "-q", cwd]);
1729
- await execFileAsync("git", ["-C", cwd, "add", ".freeflow/local.json"]);
1730
-
1731
- const { commands } = loadExtension();
1732
- const freeflowCommand = commands.find((command) => command.name === "freeflow");
1733
- assert.ok(freeflowCommand);
1734
- const ctx = context(cwd);
1735
- ctx.ui.custom = async (factory) => {
1736
- let result;
1737
- const component = factory({ requestRender() {} }, testTheme, {}, (value) => {
1738
- result = value;
1739
- });
1740
-
1741
- for (let index = 0; index < 3; index++) component.handleInput("\u001b[B");
1742
- component.handleInput("\r");
1743
- component.handleInput("\u001b[B");
1744
- component.handleInput("\r"); // Session mode succeeds.
1745
- await component.waitForWrites();
1746
-
1747
- component.handleInput("\u001b[A");
1748
- component.handleInput("\u001b[A");
1749
- component.handleInput("\r");
1750
- component.handleInput("\u001b[B");
1751
- component.handleInput("\u001b[B");
1752
- component.handleInput("\r"); // Tracked local config write fails.
1753
- await component.waitForWrites();
1754
- component.handleInput("\u001b");
1755
- return result;
1756
- };
1757
-
1758
- await freeflowCommand.definition.handler("settings", ctx);
1759
- assert.equal((await readModeState(cwd)).sessionMode, "conversation");
1760
- assert.equal(ctx.reloads.length, 0);
1761
- assert.match(ctx.notifications.map((notification) => notification.message).join("\n"), /Write failed:/);
1762
- assert.doesNotMatch(
1763
- ctx.notifications.map((notification) => notification.message).join("\n"),
1764
- /personal overrides saved/,
1765
- );
1766
- assert.deepEqual(JSON.parse(await readFile(join(cwd, ".freeflow/local.json"), "utf8")), {});
1767
- } finally {
1768
- restoreModeOverride(context(cwd));
1769
- await rm(cwd, { recursive: true, force: true });
1770
- }
1771
- });
1772
-
1773
- test("Pi /freeflow settings repo edits shared defaults and shows local effective sources", async () => {
1774
- const cwd = await mkdtemp(join(tmpdir(), "freeflow-pi-repository-settings-"));
1775
- try {
1776
- await mkdir(join(cwd, ".freeflow"));
1777
- const repositoryPath = join(cwd, ".freeflow/config.json");
1778
- const localPath = join(cwd, ".freeflow/local.json");
1779
- await writeFile(repositoryPath, JSON.stringify({ defaultMode: "workflow" }, null, 2), "utf8");
1780
- await writeFile(
1781
- localPath,
1782
- JSON.stringify(
1783
- {
1784
- defaultMode: "conversation",
1785
- processing: { unsafeUnsandboxed: { enabled: true } },
1786
- },
1787
- null,
1788
- 2,
1789
- ),
1790
- "utf8",
1791
- );
1792
-
1793
- const { commands } = loadExtension();
1794
- const freeflowCommand = commands.find((command) => command.name === "freeflow");
1795
- assert.ok(freeflowCommand);
1796
- const ctx = context(cwd);
1797
- ctx.ui.custom = async (factory) => {
1798
- let result;
1799
- const component = factory({ requestRender() {} }, testTheme, {}, (value) => {
1800
- result = value;
1801
- });
1802
- const rootText = renderText(component);
1803
- assert.match(rootText, /Freeflow Repository Settings · modifies \.freeflow\/config\.json/);
1804
- assert.match(rootText, /Default mode\s+workflow \(effective conversation · local\)/);
1805
- for (let index = 0; index < 4; index++) {
1806
- component.handleInput("\u001b[B");
1807
- }
1808
- component.handleInput("\r");
1809
- component.handleInput("\u001b[B");
1810
- component.handleInput("\r");
1811
- component.handleInput("\u001b");
1812
- return result;
1813
- };
1814
-
1815
- await freeflowCommand.definition.handler("settings repo", ctx);
1816
- const repositoryAfter = JSON.parse(await readFile(repositoryPath, "utf8"));
1817
- const localAfter = JSON.parse(await readFile(localPath, "utf8"));
1818
- assert.equal(repositoryAfter.defaultMode, "strict-workflow");
1819
- assert.equal(localAfter.defaultMode, "conversation");
1820
- assert.equal(localAfter.processing.unsafeUnsandboxed.enabled, true);
1821
- assert.equal(ctx.reloads.length, 1);
1822
- } finally {
1823
- await rm(cwd, { recursive: true, force: true });
1824
- }
1825
- });
1826
-
1827
- test("Pi /freeflow settings groups capability settings", async () => {
1828
- const cwd = await mkdtemp(join(tmpdir(), "freeflow-pi-freeflow-grouped-settings-"));
1829
- try {
1830
- await mkdir(join(cwd, ".freeflow"));
1831
- await writeFile(
1832
- join(cwd, ".freeflow/config.json"),
1833
- JSON.stringify({ defaultMode: "workflow", outputRouter: { enabled: true } }, null, 2),
1834
- "utf8",
1835
- );
1836
-
1837
- const { commands } = loadExtension();
1838
- const freeflowCommand = commands.find((command) => command.name === "freeflow");
1839
- assert.ok(freeflowCommand);
1840
-
1841
- const settingsCtx = context(cwd);
1842
- settingsCtx.ui.custom = async (factory) => {
1843
- let result;
1844
- const component = factory({ requestRender() {} }, testTheme, {}, (value) => {
1845
- result = value;
1846
- });
1847
- const rootText = renderText(component);
1848
- assert.match(rootText, /^─+/);
1849
- assert.match(rootText, /Output Router\s+enabled \(22\) \(repository\)/);
1850
- assert.doesNotMatch(rootText, /Native safety net/);
1851
-
1852
- for (let index = 0; index < 5; index++) {
1853
- component.handleInput("\u001b[B");
1854
- }
1855
- component.handleInput("\r");
1856
- const routerText = renderText(component);
1857
- assert.match(routerText, /Freeflow Settings · Personal overrides › Output Router/);
1858
- assert.match(routerText, /Native safety net/);
1859
- component.handleInput("\u001b");
1860
- component.handleInput("\u001b");
1861
- return result;
1862
- };
1863
-
1864
- await freeflowCommand.definition.handler("settings", settingsCtx);
1865
- assert.equal(settingsCtx.reloads.length, 0);
1866
- } finally {
1867
- await rm(cwd, { recursive: true, force: true });
1868
- }
1869
- });
1870
-
1871
- test("Pi before_agent_start keeps output-router disabled by default", async () => {
1872
- const cwd = await mkdtemp(join(tmpdir(), "freeflow-pi-router-default-disabled-"));
1873
- try {
1874
- await mkdir(join(cwd, ".freeflow"));
1875
- await writeFile(join(cwd, ".freeflow/config.json"), JSON.stringify({ defaultMode: "workflow" }, null, 2), "utf8");
1876
-
1877
- const { handlers, activeToolNames } = loadExtension();
1878
- const beforeAgentStart = handlers.get("before_agent_start");
1879
- assert.ok(beforeAgentStart);
1880
-
1881
- const result = await beforeAgentStart({ systemPrompt: "base prompt" }, context(cwd));
1882
-
1883
- assert.match(result.systemPrompt, /# Freeflow Interaction Contract/);
1884
- assert.doesNotMatch(result.systemPrompt, /# Freeflow Runtime Kernel/);
1885
- assert.doesNotMatch(result.systemPrompt, /## Loaded Mode Contract Skill/);
1886
- assert.doesNotMatch(result.systemPrompt, /## Loaded Workflow Skill/);
1887
- assert.doesNotMatch(result.systemPrompt, /## Loaded Decision Gate Skill/);
1888
- assert.doesNotMatch(result.systemPrompt, /## Discovery-light/);
1889
- assert.match(result.systemPrompt, /Output router: disabled/);
1890
- assert.doesNotMatch(result.systemPrompt, /## Loaded Output Router Skill/);
1891
- assert.doesNotMatch(result.systemPrompt, /freeflow_search/);
1892
-
1893
- assert.doesNotMatch(result.systemPrompt, /Legacy `FFRESULT`/);
1894
- assert.doesNotMatch(result.systemPrompt, /# Context Locality/);
1895
- assert.doesNotMatch(result.systemPrompt, /freeflow_run/);
1896
- assert.doesNotMatch(result.systemPrompt, /freeflow_capture/);
1897
- assert.doesNotMatch(result.systemPrompt, /freeflow_search action=transform/);
1898
- assert.doesNotMatch(result.systemPrompt, /Native tools stay direct/);
1899
- assert.doesNotMatch(result.systemPrompt, /## Loaded Workflow Map/);
1900
- assert.doesNotMatch(result.systemPrompt, /## Loaded Output Router Safety Policy/);
1901
- assert.doesNotMatch(result.systemPrompt, /Do not silently summarize or compress exactness-sensitive output/);
1902
- assert.doesNotMatch(result.systemPrompt, /large native read\/bash outputs may be vaulted/);
1903
- assert.doesNotMatch(result.systemPrompt, /Output-router config note/);
1904
- assert.ok(!activeToolNames().includes("freeflow_search"));
1905
- assert.ok(!activeToolNames().includes("freeflow_run"));
1906
- assert.ok(!activeToolNames().includes("freeflow_batch"));
1907
- } finally {
1908
- await rm(cwd, { recursive: true, force: true });
1909
- }
1910
- });
1911
-
1912
- test("Pi before_agent_start keeps the per-turn system context to the compact interaction contract", async () => {
1913
- const cwd = await mkdtemp(join(tmpdir(), "freeflow-pi-interaction-contract-"));
1914
- try {
1915
- await mkdir(join(cwd, ".freeflow"));
1916
- await writeFile(join(cwd, ".freeflow/config.json"), JSON.stringify({ defaultMode: "workflow" }, null, 2), "utf8");
1917
-
1918
- const { handlers } = loadExtension();
1919
- const beforeAgentStart = handlers.get("before_agent_start");
1920
- assert.ok(beforeAgentStart);
1921
-
1922
- const result = await beforeAgentStart({ systemPrompt: "base prompt" }, context(cwd));
1923
-
1924
- assert.match(result.systemPrompt, /# Freeflow Interaction Contract/);
1925
- assert.doesNotMatch(result.systemPrompt, /# Freeflow Runtime Kernel/);
1926
- assert.match(result.systemPrompt, /Runtime delivery: confirmed for this Pi `before_agent_start` invocation/);
1927
- assert.doesNotMatch(result.systemPrompt, /## Conversation Mode Boundary/);
1928
- assert.doesNotMatch(result.systemPrompt, /## Strict Workflow Overlay/);
1929
- assert.match(result.systemPrompt, /Answer questions without inferring action/);
1930
- assert.match(result.systemPrompt, /enough shared\s+understanding for\s+the next sound action/);
1931
- assert.doesNotMatch(result.systemPrompt, /self-review|final assurance|standing authorization/i);
1932
- assert.doesNotMatch(result.systemPrompt, /## Loaded Mode Contract Skill/);
1933
- assert.doesNotMatch(result.systemPrompt, /## Loaded Workflow Skill/);
1934
- assert.doesNotMatch(result.systemPrompt, /## Loaded Decision Gate Skill/);
1935
- assert.doesNotMatch(result.systemPrompt, /## Discovery-light/);
1936
- } finally {
1937
- await rm(cwd, { recursive: true, force: true });
1938
- }
1939
- });
1940
-
1941
- test("Pi loads the full Workflow skill as one persistent first-turn message", async () => {
1942
- const cwd = await mkdtemp(join(tmpdir(), "freeflow-pi-workflow-bootstrap-"));
1943
- try {
1944
- const workflowSource = await readFile(new URL("../../../skills/workflow/SKILL.md", import.meta.url), "utf8");
1945
- const workflowLines = workflowSource.split(/\r?\n/);
1946
- const workflowOwnerLine = workflowLines.find((line) => line.startsWith("The active agent owns "));
1947
- const workflowSelfReviewLine = workflowLines.find((line) => line.startsWith("Self-review is silent "));
1948
- assert.ok(workflowOwnerLine, "canonical Workflow owner sentinel missing");
1949
- assert.ok(workflowSelfReviewLine, "canonical Workflow self-review sentinel missing");
1950
-
1951
- await mkdir(join(cwd, ".freeflow"));
1952
- await writeFile(join(cwd, ".freeflow/config.json"), JSON.stringify({ defaultMode: "workflow" }, null, 2), "utf8");
1953
-
1954
- const { handlers } = loadExtension();
1955
- const beforeAgentStart = handlers.get("before_agent_start");
1956
- assert.ok(beforeAgentStart);
1957
-
1958
- const sessionEntries = [];
1959
- const ctx = context(cwd, sessionEntries);
1960
- const first = await beforeAgentStart({ prompt: "hi", systemPrompt: "base prompt" }, ctx);
1961
-
1962
- assert.equal(first.message.customType, "freeflow-workflow-bootstrap");
1963
- assert.equal(first.message.display, false);
1964
- assert.match(first.message.content, /^# Freeflow Workflow Bootstrap/);
1965
- assert.match(first.message.content, /# Workflow/);
1966
- assert.ok(first.message.content.includes(workflowOwnerLine));
1967
- assert.ok(first.message.content.includes(workflowSelfReviewLine));
1968
- assert.match(first.message.content, /evidence/i);
1969
- assert.doesNotMatch(first.systemPrompt, /# Freeflow Workflow Bootstrap/);
1970
-
1971
- sessionEntries.push({
1972
- type: "custom_message",
1973
- id: "workflow-bootstrap-entry",
1974
- parentId: null,
1975
- timestamp: new Date().toISOString(),
1976
- customType: first.message.customType,
1977
- content: first.message.content,
1978
- display: first.message.display,
1979
- details: first.message.details,
1980
- });
1981
- const later = await beforeAgentStart({ prompt: "implement the feature", systemPrompt: "base prompt" }, ctx);
1982
-
1983
- assert.equal(later.message, undefined);
1984
- assert.match(later.systemPrompt, /# Freeflow Interaction Contract/);
1985
- assert.doesNotMatch(later.systemPrompt, /# Freeflow Runtime Kernel/);
1986
-
1987
- const compactedContext = context(cwd, sessionEntries, []);
1988
- const afterCompaction = await beforeAgentStart(
1989
- { prompt: "continue", systemPrompt: "base prompt" },
1990
- compactedContext,
1991
- );
1992
- assert.equal(afterCompaction.message.customType, "freeflow-workflow-bootstrap");
1993
- } finally {
1994
- await rm(cwd, { recursive: true, force: true });
1995
- }
1996
- });
1997
-
1998
- test("Pi suppresses persisted Workflow context while Skills are disabled", async () => {
1999
- const cwd = await mkdtemp(join(tmpdir(), "freeflow-pi-workflow-bootstrap-disabled-"));
2000
- try {
2001
- await mkdir(join(cwd, ".freeflow"));
2002
- const configPath = join(cwd, ".freeflow/config.json");
2003
- await writeFile(
2004
- configPath,
2005
- JSON.stringify({ defaultMode: "workflow", skills: { enabled: false } }, null, 2),
2006
- "utf8",
2007
- );
2008
-
2009
- const { handlers } = loadExtension();
2010
- const contextHandler = handlers.get("context");
2011
- assert.ok(contextHandler);
2012
-
2013
- const workflowMessage = {
2014
- role: "custom",
2015
- customType: "freeflow-workflow-bootstrap",
2016
- content: "# Freeflow Workflow Bootstrap",
2017
- display: false,
2018
- timestamp: Date.now(),
2019
- };
2020
- const userMessage = {
2021
- role: "user",
2022
- content: "hello",
2023
- timestamp: Date.now(),
2024
- };
2025
- const disabled = await contextHandler({ messages: [workflowMessage, userMessage] }, context(cwd));
2026
- assert.deepEqual(disabled.messages, [userMessage]);
2027
-
2028
- await writeFile(configPath, JSON.stringify({ defaultMode: "workflow" }, null, 2), "utf8");
2029
- const enabled = await contextHandler({ messages: [workflowMessage, userMessage] }, context(cwd));
2030
- assert.equal(enabled, undefined);
2031
- } finally {
2032
- await rm(cwd, { recursive: true, force: true });
2033
- }
2034
- });
2035
-
2036
- test("Pi before_agent_start injects the Freeflow interaction contract on every turn", async () => {
2037
- const cwd = await mkdtemp(join(tmpdir(), "freeflow-pi-core-context-"));
2038
- try {
2039
- await mkdir(join(cwd, ".freeflow"));
2040
- await writeFile(join(cwd, ".freeflow/config.json"), JSON.stringify({ defaultMode: "workflow" }, null, 2), "utf8");
2041
-
2042
- const { handlers } = loadExtension();
2043
- const beforeAgentStart = handlers.get("before_agent_start");
2044
- assert.ok(beforeAgentStart);
2045
-
2046
- const first = await beforeAgentStart({ systemPrompt: "base prompt" }, context(cwd));
2047
- const second = await beforeAgentStart({ systemPrompt: "base prompt" }, context(cwd));
2048
-
2049
- for (const result of [first, second]) {
2050
- assert.match(result.systemPrompt, /# Freeflow Runtime Context/);
2051
- assert.match(result.systemPrompt, /# Freeflow Interaction Contract/);
2052
- assert.doesNotMatch(result.systemPrompt, /# Freeflow Runtime Kernel/);
2053
- assert.match(result.systemPrompt, /Answer questions without inferring action/);
2054
- assert.doesNotMatch(result.systemPrompt, /## Freeflow Runtime Priority/);
2055
- assert.doesNotMatch(result.systemPrompt, /## Loaded Mode Contract Skill/);
2056
- assert.doesNotMatch(result.systemPrompt, /## Loaded Workflow Skill/);
2057
- assert.doesNotMatch(result.systemPrompt, /## Loaded Decision Gate Skill/);
2058
- assert.doesNotMatch(result.systemPrompt, /## Discovery-light/);
2059
- assert.match(result.systemPrompt, /Output router: disabled/);
2060
- assert.doesNotMatch(result.systemPrompt, /## Loaded Output Router Skill/);
2061
- assert.doesNotMatch(result.systemPrompt, /freeflow_search action=transform/);
2062
- assert.doesNotMatch(result.systemPrompt, /## Freeflow Output Router Reminder/);
2063
- assert.doesNotMatch(result.systemPrompt, /## Loaded Workflow Map/);
2064
- assert.doesNotMatch(result.systemPrompt, /## Loaded Output Router Safety Policy/);
2065
- }
2066
- } finally {
2067
- await rm(cwd, { recursive: true, force: true });
2068
- }
2069
- });
2070
-
2071
- test("Pi session_start and session_compact keep the interaction contract on later turns", async () => {
2072
- const cwd = await mkdtemp(join(tmpdir(), "freeflow-pi-session-cache-"));
2073
- try {
2074
- await mkdir(join(cwd, ".freeflow"));
2075
- await writeFile(join(cwd, ".freeflow/config.json"), JSON.stringify({ defaultMode: "workflow" }, null, 2), "utf8");
2076
-
2077
- const { handlers } = loadExtension();
2078
- const beforeAgentStart = handlers.get("before_agent_start");
2079
- const sessionStart = handlers.get("session_start");
2080
- const sessionCompact = handlers.get("session_compact");
2081
- assert.ok(beforeAgentStart);
2082
- assert.ok(sessionStart);
2083
- assert.ok(sessionCompact);
2084
-
2085
- await beforeAgentStart({ systemPrompt: "base prompt" }, context(cwd));
2086
- const afterFirst = await beforeAgentStart({ systemPrompt: "base prompt" }, context(cwd));
2087
- assert.match(afterFirst.systemPrompt, /# Freeflow Interaction Contract/);
2088
-
2089
- await sessionCompact({ reason: "manual" }, context(cwd));
2090
- const afterCompact = await beforeAgentStart({ systemPrompt: "base prompt" }, context(cwd));
2091
- assert.match(afterCompact.systemPrompt, /# Freeflow Interaction Contract/);
2092
- assert.doesNotMatch(afterCompact.systemPrompt, /## Loaded Workflow Skill/);
2093
- assert.doesNotMatch(afterCompact.systemPrompt, /## Loaded Output Router Skill/);
2094
-
2095
- await sessionStart({ reason: "resume" }, context(cwd));
2096
- const afterResume = await beforeAgentStart({ systemPrompt: "base prompt" }, context(cwd));
2097
- assert.match(afterResume.systemPrompt, /# Freeflow Interaction Contract/);
2098
- assert.doesNotMatch(afterResume.systemPrompt, /## Loaded Workflow Skill/);
2099
- assert.doesNotMatch(afterResume.systemPrompt, /## Loaded Output Router Skill/);
2100
- } finally {
2101
- await rm(cwd, { recursive: true, force: true });
2102
- }
2103
- });
2104
-
2105
- test("Pi capability config disables output-router context, active tools, and execution", async () => {
2106
- const cwd = await mkdtemp(join(tmpdir(), "freeflow-pi-output-router-disabled-"));
2107
- try {
2108
- await mkdir(join(cwd, ".freeflow"));
2109
- await writeFile(
2110
- join(cwd, ".freeflow/config.json"),
2111
- JSON.stringify({ defaultMode: "workflow", outputRouter: { enabled: false } }, null, 2),
2112
- "utf8",
2113
- );
2114
-
2115
- const { handlers, tools, activeToolNames } = loadExtension();
2116
- const beforeAgentStart = handlers.get("before_agent_start");
2117
- assert.ok(beforeAgentStart);
2118
-
2119
- const result = await beforeAgentStart({ systemPrompt: "base prompt" }, context(cwd));
2120
- assert.match(result.systemPrompt, /Output router: disabled/);
2121
- assert.doesNotMatch(result.systemPrompt, /## Loaded Output Router Skill/);
2122
- assert.ok(!activeToolNames().includes("freeflow_search"));
2123
- assert.ok(!activeToolNames().includes("freeflow_run"));
2124
- assert.ok(!activeToolNames().includes("freeflow_batch"));
2125
-
2126
- const searchTool = tools.find((tool) => tool.name === "freeflow_search");
2127
- assert.ok(searchTool);
2128
- const disabled = await searchTool.execute(
2129
- "search-disabled",
2130
- { action: "locate", query: "x" },
2131
- undefined,
2132
- undefined,
2133
- context(cwd),
2134
- );
2135
- assert.match(disabled.content[0].text, /freeflow_search\|disabled_by_config/);
2136
-
2137
- const guard = handlers.get("tool_call");
2138
- const blocked = await guard({ toolName: "freeflow_search" }, context(cwd));
2139
- assert.equal(blocked.block, true);
2140
- assert.match(blocked.reason, /disabled by Freeflow config/);
2141
- } finally {
2142
- await rm(cwd, { recursive: true, force: true });
2143
- }
2144
- });
2145
-
2146
- test("Pi output-router command updates config and reloads", async () => {
2147
- const cwd = await mkdtemp(join(tmpdir(), "freeflow-pi-capability-command-"));
2148
- try {
2149
- await mkdir(join(cwd, ".freeflow"));
2150
- await writeFile(join(cwd, ".freeflow/config.json"), JSON.stringify({}, null, 2), "utf8");
2151
-
2152
- const { commands } = loadExtension();
2153
- const outputRouterCommand = commands.find((command) => command.name === "output-router");
2154
- assert.ok(outputRouterCommand);
2155
-
2156
- const outputCtx = context(cwd);
2157
- await outputRouterCommand.definition.handler("enable", outputCtx);
2158
- const afterOutput = JSON.parse(await readFile(join(cwd, ".freeflow/config.json"), "utf8"));
2159
- assert.equal(afterOutput.outputRouter.enabled, true);
2160
- assert.equal(outputCtx.reloads.length, 1);
2161
- } finally {
2162
- await rm(cwd, { recursive: true, force: true });
2163
- }
2164
- });
2165
-
2166
- test("Pi output-router settings UI toggles multiple config values and reloads once", async () => {
2167
- const cwd = await mkdtemp(join(tmpdir(), "freeflow-pi-output-router-settings-"));
2168
- try {
2169
- await mkdir(join(cwd, ".freeflow"));
2170
- await writeFile(
2171
- join(cwd, ".freeflow/config.json"),
2172
- JSON.stringify(
2173
- {
2174
- defaultMode: "workflow",
2175
- outputRouter: { generatedPaths: ["graphify-out/**"] },
2176
- },
2177
- null,
2178
- 2,
2179
- ),
2180
- "utf8",
2181
- );
2182
-
2183
- const { commands } = loadExtension();
2184
- const outputRouterCommand = commands.find((command) => command.name === "output-router");
2185
- assert.ok(outputRouterCommand);
2186
-
2187
- const ctx = context(cwd);
2188
- ctx.ui.custom = async (factory) => {
2189
- let result;
2190
- const component = factory({ requestRender() {} }, testTheme, {}, (value) => {
2191
- result = value;
2192
- });
2193
- assert.match(renderText(component), /Output Router Settings/);
2194
- component.handleInput("\r");
2195
- component.handleInput("\u001b[A");
2196
- component.handleInput("\r"); // Output Router enabled
2197
- await component.waitForWrites();
2198
- for (let index = 0; index < 9; index++) {
2199
- component.handleInput("\u001b[B");
2200
- }
2201
- component.handleInput("\r");
2202
- component.handleInput("\u001b[A");
2203
- component.handleInput("\r"); // Script transform enabled
2204
- await component.waitForWrites();
2205
- component.handleInput("\u001b");
2206
- return result;
2207
- };
2208
-
2209
- await outputRouterCommand.definition.handler("", ctx);
2210
-
2211
- const after = JSON.parse(await readFile(join(cwd, ".freeflow/config.json"), "utf8"));
2212
- assert.equal(after.outputRouter.enabled, true);
2213
- assert.deepEqual(after.outputRouter.hints.generatedPathGlobs, ["graphify-out/**"]);
2214
- assert.equal(after.outputRouter.generatedPaths, undefined);
2215
- assert.equal(after.outputRouter.scriptTransform.enabled, true);
2216
- assert.equal(ctx.reloads.length, 1);
2217
- } finally {
2218
- await rm(cwd, { recursive: true, force: true });
2219
- }
2220
- });
2221
-
2222
- test("Pi output-router status summarizes master and subfeature state", async () => {
2223
- const cwd = await mkdtemp(join(tmpdir(), "freeflow-pi-output-router-status-"));
2224
- try {
2225
- await mkdir(join(cwd, ".freeflow"));
2226
- await writeFile(
2227
- join(cwd, ".freeflow/config.json"),
2228
- JSON.stringify(
2229
- {
2230
- defaultMode: "workflow",
2231
- outputRouter: {
2232
- enabled: true,
2233
- postToolRouting: "safety-net",
2234
- scriptTransform: { enabled: true },
2235
- observedRouting: { enabled: true },
2236
- },
2237
- },
2238
- null,
2239
- 2,
2240
- ),
2241
- "utf8",
2242
- );
2243
-
2244
- const { commands } = loadExtension();
2245
- const outputRouterCommand = commands.find((command) => command.name === "output-router");
2246
- assert.ok(outputRouterCommand);
2247
-
2248
- const ctx = context(cwd);
2249
- await outputRouterCommand.definition.handler("status", ctx);
2250
-
2251
- assert.equal(ctx.notifications.length, 1);
2252
- assert.match(ctx.notifications[0].message, /Output Router: enabled/);
2253
- assert.match(ctx.notifications[0].message, /script transform: enabled/);
2254
- assert.match(ctx.notifications[0].message, /observed routing: enabled/);
2255
- assert.match(ctx.notifications[0].message, /native safety net: safety-net/);
2256
- } finally {
2257
- await rm(cwd, { recursive: true, force: true });
2258
- }
2259
- });
2260
-
2261
- test("Pi freeflow_status reports effective defaults without writing config", async () => {
2262
- const cwd = await mkdtemp(join(tmpdir(), "freeflow-pi-status-minimal-"));
2263
- try {
2264
- await mkdir(join(cwd, ".freeflow"));
2265
- const configPath = join(cwd, ".freeflow/config.json");
2266
- const configText = JSON.stringify({ defaultMode: "workflow" }, null, 2);
2267
- await writeFile(configPath, configText, "utf8");
2268
-
2269
- const { tools } = loadExtension();
2270
- const statusTool = tools.find((tool) => tool.name === "freeflow_status");
2271
- assert.ok(statusTool);
2272
-
2273
- const result = await statusTool.execute("status-minimal", { action: "doctor" }, undefined, undefined, context(cwd));
2274
- const report = JSON.parse(result.content[0].text);
2275
-
2276
- assert.equal(report.toolStatus, "ok");
2277
- assert.equal(report.action, "doctor");
2278
- assert.equal(report.mode.defaultMode, "workflow");
2279
- assert.deepEqual(report.effectiveConfig.interactionContract, {
2280
- enabled: true,
2281
- effective: true,
2282
- });
2283
- assert.equal(report.effectiveDefaults.interactionContract, true);
2284
- assert.equal(report.effectiveConfig.outputRouter.enabled, false);
2285
- assert.equal(report.effectiveConfig.outputRouter.profile, "standard");
2286
- assert.equal(report.effectiveConfig.outputRouter.postToolRouting, "off");
2287
- assert.equal("capture" in report.effectiveConfig, false);
2288
- assert.equal("providers" in report.effectiveConfig, false);
2289
- assert.equal(report.effectiveConfig.outputRouter.observedRouting.enabled, false);
2290
- assert.deepEqual(report.effectiveConfig.outputRouter.observedRouting.mcp.servers, {});
2291
- assert.equal(report.effectiveConfig.outputRouter.scriptTransform.enabled, false);
2292
- assert.equal(report.effectiveLocalConfig.processing.unsafeUnsandboxed.enabled, false);
2293
- assert.equal(report.processing.unsafeUnsandboxed.enabled, false);
2294
- assert.equal(report.processing.unsafeUnsandboxed.status, "disabled");
2295
- assert.equal(report.localConfigExists, false);
2296
- assert.deepEqual(report.localConfigWarnings, []);
2297
- assert.equal(report.scriptTransform.enabled, false);
2298
- assert.equal(report.scriptTransform.executionStatus, "disabled");
2299
- assert.ok(["available", "unavailable"].includes(report.scriptTransform.adapterStatus));
2300
- assert.equal(report.scriptTransform.adapterContractVersion, 1);
2301
- assert.deepEqual(report.scriptTransform.configuredLanguages, ["javascript", "python", "jq"]);
2302
- const reportedLanguages = [
2303
- ...report.scriptTransform.availableLanguages,
2304
- ...report.scriptTransform.unavailableLanguages.map((entry) => entry.language),
2305
- ].sort();
2306
- assert.deepEqual(reportedLanguages, ["javascript", "jq", "python"]);
2307
- assert.ok(report.scriptTransform.requiredProofs.includes("network_access_denied"));
2308
- assert.ok(
2309
- report.scriptTransform.candidateMechanisms.some(
2310
- (candidate) => candidate.id === "node-vm" && candidate.status === "rejected",
2311
- ),
2312
- );
2313
- assert.ok(
2314
- report.scriptTransform.candidateMechanisms.some(
2315
- (candidate) => candidate.id === "os-sandbox-adapter" && candidate.status === "candidate_unproven",
2316
- ),
2317
- );
2318
- assert.equal(report.scriptTransform.network, "off");
2319
- assert.equal(report.scriptTransform.rawScriptPersistence, "disabled");
2320
- assert.equal(report.observedRouting.host.name, "pi");
2321
- assert.equal(report.observedRouting.host.outputReplacement, "available");
2322
- assert.equal(report.vaultIndex.engine, "local-json-sidecar");
2323
- assert.equal(typeof report.vaultIndex.available, "boolean");
2324
- assert.equal(report.vaultIndex.degraded, false);
2325
- assert.equal(report.vaultIndex.stale, false);
2326
- assert.equal(report.vaultIndex.rebuildRecommended, false);
2327
- assert.equal(typeof report.vaultIndex.entryCount, "number");
2328
- assert.ok(report.vaultIndex.entryCount >= 0);
2329
- assert.equal(report.observedRouting.unsupportedPersistenceModes.includes("redacted"), true);
2330
- assert.deepEqual(report.configWarnings, []);
2331
- assert.match(report.vault.root, /freeflow-router\/vault$/);
2332
- assert.ok(
2333
- [
2334
- "writable",
2335
- "missing_ancestor_writable",
2336
- "missing_ancestor_unavailable",
2337
- "not_directory",
2338
- "not_writable",
2339
- "unknown",
2340
- ].includes(report.vault.writability.status),
2341
- );
2342
- assert.equal(await readFile(configPath, "utf8"), configText);
2343
- } finally {
2344
- await rm(cwd, { recursive: true, force: true });
2345
- }
2346
- });
2347
-
2348
- test("Pi freeflow_status reports layered core sources and dormant mode", async () => {
2349
- const cwd = await mkdtemp(join(tmpdir(), "freeflow-pi-status-layered-core-"));
2350
- try {
2351
- await mkdir(join(cwd, ".freeflow"));
2352
- await writeFile(
2353
- join(cwd, ".freeflow/config.json"),
2354
- JSON.stringify({
2355
- enabled: true,
2356
- interactionContract: true,
2357
- defaultMode: "workflow",
2358
- skills: { enabled: true },
2359
- }),
2360
- "utf8",
2361
- );
2362
- await writeFile(
2363
- join(cwd, ".freeflow/local.json"),
2364
- JSON.stringify({
2365
- interactionContract: false,
2366
- defaultMode: "strict-workflow",
2367
- skills: { enabled: false },
2368
- }),
2369
- "utf8",
2370
- );
2371
-
2372
- const { tools } = loadExtension();
2373
- const statusTool = tools.find((tool) => tool.name === "freeflow_status");
2374
- assert.ok(statusTool);
2375
- const result = await statusTool.execute(
2376
- "status-layered-core",
2377
- { action: "status" },
2378
- undefined,
2379
- undefined,
2380
- context(cwd),
2381
- );
2382
- const report = JSON.parse(result.content[0].text);
2383
-
2384
- assert.equal(report.configValid, true);
2385
- assert.equal(report.localConfigValid, true);
2386
- assert.deepEqual(report.configuration.sources, {
2387
- enabled: "repository",
2388
- interactionContract: "local",
2389
- skillsEnabled: "local",
2390
- defaultMode: "local",
2391
- });
2392
- assert.equal(report.effectiveConfig.enabled, true);
2393
- assert.equal(report.effectiveConfig.defaultMode, "strict-workflow");
2394
- assert.deepEqual(report.effectiveConfig.interactionContract, {
2395
- enabled: false,
2396
- effective: false,
2397
- });
2398
- assert.deepEqual(report.effectiveConfig.skills, {
2399
- enabled: false,
2400
- effective: false,
2401
- });
2402
- assert.equal(report.mode.repositoryDefaultMode, "workflow");
2403
- assert.equal(report.mode.repositoryDefaultModeSource, "repository");
2404
- assert.equal(report.mode.personalDefaultMode, "strict-workflow");
2405
- assert.equal(report.mode.defaultMode, "strict-workflow");
2406
- assert.equal(report.mode.defaultModeSource, "local");
2407
- assert.equal(report.mode.sessionMode, null);
2408
- assert.equal(report.mode.active, false);
2409
- assert.equal(report.mode.resolvedMode, "strict-workflow");
2410
- assert.equal(report.mode.effectiveMode, null);
2411
-
2412
- const expanded = renderText(statusTool.renderResult(result, { expanded: true }, testTheme));
2413
- assert.match(expanded, /repository:\s+valid/);
2414
- assert.match(expanded, /personal:\s+valid/);
2415
- assert.match(expanded, /interactionContract:\s+false\s+\(local\)/);
2416
- assert.match(expanded, /skills:\s+false\s+\(local\)/);
2417
- assert.match(expanded, /active:\s+false/);
2418
- assert.match(expanded, /effective:\s+none/);
2419
- assert.match(expanded, /resolved:\s+strict-workflow/);
2420
- assert.match(expanded, /repository default:\s+workflow\s+\(repository\)/);
2421
- assert.match(expanded, /personal default override:\s+strict-workflow/);
2422
- assert.match(expanded, /configured default:\s+strict-workflow\s+\(local\)/);
2423
- } finally {
2424
- await rm(cwd, { recursive: true, force: true });
2425
- }
2426
- });
2427
-
2428
- test("Pi freeflow_status fails closed and explains invalid local core config", async () => {
2429
- const cwd = await mkdtemp(join(tmpdir(), "freeflow-pi-status-invalid-local-"));
2430
- try {
2431
- await mkdir(join(cwd, ".freeflow"));
2432
- await writeFile(join(cwd, ".freeflow/config.json"), JSON.stringify({ defaultMode: "workflow" }), "utf8");
2433
- await writeFile(join(cwd, ".freeflow/local.json"), JSON.stringify({ skills: { enabled: "no" } }), "utf8");
2434
-
2435
- const { tools } = loadExtension();
2436
- const statusTool = tools.find((tool) => tool.name === "freeflow_status");
2437
- assert.ok(statusTool);
2438
- const result = await statusTool.execute(
2439
- "status-invalid-local",
2440
- { action: "doctor" },
2441
- undefined,
2442
- undefined,
2443
- context(cwd),
2444
- );
2445
- const report = JSON.parse(result.content[0].text);
2446
-
2447
- assert.equal(report.configValid, false);
2448
- assert.equal(report.repositoryConfigValid, true);
2449
- assert.equal(report.localConfigValid, false);
2450
- assert.equal(report.effectiveConfig.configured, false);
2451
- assert.equal(report.effectiveConfig.enabled, false);
2452
- assert.equal(report.mode.active, false);
2453
- assert.match(report.configuration.personal.error, /skills\.enabled/);
2454
- assert.ok(
2455
- report.localConfigWarnings.some(
2456
- (warning) => warning.includes(".freeflow/local.json is invalid") && warning.includes("runtime is inactive"),
2457
- ),
2458
- );
2459
-
2460
- const collapsed = renderText(statusTool.renderResult(result, { expanded: false }, testTheme));
2461
- assert.match(collapsed, /freeflow off/);
2462
- assert.match(collapsed, /mode dormant \(workflow\)/);
2463
- assert.match(collapsed, /warnings=1/);
2464
- } finally {
2465
- await rm(cwd, { recursive: true, force: true });
2466
- }
2467
- });
2468
-
2469
- test("Pi freeflow_status reports vault writability without creating directories", async () => {
2470
- const cwd = await mkdtemp(join(tmpdir(), "freeflow-pi-status-vault-"));
2471
- try {
2472
- await mkdir(join(cwd, ".freeflow"));
2473
- const nestedVault = join(cwd, "missing", "nested", "vault");
2474
- await writeFile(
2475
- join(cwd, ".freeflow/config.json"),
2476
- JSON.stringify({
2477
- defaultMode: "workflow",
2478
- outputRouter: { vaultRoot: nestedVault },
2479
- }),
2480
- "utf8",
2481
- );
2482
-
2483
- const { tools } = loadExtension();
2484
- const statusTool = tools.find((tool) => tool.name === "freeflow_status");
2485
- const missingResult = await statusTool.execute(
2486
- "status-vault-missing",
2487
- { action: "doctor" },
2488
- undefined,
2489
- undefined,
2490
- context(cwd),
2491
- );
2492
- const missingReport = JSON.parse(missingResult.content[0].text);
2493
-
2494
- assert.equal(missingReport.vault.writability.status, "missing_ancestor_writable");
2495
- await assert.rejects(readFile(nestedVault, "utf8"));
2496
-
2497
- const fileVault = join(cwd, "vault-file");
2498
- await writeFile(fileVault, "not a directory", "utf8");
2499
- await writeFile(
2500
- join(cwd, ".freeflow/config.json"),
2501
- JSON.stringify({
2502
- defaultMode: "workflow",
2503
- outputRouter: { enabled: true, vaultRoot: fileVault },
2504
- }),
2505
- "utf8",
2506
- );
2507
- const fileResult = await statusTool.execute(
2508
- "status-vault-file",
2509
- { action: "doctor" },
2510
- undefined,
2511
- undefined,
2512
- context(cwd),
2513
- );
2514
- const fileReport = JSON.parse(fileResult.content[0].text);
2515
- assert.equal(fileReport.vault.writability.status, "not_directory");
2516
- } finally {
2517
- await rm(cwd, { recursive: true, force: true });
2518
- }
2519
- });
2520
-
2521
- test("Pi freeflow_status reports local unsafe processing opt-in without shared config", async () => {
2522
- const cwd = await mkdtemp(join(tmpdir(), "freeflow-pi-status-local-processing-"));
2523
- try {
2524
- await mkdir(join(cwd, ".freeflow"));
2525
- await writeFile(join(cwd, ".freeflow/config.json"), JSON.stringify({ defaultMode: "workflow" }), "utf8");
2526
- await writeFile(
2527
- join(cwd, ".freeflow/local.json"),
2528
- JSON.stringify({ processing: { unsafeUnsandboxed: { enabled: true } } }),
2529
- "utf8",
2530
- );
2531
-
2532
- const { tools } = loadExtension();
2533
- const statusTool = tools.find((tool) => tool.name === "freeflow_status");
2534
- const result = await statusTool.execute(
2535
- "status-local-processing",
2536
- { action: "doctor" },
2537
- undefined,
2538
- undefined,
2539
- context(cwd),
2540
- );
2541
- const report = JSON.parse(result.content[0].text);
2542
-
2543
- assert.equal(report.localConfigExists, true);
2544
- assert.equal(report.effectiveLocalConfig.processing.unsafeUnsandboxed.enabled, true);
2545
- assert.equal(report.processing.unsafeUnsandboxed.enabled, true);
2546
- assert.equal(report.processing.unsafeUnsandboxed.status, "enabled_unsafe");
2547
- assert.deepEqual(report.configWarnings, []);
2548
- assert.deepEqual(report.localConfigWarnings, []);
2549
- } finally {
2550
- await rm(cwd, { recursive: true, force: true });
2551
- }
2552
- });
2553
-
2554
- test("Pi freeflow_status warns that shared config cannot enable unsafe processing", async () => {
2555
- const cwd = await mkdtemp(join(tmpdir(), "freeflow-pi-status-shared-processing-"));
2556
- try {
2557
- await mkdir(join(cwd, ".freeflow"));
2558
- await writeFile(
2559
- join(cwd, ".freeflow/config.json"),
2560
- JSON.stringify({
2561
- defaultMode: "workflow",
2562
- processing: { unsafeUnsandboxed: { enabled: true } },
2563
- }),
2564
- "utf8",
2565
- );
2566
-
2567
- const { tools } = loadExtension();
2568
- const statusTool = tools.find((tool) => tool.name === "freeflow_status");
2569
- const result = await statusTool.execute(
2570
- "status-shared-processing",
2571
- { action: "doctor" },
2572
- undefined,
2573
- undefined,
2574
- context(cwd),
2575
- );
2576
- const report = JSON.parse(result.content[0].text);
2577
-
2578
- assert.equal(report.effectiveLocalConfig.processing.unsafeUnsandboxed.enabled, false);
2579
- assert.equal(report.processing.unsafeUnsandboxed.enabled, false);
2580
- assert.ok(report.configWarnings.some((warning) => warning.includes("processing config is ignored")));
2581
- } finally {
2582
- await rm(cwd, { recursive: true, force: true });
2583
- }
2584
- });
2585
-
2586
- test("Pi freeflow_status reports configured observed routing", async () => {
2587
- const cwd = await mkdtemp(join(tmpdir(), "freeflow-pi-status-observed-"));
2588
- try {
2589
- await mkdir(join(cwd, ".freeflow"));
2590
- await writeFile(
2591
- join(cwd, ".freeflow/config.json"),
2592
- JSON.stringify({
2593
- defaultMode: "workflow",
2594
- outputRouter: {
2595
- enabled: true,
2596
- observedRouting: {
2597
- enabled: true,
2598
- onRoutingFailure: "fail-open",
2599
- mcp: {
2600
- servers: {
2601
- github: { enabled: true, persistence: "exact" },
2602
- gmail: { enabled: true, persistence: "metadata-only" },
2603
- },
2604
- },
2605
- web: { enabled: true, persistence: "exact" },
2606
- fetch: { enabled: false },
2607
- codeSearch: { enabled: true, persistence: "none" },
2608
- },
2609
- scriptTransform: {
2610
- enabled: true,
2611
- languages: ["python"],
2612
- limits: {
2613
- timeoutMs: 1000,
2614
- maxInputBytes: 2048,
2615
- maxOutputBytes: 4096,
2616
- },
2617
- },
2618
- },
2619
- }),
2620
- "utf8",
2621
- );
2622
-
2623
- const { tools } = loadExtension();
2624
- const statusTool = tools.find((tool) => tool.name === "freeflow_status");
2625
- const result = await statusTool.execute(
2626
- "status-observed",
2627
- { action: "doctor" },
2628
- undefined,
2629
- undefined,
2630
- context(cwd),
2631
- );
2632
- const report = JSON.parse(result.content[0].text);
2633
-
2634
- assert.equal(report.effectiveConfig.outputRouter.observedRouting.enabled, true);
2635
- assert.equal(report.effectiveConfig.outputRouter.observedRouting.onRoutingFailure, "fail-open");
2636
- assert.deepEqual(report.effectiveConfig.outputRouter.observedRouting.mcp.servers.github, {
2637
- enabled: true,
2638
- persistence: "exact",
2639
- });
2640
- assert.deepEqual(report.effectiveConfig.outputRouter.observedRouting.mcp.servers.gmail, {
2641
- enabled: true,
2642
- persistence: "metadata-only",
2643
- });
2644
- assert.deepEqual(report.effectiveConfig.outputRouter.observedRouting.web, {
2645
- enabled: true,
2646
- persistence: "exact",
2647
- });
2648
- assert.deepEqual(report.effectiveConfig.outputRouter.observedRouting.fetch, {
2649
- enabled: false,
2650
- persistence: "none",
2651
- });
2652
- assert.deepEqual(report.effectiveConfig.outputRouter.observedRouting.codeSearch, {
2653
- enabled: true,
2654
- persistence: "none",
2655
- });
2656
- assert.equal(report.observedRouting.enabled, true);
2657
- assert.equal(report.observedRouting.mcp.configuredServerCount, 2);
2658
- assert.equal(report.effectiveConfig.outputRouter.scriptTransform.enabled, true);
2659
- assert.deepEqual(report.effectiveConfig.outputRouter.scriptTransform.languages, ["python"]);
2660
- assert.equal(report.scriptTransform.enabled, true);
2661
- assert.ok(["available", "adapter_unavailable"].includes(report.scriptTransform.executionStatus));
2662
- assert.equal(typeof report.scriptTransform.adapterAvailable, "boolean");
2663
- if (report.scriptTransform.adapterAvailable) {
2664
- assert.ok(report.scriptTransform.availableLanguages.includes("python"));
2665
- }
2666
- assert.deepEqual(report.observedRouting.persistenceModes, ["exact", "metadata-only", "none"]);
2667
- assert.deepEqual(report.configWarnings, []);
2668
- } finally {
2669
- await rm(cwd, { recursive: true, force: true });
2670
- }
2671
- });
2672
-
2673
- test("Pi freeflow_status reports invalid config warnings and safe fallbacks", async () => {
2674
- const cwd = await mkdtemp(join(tmpdir(), "freeflow-pi-status-invalid-"));
2675
- try {
2676
- await mkdir(join(cwd, ".freeflow"));
2677
- await writeFile(
2678
- join(cwd, ".freeflow/config.json"),
2679
- JSON.stringify({
2680
- defaultMode: "workflow",
2681
- outputRouter: {
2682
- enabled: "yes",
2683
- profile: "future",
2684
- scriptTransform: {
2685
- enabled: "yes",
2686
- sandbox: "none",
2687
- languages: ["ruby"],
2688
- network: "on",
2689
- },
2690
- observedRouting: {
2691
- enabled: "yes",
2692
- mcp: {
2693
- servers: { github: { enabled: true, persistence: "redacted" } },
2694
- },
2695
- web: { enabled: true },
2696
- },
2697
- },
2698
- capture: { freeflowMediated: "metadata-only", directHostTools: "raw" },
2699
- providers: { enabled: [{ id: "serena", mode: "write" }] },
2700
- }),
2701
- "utf8",
2702
- );
2703
-
2704
- const { tools } = loadExtension();
2705
- const statusTool = tools.find((tool) => tool.name === "freeflow_status");
2706
- const result = await statusTool.execute("status-invalid", { action: "doctor" }, undefined, undefined, context(cwd));
2707
- const report = JSON.parse(result.content[0].text);
2708
-
2709
- assert.equal(report.effectiveConfig.outputRouter.enabled, false);
2710
- assert.equal(report.effectiveConfig.outputRouter.profile, "standard");
2711
- assert.equal("capture" in report.effectiveConfig, false);
2712
- assert.equal("providers" in report.effectiveConfig, false);
2713
- assert.equal(report.effectiveConfig.outputRouter.observedRouting.enabled, false);
2714
- assert.equal(report.effectiveConfig.outputRouter.scriptTransform.enabled, false);
2715
- assert.deepEqual(report.effectiveConfig.outputRouter.scriptTransform.languages, ["javascript", "python", "jq"]);
2716
- assert.equal(report.effectiveConfig.outputRouter.observedRouting.mcp.servers.github.persistence, "metadata-only");
2717
- assert.equal(report.effectiveConfig.outputRouter.observedRouting.web.persistence, "metadata-only");
2718
- assert.ok(report.configWarnings.some((warning) => warning.includes("outputRouter.enabled")));
2719
- assert.ok(report.configWarnings.some((warning) => warning.includes("outputRouter.profile")));
2720
- assert.ok(report.configWarnings.some((warning) => warning.includes("outputRouter.scriptTransform.enabled")));
2721
- assert.ok(report.configWarnings.some((warning) => warning.includes("outputRouter.scriptTransform.sandbox")));
2722
- assert.ok(report.configWarnings.some((warning) => warning.includes("outputRouter.scriptTransform.languages")));
2723
- assert.ok(report.configWarnings.some((warning) => warning.includes("outputRouter.scriptTransform.network")));
2724
- assert.ok(report.configWarnings.some((warning) => warning.includes("outputRouter.observedRouting.enabled")));
2725
- assert.ok(
2726
- report.configWarnings.some(
2727
- (warning) =>
2728
- warning.includes("outputRouter.observedRouting.mcp.servers.github.persistence") &&
2729
- warning.includes("redacted"),
2730
- ),
2731
- );
2732
- assert.ok(
2733
- report.configWarnings.some((warning) => warning.includes("outputRouter.observedRouting.web.persistence")),
2734
- );
2735
- } finally {
2736
- await rm(cwd, { recursive: true, force: true });
2737
- }
2738
- });
2739
-
2740
- test("Pi freeflow_status migration recommendations are non-destructive", async () => {
2741
- const cwd = await mkdtemp(join(tmpdir(), "freeflow-pi-status-migration-"));
2742
- try {
2743
- await mkdir(join(cwd, ".freeflow"));
2744
- const configPath = join(cwd, ".freeflow/config.json");
2745
- const configText = JSON.stringify(
2746
- {
2747
- defaultMode: "workflow",
2748
- outputRouter: {
2749
- enabled: true,
2750
- profile: "standard",
2751
- postToolRouting: "off",
2752
- largeOutputBytes: 64000,
2753
- largeOutputLines: 1000,
2754
- vaultRoot: "~/.cache/freeflow-router/vault",
2755
- vaultRetentionDays: 7,
2756
- },
2757
- capture: { freeflowMediated: "raw", directHostTools: "off" },
2758
- providers: { enabled: [] },
2759
- },
2760
- null,
2761
- 2,
2762
- );
2763
- await writeFile(configPath, configText, "utf8");
2764
-
2765
- const { tools } = loadExtension();
2766
- const statusTool = tools.find((tool) => tool.name === "freeflow_status");
2767
- const result = await statusTool.execute(
2768
- "status-migration",
2769
- { action: "migration" },
2770
- undefined,
2771
- undefined,
2772
- context(cwd),
2773
- );
2774
- const report = JSON.parse(result.content[0].text);
2775
-
2776
- assert.equal(report.action, "migration");
2777
- assert.equal(report.migration.applied, false);
2778
- assert.equal(report.migration.requiresConfirmation, true);
2779
- assert.ok(
2780
- report.migration.recommendations.some((recommendation) => recommendation.path === "outputRouter.postToolRouting"),
2781
- );
2782
- assert.ok(report.migration.recommendations.some((recommendation) => recommendation.path === "capture"));
2783
- assert.ok(report.migration.recommendations.some((recommendation) => recommendation.path === "providers"));
2784
- assert.equal(await readFile(configPath, "utf8"), configText);
2785
- } finally {
2786
- await rm(cwd, { recursive: true, force: true });
2787
- }
2788
- });
2789
-
2790
- test("Pi outputRouter.enabled=false suppresses router context and native safety net", async () => {
2791
- const cwd = await mkdtemp(join(tmpdir(), "freeflow-pi-router-disabled-"));
2792
- try {
2793
- await mkdir(join(cwd, ".freeflow"));
2794
- await writeFile(
2795
- join(cwd, ".freeflow/config.json"),
2796
- JSON.stringify({
2797
- defaultMode: "workflow",
2798
- outputRouter: {
2799
- enabled: false,
2800
- postToolRouting: "safety-net",
2801
- largeOutputLines: 1,
2802
- largeOutputBytes: 1,
2803
- },
2804
- }),
2805
- "utf8",
2806
- );
2807
-
2808
- const { handlers } = loadExtension();
2809
- const beforeAgentStart = handlers.get("before_agent_start");
2810
- const result = await beforeAgentStart({ systemPrompt: "base prompt" }, context(cwd));
2811
- assert.doesNotMatch(result.systemPrompt, /## Loaded Output Router Skill/);
2812
-
2813
- const toolResult = await handlers.get("tool_result")(
2814
- {
2815
- type: "tool_result",
2816
- toolName: "read",
2817
- toolCallId: "read-router-disabled",
2818
- input: { path: "large.txt" },
2819
- content: [{ type: "text", text: "line 1\nline 2" }],
2820
- details: undefined,
2821
- isError: false,
2822
- },
2823
- context(cwd),
2824
- );
2825
- assert.equal(toolResult, undefined);
2826
- } finally {
2827
- await rm(cwd, { recursive: true, force: true });
2828
- }
2829
- });
2830
-
2831
- test("Pi output-router context mentions native safety net only when config enables it", async () => {
2832
- const cwd = await mkdtemp(join(tmpdir(), "freeflow-pi-config-"));
2833
- try {
2834
- await mkdir(join(cwd, ".freeflow"));
2835
- await writeFile(
2836
- join(cwd, ".freeflow/config.json"),
2837
- JSON.stringify({
2838
- defaultMode: "workflow",
2839
- outputRouter: { enabled: true, postToolRouting: "safety-net" },
2840
- }),
2841
- "utf8",
2842
- );
2843
-
2844
- const { handlers } = loadExtension();
2845
- const beforeAgentStart = handlers.get("before_agent_start");
2846
- const result = await beforeAgentStart({ systemPrompt: "base prompt" }, context(cwd));
2847
-
2848
- assert.match(result.systemPrompt, /large native read\/bash outputs may be vaulted/);
2849
- assert.match(result.systemPrompt, /## Loaded Output Router Skill/);
2850
- assert.doesNotMatch(result.systemPrompt, /## Loaded Output Router Safety Policy/);
2851
- assert.match(result.systemPrompt, /freeflow_search/);
2852
- assert.match(result.systemPrompt, /freeflow_search action=transform/);
2853
- } finally {
2854
- await rm(cwd, { recursive: true, force: true });
2855
- }
2856
- });
2857
-
2858
- test("Pi freeflow_search renders compact and expanded routed evidence UI", () => {
2859
- const { tools } = loadExtension();
2860
- const searchTool = tools.find((tool) => tool.name === "freeflow_search");
2861
- assert.ok(searchTool);
2862
- assert.ok(searchTool.parameters.properties.action.enum.includes("get"));
2863
-
2864
- const call = renderText(
2865
- searchTool.renderCall(
2866
- {
2867
- action: "query",
2868
- source: {
2869
- kind: "repo",
2870
- path: "docs/codex-cli-agent-harness/passes/2026-06-12-pass-3-sandboxing-and-permissions.md",
2871
- },
2872
- query: "SandboxPermissions Plain-language meaning",
2873
- },
2874
- testTheme,
2875
- ),
2876
- 200,
2877
- );
2878
- assert.match(call, /freeflow_search query repo/);
2879
- assert.match(call, /SandboxPermissions/);
2880
-
2881
- const toolResult = {
2882
- content: [{ type: "text", text: "raw json should not be the visible UI" }],
2883
- details: {
2884
- result: {
2885
- toolStatus: "ok",
2886
- decisionId: "ffdec_test",
2887
- preserve: "important",
2888
- source: { kind: "repo", path: "docs/example.md" },
2889
- routing: {
2890
- status: "routed",
2891
- route: "search",
2892
- reason: "Deterministic test route.",
2893
- },
2894
- evidence: [
2895
- {
2896
- id: "ev_test",
2897
- source: { kind: "repo", path: "docs/example.md" },
2898
- path: "docs/example.md",
2899
- lines: "523-527",
2900
- excerpt: "### Sandbox Permissions\n\n`SandboxPermissions` is a per-command request shape.",
2901
- why: "Matched exact heading and identifier.",
2902
- window: "small",
2903
- expandable: true,
2904
- },
2905
- ],
2906
- recovery: {
2907
- how: "Use freeflow_search action=expand with evidenceId=ev_test.",
2908
- evidenceId: "ev_test",
2909
- },
2910
- },
2911
- },
2912
- };
2913
-
2914
- const collapsed = renderText(searchTool.renderResult(toolResult, { expanded: false }, testTheme));
2915
- assert.match(collapsed, /1 evidence packet/);
2916
- assert.match(collapsed, /docs\/example\.md:523-527/);
2917
- assert.match(collapsed, /ctrl\+o to expand/);
2918
- assert.doesNotMatch(collapsed, /raw json/);
2919
-
2920
- const expanded = renderText(searchTool.renderResult(toolResult, { expanded: true }, testTheme));
2921
- assert.match(expanded, /Source/);
2922
- assert.match(expanded, /preserve: important/);
2923
- assert.match(expanded, /Storage/);
2924
- assert.match(expanded, /decisionId: ffdec_test/);
2925
- assert.match(expanded, /Evidence/);
2926
- assert.match(expanded, /evidenceId: ev_test/);
2927
- assert.match(expanded, /source: repo docs\/example\.md/);
2928
- assert.match(expanded, /expandable: true/);
2929
- assert.match(expanded, /exact search: action=retrieve source.kind=repo lineRange=523-527 path=docs\/example\.md/);
2930
- assert.match(expanded, /### Sandbox Permissions/);
2931
- assert.match(expanded, /Recovery/);
2932
- assert.match(expanded, /expand hint: freeflow_search action=expand evidenceId=ev_test/);
2933
- });
2934
-
2935
- test("Pi freeflow_run exposes declarative filter schema", () => {
2936
- const { tools } = loadExtension();
2937
- const runTool = tools.find((tool) => tool.name === "freeflow_run");
2938
- assert.ok(runTool);
2939
-
2940
- assert.deepEqual(runTool.parameters.oneOf, [{ required: ["command"] }, { required: ["script"] }]);
2941
-
2942
- const script = runTool.parameters.properties.script;
2943
- assert.equal(script.type, "object");
2944
- assert.equal(script.additionalProperties, false);
2945
- assert.deepEqual(script.properties.language.enum, ["javascript", "python", "jq"]);
2946
- assert.equal(script.properties.code.minLength, 1);
2947
- assert.equal(script.properties.limits.properties.timeoutMs.maximum, 30000);
2948
- assert.deepEqual(script.required, ["language", "code"]);
2949
-
2950
- const filters = runTool.parameters.properties.filters;
2951
- assert.equal(filters.type, "object");
2952
- assert.equal(filters.additionalProperties, false);
2953
- assert.deepEqual(filters.properties.stream.enum, ["stdout", "stderr", "combined"]);
2954
- assert.equal(filters.properties.include.items.minLength, 1);
2955
- assert.equal(filters.properties.exclude.items.minLength, 1);
2956
- assert.match(filters.properties.flags.pattern, /gimsu/);
2957
- assert.equal(filters.properties.head.minimum, 1);
2958
- assert.equal(filters.properties.tail.minimum, 1);
2959
- assert.equal(filters.properties.maxLines.minimum, 1);
2960
- assert.equal(filters.properties.maxBytes.minimum, 1);
2961
-
2962
- const scriptFilter = runTool.parameters.properties.scriptFilter;
2963
- assert.equal(scriptFilter.type, "object");
2964
- assert.equal(scriptFilter.additionalProperties, false);
2965
- assert.deepEqual(scriptFilter.properties.language.enum, ["javascript", "python", "jq"]);
2966
- assert.equal(scriptFilter.properties.code.minLength, 1);
2967
- assert.equal(scriptFilter.properties.limits.properties.timeoutMs.maximum, 30000);
2968
- assert.deepEqual(scriptFilter.required, ["language", "code"]);
2969
- });
2970
-
2971
- test("Pi freeflow_run renders compact and expanded status, evidence, and vault UI", () => {
2972
- const { tools } = loadExtension();
2973
- const runTool = tools.find((tool) => tool.name === "freeflow_run");
2974
- assert.ok(runTool);
2975
-
2976
- const call = renderText(runTool.renderCall({ command: "npm test -- --runInBand", preserve: "important" }, testTheme));
2977
- assert.match(call, /freeflow_run \$ npm test/);
2978
- assert.match(call, /preserve=important/);
2979
-
2980
- const toolResult = {
2981
- content: [{ type: "text", text: "raw json should not be the visible UI" }],
2982
- details: {
2983
- result: {
2984
- toolStatus: "ok",
2985
- decisionId: "ffdec_run_test",
2986
- preserve: "important",
2987
- outputId: "ffout_test123",
2988
- recordId: "ffrec_test123",
2989
- execution: { status: "failed", exitCode: 1, durationMs: 842 },
2990
- routing: {
2991
- status: "routed",
2992
- route: "run",
2993
- reason: "Command failed; exact failure evidence was returned and raw output was vaulted before routing.",
2994
- },
2995
- summary: "Command failed with exitCode=1.",
2996
- parser: {
2997
- name: "test-runner",
2998
- confidence: 0.92,
2999
- fidelity: "exact",
3000
- compressed: true,
3001
- counts: { testsFailed: 1 },
3002
- },
3003
- persistence: {
3004
- status: "vaulted",
3005
- recoverability: "exact",
3006
- recoveryOutputId: "ffout_test123",
3007
- },
3008
- filters: {
3009
- stream: "stderr",
3010
- include: ["AssertionError"],
3011
- sourceLines: 2,
3012
- selectedLines: 1,
3013
- },
3014
- scriptFilter: {
3015
- status: "success",
3016
- language: "javascript",
3017
- label: "failures-only",
3018
- rawOutputId: "ffout_test123",
3019
- sourceAliases: ["stdout", "stderr", "combined"],
3020
- outputId: "ffout_script123",
3021
- operation: {
3022
- kind: "script",
3023
- language: "javascript",
3024
- codeSha256: "sha256_abc",
3025
- },
3026
- },
3027
- importantLines: [
3028
- {
3029
- stream: "stderr",
3030
- lines: "14-16",
3031
- excerpt: "AssertionError: expected false to equal true\nSTACK_BENCH_MARKER exact failure line",
3032
- },
3033
- ],
3034
- recovery: {
3035
- how: "Use freeflow_search with source.kind=vault and outputId=ffout_test123 to recover exact command output.",
3036
- outputId: "ffout_test123",
3037
- },
3038
- },
3039
- },
3040
- };
3041
-
3042
- const collapsed = renderText(runTool.renderResult(toolResult, { expanded: false }, testTheme));
3043
- assert.match(collapsed, /execution: failed/);
3044
- assert.match(collapsed, /routing: routed/);
3045
- assert.match(collapsed, /ffout_test123/);
3046
- assert.match(collapsed, /ffout_script123/);
3047
- assert.match(collapsed, /parser test-runner 0\.92/);
3048
- assert.match(collapsed, /raw and script output recoverable from vault/);
3049
- assert.match(collapsed, /ctrl\+o to expand/);
3050
- assert.doesNotMatch(collapsed, /raw json/);
3051
-
3052
- const expanded = renderText(
3053
- runTool.renderResult(toolResult, { expanded: true }, testTheme, {
3054
- args: { command: "npm test -- --runInBand" },
3055
- }),
3056
- );
3057
- assert.match(expanded, /Status/);
3058
- assert.match(expanded, /execution.status: failed/);
3059
- assert.match(expanded, /Storage/);
3060
- assert.match(expanded, /decisionId: ffdec_run_test/);
3061
- assert.match(expanded, /recordId: ffrec_test123/);
3062
- assert.match(expanded, /persistence: vaulted \/ exact/);
3063
- assert.match(expanded, /Filters/);
3064
- assert.match(expanded, /stream=stderr/);
3065
- assert.match(expanded, /include=AssertionError/);
3066
- assert.match(expanded, /selected=1\/2/);
3067
- assert.match(expanded, /Script filter/);
3068
- assert.match(expanded, /javascript:success/);
3069
- assert.match(expanded, /rawOutputId: ffout_test123/);
3070
- assert.match(expanded, /sources: stdout, stderr, combined/);
3071
- assert.match(expanded, /sha256_abc/);
3072
- assert.match(expanded, /Parser/);
3073
- assert.match(expanded, /confidence: 0\.92/);
3074
- assert.match(expanded, /counts:.*testsFailed/);
3075
- assert.match(expanded, /Evidence/);
3076
- assert.match(expanded, /AssertionError/);
3077
- assert.match(expanded, /Vault recovery/);
3078
- assert.match(expanded, /source.kind=vault/);
3079
- assert.match(
3080
- expanded,
3081
- /exact search: action=retrieve source.kind=vault lineRange=14-16 stream=raw outputId=ffout_script123/,
3082
- );
3083
- assert.match(expanded, /raw command starting point: freeflow_search source.kind=vault outputId=ffout_test123/);
3084
- assert.match(expanded, /details\.result/);
3085
- });
3086
-
3087
- test("Pi freeflow_run returns compact model-visible text with full structured details", async () => {
3088
- const cwd = await mkdtemp(join(tmpdir(), "freeflow-pi-run-compact-text-"));
3089
- try {
3090
- await mkdir(join(cwd, ".freeflow"));
3091
- await writeFile(
3092
- join(cwd, ".freeflow/config.json"),
3093
- JSON.stringify({
3094
- defaultMode: "workflow",
3095
- outputRouter: { enabled: true },
3096
- }),
3097
- "utf8",
3098
- );
3099
- const tools = [];
3100
- const pi = {
3101
- registerTool(tool) {
3102
- tools.push(tool);
3103
- },
3104
- registerCommand() {},
3105
- on() {},
3106
- appendEntry() {},
3107
- sendUserMessage() {},
3108
- async exec() {
3109
- return {
3110
- stdout: "Tests: 1 failed, 24 passed, 25 total\n",
3111
- stderr: "AssertionError: expected false to equal true\nSTACK_BENCH_MARKER exact failure line\n",
3112
- code: 1,
3113
- killed: false,
3114
- };
3115
- },
3116
- };
3117
- freeflowExtension(pi);
3118
- const runTool = tools.find((tool) => tool.name === "freeflow_run");
3119
- assert.ok(runTool);
3120
-
3121
- const result = await runTool.execute(
3122
- "tool-call",
3123
- { command: "npm test", goal: "verification" },
3124
- undefined,
3125
- undefined,
3126
- context(cwd),
3127
- );
3128
-
3129
- const visibleText = result.content[0].text;
3130
- const detailsText = JSON.stringify(result.details.result, null, 2);
3131
-
3132
- assert.match(visibleText, /freeflow_run\|failed/);
3133
- assert.match(visibleText, /exit=1/);
3134
- assert.match(visibleText, /raw=ffout_/);
3135
- assert.match(visibleText, /STACK_BENCH_MARKER exact failure line/);
3136
- assert.match(visibleText, /rec\|vault\|ffout_[^|]+\|stderr\|1-3/);
3137
- assert.match(visibleText, /details\.result/);
3138
- assert.doesNotMatch(visibleText, /^\s*\{/);
3139
- assert.ok(Buffer.byteLength(visibleText, "utf8") < Buffer.byteLength(detailsText, "utf8"));
3140
- assert.ok(Buffer.byteLength(visibleText, "utf8") < 900);
3141
-
3142
- assert.equal(result.details.result.toolStatus, "ok");
3143
- assert.equal(result.details.result.execution.status, "failed");
3144
- assert.equal(result.details.result.execution.exitCode, 1);
3145
- assert.ok(result.details.result.recovery.outputId.startsWith("ffout_"));
3146
- assert.ok(Array.isArray(result.details.result.importantLines));
3147
- } finally {
3148
- await rm(cwd, { recursive: true, force: true });
3149
- }
3150
- });
3151
-
3152
- test("Pi freeflow_run forwards script producer and does not call host exec when sandbox is disabled", async () => {
3153
- const cwd = await mkdtemp(join(tmpdir(), "freeflow-pi-run-script-producer-"));
3154
- try {
3155
- await mkdir(join(cwd, ".freeflow"));
3156
- await writeFile(
3157
- join(cwd, ".freeflow/config.json"),
3158
- JSON.stringify({
3159
- defaultMode: "workflow",
3160
- outputRouter: { enabled: true },
3161
- }),
3162
- "utf8",
3163
- );
3164
- let execCalls = 0;
3165
- const tools = [];
3166
- const pi = {
3167
- registerTool(tool) {
3168
- tools.push(tool);
3169
- },
3170
- registerCommand() {},
3171
- on() {},
3172
- appendEntry() {},
3173
- sendUserMessage() {},
3174
- async exec() {
3175
- execCalls += 1;
3176
- throw new Error("host exec should not be called for sandboxed script producers");
3177
- },
3178
- };
3179
- freeflowExtension(pi);
3180
- const runTool = tools.find((tool) => tool.name === "freeflow_run");
3181
- assert.ok(runTool);
3182
-
3183
- const result = await runTool.execute(
3184
- "tool-call-script-producer",
3185
- {
3186
- script: {
3187
- language: "javascript",
3188
- code: "RAW_PI_SCRIPT",
3189
- label: "pi-script",
3190
- },
3191
- },
3192
- undefined,
3193
- undefined,
3194
- context(cwd),
3195
- );
3196
-
3197
- assert.equal(execCalls, 0);
3198
- assert.match(result.content[0].text, /freeflow_run\|failed/);
3199
- assert.equal(result.details.result.toolStatus, "error");
3200
- assert.equal(result.details.result.producer.kind, "script");
3201
- assert.equal(result.details.result.producer.name, "pi-script");
3202
- assert.equal(result.details.result.failure.kind, "script_transform_disabled");
3203
- assert.equal(result.details.result.scriptProducer.status, "unavailable");
3204
- assert.doesNotMatch(JSON.stringify(result), /RAW_PI_SCRIPT/);
3205
- } finally {
3206
- await rm(cwd, { recursive: true, force: true });
3207
- }
3208
- });
3209
-
3210
- test("Pi freeflow_batch returns compact summary and preserves child details", async () => {
3211
- const cwd = await mkdtemp(join(tmpdir(), "freeflow-pi-batch-compact-text-"));
3212
- try {
3213
- await mkdir(join(cwd, ".freeflow"));
3214
- await writeFile(
3215
- join(cwd, ".freeflow/config.json"),
3216
- JSON.stringify({
3217
- defaultMode: "workflow",
3218
- outputRouter: { enabled: true, vaultRoot: join(cwd, "vault") },
3219
- }),
3220
- "utf8",
3221
- );
3222
-
3223
- const tools = [];
3224
- const pi = {
3225
- registerTool(tool) {
3226
- tools.push(tool);
3227
- },
3228
- registerCommand() {},
3229
- on() {},
3230
- appendEntry() {},
3231
- sendUserMessage() {},
3232
- async exec(_bin, args) {
3233
- const command = args?.[1] ?? "unknown";
3234
- const prefix = command.includes("one") ? "ONE" : "TWO";
3235
- return {
3236
- stdout:
3237
- Array.from({ length: 20 }, (_, index) => `${prefix}_VISIBLE_BATCH_SENTINEL_${index + 1}`).join("\n") + "\n",
3238
- stderr: "",
3239
- code: 0,
3240
- killed: false,
3241
- };
3242
- },
3243
- };
3244
- freeflowExtension(pi);
3245
- const runTool = tools.find((tool) => tool.name === "freeflow_run");
3246
- const batchTool = tools.find((tool) => tool.name === "freeflow_batch");
3247
- assert.ok(runTool);
3248
- assert.ok(batchTool);
3249
-
3250
- const batchCtx = context(cwd);
3251
- batchCtx.sessionManager.getSessionId = () => "batch-compact";
3252
- const batch = await batchTool.execute(
3253
- "batch-call",
3254
- {
3255
- steps: [
3256
- { id: "one", kind: "run", input: { command: "fixture one" } },
3257
- { id: "two", kind: "run", input: { command: "fixture two" } },
3258
- ],
3259
- },
3260
- undefined,
3261
- undefined,
3262
- batchCtx,
3263
- );
3264
-
3265
- const separateCtx = context(cwd);
3266
- separateCtx.sessionManager.getSessionId = () => "batch-separate";
3267
- const separateOne = await runTool.execute("run-one", { command: "fixture one" }, undefined, undefined, separateCtx);
3268
- const separateTwo = await runTool.execute("run-two", { command: "fixture two" }, undefined, undefined, separateCtx);
3269
- const separateVisible = `${separateOne.content[0].text}\n${separateTwo.content[0].text}`;
3270
- const visibleText = batch.content[0].text;
3271
-
3272
- assert.match(visibleText, /freeflow_batch\|routed/);
3273
- assert.match(visibleText, /steps=2/);
3274
- assert.match(visibleText, /details\.result\.steps/);
3275
- assert.doesNotMatch(visibleText, /^\s*\{/);
3276
- assert.doesNotMatch(visibleText, /VISIBLE_BATCH_SENTINEL/);
3277
- assert.ok(Buffer.byteLength(visibleText, "utf8") < Buffer.byteLength(separateVisible, "utf8"));
3278
-
3279
- const payload = batch.details.result;
3280
- assert.equal(payload.stepCount, 2);
3281
- assert.equal(payload.okCount, 2);
3282
- assert.equal(payload.failedCount, 0);
3283
- assert.equal(payload.steps[0].result.importantLines[0].excerpt.includes("ONE_VISIBLE_BATCH_SENTINEL_1"), true);
3284
- assert.equal(payload.steps[1].result.importantLines[0].excerpt.includes("TWO_VISIBLE_BATCH_SENTINEL_1"), true);
3285
-
3286
- const expanded = renderText(batchTool.renderResult(batch, { expanded: true }, testTheme));
3287
- assert.match(expanded, /Steps/);
3288
- assert.match(expanded, /#1 one/);
3289
- assert.match(expanded, /#2 two/);
3290
- assert.match(expanded, /details\.result\.steps/);
3291
- } finally {
3292
- await rm(cwd, { recursive: true, force: true });
3293
- }
3294
- });
3295
-
3296
- test("Pi freeflow_batch accepts queries and renders compact answers", async () => {
3297
- const cwd = await mkdtemp(join(tmpdir(), "freeflow-pi-batch-query-text-"));
3298
- try {
3299
- await mkdir(join(cwd, ".freeflow"));
3300
- await writeFile(
3301
- join(cwd, ".freeflow/config.json"),
3302
- JSON.stringify({
3303
- defaultMode: "workflow",
3304
- outputRouter: { enabled: true, vaultRoot: join(cwd, "vault") },
3305
- }),
3306
- "utf8",
3307
- );
3308
-
3309
- const tools = [];
3310
- const pi = {
3311
- registerTool(tool) {
3312
- tools.push(tool);
3313
- },
3314
- registerCommand() {},
3315
- on() {},
3316
- appendEntry() {},
3317
- sendUserMessage() {},
3318
- async exec() {
3319
- return {
3320
- stdout: "query answer source\nBATCH_QUERY_VISIBLE_FACT_99\n",
3321
- stderr: "",
3322
- code: 0,
3323
- killed: false,
3324
- };
3325
- },
3326
- };
3327
- freeflowExtension(pi);
3328
- const batchTool = tools.find((tool) => tool.name === "freeflow_batch");
3329
- assert.ok(batchTool);
3330
- assert.ok(batchTool.parameters.properties.queries);
3331
-
3332
- const batchCtx = context(cwd);
3333
- batchCtx.sessionManager.getSessionId = () => "batch-query-render";
3334
- const batch = await batchTool.execute(
3335
- "batch-query-call",
3336
- {
3337
- queries: ["BATCH_QUERY_VISIBLE_FACT_99"],
3338
- steps: [
3339
- {
3340
- id: "fact",
3341
- kind: "run",
3342
- input: { command: "fixture fact", preserve: "full" },
3343
- },
3344
- ],
3345
- },
3346
- undefined,
3347
- undefined,
3348
- batchCtx,
3349
- );
3350
-
3351
- const visibleText = batch.content[0].text;
3352
- assert.match(visibleText, /q\|answered\|BATCH_QUERY_VISIBLE_FACT_99/);
3353
- assert.match(visibleText, /BATCH_QUERY_VISIBLE_FACT_99/);
3354
- assert.equal(batch.details.result.queries[0].status, "answered");
3355
-
3356
- const expanded = renderText(batchTool.renderResult(batch, { expanded: true }, testTheme));
3357
- assert.match(expanded, /Query answers/);
3358
- assert.match(expanded, /BATCH_QUERY_VISIBLE_FACT_99/);
3359
- } finally {
3360
- await rm(cwd, { recursive: true, force: true });
3361
- }
3362
- });
3363
-
3364
- test("Pi freeflow_run uses outputRouter thresholds and vault root from repo config", async () => {
3365
- const cwd = await mkdtemp(join(tmpdir(), "freeflow-pi-run-config-"));
3366
- try {
3367
- await mkdir(join(cwd, ".freeflow"));
3368
- await writeFile(
3369
- join(cwd, ".freeflow/config.json"),
3370
- JSON.stringify({
3371
- defaultMode: "workflow",
3372
- outputRouter: {
3373
- enabled: true,
3374
- largeOutputLines: 1,
3375
- largeOutputBytes: 10_000,
3376
- vaultRoot: join(cwd, "vault"),
3377
- },
3378
- }),
3379
- "utf8",
3380
- );
3381
-
3382
- const tools = [];
3383
- const pi = {
3384
- registerTool(tool) {
3385
- tools.push(tool);
3386
- },
3387
- registerCommand() {},
3388
- on() {},
3389
- appendEntry() {},
3390
- sendUserMessage() {},
3391
- async exec() {
3392
- return { stdout: "one\ntwo\n", stderr: "", code: 0, killed: false };
3393
- },
3394
- };
3395
- freeflowExtension(pi);
3396
- const runTool = tools.find((tool) => tool.name === "freeflow_run");
3397
- assert.ok(runTool);
3398
-
3399
- const result = await runTool.execute("tool-call", { command: "fixture" }, undefined, undefined, context(cwd));
3400
- const visibleText = result.content[0].text;
3401
- const payload = result.details.result;
3402
-
3403
- assert.match(visibleText, /freeflow_run\|success/);
3404
- assert.match(visibleText, /route=partial/);
3405
- assert.match(visibleText, /raw=ffout_/);
3406
- assert.doesNotMatch(visibleText, /^\s*\{/);
3407
- assert.equal(payload.toolStatus, "ok");
3408
- assert.equal(payload.routing.status, "partial");
3409
- assert.ok(payload.recovery.outputId.startsWith("ffout_"));
3410
- } finally {
3411
- await rm(cwd, { recursive: true, force: true });
3412
- }
3413
- });
3414
-
3415
- test("Pi freeflow_search applies configured generated path hints", async () => {
3416
- const cwd = await mkdtemp(join(tmpdir(), "freeflow-pi-generated-path-hints-"));
3417
- try {
3418
- await mkdir(join(cwd, ".freeflow"));
3419
- await mkdir(join(cwd, "custom-generated"));
3420
- await writeFile(
3421
- join(cwd, ".freeflow/config.json"),
3422
- JSON.stringify({
3423
- defaultMode: "workflow",
3424
- outputRouter: {
3425
- enabled: true,
3426
- generatedPaths: ["custom-generated/**"],
3427
- },
3428
- }),
3429
- "utf8",
3430
- );
3431
- await writeFile(join(cwd, "target.md"), "PI_GENERATED_HINT_MARKER source truth", "utf8");
3432
- await writeFile(
3433
- join(cwd, "custom-generated", "decoy.md"),
3434
- `${"PI_GENERATED_HINT_MARKER source truth ".repeat(1000)}pihintsentinel`,
3435
- "utf8",
3436
- );
3437
-
3438
- const { tools } = loadExtension();
3439
- const searchTool = tools.find((tool) => tool.name === "freeflow_search");
3440
- assert.ok(searchTool);
3441
-
3442
- const broad = await searchTool.execute(
3443
- "search-generated-hints",
3444
- {
3445
- action: "query",
3446
- source: { kind: "repo" },
3447
- query: "PI_GENERATED_HINT_MARKER source truth",
3448
- },
3449
- undefined,
3450
- undefined,
3451
- context(cwd),
3452
- );
3453
- const broadVisible = broad.content[0].text;
3454
- const broadPayload = broad.details.result;
3455
- assert.match(broadVisible, /freeflow_search\|routed/);
3456
- assert.doesNotMatch(broadVisible, /^\s*\{/);
3457
- assert.ok(
3458
- Buffer.byteLength(broadVisible, "utf8") < Buffer.byteLength(JSON.stringify(broadPayload, null, 2), "utf8"),
3459
- );
3460
- assert.equal(broadPayload.evidence[0].path, "target.md");
3461
- assert.doesNotMatch(broadPayload.evidence[0].excerpt, /pihintsentinel/);
3462
-
3463
- const explicit = await searchTool.execute(
3464
- "search-generated-explicit",
3465
- {
3466
- action: "query",
3467
- source: { kind: "repo", path: "custom-generated/decoy.md" },
3468
- query: "pihintsentinel",
3469
- },
3470
- undefined,
3471
- undefined,
3472
- context(cwd),
3473
- );
3474
- const explicitPayload = explicit.details.result;
3475
- assert.equal(explicitPayload.evidence[0].path, "custom-generated/decoy.md");
3476
- } finally {
3477
- await rm(cwd, { recursive: true, force: true });
3478
- }
3479
- });
3480
-
3481
- test("Pi freeflow_search supports vault-wide query without outputId", async () => {
3482
- const cwd = await mkdtemp(join(tmpdir(), "freeflow-pi-vault-wide-query-"));
3483
- try {
3484
- await mkdir(join(cwd, ".freeflow"));
3485
- const vaultRoot = join(cwd, "vault");
3486
- await writeFile(
3487
- join(cwd, ".freeflow/config.json"),
3488
- JSON.stringify({
3489
- defaultMode: "workflow",
3490
- outputRouter: { enabled: true, vaultRoot },
3491
- }),
3492
- "utf8",
3493
- );
3494
-
3495
- const sessionId = "pi-vault-wide-query-session";
3496
- const stored = await storeTextOutput(createVault({ root: vaultRoot }), {
3497
- sessionId,
3498
- sourceKind: "mcp",
3499
- raw: "PI_VAULT_WIDE_TARGET through registered Pi tool",
3500
- producer: { kind: "mcp", server: "github", tool: "search_issues" },
3501
- createdAt: "2026-06-16T00:00:00.000Z",
3502
- });
3503
-
3504
- const { tools } = loadExtension();
3505
- const searchTool = tools.find((tool) => tool.name === "freeflow_search");
3506
- assert.ok(searchTool);
3507
- const ctx = context(cwd);
3508
- ctx.sessionManager.getSessionId = () => sessionId;
3509
-
3510
- const result = await searchTool.execute(
3511
- "search-vault-wide-query",
3512
- {
3513
- action: "query",
3514
- source: { kind: "vault" },
3515
- query: "PI_VAULT_WIDE_TARGET",
3516
- filters: { producerKind: "mcp", server: "github" },
3517
- },
3518
- undefined,
3519
- undefined,
3520
- ctx,
3521
- );
3522
- const visibleText = result.content[0].text;
3523
- const payload = result.details.result;
3524
- assert.match(visibleText, /freeflow_search\|routed/);
3525
- assert.match(visibleText, /PI_VAULT_WIDE_TARGET/);
3526
- assert.doesNotMatch(visibleText, /^\s*\{/);
3527
- assert.equal(payload.toolStatus, "ok");
3528
- assert.equal(payload.evidence[0].source.outputId, stored.outputId);
3529
- assert.equal(payload.evidence[0].source.stream, "raw");
3530
- } finally {
3531
- await rm(cwd, { recursive: true, force: true });
3532
- }
3533
- });
3534
-
3535
- test("Pi reports invalid outputRouter config warnings", async () => {
3536
- const cwd = await mkdtemp(join(tmpdir(), "freeflow-pi-invalid-config-"));
3537
- try {
3538
- await mkdir(join(cwd, ".freeflow"));
3539
- await writeFile(
3540
- join(cwd, ".freeflow/config.json"),
3541
- JSON.stringify({
3542
- defaultMode: "workflow",
3543
- outputRouter: { postToolRouting: "always" },
3544
- }),
3545
- "utf8",
3546
- );
3547
-
3548
- const { handlers } = loadExtension();
3549
- const notifications = [];
3550
- const ctx = context(cwd);
3551
- ctx.ui.notify = (message, level) => notifications.push({ message, level });
3552
- await handlers.get("session_start")({}, ctx);
3553
-
3554
- assert.equal(notifications[0].level, "warning");
3555
- assert.match(notifications[0].message, /postToolRouting/);
3556
- } finally {
3557
- await rm(cwd, { recursive: true, force: true });
3558
- }
3559
- });
3560
-
3561
- test("Pi observed routing vaults and labels configured MCP output before native safety net", async () => {
3562
- const cwd = await mkdtemp(join(tmpdir(), "freeflow-pi-observed-mcp-"));
3563
- try {
3564
- await mkdir(join(cwd, ".freeflow"));
3565
- await writeFile(
3566
- join(cwd, ".freeflow/config.json"),
3567
- JSON.stringify({
3568
- defaultMode: "workflow",
3569
- outputRouter: {
3570
- enabled: true,
3571
- vaultRoot: join(cwd, "vault"),
3572
- observedRouting: {
3573
- enabled: true,
3574
- mcp: {
3575
- servers: { github: { enabled: true, persistence: "exact" } },
3576
- },
3577
- },
3578
- },
3579
- }),
3580
- "utf8",
3581
- );
3582
-
3583
- const { handlers } = loadExtension();
3584
- const toolResult = handlers.get("tool_result");
3585
- const result = await toolResult(
3586
- {
3587
- type: "tool_result",
3588
- toolName: "mcp",
3589
- toolCallId: "mcp-1",
3590
- input: { server: "github", tool: "search_issues" },
3591
- content: [
3592
- {
3593
- type: "text",
3594
- text: JSON.stringify({
3595
- items: [
3596
- {
3597
- id: 1,
3598
- title: "Alpha",
3599
- html_url: "https://github.com/acme/repo/issues/1",
3600
- body: "x".repeat(500),
3601
- },
3602
- {
3603
- id: 2,
3604
- title: "Beta",
3605
- html_url: "https://github.com/acme/repo/issues/2",
3606
- body: "y".repeat(500),
3607
- },
3608
- {
3609
- id: 3,
3610
- title: "Gamma",
3611
- html_url: "https://github.com/acme/repo/issues/3",
3612
- body: "z".repeat(500),
3613
- },
3614
- {
3615
- id: 4,
3616
- title: "Delta",
3617
- html_url: "https://github.com/acme/repo/issues/4",
3618
- body: "w".repeat(500),
3619
- },
3620
- ],
3621
- }),
3622
- },
3623
- ],
3624
- details: undefined,
3625
- isError: false,
3626
- },
3627
- context(cwd),
3628
- );
3629
-
3630
- assert.ok(result);
3631
- assert.match(result.content[0].text, /Freeflow routed this observed mcp result/);
3632
- assert.match(result.content[0].text, /outputId=ffout_/);
3633
- assert.match(result.content[0].text, /Alpha/);
3634
- assert.match(result.content[0].text, /https:\/\/github\.com\/acme\/repo\/issues\/1/);
3635
- assert.doesNotMatch(result.content[0].text, /xxxxxxxxxxxxxxxx/);
3636
- assert.equal(result.details.freeflowObservedRouting.route, "observed");
3637
- assert.equal(result.details.freeflowObservedRouting.producer.server, "github");
3638
- } finally {
3639
- await rm(cwd, { recursive: true, force: true });
3640
- }
3641
- });
3642
-
3643
- test("Pi observed routing leaves disabled MCP producer result unchanged", async () => {
3644
- const cwd = await mkdtemp(join(tmpdir(), "freeflow-pi-observed-disabled-"));
3645
- try {
3646
- await mkdir(join(cwd, ".freeflow"));
3647
- await writeFile(
3648
- join(cwd, ".freeflow/config.json"),
3649
- JSON.stringify({
3650
- defaultMode: "workflow",
3651
- outputRouter: {
3652
- enabled: true,
3653
- observedRouting: {
3654
- enabled: true,
3655
- mcp: { servers: { github: { enabled: false } } },
3656
- },
3657
- },
3658
- }),
3659
- "utf8",
3660
- );
3661
-
3662
- const { handlers } = loadExtension();
3663
- const result = await handlers.get("tool_result")(
3664
- {
3665
- type: "tool_result",
3666
- toolName: "mcp",
3667
- toolCallId: "mcp-disabled",
3668
- input: { server: "github", tool: "search_issues" },
3669
- content: [{ type: "text", text: "unchanged" }],
3670
- details: undefined,
3671
- isError: false,
3672
- },
3673
- context(cwd),
3674
- );
3675
-
3676
- assert.equal(result, undefined);
3677
- } finally {
3678
- await rm(cwd, { recursive: true, force: true });
3679
- }
3680
- });
3681
-
3682
- test("Pi observed routing fails open without losing MCP output", async () => {
3683
- const cwd = await mkdtemp(join(tmpdir(), "freeflow-pi-observed-fail-open-"));
3684
- try {
3685
- await mkdir(join(cwd, ".freeflow"));
3686
- const fileVault = join(cwd, "vault-file");
3687
- await writeFile(fileVault, "not a directory", "utf8");
3688
- await writeFile(
3689
- join(cwd, ".freeflow/config.json"),
3690
- JSON.stringify({
3691
- defaultMode: "workflow",
3692
- outputRouter: {
3693
- enabled: true,
3694
- vaultRoot: fileVault,
3695
- observedRouting: {
3696
- enabled: true,
3697
- mcp: {
3698
- servers: { github: { enabled: true, persistence: "exact" } },
3699
- },
3700
- },
3701
- },
3702
- }),
3703
- "utf8",
3704
- );
3705
-
3706
- const { handlers } = loadExtension();
3707
- const result = await handlers.get("tool_result")(
3708
- {
3709
- type: "tool_result",
3710
- toolName: "mcp",
3711
- toolCallId: "mcp-fail-open",
3712
- input: { server: "github", tool: "search_issues" },
3713
- content: [{ type: "text", text: "original mcp output survives" }],
3714
- details: undefined,
3715
- isError: false,
3716
- },
3717
- context(cwd),
3718
- );
3719
-
3720
- assert.ok(result);
3721
- assert.match(result.content[0].text, /Freeflow observed-routing warning/);
3722
- assert.match(result.content[0].text, /original mcp output survives/);
3723
- assert.equal(result.details.freeflowObservedRouting.routingStatus, "failed");
3724
- } finally {
3725
- await rm(cwd, { recursive: true, force: true });
3726
- }
3727
- });
3728
-
3729
- test("Pi post-tool safety net passes native output unchanged when config is off", async () => {
3730
- const { handlers } = loadExtension();
3731
- const toolResult = handlers.get("tool_result");
3732
- assert.ok(toolResult);
3733
-
3734
- const raw = Array.from({ length: 20 }, (_, index) => `line ${index + 1}`).join("\n");
3735
- const result = await toolResult(
3736
- {
3737
- type: "tool_result",
3738
- toolName: "read",
3739
- toolCallId: "read-1",
3740
- input: { path: "large.txt" },
3741
- content: [{ type: "text", text: raw }],
3742
- details: undefined,
3743
- isError: false,
3744
- },
3745
- context(),
3746
- );
3747
-
3748
- assert.equal(result, undefined);
3749
- });
3750
-
3751
- test("Pi post-tool safety net vaults and labels large native bash output when enabled", async () => {
3752
- const cwd = await mkdtemp(join(tmpdir(), "freeflow-pi-safety-net-"));
3753
- try {
3754
- await mkdir(join(cwd, ".freeflow"));
3755
- await writeFile(
3756
- join(cwd, ".freeflow/config.json"),
3757
- JSON.stringify({
3758
- defaultMode: "workflow",
3759
- outputRouter: {
3760
- enabled: true,
3761
- postToolRouting: "safety-net",
3762
- largeOutputLines: 3,
3763
- largeOutputBytes: 100_000,
3764
- vaultRoot: join(cwd, "vault"),
3765
- },
3766
- }),
3767
- "utf8",
3768
- );
3769
-
3770
- const { handlers, tools } = loadExtension();
3771
- const toolResult = handlers.get("tool_result");
3772
- const raw = Array.from({ length: 20 }, (_, index) => `line ${index + 1}`).join("\n");
3773
- const result = await toolResult(
3774
- {
3775
- type: "tool_result",
3776
- toolName: "bash",
3777
- toolCallId: "bash-1",
3778
- input: { command: "npm test" },
3779
- content: [{ type: "text", text: raw }],
3780
- details: undefined,
3781
- isError: false,
3782
- },
3783
- context(cwd),
3784
- );
3785
-
3786
- assert.ok(result);
3787
- const routedText = result.content[0].text;
3788
- assert.match(routedText, /Freeflow routed this native bash result/);
3789
- assert.match(routedText, /outputId=ffout_/);
3790
- assert.doesNotMatch(routedText, /line 20/);
3791
-
3792
- const outputId = routedText.match(/outputId=(ffout_[a-f0-9]+)/)?.[1];
3793
- assert.ok(outputId);
3794
- const searchTool = tools.find((tool) => tool.name === "freeflow_search");
3795
- const searched = await searchTool.execute(
3796
- "search-1",
3797
- {
3798
- action: "retrieve",
3799
- source: { kind: "vault", outputId, stream: "raw" },
3800
- lineRange: { start: 18, end: 20 },
3801
- },
3802
- undefined,
3803
- undefined,
3804
- context(cwd),
3805
- );
3806
- const payload = searched.details.result;
3807
- assert.doesNotMatch(searched.content[0].text, /^\s*\{/);
3808
- assert.equal(payload.evidence[0].excerpt, "line 18\nline 19\nline 20");
3809
- } finally {
3810
- await rm(cwd, { recursive: true, force: true });
3811
- }
3812
- });
3813
-
3814
- test("Pi post-tool safety net notes exact duplicate native output", async () => {
3815
- const cwd = await mkdtemp(join(tmpdir(), "freeflow-pi-safety-net-duplicate-"));
3816
- try {
3817
- await mkdir(join(cwd, ".freeflow"));
3818
- await writeFile(
3819
- join(cwd, ".freeflow/config.json"),
3820
- JSON.stringify({
3821
- defaultMode: "workflow",
3822
- outputRouter: {
3823
- enabled: true,
3824
- postToolRouting: "safety-net",
3825
- largeOutputLines: 3,
3826
- largeOutputBytes: 100_000,
3827
- vaultRoot: join(cwd, "vault"),
3828
- },
3829
- }),
3830
- "utf8",
3831
- );
3832
-
3833
- const { handlers } = loadExtension();
3834
- const toolResult = handlers.get("tool_result");
3835
- const raw = Array.from({ length: 20 }, (_, index) => `line ${index + 1}`).join("\n");
3836
- const first = await toolResult(
3837
- {
3838
- type: "tool_result",
3839
- toolName: "bash",
3840
- toolCallId: "bash-duplicate-1",
3841
- input: { command: "npm test" },
3842
- content: [{ type: "text", text: raw }],
3843
- details: undefined,
3844
- isError: false,
3845
- },
3846
- context(cwd),
3847
- );
3848
- const firstOutputId = first.content[0].text.match(/outputId=(ffout_[a-f0-9]+)/)?.[1];
3849
- assert.ok(firstOutputId);
3850
-
3851
- const second = await toolResult(
3852
- {
3853
- type: "tool_result",
3854
- toolName: "bash",
3855
- toolCallId: "bash-duplicate-2",
3856
- input: { command: "npm test" },
3857
- content: [{ type: "text", text: raw }],
3858
- details: undefined,
3859
- isError: false,
3860
- },
3861
- context(cwd),
3862
- );
3863
-
3864
- assert.match(
3865
- second.content[0].text,
3866
- new RegExp(`Duplicate: exact native output matches previous outputId=${firstOutputId}`),
3867
- );
3868
- assert.equal(second.details.freeflowOutputRouter.duplicateOfOutputId, firstOutputId);
3869
- assert.match(second.content[0].text, /current raw output was vaulted as outputId=ffout_/);
3870
- } finally {
3871
- await rm(cwd, { recursive: true, force: true });
3872
- }
3873
- });
3874
-
3875
- test("Pi post-tool safety net leaves small native output alone when enabled", async () => {
3876
- const cwd = await mkdtemp(join(tmpdir(), "freeflow-pi-small-output-"));
3877
- try {
3878
- await mkdir(join(cwd, ".freeflow"));
3879
- await writeFile(
3880
- join(cwd, ".freeflow/config.json"),
3881
- JSON.stringify({
3882
- defaultMode: "workflow",
3883
- outputRouter: {
3884
- enabled: true,
3885
- postToolRouting: "safety-net",
3886
- largeOutputLines: 100,
3887
- largeOutputBytes: 10_000,
3888
- },
3889
- }),
3890
- "utf8",
3891
- );
3892
-
3893
- const { handlers } = loadExtension();
3894
- const result = await handlers.get("tool_result")(
3895
- {
3896
- type: "tool_result",
3897
- toolName: "bash",
3898
- toolCallId: "bash-small",
3899
- input: { command: "pwd" },
3900
- content: [{ type: "text", text: "small output" }],
3901
- details: undefined,
3902
- isError: false,
3903
- },
3904
- context(cwd),
3905
- );
3906
-
3907
- assert.equal(result, undefined);
3908
- } finally {
3909
- await rm(cwd, { recursive: true, force: true });
3910
- }
3911
- });
3912
-
3913
- test("Pi post-tool safety net fails open without losing native output", async () => {
3914
- const cwd = await mkdtemp(join(tmpdir(), "freeflow-pi-safety-fail-open-"));
3915
- try {
3916
- await mkdir(join(cwd, ".freeflow"));
3917
- const blockedVaultPath = join(cwd, "vault-file");
3918
- await writeFile(blockedVaultPath, "not a directory", "utf8");
3919
- await writeFile(
3920
- join(cwd, ".freeflow/config.json"),
3921
- JSON.stringify({
3922
- defaultMode: "workflow",
3923
- outputRouter: {
3924
- enabled: true,
3925
- postToolRouting: "safety-net",
3926
- largeOutputLines: 1,
3927
- largeOutputBytes: 1,
3928
- vaultRoot: blockedVaultPath,
3929
- },
3930
- }),
3931
- "utf8",
3932
- );
3933
-
3934
- const { handlers } = loadExtension();
3935
- const raw = "line 1\nline 2";
3936
- const result = await handlers.get("tool_result")(
3937
- {
3938
- type: "tool_result",
3939
- toolName: "read",
3940
- toolCallId: "read-fail-open",
3941
- input: { path: "large.txt" },
3942
- content: [{ type: "text", text: raw }],
3943
- details: undefined,
3944
- isError: false,
3945
- },
3946
- context(cwd),
3947
- );
3948
-
3949
- assert.ok(result);
3950
- assert.deepEqual(result.content[0], { type: "text", text: raw });
3951
- assert.match(result.content.at(-1).text, /Freeflow safety-net warning/);
3952
- } finally {
3953
- await rm(cwd, { recursive: true, force: true });
3954
- }
3955
- });
3956
-
3957
- test("Pi appends the interaction contract after existing project context", async () => {
3958
- const cwd = await mkdtemp(join(tmpdir(), "freeflow-pi-already-core-"));
3959
- try {
3960
- await mkdir(join(cwd, ".freeflow"));
3961
- await writeFile(
3962
- join(cwd, ".freeflow/config.json"),
3963
- JSON.stringify({
3964
- defaultMode: "workflow",
3965
- outputRouter: { enabled: true },
3966
- }),
3967
- "utf8",
3968
- );
3969
- const { handlers } = loadExtension();
3970
- const beforeAgentStart = handlers.get("before_agent_start");
3971
-
3972
- const existingPrompt = "# Project Instructions\n\nKeep existing repo guidance.";
3973
- const result = await beforeAgentStart({ systemPrompt: existingPrompt }, context(cwd));
3974
-
3975
- assert.match(result.systemPrompt, /^# Project Instructions/);
3976
- assert.match(result.systemPrompt, /# Freeflow Interaction Contract/);
3977
- assert.doesNotMatch(result.systemPrompt, /# Freeflow Runtime Kernel/);
3978
- assert.doesNotMatch(result.systemPrompt, /## Loaded Workflow Skill/);
3979
- assert.doesNotMatch(result.systemPrompt, /## Discovery-light/);
3980
- assert.match(result.systemPrompt, /## Loaded Output Router Skill/);
3981
- assert.match(result.systemPrompt, /freeflow_search/);
3982
- assert.match(result.systemPrompt, /freeflow_run/);
3983
- assert.match(result.systemPrompt, /freeflow_search action=transform/);
3984
- assert.doesNotMatch(result.systemPrompt, /## Freeflow Output Router Reminder/);
3985
- assert.doesNotMatch(result.systemPrompt, /## Loaded Output Router Safety Policy/);
3986
- } finally {
3987
- await rm(cwd, { recursive: true, force: true });
3988
- }
3989
- });
3990
-
3991
- test("Pi keeps enabled capability context alongside the interaction contract", async () => {
3992
- const cwd = await mkdtemp(join(tmpdir(), "freeflow-pi-already-full-"));
3993
- try {
3994
- await mkdir(join(cwd, ".freeflow"));
3995
- await writeFile(
3996
- join(cwd, ".freeflow/config.json"),
3997
- JSON.stringify({
3998
- defaultMode: "workflow",
3999
- outputRouter: { enabled: true },
4000
- }),
4001
- "utf8",
4002
- );
4003
- const { handlers } = loadExtension();
4004
- const beforeAgentStart = handlers.get("before_agent_start");
4005
-
4006
- const result = await beforeAgentStart({ systemPrompt: "base prompt" }, context(cwd));
4007
-
4008
- assert.match(result.systemPrompt, /# Freeflow Interaction Contract/);
4009
- assert.doesNotMatch(result.systemPrompt, /# Freeflow Runtime Kernel/);
4010
- assert.doesNotMatch(result.systemPrompt, /## Loaded Workflow Skill/);
4011
- assert.doesNotMatch(result.systemPrompt, /## Discovery-light/);
4012
- assert.match(result.systemPrompt, /## Loaded Output Router Skill/);
4013
- assert.match(result.systemPrompt, /name: output-router/);
4014
- assert.doesNotMatch(result.systemPrompt, /## Freeflow Output Router Reminder/);
4015
- assert.doesNotMatch(result.systemPrompt, /## Loaded Output Router Safety Policy/);
4016
- assert.doesNotMatch(result.systemPrompt, /Capture raw evidence before transformation/);
4017
- } finally {
4018
- await rm(cwd, { recursive: true, force: true });
4019
- }
4020
- });
850
+ };
851
+ }