@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
@@ -4,53 +4,33 @@ import { dirname, isAbsolute, join, resolve } from "node:path";
4
4
  import { promisify } from "node:util";
5
5
 
6
6
  import {
7
- DEFAULT_OBSERVED_ROUTING_CONFIG,
8
- DEFAULT_OUTPUT_ROUTER_ENABLED,
9
- DEFAULT_POST_TOOL_ROUTING,
10
- DEFAULT_ROUTER_THRESHOLDS,
11
- DEFAULT_SCRIPT_TRANSFORM_CONFIG,
12
- DEFAULT_STORAGE_POLICY,
13
- DEFAULT_VAULT_RETENTION,
14
- DEFAULT_VAULT_ROOT,
15
- normalizeFreeflowConfig,
16
- } from "../../../router/dist/index.js";
17
- import {
18
- handleModeCommand,
19
7
  readCapabilityState,
20
8
  readFreeflowConfig,
21
9
  readFreeflowConfigLayers,
22
10
  readFreeflowLocalConfig,
23
- readModeState,
24
11
  resetSessionOverrides,
25
12
  setSessionCoreOverride,
26
- VALID_MODES,
27
13
  } from "../runtime/runtime-context.js";
28
14
  import {
29
15
  PiSettingsComponent,
30
16
  type SettingsCommitResult,
31
17
  type SettingsEntry,
32
18
  type SettingsSessionResult,
19
+ type SettingsWizard,
20
+ type SettingsWizardStep,
33
21
  } from "./settings-tui.js";
22
+ import { supportsPiSessionModelStateApi } from "../cognitive-routing/pi-session-control.js";
23
+ import { isPiFlowHost } from "../runtime/runtime-identity.js";
24
+ import type {
25
+ CognitiveRoutingCapabilityState,
26
+ CognitiveRoutingProfile,
27
+ CognitiveRoutingProfileName,
28
+ CognitiveRoutingThinkingLevel,
29
+ } from "../cognitive-routing/types.js";
34
30
 
35
- const POST_TOOL_ROUTING_VALUES = ["off", "safety-net", "strict"] as const;
36
- const STORAGE_POLICY_VALUES = ["hybrid-dedupe", "store-everything"] as const;
37
- const OBSERVED_PERSISTENCE_VALUES = ["none", "metadata-only", "exact"] as const;
38
- const SCRIPT_LANGUAGES = ["javascript", "python", "jq"] as const;
39
31
  const DEFAULT_FREEFLOW_ENABLED = true;
40
- const DEFAULT_INTERACTION_CONTRACT_ENABLED = true;
41
- const DEFAULT_SKILLS_ENABLED = true;
42
- const MODE_VALUES = ["conversation", "workflow", "strict-workflow"];
43
- const MODE_LABELS = {
44
- default: "Use configured default",
45
- conversation: "conversation",
46
- workflow: "workflow",
47
- "strict-workflow": "strict-workflow",
48
- };
49
- const MODE_DESCRIPTIONS = {
50
- conversation: "Discussion and read-only inspection",
51
- workflow: "Adaptive workflow for consequential work",
52
- "strict-workflow": "Stronger decision and evidence pressure for high-risk work",
53
- };
32
+ const DEFAULT_CONTEXT_VIRTUALIZATION_ENABLED = false;
33
+ const DEFAULT_CONVERSATION_HISTORY_ENABLED = false;
54
34
  const LOCAL_INHERIT = "inherit";
55
35
  const execFileAsync = promisify(execFile);
56
36
 
@@ -58,6 +38,7 @@ type ConfigScope = "session" | "local" | "repository";
58
38
  type ConfigSource = "session" | "local" | "repository" | "builtin";
59
39
 
60
40
  type SettingKind = "boolean" | "enum" | "integer" | "string" | "list" | "json" | "group";
41
+ type SettingsValue = boolean | number | string | null | SettingsValue[] | { [key: string]: SettingsValue };
61
42
 
62
43
  type SettingsItem = {
63
44
  id: string;
@@ -71,8 +52,10 @@ type SettingsItem = {
71
52
  valueLabels?: Record<string, string>;
72
53
  valueDescriptions?: Record<string, string>;
73
54
  inactive?: boolean;
55
+ runtimeInactive?: boolean;
74
56
  displaySuffix?: string;
75
57
  children?: SettingsItem[];
58
+ wizard?: () => SettingsWizard;
76
59
  parse?: (text: string) => unknown;
77
60
  format?: (value: unknown) => string;
78
61
  configScope?: ConfigScope;
@@ -85,6 +68,18 @@ type SettingsItem = {
85
68
  transient?: boolean;
86
69
  };
87
70
 
71
+ type CognitiveRoutingSettingsController = {
72
+ state(): { effective: boolean; controlMode: string; activeProfile?: string };
73
+ setManualProfile(profile: "standard" | "reasoning", mechanism?: string): Promise<{ status: string; reason?: string }>;
74
+ setAutomaticControl(mechanism?: string): Promise<{ status: string; reason?: string }>;
75
+ };
76
+
77
+ type AfterChangeOptions = {
78
+ reconcileCognitiveRouting?: boolean;
79
+ };
80
+
81
+ type AfterChange = (changed: boolean, options?: AfterChangeOptions) => Promise<void> | void;
82
+
88
83
  type OpenSettingsOptions = {
89
84
  title: string;
90
85
  items: SettingsItem[];
@@ -106,13 +101,13 @@ function cloneJson<T>(value: T): T {
106
101
  }
107
102
  }
108
103
 
109
- function getPath(source: unknown, path: string[]): unknown {
110
- let current = source;
104
+ function getPath(source: unknown, path: string[]): SettingsValue | undefined {
105
+ let current: unknown = source;
111
106
  for (const key of path) {
112
107
  if (!isRecord(current)) return undefined;
113
- current = current[key];
108
+ current = current[key] as SettingsValue | undefined;
114
109
  }
115
- return current;
110
+ return current as SettingsValue | undefined;
116
111
  }
117
112
 
118
113
  function setPath(target: Record<string, unknown>, path: string[], value: unknown) {
@@ -170,318 +165,29 @@ function setConfigValue(config: Record<string, unknown>, item: SettingsItem, val
170
165
  setPath(config, item.path, value);
171
166
  }
172
167
 
173
- function parseStringList(text: string): string[] {
174
- return text
175
- .split(",")
176
- .map((item) => item.trim())
177
- .filter(Boolean);
178
- }
179
-
180
- function parsePositiveInteger(text: string): number {
181
- const value = Number(text.trim());
182
- if (!Number.isInteger(value) || value <= 0) {
183
- throw new Error("Expected a positive integer.");
184
- }
185
- return value;
186
- }
187
-
188
- function parseJsonObject(text: string): unknown {
189
- const trimmed = text.trim();
190
- if (!trimmed) return {};
191
- let parsed: unknown;
192
- try {
193
- parsed = JSON.parse(trimmed);
194
- } catch (error) {
195
- throw new Error(`Expected valid JSON: ${error instanceof Error ? error.message : String(error)}`);
196
- }
197
- if (!isRecord(parsed)) {
198
- throw new Error("Expected a JSON object.");
199
- }
200
- return parsed;
201
- }
202
-
203
- function formatList(value: unknown): string {
204
- return Array.isArray(value) ? value.join(", ") : "";
205
- }
206
-
207
- function formatJson(value: unknown): string {
208
- return JSON.stringify(isRecord(value) ? value : {});
209
- }
210
-
211
168
  function booleanValue(value: unknown): string {
212
169
  return value === true ? "enabled" : "disabled";
213
170
  }
214
171
 
215
- function normalizedSettingsConfig(rawConfig: Record<string, unknown>) {
216
- const rawRouter = isRecord(rawConfig.outputRouter) ? rawConfig.outputRouter : {};
217
- const normalized = normalizeFreeflowConfig({
218
- ...rawConfig,
219
- outputRouter: { ...rawRouter, enabled: true },
220
- });
221
- return normalized.config;
222
- }
223
-
224
- function rawValue(rawConfig: Record<string, unknown>, path: string[], fallback: unknown): unknown {
225
- const value = getPath(rawConfig, path);
226
- return value === undefined ? fallback : value;
227
- }
228
-
229
- function outputRouterItems(rawConfig: Record<string, unknown>, freeflowInactive = false): SettingsItem[] {
230
- const effective = normalizedSettingsConfig(rawConfig);
231
- const routerEnabled = normalizeFreeflowConfig(rawConfig).config.outputRouter.enabled;
232
- const router = effective.outputRouter;
233
- const scriptTransform = effective.scriptTransform;
234
- const observedRouting = effective.observedRouting;
235
- const inactive = freeflowInactive || !routerEnabled;
236
-
237
- return [
238
- {
239
- id: "outputRouter.enabled",
240
- label: "Output Router",
241
- description: "Master switch for Freeflow routed evidence tools and router runtime guidance.",
242
- path: ["outputRouter", "enabled"],
243
- kind: "boolean",
244
- value: routerEnabled,
245
- defaultValue: DEFAULT_OUTPUT_ROUTER_ENABLED,
246
- inactive: freeflowInactive,
247
- },
248
- {
249
- id: "outputRouter.postToolRouting",
250
- label: "Native safety net",
251
- description:
252
- "Post-process large/noisy native read/bash output. safety-net can replace oversized native output with vaulted routed evidence; strict is stronger.",
253
- path: ["outputRouter", "postToolRouting"],
254
- kind: "enum",
255
- value: router.postToolRouting,
256
- values: [...POST_TOOL_ROUTING_VALUES],
257
- valueDescriptions: {
258
- off: "Leave native tool output unchanged",
259
- "safety-net": "Route oversized or noisy native output",
260
- strict: "Apply stronger native output routing",
261
- },
262
- defaultValue: DEFAULT_POST_TOOL_ROUTING,
263
- inactive,
264
- },
265
- {
266
- id: "outputRouter.storagePolicy",
267
- label: "Storage policy",
268
- description: "Vault storage behavior for exactness-sensitive and noisy output.",
269
- path: ["outputRouter", "storagePolicy"],
270
- kind: "enum",
271
- value: router.storagePolicy,
272
- values: [...STORAGE_POLICY_VALUES],
273
- defaultValue: DEFAULT_STORAGE_POLICY,
274
- inactive,
275
- },
276
- {
277
- id: "outputRouter.thresholds.largeOutputBytes",
278
- label: "Large output bytes",
279
- description: "Byte threshold for treating command/tool output as large.",
280
- path: ["outputRouter", "thresholds", "largeOutputBytes"],
281
- kind: "integer",
282
- value: router.thresholds.largeOutputBytes,
283
- defaultValue: DEFAULT_ROUTER_THRESHOLDS.largeOutputBytes,
284
- parse: parsePositiveInteger,
285
- inactive,
286
- },
287
- {
288
- id: "outputRouter.thresholds.largeOutputLines",
289
- label: "Large output lines",
290
- description: "Line threshold for treating command/tool output as large.",
291
- path: ["outputRouter", "thresholds", "largeOutputLines"],
292
- kind: "integer",
293
- value: router.thresholds.largeOutputLines,
294
- defaultValue: DEFAULT_ROUTER_THRESHOLDS.largeOutputLines,
295
- parse: parsePositiveInteger,
296
- inactive,
297
- },
298
- {
299
- id: "outputRouter.vault.root",
300
- label: "Vault root",
301
- description: "Root directory for recoverable routed output.",
302
- path: ["outputRouter", "vault", "root"],
303
- kind: "string",
304
- value: router.vault.root,
305
- defaultValue: DEFAULT_VAULT_ROOT,
306
- inactive,
307
- },
308
- {
309
- id: "outputRouter.vault.retention.ttlDays",
310
- label: "Vault retention days",
311
- description: "TTL retention for vaulted output. Set a positive integer; default is 7 days.",
312
- path: ["outputRouter", "vault", "retention", "ttlDays"],
313
- kind: "integer",
314
- value:
315
- router.vault.retention?.strategy === "ttl" ? router.vault.retention.ttlDays : DEFAULT_VAULT_RETENTION.ttlDays,
316
- defaultValue: DEFAULT_VAULT_RETENTION.ttlDays,
317
- parse: parsePositiveInteger,
318
- inactive,
319
- },
320
- {
321
- id: "outputRouter.generatedPaths",
322
- label: "Generated paths",
323
- description:
324
- "Comma-separated globs for generated repo paths that should be treated as noisy or lower-priority evidence.",
325
- path: ["outputRouter", "hints", "generatedPathGlobs"],
326
- kind: "list",
327
- value: rawValue(
328
- rawConfig,
329
- ["outputRouter", "hints", "generatedPathGlobs"],
330
- rawValue(rawConfig, ["outputRouter", "generatedPaths"], router.hints?.generatedPathGlobs ?? []),
331
- ),
332
- format: formatList,
333
- parse: parseStringList,
334
- inactive,
335
- },
336
- {
337
- id: "outputRouter.noisyCommandHints",
338
- label: "Noisy command hints",
339
- description: "Comma-separated command patterns that are likely to produce noisy output.",
340
- path: ["outputRouter", "hints", "noisyCommandPatterns"],
341
- kind: "list",
342
- value: rawValue(
343
- rawConfig,
344
- ["outputRouter", "hints", "noisyCommandPatterns"],
345
- rawValue(rawConfig, ["outputRouter", "noisyCommandHints"], router.hints?.noisyCommandPatterns ?? []),
346
- ),
347
- format: formatList,
348
- parse: parseStringList,
349
- inactive,
350
- },
351
- {
352
- id: "outputRouter.scriptTransform.enabled",
353
- label: "Script transform",
354
- description:
355
- "Enable sandboxed script transforms/producers. Requires proof-backed adapters; no unsandboxed fallback.",
356
- path: ["outputRouter", "scriptTransform", "enabled"],
357
- kind: "boolean",
358
- value: scriptTransform.enabled,
359
- defaultValue: DEFAULT_SCRIPT_TRANSFORM_CONFIG.enabled,
360
- inactive,
361
- },
362
- {
363
- id: "outputRouter.scriptTransform.languages",
364
- label: "Script languages",
365
- description: "Comma-separated sandbox languages to allow: javascript, python, jq.",
366
- path: ["outputRouter", "scriptTransform", "languages"],
367
- kind: "list",
368
- value: scriptTransform.languages,
369
- defaultValue: DEFAULT_SCRIPT_TRANSFORM_CONFIG.languages,
370
- format: formatList,
371
- parse: (text) => parseStringList(text).filter((value) => (SCRIPT_LANGUAGES as readonly string[]).includes(value)),
372
- inactive,
373
- },
374
- {
375
- id: "outputRouter.scriptTransform.limits.timeoutMs",
376
- label: "Script timeout ms",
377
- description: "Maximum sandboxed script execution time.",
378
- path: ["outputRouter", "scriptTransform", "limits", "timeoutMs"],
379
- kind: "integer",
380
- value: scriptTransform.limits.timeoutMs,
381
- defaultValue: DEFAULT_SCRIPT_TRANSFORM_CONFIG.limits.timeoutMs,
382
- parse: parsePositiveInteger,
383
- inactive,
384
- },
385
- {
386
- id: "outputRouter.scriptTransform.limits.maxInputBytes",
387
- label: "Script max input bytes",
388
- description: "Maximum input size for sandboxed scripts.",
389
- path: ["outputRouter", "scriptTransform", "limits", "maxInputBytes"],
390
- kind: "integer",
391
- value: scriptTransform.limits.maxInputBytes,
392
- defaultValue: DEFAULT_SCRIPT_TRANSFORM_CONFIG.limits.maxInputBytes,
393
- parse: parsePositiveInteger,
394
- inactive,
395
- },
396
- {
397
- id: "outputRouter.scriptTransform.limits.maxOutputBytes",
398
- label: "Script max output bytes",
399
- description: "Maximum output size for sandboxed scripts.",
400
- path: ["outputRouter", "scriptTransform", "limits", "maxOutputBytes"],
401
- kind: "integer",
402
- value: scriptTransform.limits.maxOutputBytes,
403
- defaultValue: DEFAULT_SCRIPT_TRANSFORM_CONFIG.limits.maxOutputBytes,
404
- parse: parsePositiveInteger,
405
- inactive,
406
- },
407
- {
408
- id: "outputRouter.observedRouting.enabled",
409
- label: "Observed routing",
410
- description: "Route configured host tool results after they run, e.g. MCP/web/fetch/code-search output.",
411
- path: ["outputRouter", "observedRouting", "enabled"],
412
- kind: "boolean",
413
- value: observedRouting.enabled,
414
- defaultValue: DEFAULT_OBSERVED_ROUTING_CONFIG.enabled,
415
- inactive,
416
- },
417
- ...observedProducerItems("web", "Web search", observedRouting.web, inactive),
418
- ...observedProducerItems("fetch", "Fetch content", observedRouting.fetch, inactive),
419
- ...observedProducerItems("codeSearch", "Code search", observedRouting.codeSearch, inactive),
420
- {
421
- id: "outputRouter.observedRouting.mcp.servers",
422
- label: "MCP servers JSON",
423
- description: 'JSON object keyed by MCP server id. Example: {"github":{"enabled":true,"persistence":"exact"}}',
424
- path: ["outputRouter", "observedRouting", "mcp", "servers"],
425
- kind: "json",
426
- value: observedRouting.mcp?.servers ?? {},
427
- defaultValue: DEFAULT_OBSERVED_ROUTING_CONFIG.mcp.servers,
428
- format: formatJson,
429
- parse: parseJsonObject,
430
- inactive,
431
- },
432
- ];
433
- }
434
-
435
- function observedProducerItems(
436
- id: "web" | "fetch" | "codeSearch",
437
- label: string,
438
- value: any,
439
- inactive: boolean,
440
- ): SettingsItem[] {
441
- return [
442
- {
443
- id: `outputRouter.observedRouting.${id}.enabled`,
444
- label: `${label} observed`,
445
- description: `Enable observed routing for ${label.toLowerCase()} output.`,
446
- path: ["outputRouter", "observedRouting", id, "enabled"],
447
- kind: "boolean",
448
- value: value?.enabled === true,
449
- defaultValue: false,
450
- inactive,
451
- },
452
- {
453
- id: `outputRouter.observedRouting.${id}.persistence`,
454
- label: `${label} persistence`,
455
- description: "Persistence mode for this observed producer: none, metadata-only, or exact.",
456
- path: ["outputRouter", "observedRouting", id, "persistence"],
457
- kind: "enum",
458
- value: value?.persistence ?? "none",
459
- values: [...OBSERVED_PERSISTENCE_VALUES],
460
- valueDescriptions: {
461
- none: "Do not persist observed output",
462
- "metadata-only": "Persist metadata without raw output",
463
- exact: "Persist exact recoverable output",
464
- },
465
- defaultValue: value?.enabled === true ? undefined : "none",
466
- inactive,
467
- },
468
- ];
469
- }
470
-
471
- function configValueForChoice(item: SettingsItem, value: unknown): unknown {
172
+ function configValueForChoice(item: SettingsItem, value: unknown): SettingsValue | undefined {
472
173
  const key = String(value);
473
174
  if (item.configValues && Object.hasOwn(item.configValues, key)) {
474
- return item.configValues[key];
175
+ return item.configValues[key] as SettingsValue | undefined;
475
176
  }
476
- return value;
177
+ return value as SettingsValue | undefined;
477
178
  }
478
179
 
479
- function effectiveItemValue(item: SettingsItem): unknown {
480
- return item.effectiveValue !== undefined ? item.effectiveValue : item.value;
180
+ function effectiveItemValue(item: SettingsItem): SettingsValue | undefined {
181
+ return (item.effectiveValue === undefined ? item.value : item.effectiveValue) as SettingsValue | undefined;
481
182
  }
482
183
 
483
184
  function formatCoreValue(value: unknown): string {
484
- return typeof value === "boolean" ? booleanValue(value) : String(value ?? "");
185
+ if (typeof value === "boolean") return booleanValue(value);
186
+ if (isRecord(value) && typeof value.provider === "string" && typeof value.model === "string") {
187
+ const effort = typeof value.thinkingLevel === "string" ? ` · ${value.thinkingLevel}` : "";
188
+ return `${value.provider}/${value.model}${effort}`;
189
+ }
190
+ return String(value ?? "");
485
191
  }
486
192
 
487
193
  function coreDisplaySuffix(item: SettingsItem, inactive = false): string {
@@ -585,120 +291,38 @@ function createScopedBooleanItem(options: ScopedBooleanItemOptions): SettingsIte
585
291
  return item;
586
292
  }
587
293
 
588
- type ScopedDefaultModeItemOptions = {
589
- scope: ConfigScope;
590
- repositoryDefaultMode: string;
591
- repositoryDefaultSource: ConfigSource;
592
- localDefaultMode?: string;
593
- effectiveValue: string;
594
- effectiveSource: ConfigSource;
595
- };
596
-
597
- function createScopedDefaultModeItem(options: ScopedDefaultModeItemOptions): SettingsItem {
598
- let item: SettingsItem;
599
- if (options.scope === "local") {
600
- item = {
601
- id: "freeflow.defaultMode",
602
- label: "Default mode",
603
- description:
604
- "Personal default mode for this checkout. Choose inherit to use the repository default; use /freeflow settings repo to edit shared defaults.",
605
- path: ["defaultMode"],
606
- kind: "enum",
607
- value: options.localDefaultMode ?? LOCAL_INHERIT,
608
- values: [LOCAL_INHERIT, ...MODE_VALUES],
609
- valueLabels: {
610
- inherit: "Inherit repository",
611
- ...MODE_LABELS,
612
- },
613
- valueDescriptions: {
614
- inherit: `Use ${options.repositoryDefaultMode} from ${options.repositoryDefaultSource}.`,
615
- ...MODE_DESCRIPTIONS,
616
- },
617
- format: (value) => String(value),
618
- configScope: "local",
619
- configValues: { inherit: undefined },
620
- effectiveValue: options.effectiveValue,
621
- effectiveSource: options.effectiveSource,
622
- inheritedValue: options.repositoryDefaultMode,
623
- inheritedSource: options.repositoryDefaultSource,
624
- };
625
- } else {
626
- item = {
627
- id: "freeflow.defaultMode",
628
- label: "Default mode",
629
- description:
630
- "Shared repository default Freeflow mode used when Skills are enabled. This edits .freeflow/config.json.",
631
- path: ["defaultMode"],
632
- kind: "enum",
633
- value: options.repositoryDefaultMode,
634
- values: MODE_VALUES,
635
- valueLabels: MODE_LABELS,
636
- valueDescriptions: MODE_DESCRIPTIONS,
637
- configScope: "repository",
638
- effectiveValue: options.effectiveValue,
639
- effectiveSource: options.effectiveSource,
640
- localOverrideValue: options.localDefaultMode,
641
- };
642
- }
643
- item.displaySuffix = coreDisplaySuffix(item);
644
- return item;
645
- }
646
-
647
- function sessionModeDisplaySuffix(
648
- sessionMode: string,
649
- defaultMode: string,
650
- defaultSource: ConfigSource,
651
- inactive: boolean,
652
- ): string | undefined {
653
- if (sessionMode !== "default") return undefined;
654
- const inactiveSuffix = inactive ? " · inactive" : "";
655
- return `(${defaultMode} · ${defaultSource}${inactiveSuffix})`;
656
- }
657
-
658
- function validModeOrUndefined(value: unknown): string | undefined {
659
- return typeof value === "string" && VALID_MODES.has(value) ? value : undefined;
660
- }
661
-
662
294
  function resolveSettingsCoreView(
663
295
  rawConfig: Record<string, unknown>,
664
296
  layers?: Awaited<ReturnType<typeof readFreeflowConfigLayers>>,
665
297
  ) {
666
298
  const localConfig = layers?.local.valid && isRecord(layers.local.parsed) ? layers.local.parsed : {};
667
- const repositorySkillsValue = getPath(rawConfig, ["skills"]);
668
- const repositorySkills = isRecord(repositorySkillsValue) ? repositorySkillsValue : {};
669
299
  const fallbackCore = {
670
300
  enabled: getPath(rawConfig, ["enabled"]) !== false,
671
- interactionContract: getPath(rawConfig, ["interactionContract"]) !== false,
672
- skills: { enabled: getPath(repositorySkills, ["enabled"]) !== false },
673
- defaultMode: validModeOrUndefined(rawConfig.defaultMode) ?? "workflow",
301
+ contextVirtualization: getPath(rawConfig, ["contextVirtualization"]) === true,
302
+ conversationHistory: getPath(rawConfig, ["conversationHistory"]) === true,
674
303
  };
675
304
  const fallbackSources = {
676
305
  enabled: typeof getPath(rawConfig, ["enabled"]) === "boolean" ? "repository" : "builtin",
677
- interactionContract: typeof getPath(rawConfig, ["interactionContract"]) === "boolean" ? "repository" : "builtin",
678
- skillsEnabled: typeof getPath(repositorySkills, ["enabled"]) === "boolean" ? "repository" : "builtin",
679
- defaultMode: validModeOrUndefined(rawConfig.defaultMode) !== undefined ? "repository" : "builtin",
306
+ contextVirtualization:
307
+ typeof getPath(rawConfig, ["contextVirtualization"]) === "boolean" ? "repository" : "builtin",
308
+ conversationHistory: typeof getPath(rawConfig, ["conversationHistory"]) === "boolean" ? "repository" : "builtin",
680
309
  } as const;
681
310
  return {
682
311
  localConfig,
683
312
  core: layers?.coreConfig ?? fallbackCore,
684
313
  sources: (layers?.sources ?? fallbackSources) as {
685
314
  enabled: ConfigSource;
686
- interactionContract: ConfigSource;
687
- skillsEnabled: ConfigSource;
688
- defaultMode: ConfigSource;
315
+ contextVirtualization: ConfigSource;
316
+ conversationHistory: ConfigSource;
689
317
  },
690
318
  };
691
319
  }
692
320
 
693
- function groupEnabled(items: SettingsItem[], id: string): boolean {
694
- return items.find((item) => item.id === id)?.value === true;
695
- }
696
-
697
321
  function createSessionBooleanItem(options: {
698
322
  id: string;
699
323
  label: string;
700
324
  description: string;
701
- key: "enabled" | "interactionContract" | "skillsEnabled";
325
+ key: "enabled" | "contextVirtualization" | "conversationHistory";
702
326
  inheritedValue: boolean;
703
327
  inheritedSource: ConfigSource;
704
328
  effectiveValue: boolean;
@@ -744,19 +368,19 @@ function createSessionBooleanItem(options: {
744
368
 
745
369
  function sessionFreeflowItems(
746
370
  state: Awaited<ReturnType<typeof readCapabilityState>>,
747
- modeState: Awaited<ReturnType<typeof readModeState>>,
371
+ cognitiveRoutingController?: CognitiveRoutingSettingsController,
748
372
  ): SettingsItem[] {
749
373
  const sessionOverrides = state.sessionOverrides as Record<string, boolean>;
750
374
  const configured = state.configuredCoreConfig;
751
375
  const configuredSources = state.configuredSources as {
752
376
  enabled: ConfigSource;
753
- interactionContract: ConfigSource;
754
- skillsEnabled: ConfigSource;
377
+ contextVirtualization: ConfigSource;
378
+ conversationHistory: ConfigSource;
755
379
  };
756
380
  const effectiveSources = state.configSources as {
757
381
  enabled: ConfigSource;
758
- interactionContract: ConfigSource;
759
- skillsEnabled: ConfigSource;
382
+ contextVirtualization: ConfigSource;
383
+ conversationHistory: ConfigSource;
760
384
  };
761
385
 
762
386
  const freeflowItem = createSessionBooleanItem({
@@ -770,70 +394,71 @@ function sessionFreeflowItems(
770
394
  effectiveSource: effectiveSources.enabled,
771
395
  sessionOverrides,
772
396
  });
773
- const interactionItem = createSessionBooleanItem({
774
- id: "freeflow.interactionContract",
775
- label: "Interaction Contract",
776
- description: "Temporary Interaction Contract override for this Pi session.",
777
- key: "interactionContract",
778
- inheritedValue: configured.interactionContract,
779
- inheritedSource: configuredSources.interactionContract,
780
- effectiveValue: state.interactionContract.enabled,
781
- effectiveSource: effectiveSources.interactionContract,
397
+ const contextVirtualizationItem = createSessionBooleanItem({
398
+ id: "freeflow.contextVirtualization",
399
+ label: "Context Virtualization",
400
+ description: "Temporary Context Virtualization override for this Pi session.",
401
+ key: "contextVirtualization",
402
+ inheritedValue: configured.contextVirtualization,
403
+ inheritedSource: configuredSources.contextVirtualization,
404
+ effectiveValue: state.contextVirtualization.enabled,
405
+ effectiveSource: effectiveSources.contextVirtualization,
782
406
  sessionOverrides,
783
407
  });
784
- const skillsItem = createSessionBooleanItem({
785
- id: "freeflow.skills.enabled",
786
- label: "Skills",
787
- description: "Temporary Freeflow Skills override for this Pi session.",
788
- key: "skillsEnabled",
789
- inheritedValue: configured.skills.enabled,
790
- inheritedSource: configuredSources.skillsEnabled,
791
- effectiveValue: state.skills.enabled,
792
- effectiveSource: effectiveSources.skillsEnabled,
408
+ const conversationHistoryItem = createSessionBooleanItem({
409
+ id: "freeflow.conversationHistory",
410
+ label: "Conversation History",
411
+ description: "Temporary Conversation History override for this Pi session.",
412
+ key: "conversationHistory",
413
+ inheritedValue: configured.conversationHistory,
414
+ inheritedSource: configuredSources.conversationHistory,
415
+ effectiveValue: state.conversationHistory.enabled,
416
+ effectiveSource: effectiveSources.conversationHistory,
793
417
  sessionOverrides,
794
418
  });
795
419
 
796
420
  const freeflowInactive = !state.enabled;
797
- const skillsEnabled = state.skills.enabled;
798
- interactionItem.inactive = freeflowInactive;
799
- skillsItem.inactive = freeflowInactive;
800
-
801
- const sessionMode = modeState.currentMode ?? "default";
802
- const sessionModeItem: SettingsItem = {
803
- id: "freeflow.sessionMode",
804
- label: "Mode",
805
- description: "Temporary mode override for this Pi session.",
806
- kind: "enum",
807
- value: sessionMode,
808
- values: ["default", ...MODE_VALUES],
809
- valueLabels: {
810
- ...MODE_LABELS,
811
- default: "Use configured default",
812
- },
813
- valueDescriptions: {
814
- default: `${modeState.defaultMode} from ${modeState.defaultModeSource}`,
815
- ...MODE_DESCRIPTIONS,
816
- },
817
- inactive: freeflowInactive || !skillsEnabled,
818
- displaySuffix: sessionModeDisplaySuffix(
819
- sessionMode,
820
- modeState.defaultMode,
821
- modeState.defaultModeSource,
822
- freeflowInactive || !skillsEnabled,
823
- ),
824
- inheritedValue: modeState.defaultMode,
825
- inheritedSource: modeState.defaultModeSource,
826
- };
421
+ contextVirtualizationItem.inactive = freeflowInactive;
422
+ conversationHistoryItem.inactive = freeflowInactive;
423
+
424
+ const cognitiveRoutingState = cognitiveRoutingController?.state();
425
+ const cognitiveRoutingProfile =
426
+ cognitiveRoutingState?.controlMode === "manual-standard"
427
+ ? "standard"
428
+ : cognitiveRoutingState?.controlMode === "manual-reasoning"
429
+ ? "reasoning"
430
+ : "auto";
431
+ const cognitiveRoutingItem: SettingsItem | undefined = state.cognitiveRouting
432
+ ? {
433
+ id: "freeflow.cognitiveRouting.profile",
434
+ label: "Cognitive Routing",
435
+ description: "Hold a profile manually or release the hold for automatic model control.",
436
+ kind: "enum",
437
+ value: cognitiveRoutingProfile,
438
+ values: ["auto", "standard", "reasoning"],
439
+ valueLabels: {
440
+ auto: "automatic",
441
+ standard: "manual · standard",
442
+ reasoning: "manual · reasoning",
443
+ },
444
+ valueDescriptions: {
445
+ auto: "Release the manual hold without forcing a model transition.",
446
+ standard: "Hold the standard profile until /freeflow profile auto.",
447
+ reasoning: "Hold the reasoning profile until /freeflow profile auto.",
448
+ },
449
+ inactive: freeflowInactive || !state.cognitiveRouting.effective || cognitiveRoutingController === undefined,
450
+ runtimeInactive: state.cognitiveRouting.blockingReason?.code === "runtime_disabled",
451
+ displaySuffix: cognitiveRoutingState?.effective ? cognitiveRoutingProfile : "unavailable",
452
+ }
453
+ : undefined;
827
454
 
828
455
  return [
829
456
  freeflowItem,
830
- interactionItem,
831
- skillsItem,
832
- sessionModeItem,
457
+ ...(cognitiveRoutingItem ? [cognitiveRoutingItem] : []),
833
458
  {
834
459
  id: "freeflow.session.reset",
835
460
  label: "Reset session overrides",
836
- description: "Clear Freeflow, Interaction Contract, Skills, and mode overrides for this Pi session.",
461
+ description: "Clear Freeflow, Context Virtualization, and Conversation History overrides for this Pi session.",
837
462
  kind: "enum",
838
463
  value: "available",
839
464
  values: ["reset"],
@@ -842,15 +467,370 @@ function sessionFreeflowItems(
842
467
  format: () => "available",
843
468
  transient: true,
844
469
  },
470
+ {
471
+ id: "freeflow.context",
472
+ label: "Freeflow Context",
473
+ description: "Choose which context projection and conversation-history operations are available to the model.",
474
+ kind: "group",
475
+ value: contextVirtualizationItem.effectiveValue === true || conversationHistoryItem.effectiveValue === true,
476
+ displaySuffix: `${[contextVirtualizationItem, conversationHistoryItem].filter((item) => item.effectiveValue === true).length}/2 enabled`,
477
+ children: [contextVirtualizationItem, conversationHistoryItem],
478
+ },
479
+ ];
480
+ }
481
+
482
+ const COGNITIVE_ROUTING_THINKING_LEVELS: CognitiveRoutingThinkingLevel[] = [
483
+ "off",
484
+ "minimal",
485
+ "low",
486
+ "medium",
487
+ "high",
488
+ "xhigh",
489
+ "max",
490
+ ];
491
+
492
+ const COGNITIVE_ROUTING_EFFORT_DESCRIPTIONS: Record<CognitiveRoutingThinkingLevel, string> = {
493
+ off: "No reasoning",
494
+ minimal: "Very brief reasoning",
495
+ low: "Light reasoning",
496
+ medium: "Moderate reasoning",
497
+ high: "Deep reasoning",
498
+ xhigh: "Extra-high reasoning",
499
+ max: "Maximum reasoning",
500
+ };
501
+
502
+ type CognitiveRoutingModelOption = {
503
+ key: string;
504
+ provider: string;
505
+ model: string;
506
+ label: string;
507
+ description: string;
508
+ thinkingLevels: CognitiveRoutingThinkingLevel[];
509
+ };
510
+
511
+ function cognitiveRoutingModelKey(provider: string, model: string): string {
512
+ return `${provider}/${model}`;
513
+ }
514
+
515
+ function isCognitiveRoutingProfile(value: unknown): value is CognitiveRoutingProfile {
516
+ return (
517
+ isRecord(value) &&
518
+ typeof value.provider === "string" &&
519
+ typeof value.model === "string" &&
520
+ typeof value.thinkingLevel === "string" &&
521
+ COGNITIVE_ROUTING_THINKING_LEVELS.includes(value.thinkingLevel as CognitiveRoutingThinkingLevel)
522
+ );
523
+ }
524
+
525
+ function cognitiveRoutingThinkingLevels(model: any, registry: any): CognitiveRoutingThinkingLevel[] {
526
+ if (typeof registry?.clampThinkingLevel !== "function") {
527
+ return model?.reasoning === true ? [...COGNITIVE_ROUTING_THINKING_LEVELS] : ["off"];
528
+ }
529
+ return COGNITIVE_ROUTING_THINKING_LEVELS.filter((level) => {
530
+ try {
531
+ return registry.clampThinkingLevel(model, level) === level;
532
+ } catch {
533
+ return false;
534
+ }
535
+ });
536
+ }
537
+
538
+ function cognitiveRoutingModelOptions(ctx: any): CognitiveRoutingModelOption[] {
539
+ const registry = ctx?.modelRegistry;
540
+ if (typeof registry?.getAvailable !== "function") return [];
541
+ let models: any[];
542
+ try {
543
+ models = registry.getAvailable();
544
+ } catch {
545
+ return [];
546
+ }
547
+ if (!Array.isArray(models)) return [];
548
+
549
+ return models
550
+ .filter((model) => typeof model?.provider === "string" && typeof model?.id === "string")
551
+ .map((model) => {
552
+ const thinkingLevels = cognitiveRoutingThinkingLevels(model, registry);
553
+ const provider = model.provider as string;
554
+ const modelId = model.id as string;
555
+ const key = cognitiveRoutingModelKey(provider, modelId);
556
+ const displayName = typeof model.name === "string" && model.name !== modelId ? model.name : undefined;
557
+ return {
558
+ key,
559
+ provider,
560
+ model: modelId,
561
+ label: key,
562
+ description: displayName ? `${displayName} · ${thinkingLevels.join(", ")}` : thinkingLevels.join(", "),
563
+ thinkingLevels,
564
+ };
565
+ })
566
+ .filter((model) => model.thinkingLevels.length > 0)
567
+ .sort((a, b) => a.label.localeCompare(b.label));
568
+ }
569
+
570
+ function cognitiveRoutingProfileDisplay(value: unknown): string {
571
+ return isCognitiveRoutingProfile(value) ? formatCoreValue(value) : "not configured";
572
+ }
573
+
574
+ function cognitiveRoutingSettingsSource(source: unknown): ConfigSource {
575
+ if (source === "repository") return "repository";
576
+ if (source === "personal" || source === "local") return "local";
577
+ return "builtin";
578
+ }
579
+
580
+ function cognitiveRoutingProfileDisplaySuffix(source: unknown, scope: ConfigScope): string | undefined {
581
+ if (!source) return undefined;
582
+ if (source === "personal" && scope === "repository") return "(effective personal)";
583
+ return `(${source})`;
584
+ }
585
+
586
+ function cognitiveRoutingConfirmStep(summary: string): SettingsWizardStep {
587
+ return {
588
+ title: "Confirm preset",
589
+ choices: [
590
+ {
591
+ key: "__save__",
592
+ value: "save",
593
+ label: `Save ${summary}`,
594
+ description: "Write the complete profile atomically.",
595
+ },
596
+ { key: "__cancel__", value: "cancel", label: "Cancel", description: "Leave the previous preset unchanged." },
597
+ ],
598
+ selectedKey: "__save__",
599
+ };
600
+ }
601
+
602
+ function createCognitiveRoutingProfileWizard(
603
+ currentValue: unknown,
604
+ models: CognitiveRoutingModelOption[],
605
+ allowInherit: boolean,
606
+ ): SettingsWizard {
607
+ const currentProfile = isCognitiveRoutingProfile(currentValue) ? currentValue : undefined;
608
+ const currentModelKey = currentProfile
609
+ ? cognitiveRoutingModelKey(currentProfile.provider, currentProfile.model)
610
+ : undefined;
611
+ const firstChoices = [
612
+ ...(allowInherit
613
+ ? [
614
+ {
615
+ key: LOCAL_INHERIT,
616
+ value: LOCAL_INHERIT,
617
+ label: "Inherit repository preset",
618
+ description: "Remove the personal override without changing the repository preset.",
619
+ },
620
+ ]
621
+ : []),
622
+ ...models.map((model) => ({
623
+ key: model.key,
624
+ value: model,
625
+ label: model.label,
626
+ description: model.description,
627
+ })),
845
628
  ];
629
+ if (firstChoices.length === 0) {
630
+ firstChoices.push({
631
+ key: "__cancel__",
632
+ value: "cancel",
633
+ label: "No authenticated models available",
634
+ description: "Authenticate a provider before configuring this preset.",
635
+ });
636
+ }
637
+
638
+ return {
639
+ firstStep: () => ({
640
+ title: "Choose model",
641
+ choices: firstChoices,
642
+ selectedKey:
643
+ currentValue === LOCAL_INHERIT
644
+ ? LOCAL_INHERIT
645
+ : firstChoices.some((choice) => choice.key === currentModelKey)
646
+ ? currentModelKey
647
+ : firstChoices[0]?.key,
648
+ }),
649
+ nextStep: (selectedValues) => {
650
+ if (selectedValues.length === 1) {
651
+ const selected = selectedValues[0];
652
+ if (selected === LOCAL_INHERIT) {
653
+ return cognitiveRoutingConfirmStep("inherit repository preset");
654
+ }
655
+ const model = selected as CognitiveRoutingModelOption;
656
+ if (!model?.key || model.thinkingLevels.length === 0) return undefined;
657
+ const currentEffort =
658
+ currentProfile && currentModelKey === model.key ? currentProfile.thinkingLevel : undefined;
659
+ return {
660
+ title: `Choose effort · ${model.label}`,
661
+ choices: model.thinkingLevels.map((level) => ({
662
+ key: level,
663
+ value: level,
664
+ label: level,
665
+ description: COGNITIVE_ROUTING_EFFORT_DESCRIPTIONS[level],
666
+ })),
667
+ selectedKey:
668
+ currentEffort && model.thinkingLevels.includes(currentEffort) ? currentEffort : model.thinkingLevels[0],
669
+ };
670
+ }
671
+ if (selectedValues.length === 2) {
672
+ const model = selectedValues[0] as CognitiveRoutingModelOption;
673
+ const effort = selectedValues[1] as CognitiveRoutingThinkingLevel;
674
+ return cognitiveRoutingConfirmStep(`${model.label} · ${effort}`);
675
+ }
676
+ return undefined;
677
+ },
678
+ valueFromSelections: (selectedValues) => {
679
+ if (selectedValues[0] === LOCAL_INHERIT) return undefined;
680
+ const model = selectedValues[0] as CognitiveRoutingModelOption;
681
+ return {
682
+ provider: model.provider,
683
+ model: model.model,
684
+ thinkingLevel: selectedValues[1] as CognitiveRoutingThinkingLevel,
685
+ } satisfies CognitiveRoutingProfile;
686
+ },
687
+ };
688
+ }
689
+
690
+ function cognitiveRoutingProfileItem(options: {
691
+ name: CognitiveRoutingProfileName;
692
+ scope: ConfigScope;
693
+ rawConfig: Record<string, unknown>;
694
+ localConfig: Record<string, unknown>;
695
+ capabilityState: CognitiveRoutingCapabilityState | undefined;
696
+ ctx: any;
697
+ }): SettingsItem {
698
+ const path = ["cognitiveRouting", "profiles", options.name];
699
+ const repositoryProfile = getPath(options.rawConfig, path);
700
+ const localProfile = getPath(options.localConfig, path);
701
+ const effectiveProfile = options.capabilityState?.profiles?.[options.name] ?? repositoryProfile;
702
+ const localValue = isCognitiveRoutingProfile(localProfile) ? localProfile : LOCAL_INHERIT;
703
+ const value = options.scope === "local" ? localValue : repositoryProfile;
704
+ const source =
705
+ options.capabilityState?.profileSources?.[options.name] ??
706
+ (isCognitiveRoutingProfile(repositoryProfile) ? "repository" : undefined);
707
+ const models = cognitiveRoutingModelOptions(options.ctx);
708
+ const inactive = options.scope === "local" && models.length === 0 && localValue !== LOCAL_INHERIT;
709
+
710
+ return {
711
+ id: `freeflow.cognitiveRouting.${options.name}`,
712
+ label: `${options.name.charAt(0).toUpperCase()}${options.name.slice(1)} preset`,
713
+ description:
714
+ "Choose an authenticated available model and one effort supported by that model. Confirming writes the complete preset; cancel leaves it unchanged.",
715
+ path,
716
+ kind: "string",
717
+ value,
718
+ format: cognitiveRoutingProfileDisplay,
719
+ configScope: options.scope,
720
+ effectiveValue: effectiveProfile,
721
+ effectiveSource: cognitiveRoutingSettingsSource(source),
722
+ inheritedValue: repositoryProfile,
723
+ inheritedSource: "repository",
724
+ inactive,
725
+ displaySuffix: cognitiveRoutingProfileDisplaySuffix(source, options.scope),
726
+ wizard: () => createCognitiveRoutingProfileWizard(value, models, options.scope === "local"),
727
+ };
728
+ }
729
+
730
+ type CognitiveRoutingSessionStartSetting = "control" | "profile";
731
+
732
+ function cognitiveRoutingSessionStartItem(options: {
733
+ setting: CognitiveRoutingSessionStartSetting;
734
+ scope: ConfigScope;
735
+ rawConfig: Record<string, unknown>;
736
+ localConfig: Record<string, unknown>;
737
+ capabilityState: CognitiveRoutingCapabilityState | undefined;
738
+ }): SettingsItem {
739
+ const path = ["cognitiveRouting", "sessionStart", options.setting];
740
+ const defaultValue = options.setting === "control" ? "automatic" : "reasoning";
741
+ const values = options.setting === "control" ? ["automatic", "manual"] : ["standard", "reasoning"];
742
+ const labels = Object.fromEntries(values.map((value) => [value, value]));
743
+ const descriptions =
744
+ options.setting === "control"
745
+ ? {
746
+ automatic: "Start the new session in Reasoning; automatic delegation may use Standard during execution.",
747
+ manual: "Hold the selected profile for the new session until the user changes it.",
748
+ }
749
+ : {
750
+ standard: "Start a new manual session with the configured Standard preset.",
751
+ reasoning: "Start a new manual session with the configured Reasoning preset.",
752
+ };
753
+ const repositoryValue = getPath(options.rawConfig, path);
754
+ const localValue = getPath(options.localConfig, path);
755
+ const isValidValue = (value: unknown): value is string => values.includes(value as string);
756
+ const repositorySetting = isValidValue(repositoryValue) ? repositoryValue : undefined;
757
+ const localSetting = isValidValue(localValue) ? localValue : undefined;
758
+ const inheritedValue = repositorySetting ?? defaultValue;
759
+ const inheritedSource: ConfigSource = repositorySetting ? "repository" : "builtin";
760
+ const configuredSource =
761
+ options.capabilityState?.sessionStartSources?.[options.setting] ?? (repositorySetting ? "repository" : undefined);
762
+ const effectiveSessionStartControl =
763
+ options.capabilityState?.sessionStart?.control ??
764
+ getPath(options.localConfig, ["cognitiveRouting", "sessionStart", "control"]) ??
765
+ getPath(options.rawConfig, ["cognitiveRouting", "sessionStart", "control"]) ??
766
+ "automatic";
767
+ const effectiveValue =
768
+ options.setting === "profile" && effectiveSessionStartControl === "automatic"
769
+ ? "reasoning"
770
+ : (options.capabilityState?.sessionStart?.[options.setting] ?? inheritedValue);
771
+ const effectiveSource = cognitiveRoutingSettingsSource(configuredSource);
772
+ const label = options.setting === "control" ? "Session start control" : "Manual session-start profile";
773
+ const description =
774
+ options.setting === "control"
775
+ ? "Choose automatic or manual control for new sessions only; changing this does not alter the active session."
776
+ : "Choose the profile for manual session starts; automatic sessions always start in Reasoning.";
777
+
778
+ const item: SettingsItem =
779
+ options.scope === "local"
780
+ ? {
781
+ id: `freeflow.cognitiveRouting.sessionStart.${options.setting}`,
782
+ label,
783
+ description: `${description} Choose inherit to use the repository value; use /freeflow settings repo to edit shared defaults.`,
784
+ path,
785
+ kind: "enum",
786
+ value: localSetting ?? LOCAL_INHERIT,
787
+ values: [LOCAL_INHERIT, ...values],
788
+ valueLabels: { inherit: "Inherit repository", ...labels },
789
+ valueDescriptions: {
790
+ inherit: `Use ${inheritedValue} from ${inheritedSource}.`,
791
+ ...descriptions,
792
+ },
793
+ format: (value) => String(value),
794
+ configScope: "local",
795
+ configValues: { inherit: undefined },
796
+ effectiveValue,
797
+ effectiveSource,
798
+ inheritedValue,
799
+ inheritedSource,
800
+ }
801
+ : {
802
+ id: `freeflow.cognitiveRouting.sessionStart.${options.setting}`,
803
+ label,
804
+ description: `${description} This edits shared .freeflow/config.json.`,
805
+ path,
806
+ kind: "enum",
807
+ value: inheritedValue,
808
+ values,
809
+ valueLabels: labels,
810
+ valueDescriptions: descriptions,
811
+ format: (value) => String(value),
812
+ defaultValue,
813
+ configScope: "repository",
814
+ effectiveValue,
815
+ effectiveSource,
816
+ localOverrideValue: localSetting,
817
+ };
818
+ item.displaySuffix = coreDisplaySuffix(item);
819
+ return item;
820
+ }
821
+
822
+ function isCognitiveRoutingRuntimeAvailable(pi: any): boolean {
823
+ return isPiFlowHost(pi?.host) || supportsPiSessionModelStateApi(pi);
846
824
  }
847
825
 
848
826
  function freeflowItems(
849
827
  rawConfig: Record<string, unknown>,
850
- modeState?: Awaited<ReturnType<typeof readModeState>>,
851
828
  options: {
852
829
  scope?: ConfigScope;
853
830
  layers?: Awaited<ReturnType<typeof readFreeflowConfigLayers>>;
831
+ cognitiveRouting?: CognitiveRoutingCapabilityState;
832
+ ctx?: any;
833
+ runtimeAvailable?: boolean;
854
834
  } = {},
855
835
  ): SettingsItem[] {
856
836
  const scope = options.scope ?? "repository";
@@ -863,240 +843,128 @@ function freeflowItems(
863
843
  localConfig,
864
844
  id: "freeflow.enabled",
865
845
  label: "Freeflow",
866
- description: "Master switch for the Interaction Contract and Freeflow skills in this checkout.",
846
+ description: "Master switch for Freeflow's core guidance, skills, and optional capabilities in this checkout.",
867
847
  path: ["enabled"],
868
848
  effectiveValue: core.enabled,
869
849
  effectiveSource: sources.enabled,
870
850
  defaultValue: DEFAULT_FREEFLOW_ENABLED,
871
851
  });
872
- const interactionItem = createScopedBooleanItem({
852
+ const contextVirtualizationItem = createScopedBooleanItem({
873
853
  scope,
874
854
  rawConfig,
875
855
  localConfig,
876
- id: "freeflow.interactionContract",
877
- label: "Interaction Contract",
878
- description: "Apply Freeflow's compact turn-interpretation and collaboration guidance.",
879
- path: ["interactionContract"],
880
- effectiveValue: core.interactionContract,
881
- effectiveSource: sources.interactionContract,
882
- defaultValue: DEFAULT_INTERACTION_CONTRACT_ENABLED,
856
+ id: "freeflow.contextVirtualization",
857
+ label: "Context Virtualization",
858
+ description: "Let the model archive consumed tool results from future context while preserving session history.",
859
+ path: ["contextVirtualization"],
860
+ effectiveValue: core.contextVirtualization,
861
+ effectiveSource: sources.contextVirtualization,
862
+ defaultValue: DEFAULT_CONTEXT_VIRTUALIZATION_ENABLED,
883
863
  });
884
- const skillsItem = createScopedBooleanItem({
864
+ const conversationHistoryItem = createScopedBooleanItem({
885
865
  scope,
886
866
  rawConfig,
887
867
  localConfig,
888
- id: "freeflow.skills.enabled",
889
- label: "Skills",
890
- description: "Expose Freeflow skills and load Workflow once on the first turn.",
891
- path: ["skills", "enabled"],
892
- effectiveValue: core.skills.enabled,
893
- effectiveSource: sources.skillsEnabled,
894
- defaultValue: DEFAULT_SKILLS_ENABLED,
895
- });
896
-
897
- const repositoryModeValue = validModeOrUndefined(rawConfig.defaultMode);
898
- const repositoryDefaultMode = repositoryModeValue ?? "workflow";
899
- const repositoryDefaultSource: ConfigSource = repositoryModeValue ? "repository" : "builtin";
900
- const localDefaultMode = validModeOrUndefined(localConfig.defaultMode);
901
- const defaultModeItem = createScopedDefaultModeItem({
902
- scope,
903
- repositoryDefaultMode,
904
- repositoryDefaultSource,
905
- localDefaultMode,
906
- effectiveValue: core.defaultMode,
907
- effectiveSource: sources.defaultMode,
868
+ id: "freeflow.conversationHistory",
869
+ label: "Conversation History",
870
+ description: "Let the model search and retrieve hidden conversation history on the active branch.",
871
+ path: ["conversationHistory"],
872
+ effectiveValue: core.conversationHistory,
873
+ effectiveSource: sources.conversationHistory,
874
+ defaultValue: DEFAULT_CONVERSATION_HISTORY_ENABLED,
908
875
  });
909
876
 
910
877
  const freeflowInactive = !core.enabled;
911
- const skillsEnabled = core.skills.enabled;
912
- interactionItem.inactive = freeflowInactive;
913
- skillsItem.inactive = freeflowInactive;
914
- defaultModeItem.inactive = freeflowInactive;
915
- defaultModeItem.displaySuffix = coreDisplaySuffix(defaultModeItem, freeflowInactive || !skillsEnabled);
916
- const sessionMode = modeState?.currentMode ?? "default";
917
- const sessionModeItem: SettingsItem = {
918
- id: "freeflow.sessionMode",
919
- label: "Session mode",
920
- description:
921
- "Temporary mode override for this Pi session. Use configured default clears the override without changing either config file.",
922
- kind: "enum",
923
- value: sessionMode,
924
- values: ["default", ...MODE_VALUES],
925
- valueLabels: {
926
- ...MODE_LABELS,
927
- default: "Use configured default",
928
- },
929
- valueDescriptions: {
930
- default: `${core.defaultMode} from ${sources.defaultMode}`,
931
- ...MODE_DESCRIPTIONS,
932
- },
933
- inactive: freeflowInactive || !skillsEnabled,
934
- displaySuffix: sessionModeDisplaySuffix(
935
- sessionMode,
936
- core.defaultMode,
937
- sources.defaultMode,
938
- freeflowInactive || !skillsEnabled,
939
- ),
940
- };
878
+ contextVirtualizationItem.inactive = freeflowInactive;
879
+ conversationHistoryItem.inactive = freeflowInactive;
880
+
881
+ const cognitiveRoutingState = options.cognitiveRouting;
882
+ const cognitiveRoutingRuntimeDisabled = options.runtimeAvailable !== true;
883
+ const cognitiveRoutingGroup = (() => {
884
+ const cognitiveRoutingEnabledItem = createScopedBooleanItem({
885
+ scope,
886
+ rawConfig,
887
+ localConfig,
888
+ id: "freeflow.cognitiveRouting.enabled",
889
+ label: "Enabled",
890
+ description: cognitiveRoutingRuntimeDisabled
891
+ ? "Requires a host model-state control API; configuration is read-only on this host."
892
+ : "Allow Cognitive Routing to own the session model lease and switch the configured profiles.",
893
+ path: ["cognitiveRouting", "enabled"],
894
+ effectiveValue: cognitiveRoutingState?.enabled ?? false,
895
+ effectiveSource: cognitiveRoutingSettingsSource(cognitiveRoutingState?.enabledSource),
896
+ defaultValue: false,
897
+ });
898
+ cognitiveRoutingEnabledItem.inactive = freeflowInactive || cognitiveRoutingRuntimeDisabled;
899
+ cognitiveRoutingEnabledItem.runtimeInactive = cognitiveRoutingRuntimeDisabled;
941
900
 
942
- const routerItems = outputRouterItems(rawConfig, freeflowInactive);
943
- const routerEnabled = groupEnabled(routerItems, "outputRouter.enabled");
901
+ const cognitiveRoutingProfiles = ["standard", "reasoning"].map((name) =>
902
+ cognitiveRoutingProfileItem({
903
+ name: name as CognitiveRoutingProfileName,
904
+ scope,
905
+ rawConfig,
906
+ localConfig,
907
+ capabilityState: cognitiveRoutingState,
908
+ ctx: options.ctx,
909
+ }),
910
+ );
911
+ const cognitiveRoutingSessionStart = (["control", "profile"] as CognitiveRoutingSessionStartSetting[]).map(
912
+ (setting) =>
913
+ cognitiveRoutingSessionStartItem({
914
+ setting,
915
+ scope,
916
+ rawConfig,
917
+ localConfig,
918
+ capabilityState: cognitiveRoutingState,
919
+ }),
920
+ );
921
+ for (const item of [...cognitiveRoutingProfiles, ...cognitiveRoutingSessionStart]) {
922
+ item.inactive ||= freeflowInactive || cognitiveRoutingRuntimeDisabled;
923
+ item.runtimeInactive = cognitiveRoutingRuntimeDisabled;
924
+ }
925
+ const cognitiveRoutingStatus = cognitiveRoutingRuntimeDisabled
926
+ ? "unavailable · host unsupported"
927
+ : cognitiveRoutingState
928
+ ? cognitiveRoutingState.effective
929
+ ? "active"
930
+ : cognitiveRoutingState.blockingReason?.code === "profile_missing"
931
+ ? "not configured"
932
+ : (cognitiveRoutingState.blockingReason?.code ?? "inactive")
933
+ : "unavailable";
934
+ return {
935
+ id: "freeflow.cognitiveRouting",
936
+ label: "Cognitive Routing",
937
+ description: cognitiveRoutingRuntimeDisabled
938
+ ? "Cognitive Routing configuration is visible for inspection but requires a host model-state control API."
939
+ : "Configure the Standard and Reasoning profiles and choose whether Freeflow may manage model state for this repository.",
940
+ kind: "group" as const,
941
+ value: cognitiveRoutingRuntimeDisabled ? false : (cognitiveRoutingState?.enabled ?? false),
942
+ inactive: freeflowInactive,
943
+ displaySuffix: cognitiveRoutingStatus,
944
+ children: [cognitiveRoutingEnabledItem, ...cognitiveRoutingProfiles, ...cognitiveRoutingSessionStart],
945
+ } satisfies SettingsItem;
946
+ })();
944
947
 
945
948
  return [
946
949
  freeflowItem,
947
- interactionItem,
948
- skillsItem,
949
- sessionModeItem,
950
- defaultModeItem,
950
+ ...(cognitiveRoutingGroup ? [cognitiveRoutingGroup] : []),
951
951
  {
952
- id: "outputRouter.group",
953
- label: "Output Router",
954
- description:
955
- "Shared repository settings for routed evidence tools, native output safety net, vault storage, script transforms, and observed tool routing.",
952
+ id: "freeflow.context",
953
+ label: "Freeflow Context",
954
+ description: "Choose which context projection and conversation-history operations are available to the model.",
956
955
  kind: "group",
957
- value: routerEnabled,
958
- inactive: freeflowInactive,
959
- displaySuffix: "(repository)",
960
- children: routerItems,
956
+ value: contextVirtualizationItem.effectiveValue === true || conversationHistoryItem.effectiveValue === true,
957
+ displaySuffix: `${[contextVirtualizationItem, conversationHistoryItem].filter((item) => item.effectiveValue === true).length}/2 enabled`,
958
+ children: [contextVirtualizationItem, conversationHistoryItem],
961
959
  },
962
960
  ];
963
961
  }
964
962
 
965
- function migrateLegacyRouterConfig(config: Record<string, unknown>) {
966
- const outputRouter = isRecord(config.outputRouter) ? config.outputRouter : undefined;
967
- if (!outputRouter) return;
968
- if (config.scriptTransform !== undefined && outputRouter.scriptTransform === undefined) {
969
- outputRouter.scriptTransform = config.scriptTransform;
970
- delete config.scriptTransform;
971
- }
972
- if (config.observedRouting !== undefined && outputRouter.observedRouting === undefined) {
973
- outputRouter.observedRouting = config.observedRouting;
974
- delete config.observedRouting;
975
- }
976
-
977
- const thresholds = isRecord(outputRouter.thresholds) ? outputRouter.thresholds : {};
978
- if (outputRouter.largeOutputBytes !== undefined && thresholds.largeOutputBytes === undefined) {
979
- thresholds.largeOutputBytes = outputRouter.largeOutputBytes;
980
- outputRouter.thresholds = thresholds;
981
- delete outputRouter.largeOutputBytes;
982
- }
983
- if (outputRouter.largeOutputLines !== undefined && thresholds.largeOutputLines === undefined) {
984
- thresholds.largeOutputLines = outputRouter.largeOutputLines;
985
- outputRouter.thresholds = thresholds;
986
- delete outputRouter.largeOutputLines;
987
- }
988
-
989
- const vault = isRecord(outputRouter.vault) ? outputRouter.vault : {};
990
- if (outputRouter.vaultRoot !== undefined && vault.root === undefined) {
991
- vault.root = outputRouter.vaultRoot;
992
- outputRouter.vault = vault;
993
- delete outputRouter.vaultRoot;
994
- }
995
- if (outputRouter.vaultRetentionDays !== undefined && !isRecord(vault.retention)) {
996
- vault.retention = {
997
- strategy: "ttl",
998
- ttlDays: outputRouter.vaultRetentionDays,
999
- };
1000
- outputRouter.vault = vault;
1001
- delete outputRouter.vaultRetentionDays;
1002
- }
1003
-
1004
- const hints = isRecord(outputRouter.hints) ? outputRouter.hints : {};
1005
- if (outputRouter.generatedPaths !== undefined && hints.generatedPathGlobs === undefined) {
1006
- hints.generatedPathGlobs = outputRouter.generatedPaths;
1007
- outputRouter.hints = hints;
1008
- delete outputRouter.generatedPaths;
1009
- }
1010
- if (outputRouter.noisyCommandHints !== undefined && hints.noisyCommandPatterns === undefined) {
1011
- hints.noisyCommandPatterns = outputRouter.noisyCommandHints;
1012
- outputRouter.hints = hints;
1013
- delete outputRouter.noisyCommandHints;
1014
- }
1015
- }
1016
-
1017
963
  function pruneKnownDefaults(config: Record<string, unknown>) {
1018
964
  const defaultPaths: Array<{ path: string[]; value: unknown }> = [
1019
965
  { path: ["enabled"], value: DEFAULT_FREEFLOW_ENABLED },
1020
- {
1021
- path: ["interactionContract"],
1022
- value: DEFAULT_INTERACTION_CONTRACT_ENABLED,
1023
- },
1024
- { path: ["skills", "enabled"], value: DEFAULT_SKILLS_ENABLED },
1025
- { path: ["outputRouter", "enabled"], value: DEFAULT_OUTPUT_ROUTER_ENABLED },
1026
- {
1027
- path: ["outputRouter", "postToolRouting"],
1028
- value: DEFAULT_POST_TOOL_ROUTING,
1029
- },
1030
- { path: ["outputRouter", "storagePolicy"], value: DEFAULT_STORAGE_POLICY },
1031
- {
1032
- path: ["outputRouter", "thresholds", "largeOutputBytes"],
1033
- value: DEFAULT_ROUTER_THRESHOLDS.largeOutputBytes,
1034
- },
1035
- {
1036
- path: ["outputRouter", "thresholds", "largeOutputLines"],
1037
- value: DEFAULT_ROUTER_THRESHOLDS.largeOutputLines,
1038
- },
1039
- { path: ["outputRouter", "vault", "root"], value: DEFAULT_VAULT_ROOT },
1040
- {
1041
- path: ["outputRouter", "vault", "retention", "ttlDays"],
1042
- value: DEFAULT_VAULT_RETENTION.ttlDays,
1043
- },
1044
- {
1045
- path: ["outputRouter", "scriptTransform", "enabled"],
1046
- value: DEFAULT_SCRIPT_TRANSFORM_CONFIG.enabled,
1047
- },
1048
- {
1049
- path: ["outputRouter", "scriptTransform", "sandbox"],
1050
- value: DEFAULT_SCRIPT_TRANSFORM_CONFIG.sandbox,
1051
- },
1052
- {
1053
- path: ["outputRouter", "scriptTransform", "languages"],
1054
- value: DEFAULT_SCRIPT_TRANSFORM_CONFIG.languages,
1055
- },
1056
- {
1057
- path: ["outputRouter", "scriptTransform", "network"],
1058
- value: DEFAULT_SCRIPT_TRANSFORM_CONFIG.network,
1059
- },
1060
- {
1061
- path: ["outputRouter", "scriptTransform", "rawScriptPersistence"],
1062
- value: DEFAULT_SCRIPT_TRANSFORM_CONFIG.rawScriptPersistence,
1063
- },
1064
- {
1065
- path: ["outputRouter", "scriptTransform", "limits", "timeoutMs"],
1066
- value: DEFAULT_SCRIPT_TRANSFORM_CONFIG.limits.timeoutMs,
1067
- },
1068
- {
1069
- path: ["outputRouter", "scriptTransform", "limits", "maxInputBytes"],
1070
- value: DEFAULT_SCRIPT_TRANSFORM_CONFIG.limits.maxInputBytes,
1071
- },
1072
- {
1073
- path: ["outputRouter", "scriptTransform", "limits", "maxOutputBytes"],
1074
- value: DEFAULT_SCRIPT_TRANSFORM_CONFIG.limits.maxOutputBytes,
1075
- },
1076
- {
1077
- path: ["outputRouter", "observedRouting", "enabled"],
1078
- value: DEFAULT_OBSERVED_ROUTING_CONFIG.enabled,
1079
- },
1080
- {
1081
- path: ["outputRouter", "observedRouting", "onRoutingFailure"],
1082
- value: DEFAULT_OBSERVED_ROUTING_CONFIG.onRoutingFailure,
1083
- },
1084
- {
1085
- path: ["outputRouter", "observedRouting", "mcp", "servers"],
1086
- value: DEFAULT_OBSERVED_ROUTING_CONFIG.mcp.servers,
1087
- },
1088
- {
1089
- path: ["outputRouter", "observedRouting", "web", "enabled"],
1090
- value: DEFAULT_OBSERVED_ROUTING_CONFIG.web.enabled,
1091
- },
1092
- {
1093
- path: ["outputRouter", "observedRouting", "fetch", "enabled"],
1094
- value: DEFAULT_OBSERVED_ROUTING_CONFIG.fetch.enabled,
1095
- },
1096
- {
1097
- path: ["outputRouter", "observedRouting", "codeSearch", "enabled"],
1098
- value: DEFAULT_OBSERVED_ROUTING_CONFIG.codeSearch.enabled,
1099
- },
966
+ { path: ["contextVirtualization"], value: DEFAULT_CONTEXT_VIRTUALIZATION_ENABLED },
967
+ { path: ["conversationHistory"], value: DEFAULT_CONVERSATION_HISTORY_ENABLED },
1100
968
  ];
1101
969
 
1102
970
  for (const item of defaultPaths) {
@@ -1104,19 +972,6 @@ function pruneKnownDefaults(config: Record<string, unknown>) {
1104
972
  deletePath(config, item.path);
1105
973
  }
1106
974
  }
1107
-
1108
- for (const producer of ["web", "fetch", "codeSearch"]) {
1109
- const enabledPath = ["outputRouter", "observedRouting", producer, "enabled"];
1110
- const persistencePath = ["outputRouter", "observedRouting", producer, "persistence"];
1111
- if (getPath(config, enabledPath) !== true && getPath(config, persistencePath) === "none") {
1112
- deletePath(config, persistencePath);
1113
- }
1114
- }
1115
-
1116
- const servers = getPath(config, ["outputRouter", "observedRouting", "mcp", "servers"]);
1117
- if (isRecord(servers) && Object.keys(servers).length === 0) {
1118
- deletePath(config, ["outputRouter", "observedRouting", "mcp", "servers"]);
1119
- }
1120
975
  }
1121
976
 
1122
977
  async function ensureLocalConfigIgnored(cwd: string) {
@@ -1190,15 +1045,20 @@ async function updateConfig(
1190
1045
  const current = await readFreeflowConfig(cwd);
1191
1046
  const next = cloneJson(current) as Record<string, unknown>;
1192
1047
  setConfigValue(next, item, value);
1193
- migrateLegacyRouterConfig(next);
1194
1048
  pruneKnownDefaults(next);
1195
1049
  await mkdir(join(cwd, ".freeflow"), { recursive: true });
1196
1050
  await writeFile(join(cwd, ".freeflow/config.json"), `${JSON.stringify(next, null, 2)}\n`, "utf8");
1197
1051
  }
1198
1052
 
1053
+ function isCognitiveRoutingProfileItem(item: SettingsItem): boolean {
1054
+ return item.id === "freeflow.cognitiveRouting.standard" || item.id === "freeflow.cognitiveRouting.reasoning";
1055
+ }
1056
+
1199
1057
  function valueForDisplay(item: SettingsItem): string {
1200
1058
  let value: string;
1201
- if (item.configScope === "local" || item.configScope === "session") {
1059
+ if (isCognitiveRoutingProfileItem(item) && item.format) {
1060
+ value = item.format(effectiveItemValue(item));
1061
+ } else if (item.configScope === "local" || item.configScope === "session") {
1202
1062
  value = formatCoreValue(effectiveItemValue(item));
1203
1063
  } else if (item.kind === "boolean") {
1204
1064
  value = booleanValue(item.value);
@@ -1233,54 +1093,46 @@ function findSettingsItem(items: SettingsItem[], id: string): SettingsItem | und
1233
1093
  function refreshSettingsDerivedState(items: SettingsItem[]) {
1234
1094
  const freeflowItem = findSettingsItem(items, "freeflow.enabled");
1235
1095
  const freeflowInactive = freeflowItem ? effectiveItemValue(freeflowItem) !== true : false;
1236
- const skillsItem = findSettingsItem(items, "freeflow.skills.enabled");
1237
- const skillsEnabled = skillsItem ? effectiveItemValue(skillsItem) === true : true;
1238
- const routerEnabled = findSettingsItem(items, "outputRouter.enabled")?.value === true;
1239
- const sessionModeItem = findSettingsItem(items, "freeflow.sessionMode");
1240
- const defaultModeItem = findSettingsItem(items, "freeflow.defaultMode");
1241
- const routerGroup = findSettingsItem(items, "outputRouter.group");
1242
-
1243
- if (sessionModeItem) {
1244
- const defaultMode = String(
1245
- defaultModeItem ? effectiveItemValue(defaultModeItem) : (sessionModeItem.inheritedValue ?? "workflow"),
1246
- );
1247
- const defaultSource = defaultModeItem?.effectiveSource ?? sessionModeItem.inheritedSource ?? "builtin";
1248
- sessionModeItem.inactive = freeflowInactive || !skillsEnabled;
1249
- sessionModeItem.displaySuffix = sessionModeDisplaySuffix(
1250
- String(sessionModeItem.value),
1251
- defaultMode,
1252
- defaultSource,
1253
- freeflowInactive || !skillsEnabled,
1096
+ const cognitiveRoutingGroup = findSettingsItem(items, "freeflow.cognitiveRouting");
1097
+ const cognitiveRoutingEnabledItem = findSettingsItem(items, "freeflow.cognitiveRouting.enabled");
1098
+ const cognitiveRoutingProfiles = [
1099
+ findSettingsItem(items, "freeflow.cognitiveRouting.standard"),
1100
+ findSettingsItem(items, "freeflow.cognitiveRouting.reasoning"),
1101
+ ];
1102
+
1103
+ if (cognitiveRoutingGroup && cognitiveRoutingEnabledItem) {
1104
+ const enabled = effectiveItemValue(cognitiveRoutingEnabledItem) === true;
1105
+ const profilesConfigured = cognitiveRoutingProfiles.every((item) =>
1106
+ item ? isCognitiveRoutingProfile(effectiveItemValue(item)) : false,
1254
1107
  );
1255
- sessionModeItem.valueDescriptions = {
1256
- default: `${defaultMode} from ${defaultSource}`,
1257
- ...MODE_DESCRIPTIONS,
1258
- };
1108
+ cognitiveRoutingGroup.value = enabled;
1109
+ cognitiveRoutingGroup.displaySuffix = enabled
1110
+ ? profilesConfigured
1111
+ ? cognitiveRoutingGroup.displaySuffix === "active"
1112
+ ? "active"
1113
+ : "configured"
1114
+ : "not configured"
1115
+ : "disabled";
1116
+ cognitiveRoutingGroup.inactive = freeflowInactive;
1259
1117
  }
1260
1118
 
1261
- if (routerGroup) {
1262
- routerGroup.value = routerEnabled;
1263
- routerGroup.inactive = freeflowInactive;
1119
+ const contextGroup = findSettingsItem(items, "freeflow.context");
1120
+ if (contextGroup?.children?.length) {
1121
+ const enabledCount = contextGroup.children.filter((item) => effectiveItemValue(item) === true).length;
1122
+ contextGroup.value = enabledCount > 0;
1123
+ contextGroup.displaySuffix = `${enabledCount}/${contextGroup.children.length} enabled`;
1264
1124
  }
1265
1125
 
1266
1126
  walkSettingsItems(items, (candidate) => {
1267
1127
  if (candidate.id === "freeflow.session.reset") {
1268
1128
  candidate.inactive = false;
1269
1129
  } else if (candidate.configScope) {
1270
- const inactive = candidate.id === "freeflow.enabled" ? false : freeflowInactive;
1271
- const displayInactive = candidate.id === "freeflow.defaultMode" ? freeflowInactive || !skillsEnabled : inactive;
1130
+ const inactive =
1131
+ candidate.id === "freeflow.enabled" ? false : candidate.runtimeInactive === true || freeflowInactive;
1272
1132
  candidate.inactive = inactive;
1273
- candidate.displaySuffix = coreDisplaySuffix(candidate, displayInactive);
1274
- } else if (candidate.id === "freeflow.sessionMode") {
1275
- candidate.inactive = freeflowInactive || !skillsEnabled;
1276
- } else if (candidate.id === "outputRouter.group") {
1277
- candidate.inactive = freeflowInactive;
1278
- } else if (candidate.id === "outputRouter.enabled") {
1279
- candidate.inactive = freeflowInactive;
1280
- } else if (candidate.id.startsWith("outputRouter.")) {
1281
- candidate.inactive = freeflowInactive || !routerEnabled;
1133
+ candidate.displaySuffix = coreDisplaySuffix(candidate, inactive);
1282
1134
  } else {
1283
- candidate.inactive = freeflowInactive;
1135
+ candidate.inactive = candidate.runtimeInactive === true || freeflowInactive;
1284
1136
  }
1285
1137
  });
1286
1138
  }
@@ -1327,6 +1179,7 @@ function settingsEntries(
1327
1179
  currentChoiceKey: () => String(item.value),
1328
1180
  choices: settingsChoices(item),
1329
1181
  children: item.children ? () => settingsEntries(item.children!, rootItems, onChange) : undefined,
1182
+ wizard: item.wizard ? () => item.wizard!() : undefined,
1330
1183
  edit:
1331
1184
  !item.children && !["boolean", "enum", "group"].includes(item.kind)
1332
1185
  ? {
@@ -1385,48 +1238,45 @@ async function openSettings(options: OpenSettingsOptions): Promise<SettingsSessi
1385
1238
  return component?.sessionResult() ?? { changed: false, configChanged: false, failed: false };
1386
1239
  }
1387
1240
 
1388
- function outputRouterStatusText(rawConfig: Record<string, unknown>): string {
1389
- const settingsConfig = normalizedSettingsConfig(rawConfig);
1390
- const actual = normalizeFreeflowConfig(rawConfig).config;
1391
- const freeflowEnabled = rawConfig.enabled !== false;
1392
- const router = actual.outputRouter;
1393
- const configured = settingsConfig;
1394
- const generatedPaths = router.hints?.generatedPathGlobs?.length ?? 0;
1395
- const noisyHints = router.hints?.noisyCommandPatterns?.length ?? 0;
1396
- return [
1397
- `Output Router: ${freeflowEnabled && router.enabled ? "enabled" : "disabled"}${freeflowEnabled ? "" : " (inactive: Freeflow off)"}`,
1398
- `script transform: ${freeflowEnabled && configured.scriptTransform.enabled ? "enabled" : "disabled"}${freeflowEnabled && router.enabled ? "" : " (inactive)"}`,
1399
- `observed routing: ${freeflowEnabled && configured.observedRouting.enabled ? "enabled" : "disabled"}${freeflowEnabled && router.enabled ? "" : " (inactive)"}`,
1400
- `native safety net: ${freeflowEnabled ? router.postToolRouting : "off"}`,
1401
- `storage: ${router.storagePolicy}`,
1402
- `generated paths: ${generatedPaths}`,
1403
- `noisy command hints: ${noisyHints}`,
1404
- ].join("; ");
1405
- }
1406
-
1407
- function freeflowStatusText(state: Awaited<ReturnType<typeof readCapabilityState>>): string {
1241
+ function freeflowStatusText(
1242
+ state: Awaited<ReturnType<typeof readCapabilityState>>,
1243
+ cognitiveRoutingController?: CognitiveRoutingSettingsController,
1244
+ ): string {
1408
1245
  if (!state.configured) {
1409
1246
  return state.configExists
1410
1247
  ? `Freeflow: inactive (invalid config: ${state.parseError ?? "unknown parse error"}); run /setup-freeflow or fix .freeflow/config.json`
1411
1248
  : "Freeflow: inactive (repo not set up); run /setup-freeflow";
1412
1249
  }
1413
1250
  const sessionSuffix = (source: ConfigSource) => (source === "session" ? " (session override)" : "");
1251
+ const routingState = cognitiveRoutingController?.state();
1252
+ const cognitiveRouting = state.cognitiveRouting;
1253
+ const cognitiveRoutingStatus = cognitiveRouting
1254
+ ? cognitiveRouting.blockingReason?.code === "runtime_disabled"
1255
+ ? "unavailable (host unsupported)"
1256
+ : cognitiveRouting.effective
1257
+ ? routingState?.effective
1258
+ ? `active (${routingState.activeProfile ?? "unknown"}, ${routingState.controlMode})`
1259
+ : "effective (inactive)"
1260
+ : cognitiveRouting.enabled
1261
+ ? `blocked (${cognitiveRouting.blockingReason.code})`
1262
+ : "disabled"
1263
+ : undefined;
1264
+ const contextEnabled = state.contextVirtualization?.effective || state.conversationHistory?.effective;
1414
1265
  return [
1415
1266
  `Freeflow: ${state.enabled ? "enabled" : "disabled"}${sessionSuffix(state.configSources.enabled as ConfigSource)}`,
1416
- `interaction contract: ${state.interactionContract.effective ? "enabled" : "disabled"}${sessionSuffix(
1417
- state.configSources.interactionContract as ConfigSource,
1418
- )}`,
1419
- `skills: ${state.skills.effective ? "enabled" : "disabled (workflow modes inactive)"}${sessionSuffix(
1420
- state.configSources.skillsEnabled as ConfigSource,
1421
- )}`,
1422
- `output router: ${state.outputRouter.enabled ? "enabled" : "disabled"}`,
1267
+ `context: ${contextEnabled ? "enabled" : "disabled"} (virtualization ${state.contextVirtualization?.effective ? "enabled" : "disabled"}, history ${state.conversationHistory?.effective ? "enabled" : "disabled"})`,
1268
+ ...(cognitiveRoutingStatus ? [`cognitive routing: ${cognitiveRoutingStatus}`] : []),
1423
1269
  ].join("; ");
1424
1270
  }
1425
1271
 
1426
1272
  const NON_TUI_SETTINGS_GUIDANCE =
1427
- "Freeflow settings require Pi TUI mode. Use /freeflow status to inspect current state; supported non-TUI changes are /freeflow enable, /freeflow disable, and /freeflow mode <conversation|workflow|strict-workflow|reset>.";
1428
- const NON_TUI_MODE_GUIDANCE =
1429
- "Freeflow mode selector requires Pi TUI mode. Use /freeflow mode conversation, /freeflow mode workflow, /freeflow mode strict-workflow, or /freeflow mode reset.";
1273
+ "Freeflow settings require Pi TUI mode. Use /freeflow status to inspect current state; supported non-TUI changes are /freeflow enable and /freeflow disable.";
1274
+
1275
+ function ensureSettingsIdle(ctx: any): boolean {
1276
+ if (typeof ctx?.isIdle !== "function" || ctx.isIdle()) return true;
1277
+ ctx.ui?.notify?.("Freeflow settings and profile changes are available only while Pi is idle.", "warning");
1278
+ return false;
1279
+ }
1430
1280
 
1431
1281
  function nonTuiGuidance(ctx: any, message: string) {
1432
1282
  if (ctx?.hasUI === false) throw new Error(message);
@@ -1437,13 +1287,14 @@ function nonTuiGuidance(ctx: any, message: string) {
1437
1287
  async function finalizeSettingsSession(
1438
1288
  session: SettingsSessionResult,
1439
1289
  ctx: any,
1440
- afterChange: (changed: boolean) => Promise<void> | void,
1290
+ afterChange: AfterChange,
1441
1291
  savedMessage: string,
1442
1292
  reloadWarning = "Run /reload for Freeflow changes to fully apply.",
1293
+ afterChangeOptions: AfterChangeOptions = {},
1443
1294
  ) {
1444
1295
  if (!session.configChanged) return false;
1445
1296
 
1446
- await afterChange(true);
1297
+ await afterChange(true, afterChangeOptions);
1447
1298
  if (session.failed) return false;
1448
1299
 
1449
1300
  ctx.ui.notify(savedMessage, "info");
@@ -1456,11 +1307,7 @@ async function finalizeSettingsSession(
1456
1307
  return true;
1457
1308
  }
1458
1309
 
1459
- async function finalizeSessionSettings(
1460
- session: SettingsSessionResult,
1461
- ctx: any,
1462
- afterChange: (changed: boolean) => Promise<void> | void,
1463
- ) {
1310
+ async function finalizeSessionSettings(session: SettingsSessionResult, ctx: any, afterChange: AfterChange) {
1464
1311
  if (!session.changed) return false;
1465
1312
 
1466
1313
  await afterChange(true);
@@ -1480,43 +1327,17 @@ async function finalizeSessionSettings(
1480
1327
  return true;
1481
1328
  }
1482
1329
 
1483
- function actionIsMutation(action: string): boolean {
1484
- return action === "" || action === "settings" || ["enable", "on", "true", "disable", "off", "false"].includes(action);
1485
- }
1486
-
1487
- async function maybeBlockLayerMutation(action: string, ctx: any, layerName: string) {
1488
- if (!actionIsMutation(action)) return false;
1489
- const state = await readCapabilityState(ctx.cwd);
1490
- if (!state.configured) {
1491
- ctx.ui.notify(
1492
- `Freeflow is installed but this repo is not set up. Run /setup-freeflow before configuring ${layerName}.`,
1493
- "warning",
1494
- );
1495
- return true;
1496
- }
1497
- if (!state.enabled) {
1498
- ctx.ui.notify(
1499
- `Freeflow is disabled for this repo. Use /freeflow enable before configuring ${layerName}.`,
1500
- "warning",
1501
- );
1502
- return true;
1503
- }
1504
- return false;
1505
- }
1506
-
1507
1330
  export async function handleFreeflowCommand(
1508
1331
  args: string | undefined,
1509
1332
  ctx: any,
1510
- afterChange: (changed: boolean) => Promise<void> | void,
1333
+ afterChange: AfterChange,
1511
1334
  pi: any,
1335
+ cognitiveRoutingController?: CognitiveRoutingSettingsController,
1512
1336
  ) {
1513
1337
  const input = (args ?? "settings").trim().toLowerCase() || "settings";
1514
1338
  const [action, ...rest] = input.split(/\s+/);
1515
1339
  const actionValue = rest.join(" ");
1516
1340
  const nonTui = ctx?.mode && ctx.mode !== "tui";
1517
- if (nonTui && action === "mode" && !actionValue) {
1518
- return nonTuiGuidance(ctx, NON_TUI_MODE_GUIDANCE);
1519
- }
1520
1341
  const settingsSelector =
1521
1342
  action === "settings" &&
1522
1343
  (!actionValue || ["session", "local", "personal", "repo", "repository", "shared"].includes(actionValue));
@@ -1524,21 +1345,20 @@ export async function handleFreeflowCommand(
1524
1345
  return nonTuiGuidance(ctx, NON_TUI_SETTINGS_GUIDANCE);
1525
1346
  }
1526
1347
 
1527
- const [layers, state, modeState] = await Promise.all([
1348
+ const [layers, state] = await Promise.all([
1528
1349
  readFreeflowConfigLayers(ctx.cwd),
1529
- readCapabilityState(ctx.cwd),
1530
- readModeState(ctx.cwd),
1350
+ readCapabilityState(ctx.cwd, ctx, pi?.host),
1531
1351
  ]);
1532
1352
  const configState = layers.repository;
1533
1353
  const raw = configState.valid ? configState.parsed : {};
1534
1354
 
1535
1355
  if (action === "status") {
1536
- ctx.ui.notify(freeflowStatusText(state), "info");
1356
+ ctx.ui.notify(freeflowStatusText(state, cognitiveRoutingController), "info");
1537
1357
  return { changed: false, reloaded: false };
1538
1358
  }
1539
1359
 
1540
1360
  if (!configState.valid) {
1541
- ctx.ui.notify(freeflowStatusText(state), "warning");
1361
+ ctx.ui.notify(freeflowStatusText(state, cognitiveRoutingController), "warning");
1542
1362
  return { changed: false, reloaded: false, error: "not_configured" };
1543
1363
  }
1544
1364
  if (layers.local.exists && !layers.local.valid) {
@@ -1549,42 +1369,15 @@ export async function handleFreeflowCommand(
1549
1369
  return { changed: false, reloaded: false, error: "invalid_local_config" };
1550
1370
  }
1551
1371
 
1552
- if (action === "mode") {
1553
- if (actionValue) {
1554
- const result = await handleModeCommand(actionValue, ctx, pi);
1555
- return { changed: result.changed, reloaded: false, error: result.error };
1556
- }
1557
- if (!state.enabled || !state.skills.effective) {
1558
- const result = await handleModeCommand(undefined, ctx, pi);
1559
- return { changed: false, reloaded: false, error: result.error };
1560
- }
1561
- if (typeof ctx?.ui?.custom !== "function") {
1562
- const result = await handleModeCommand(undefined, ctx, pi);
1563
- return { changed: false, reloaded: false, error: result.error };
1564
- }
1565
-
1566
- const item = freeflowItems(raw, modeState, {
1567
- scope: "local",
1568
- layers,
1569
- }).find((candidate) => candidate.id === "freeflow.sessionMode")!;
1570
- const session = await openSettings({
1571
- title: "Freeflow Mode",
1572
- items: [item],
1573
- initialChoice: item,
1574
- ctx,
1575
- onChange: async (_item, value) => {
1576
- const result = await handleModeCommand(String(value), ctx, pi);
1577
- return { changed: result.changed, reloadRequired: false };
1578
- },
1579
- });
1580
- return { changed: session.changed, reloaded: false, error: session.failed ? "write_failed" : undefined };
1581
- }
1582
-
1583
1372
  if (["enable", "on", "true", "disable", "off", "false"].includes(action)) {
1373
+ if (!ensureSettingsIdle(ctx)) return { changed: false, reloaded: false, error: "busy" };
1584
1374
  const enabled = ["enable", "on", "true"].includes(action);
1585
- const item = freeflowItems(raw, modeState, {
1375
+ const item = freeflowItems(raw, {
1586
1376
  scope: "repository",
1587
1377
  layers,
1378
+ cognitiveRouting: state.cognitiveRouting as CognitiveRoutingCapabilityState,
1379
+ ctx,
1380
+ runtimeAvailable: isCognitiveRoutingRuntimeAvailable(pi),
1588
1381
  }).find((candidate) => candidate.id === "freeflow.enabled")!;
1589
1382
  await updateConfig(ctx.cwd, item, enabled, "repository");
1590
1383
  await afterChange(true);
@@ -1599,7 +1392,7 @@ export async function handleFreeflowCommand(
1599
1392
 
1600
1393
  if (action && action !== "settings") {
1601
1394
  ctx.ui.notify(
1602
- "Usage: /freeflow, /freeflow settings [local|repo], /freeflow status, /freeflow mode [conversation|workflow|strict-workflow|reset], /freeflow enable, or /freeflow disable",
1395
+ "Usage: /freeflow, /freeflow settings [local|repo], /freeflow status, /freeflow enable, or /freeflow disable",
1603
1396
  "warning",
1604
1397
  );
1605
1398
  return { changed: false, reloaded: false, error: "invalid_action" };
@@ -1618,10 +1411,19 @@ export async function handleFreeflowCommand(
1618
1411
  return { changed: false, reloaded: false, error: "invalid_scope" };
1619
1412
  }
1620
1413
 
1414
+ if (!ensureSettingsIdle(ctx)) return { changed: false, reloaded: false, error: "busy" };
1415
+
1416
+ let reconcileCognitiveRouting = settingsScope === "session";
1621
1417
  const items =
1622
1418
  settingsScope === "session"
1623
- ? sessionFreeflowItems(state, modeState)
1624
- : freeflowItems(raw, modeState, { scope: settingsScope, layers });
1419
+ ? sessionFreeflowItems(state, cognitiveRoutingController)
1420
+ : freeflowItems(raw, {
1421
+ scope: settingsScope,
1422
+ layers,
1423
+ cognitiveRouting: state.cognitiveRouting as CognitiveRoutingCapabilityState,
1424
+ ctx,
1425
+ runtimeAvailable: isCognitiveRoutingRuntimeAvailable(pi),
1426
+ });
1625
1427
  const session = await openSettings({
1626
1428
  title:
1627
1429
  settingsScope === "session"
@@ -1632,9 +1434,29 @@ export async function handleFreeflowCommand(
1632
1434
  items,
1633
1435
  ctx,
1634
1436
  onChange: async (item, value) => {
1635
- if (item.id === "freeflow.sessionMode") {
1636
- const result = await handleModeCommand(String(value), ctx, pi);
1637
- return { changed: result.changed, reloadRequired: false };
1437
+ if (!ensureSettingsIdle(ctx)) return { changed: false, reloadRequired: false };
1438
+ if (!isCognitiveRoutingRuntimeAvailable(pi) && item.id.startsWith("freeflow.cognitiveRouting.")) {
1439
+ ctx.ui.notify("Cognitive Routing is unavailable because this host lacks model-state controls.", "warning");
1440
+ return { changed: false, reloadRequired: false };
1441
+ }
1442
+ if (item.id === "freeflow.cognitiveRouting.profile") {
1443
+ if (!cognitiveRoutingController) {
1444
+ ctx.ui.notify("Cognitive Routing is unavailable for this session.", "warning");
1445
+ return { changed: false, reloadRequired: false };
1446
+ }
1447
+ const result =
1448
+ value === "auto"
1449
+ ? await cognitiveRoutingController.setAutomaticControl("profile-settings")
1450
+ : await cognitiveRoutingController.setManualProfile(value as "standard" | "reasoning", "profile-settings");
1451
+ if ((result.status !== "automatic" && result.status !== "active") || result.reason) {
1452
+ ctx.ui.notify(
1453
+ `Cognitive Routing settings could not be applied: ${result.reason ?? result.status}.`,
1454
+ "warning",
1455
+ );
1456
+ return { changed: false, reloadRequired: false };
1457
+ }
1458
+ await afterChange(false);
1459
+ return { changed: true, reloadRequired: false };
1638
1460
  }
1639
1461
  if (item.id === "freeflow.session.reset") {
1640
1462
  const result = await resetSessionOverrides(ctx, pi);
@@ -1643,8 +1465,8 @@ export async function handleFreeflowCommand(
1643
1465
  if (item.configScope === "session") {
1644
1466
  const keyById = {
1645
1467
  "freeflow.enabled": "enabled",
1646
- "freeflow.interactionContract": "interactionContract",
1647
- "freeflow.skills.enabled": "skillsEnabled",
1468
+ "freeflow.contextVirtualization": "contextVirtualization",
1469
+ "freeflow.conversationHistory": "conversationHistory",
1648
1470
  } as const;
1649
1471
  const key = keyById[item.id as keyof typeof keyById];
1650
1472
  const override = value === LOCAL_INHERIT ? null : value === "true";
@@ -1652,6 +1474,9 @@ export async function handleFreeflowCommand(
1652
1474
  return { changed: result.changed, reloadRequired: result.reloadRequired === true };
1653
1475
  }
1654
1476
  await updateConfig(ctx.cwd, item, value, item.configScope ?? "repository");
1477
+ if (!item.id.startsWith("freeflow.cognitiveRouting.sessionStart.")) {
1478
+ reconcileCognitiveRouting = true;
1479
+ }
1655
1480
  return { changed: true, reloadRequired: true };
1656
1481
  },
1657
1482
  });
@@ -1667,64 +1492,8 @@ export async function handleFreeflowCommand(
1667
1492
  ctx,
1668
1493
  afterChange,
1669
1494
  `Freeflow ${savedTarget} saved. Reloading Freeflow runtime...`,
1670
- );
1671
- return { changed: session.changed, reloaded, error: session.failed ? "write_failed" : undefined };
1672
- }
1673
-
1674
- export async function handleOutputRouterCommand(
1675
- args: string | undefined,
1676
- ctx: any,
1677
- afterChange: (changed: boolean) => Promise<void> | void,
1678
- ) {
1679
- const action = (args ?? "").trim().toLowerCase();
1680
- const raw = await readFreeflowConfig(ctx.cwd);
1681
-
1682
- if (await maybeBlockLayerMutation(action, ctx, "Output Router")) {
1683
- return { changed: false, reloaded: false, error: "freeflow_inactive" };
1684
- }
1685
-
1686
- if (action === "status") {
1687
- const state = await readCapabilityState(ctx.cwd);
1688
- const prefix = !state.configured || !state.enabled ? `${freeflowStatusText(state)}; ` : "";
1689
- ctx.ui.notify(`${prefix}${outputRouterStatusText(raw)}`, !state.configured || !state.enabled ? "warning" : "info");
1690
- return { changed: false, reloaded: false };
1691
- }
1692
-
1693
- if (["enable", "on", "true", "disable", "off", "false"].includes(action)) {
1694
- const enabled = ["enable", "on", "true"].includes(action);
1695
- const item = outputRouterItems(raw).find((candidate) => candidate.id === "outputRouter.enabled")!;
1696
- await updateConfig(ctx.cwd, item, enabled);
1697
- await afterChange(true);
1698
- ctx.ui.notify(`Output Router ${enabled ? "enabled" : "disabled"}. Reloading Freeflow runtime...`, "info");
1699
- if (typeof ctx.reload === "function") {
1700
- await ctx.reload();
1701
- return { changed: true, reloaded: true };
1702
- }
1703
- ctx.ui.notify("Run /reload for Output Router changes to fully apply.", "warning");
1704
- return { changed: true, reloaded: false };
1705
- }
1706
-
1707
- if (action && action !== "settings") {
1708
- ctx.ui.notify("Usage: /output-router, /output-router settings, or /output-router status", "warning");
1709
- return { changed: false, reloaded: false, error: "invalid_action" };
1710
- }
1711
-
1712
- const session = await openSettings({
1713
- title: "Output Router Settings",
1714
- items: outputRouterItems(raw),
1715
- ctx,
1716
- onChange: async (item, value) => {
1717
- await updateConfig(ctx.cwd, item, value);
1718
- return { changed: true, reloadRequired: true };
1719
- },
1720
- });
1721
-
1722
- const reloaded = await finalizeSettingsSession(
1723
- session,
1724
- ctx,
1725
- afterChange,
1726
- "Output Router settings saved. Reloading Freeflow runtime...",
1727
- "Run /reload for Output Router changes to fully apply.",
1495
+ "Run /reload for Freeflow changes to fully apply.",
1496
+ { reconcileCognitiveRouting },
1728
1497
  );
1729
1498
  return { changed: session.changed, reloaded, error: session.failed ? "write_failed" : undefined };
1730
1499
  }