@hassangameryt/freeflow 0.4.0 → 0.6.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 (285) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +2 -1
  3. package/.codex-plugin/plugin.json +2 -1
  4. package/README.md +150 -155
  5. package/capabilities/cognitive-routing/SKILL.md +229 -0
  6. package/capabilities/context-virtualization/SKILL.md +103 -0
  7. package/capabilities/conversation-history/SKILL.md +53 -0
  8. package/command-surface.json +17 -13
  9. package/hooks/freeflow-runtime-context.mjs +393 -159
  10. package/hooks/hooks.json +26 -1
  11. package/hooks/tests/check-runtime-context-hook.sh +223 -54
  12. package/package.json +25 -16
  13. package/pi-extension/dist/cognitive-routing/commands.js +96 -0
  14. package/pi-extension/dist/cognitive-routing/config.js +236 -0
  15. package/pi-extension/dist/cognitive-routing/controller.js +862 -0
  16. package/pi-extension/dist/cognitive-routing/history.js +439 -0
  17. package/pi-extension/dist/cognitive-routing/host.js +126 -0
  18. package/pi-extension/dist/cognitive-routing/runtime.js +43 -0
  19. package/pi-extension/dist/cognitive-routing/tool.js +211 -0
  20. package/pi-extension/dist/cognitive-routing/types.js +6 -0
  21. package/pi-extension/dist/context-virtualization/commands.js +89 -0
  22. package/pi-extension/dist/context-virtualization/projector.js +42 -0
  23. package/pi-extension/dist/context-virtualization/runtime.js +356 -0
  24. package/pi-extension/dist/context-virtualization/state.js +57 -0
  25. package/pi-extension/dist/context-virtualization/types.js +10 -0
  26. package/pi-extension/dist/conversation-history/passages.js +66 -0
  27. package/pi-extension/dist/conversation-history/ranking.js +105 -0
  28. package/pi-extension/dist/conversation-history/runtime.js +226 -0
  29. package/pi-extension/dist/conversation-history/search.js +56 -0
  30. package/pi-extension/dist/conversation-history/types.js +1 -0
  31. package/pi-extension/dist/freeflow-context/resolver.js +120 -0
  32. package/pi-extension/dist/freeflow-context/runtime.js +80 -0
  33. package/pi-extension/dist/freeflow-context/source-projector.js +95 -0
  34. package/pi-extension/dist/freeflow-context/tool.js +308 -0
  35. package/pi-extension/dist/freeflow-context/types.js +12 -0
  36. package/pi-extension/dist/freeflow-context/visibility.js +28 -0
  37. package/pi-extension/dist/index.js +516 -98
  38. package/pi-extension/dist/runtime/runtime-context.js +318 -271
  39. package/pi-extension/dist/runtime/runtime-identity.js +5 -0
  40. package/pi-extension/dist/settings/settings-tui.js +78 -0
  41. package/pi-extension/dist/settings/settings-ui.js +593 -563
  42. package/pi-extension/dist/ui/text-component.js +57 -0
  43. package/pi-extension/src/cognitive-routing/commands.ts +119 -0
  44. package/pi-extension/src/cognitive-routing/config.ts +307 -0
  45. package/pi-extension/src/cognitive-routing/controller.ts +1118 -0
  46. package/pi-extension/src/cognitive-routing/history.ts +638 -0
  47. package/pi-extension/src/cognitive-routing/host.ts +172 -0
  48. package/pi-extension/src/cognitive-routing/runtime.ts +57 -0
  49. package/pi-extension/src/cognitive-routing/tool.ts +272 -0
  50. package/pi-extension/src/cognitive-routing/types.ts +89 -0
  51. package/pi-extension/src/context-virtualization/commands.ts +111 -0
  52. package/pi-extension/src/context-virtualization/projector.ts +62 -0
  53. package/pi-extension/src/context-virtualization/runtime.ts +406 -0
  54. package/pi-extension/src/context-virtualization/state.ts +79 -0
  55. package/pi-extension/src/context-virtualization/types.ts +92 -0
  56. package/pi-extension/src/conversation-history/passages.ts +81 -0
  57. package/pi-extension/src/conversation-history/ranking.ts +155 -0
  58. package/pi-extension/src/conversation-history/runtime.ts +250 -0
  59. package/pi-extension/src/conversation-history/search.ts +79 -0
  60. package/pi-extension/src/conversation-history/types.ts +31 -0
  61. package/pi-extension/src/freeflow-context/resolver.ts +139 -0
  62. package/pi-extension/src/freeflow-context/runtime.ts +100 -0
  63. package/pi-extension/src/freeflow-context/source-projector.ts +106 -0
  64. package/pi-extension/src/freeflow-context/tool.ts +360 -0
  65. package/pi-extension/src/freeflow-context/types.ts +70 -0
  66. package/pi-extension/src/freeflow-context/visibility.ts +53 -0
  67. package/pi-extension/src/index.ts +553 -101
  68. package/pi-extension/src/runtime/runtime-context.ts +339 -281
  69. package/pi-extension/src/runtime/runtime-identity.ts +8 -0
  70. package/pi-extension/src/settings/settings-tui.ts +93 -0
  71. package/pi-extension/src/settings/settings-ui.ts +700 -613
  72. package/pi-extension/src/ui/text-component.ts +66 -0
  73. package/pi-extension/tests/cognitive-routing/commands.test.js +134 -0
  74. package/pi-extension/tests/cognitive-routing/controller.test.js +1099 -0
  75. package/pi-extension/tests/cognitive-routing/extension-lifecycle.test.js +1071 -0
  76. package/pi-extension/tests/cognitive-routing/history.test.js +274 -0
  77. package/pi-extension/tests/cognitive-routing/host-fixture.js +4 -0
  78. package/pi-extension/tests/cognitive-routing/host-identity.test.js +63 -0
  79. package/pi-extension/tests/cognitive-routing/profile-resolution.test.js +236 -0
  80. package/pi-extension/tests/cognitive-routing/tool.test.js +230 -0
  81. package/pi-extension/tests/context-virtualization/extension.test.js +749 -0
  82. package/pi-extension/tests/context-virtualization/projector.test.js +110 -0
  83. package/pi-extension/tests/context-virtualization/runtime.test.js +216 -0
  84. package/pi-extension/tests/conversation-history/runtime.test.js +98 -0
  85. package/pi-extension/tests/conversation-history/search.test.js +97 -0
  86. package/pi-extension/tests/integration/pi-extension.test.js +928 -2047
  87. package/pi-extension/tests/integration/prompt-architecture.test.js +305 -0
  88. package/runtime/prompts/cognitive-routing.md +9 -0
  89. package/runtime/prompts/context-virtualization.md +3 -0
  90. package/runtime/prompts/conversation-history.md +3 -0
  91. package/runtime/prompts/core.md +13 -0
  92. package/runtime/prompts/interaction-contract.md +7 -0
  93. package/runtime/prompts/skills.md +65 -0
  94. package/skills/action-selection/SKILL.md +91 -0
  95. package/skills/bypass/SKILL.md +3 -2
  96. package/skills/commit-work/SKILL.md +1 -1
  97. package/skills/commit-work/references/staging-decisions.md +1 -1
  98. package/skills/decision-gate/SKILL.md +1 -1
  99. package/skills/design-for-depth/SKILL.md +14 -33
  100. package/skills/design-for-depth/references/design-pressure-signals.md +1 -1
  101. package/skills/design-for-depth/references/interface-design-loop.md +1 -1
  102. package/skills/design-for-depth/references/software-design-philosophy.md +1 -1
  103. package/skills/diagnose-failure/SKILL.md +20 -14
  104. package/skills/diagnose-failure/references/diagnostic-loop-catalog.md +1 -3
  105. package/skills/diagnose-failure/references/{flaky-and-performance.md → flaky-and-performance-diagnosis.md} +1 -1
  106. package/skills/discuss/SKILL.md +34 -22
  107. package/skills/discuss/references/discussion-continuity.md +71 -0
  108. package/skills/evaluate-skill/SKILL.md +5 -5
  109. package/skills/evaluate-skill/references/{definition-schema.md → evaluation-definition-schema.md} +42 -2
  110. package/skills/evaluate-skill/references/execution-and-evidence.md +8 -3
  111. package/skills/evaluate-skill/scripts/lib/definitions.mjs +114 -3
  112. package/skills/evaluate-skill/scripts/lib/grade-interaction.mjs +156 -1
  113. package/skills/evaluate-skill/scripts/lib/grade.mjs +9 -1
  114. package/skills/evaluate-skill/scripts/lib/pi.mjs +147 -32
  115. package/skills/evaluate-skill/scripts/lib/rpc.mjs +4 -4
  116. package/skills/evaluate-skill/scripts/lib/runner.mjs +36 -14
  117. package/skills/evaluate-skill/scripts/lib/sandbox.mjs +92 -0
  118. package/skills/evaluate-skill/scripts/pi-guard.mjs +1 -1
  119. package/skills/evaluate-skill/scripts/pi-observer.mjs +66 -0
  120. package/skills/evaluate-skill/scripts/skill-eval.mjs +6 -5
  121. package/skills/execute-work/SKILL.md +18 -15
  122. package/skills/execute-work/references/code-practices.md +1 -1
  123. package/skills/execute-work/references/execute-work-edges.md +42 -0
  124. package/skills/finish-branch/SKILL.md +2 -2
  125. package/skills/finish-branch/references/integration-options.md +1 -1
  126. package/skills/handoff/SKILL.md +3 -3
  127. package/skills/launch-work/SKILL.md +1 -1
  128. package/skills/launch-work/references/launch-readiness.md +2 -2
  129. package/skills/migration-work/SKILL.md +2 -2
  130. package/skills/migration-work/references/migration-lifecycle.md +1 -1
  131. package/skills/mode-contract/SKILL.md +32 -10
  132. package/skills/release-work/SKILL.md +1 -1
  133. package/skills/release-work/references/release-evidence.md +1 -1
  134. package/skills/review-artifact/SKILL.md +28 -26
  135. package/skills/review-work/SKILL.md +28 -26
  136. package/skills/review-work/references/{reviewer-prompt.md → independent-work-reviewer-contract.md} +1 -1
  137. package/skills/review-work/references/security-risk-lens.md +1 -1
  138. package/skills/setup-freeflow/SKILL.md +6 -10
  139. package/skills/setup-freeflow/references/activation-contract.md +5 -10
  140. package/skills/setup-freeflow/references/host-setup.md +3 -7
  141. package/skills/simplify-code/SKILL.md +1 -1
  142. package/skills/simplify-code/references/simplification-patterns.md +1 -1
  143. package/skills/tdd/SKILL.md +18 -38
  144. package/skills/tdd/references/test-design.md +2 -2
  145. package/skills/track-work/SKILL.md +362 -58
  146. package/skills/track-work/scripts/lib/codec.mjs +570 -0
  147. package/skills/track-work/scripts/lib/commands.mjs +568 -0
  148. package/skills/track-work/scripts/lib/contract.mjs +303 -0
  149. package/skills/track-work/scripts/lib/model.mjs +506 -0
  150. package/skills/track-work/scripts/lib/operations.mjs +965 -0
  151. package/skills/track-work/scripts/lib/record-store.mjs +264 -0
  152. package/skills/track-work/scripts/lib/result.mjs +37 -0
  153. package/skills/track-work/scripts/lib/rewrites.mjs +740 -0
  154. package/skills/track-work/scripts/lib/schema.mjs +277 -0
  155. package/skills/track-work/scripts/lib/source-inventory.mjs +162 -0
  156. package/skills/track-work/scripts/lib/views.mjs +443 -0
  157. package/skills/track-work/scripts/lib/workspace.mjs +134 -0
  158. package/skills/track-work/scripts/working-record.mjs +122 -0
  159. package/skills/verify-work/SKILL.md +14 -2
  160. package/skills/verify-work/references/browser-runtime-evidence.md +1 -1
  161. package/skills/verify-work/references/integration-evidence.md +1 -1
  162. package/skills/verify-work/references/performance-evidence.md +1 -1
  163. package/skills/workflow/SKILL.md +105 -53
  164. package/skills/workflow/references/domain-skill-composition.md +22 -42
  165. package/skills/write-plan/SKILL.md +1 -1
  166. package/skills/write-skill/SKILL.md +3 -2
  167. package/skills/write-skill/references/progressive-disclosure.md +17 -3
  168. package/pi-extension/dist/output-router/host-producer-identification.js +0 -232
  169. package/pi-extension/dist/output-router/native-safety-net.js +0 -216
  170. package/pi-extension/dist/output-router/observed-tool-routing.js +0 -122
  171. package/pi-extension/dist/output-router/renderers.js +0 -1023
  172. package/pi-extension/dist/output-router/router-tools.js +0 -554
  173. package/pi-extension/dist/output-router/schemas.js +0 -383
  174. package/pi-extension/dist/output-router/status.js +0 -744
  175. package/pi-extension/dist/output-router/utils.js +0 -502
  176. package/pi-extension/src/output-router/host-producer-identification.ts +0 -256
  177. package/pi-extension/src/output-router/native-safety-net.ts +0 -248
  178. package/pi-extension/src/output-router/observed-tool-routing.ts +0 -134
  179. package/pi-extension/src/output-router/renderers.ts +0 -1108
  180. package/pi-extension/src/output-router/router-tools.ts +0 -579
  181. package/pi-extension/src/output-router/schemas.ts +0 -387
  182. package/pi-extension/src/output-router/status.ts +0 -791
  183. package/pi-extension/src/output-router/utils.ts +0 -554
  184. package/pi-extension/tests/output-router/observed-producers.test.js +0 -132
  185. package/pi-extension/tests/output-router/search.test.js +0 -722
  186. package/router/dist/benchmarks/benchmark-harness.d.ts +0 -35
  187. package/router/dist/benchmarks/benchmark-harness.js +0 -92
  188. package/router/dist/benchmarks/benchmarks.d.ts +0 -109
  189. package/router/dist/benchmarks/benchmarks.js +0 -979
  190. package/router/dist/benchmarks/codex-qa-benchmarks.d.ts +0 -95
  191. package/router/dist/benchmarks/codex-qa-benchmarks.js +0 -500
  192. package/router/dist/benchmarks/command-benchmarks.d.ts +0 -143
  193. package/router/dist/benchmarks/command-benchmarks.js +0 -698
  194. package/router/dist/benchmarks/context-mode-normalized-benchmarks.d.ts +0 -82
  195. package/router/dist/benchmarks/context-mode-normalized-benchmarks.js +0 -773
  196. package/router/dist/benchmarks/context-mode-real-deep-benchmark.d.ts +0 -132
  197. package/router/dist/benchmarks/context-mode-real-deep-benchmark.js +0 -1380
  198. package/router/dist/benchmarks/context-saving-baseline.d.ts +0 -60
  199. package/router/dist/benchmarks/context-saving-baseline.js +0 -470
  200. package/router/dist/benchmarks/index-benchmarks.d.ts +0 -103
  201. package/router/dist/benchmarks/index-benchmarks.js +0 -862
  202. package/router/dist/benchmarks/storage-policy-benchmarks.d.ts +0 -101
  203. package/router/dist/benchmarks/storage-policy-benchmarks.js +0 -618
  204. package/router/dist/benchmarks/vault-index-benchmarks.d.ts +0 -56
  205. package/router/dist/benchmarks/vault-index-benchmarks.js +0 -322
  206. package/router/dist/config/config.d.ts +0 -103
  207. package/router/dist/config/config.js +0 -632
  208. package/router/dist/config/router-contract.d.ts +0 -10
  209. package/router/dist/config/router-contract.js +0 -54
  210. package/router/dist/config/schema.d.ts +0 -27
  211. package/router/dist/config/schema.js +0 -648
  212. package/router/dist/config/types.d.ts +0 -573
  213. package/router/dist/config/types.js +0 -41
  214. package/router/dist/evidence/bounded-evidence.d.ts +0 -40
  215. package/router/dist/evidence/bounded-evidence.js +0 -323
  216. package/router/dist/evidence/evidence-range-selector.d.ts +0 -24
  217. package/router/dist/evidence/evidence-range-selector.js +0 -232
  218. package/router/dist/evidence/evidence-search.d.ts +0 -27
  219. package/router/dist/evidence/evidence-search.js +0 -490
  220. package/router/dist/evidence/evidence.d.ts +0 -38
  221. package/router/dist/evidence/evidence.js +0 -200
  222. package/router/dist/evidence/failure-contracts.d.ts +0 -37
  223. package/router/dist/evidence/failure-contracts.js +0 -166
  224. package/router/dist/evidence/line-ranges.d.ts +0 -24
  225. package/router/dist/evidence/line-ranges.js +0 -22
  226. package/router/dist/experiments/local-index.d.ts +0 -64
  227. package/router/dist/experiments/local-index.js +0 -581
  228. package/router/dist/index.d.ts +0 -30
  229. package/router/dist/index.js +0 -32
  230. package/router/dist/local/local-traversal.d.ts +0 -18
  231. package/router/dist/local/local-traversal.js +0 -334
  232. package/router/dist/processing/engine.d.ts +0 -132
  233. package/router/dist/processing/engine.js +0 -533
  234. package/router/dist/processing/reducers.d.ts +0 -280
  235. package/router/dist/processing/reducers.js +0 -1673
  236. package/router/dist/processing/renderers.d.ts +0 -31
  237. package/router/dist/processing/renderers.js +0 -195
  238. package/router/dist/processing/scripts.d.ts +0 -91
  239. package/router/dist/processing/scripts.js +0 -386
  240. package/router/dist/repo/repo-traversal.d.ts +0 -18
  241. package/router/dist/repo/repo-traversal.js +0 -245
  242. package/router/dist/routing/observed-reducers.d.ts +0 -41
  243. package/router/dist/routing/observed-reducers.js +0 -661
  244. package/router/dist/routing/observed-routing.d.ts +0 -26
  245. package/router/dist/routing/observed-routing.js +0 -378
  246. package/router/dist/routing/parsers.d.ts +0 -16
  247. package/router/dist/routing/parsers.js +0 -521
  248. package/router/dist/routing/run-filters.d.ts +0 -60
  249. package/router/dist/routing/run-filters.js +0 -238
  250. package/router/dist/routing/run-reducers.d.ts +0 -49
  251. package/router/dist/routing/run-reducers.js +0 -122
  252. package/router/dist/sandbox/adapter-roots.d.ts +0 -46
  253. package/router/dist/sandbox/adapter-roots.js +0 -66
  254. package/router/dist/sandbox/eryx-python-adapter.d.ts +0 -13
  255. package/router/dist/sandbox/eryx-python-adapter.js +0 -868
  256. package/router/dist/sandbox/jq-wasm-adapter.d.ts +0 -9
  257. package/router/dist/sandbox/jq-wasm-adapter.js +0 -429
  258. package/router/dist/sandbox/quickjs-wasi-adapter.d.ts +0 -13
  259. package/router/dist/sandbox/quickjs-wasi-adapter.js +0 -384
  260. package/router/dist/sandbox/script-sandbox.d.ts +0 -134
  261. package/router/dist/sandbox/script-sandbox.js +0 -337
  262. package/router/dist/setup/script-transform-adapters.d.ts +0 -32
  263. package/router/dist/setup/script-transform-adapters.js +0 -278
  264. package/router/dist/tools/batch.d.ts +0 -33
  265. package/router/dist/tools/batch.js +0 -586
  266. package/router/dist/tools/run.d.ts +0 -62
  267. package/router/dist/tools/run.js +0 -1304
  268. package/router/dist/tools/search.d.ts +0 -66
  269. package/router/dist/tools/search.js +0 -1364
  270. package/router/dist/transform/engine.d.ts +0 -168
  271. package/router/dist/transform/engine.js +0 -2072
  272. package/router/dist/vault/vault-index.d.ts +0 -125
  273. package/router/dist/vault/vault-index.js +0 -416
  274. package/router/dist/vault/vault.d.ts +0 -138
  275. package/router/dist/vault/vault.js +0 -630
  276. package/runtime/interaction-contract.md +0 -16
  277. package/skills/discuss/references/checkpoints.md +0 -66
  278. package/skills/execute-work/references/execution-loop.md +0 -77
  279. package/skills/output-router/SKILL.md +0 -206
  280. package/skills/output-router/references/safety-policy.md +0 -40
  281. package/skills/setup-freeflow/references/output-router-setup.md +0 -110
  282. package/skills/track-work/references/working-record-schema.md +0 -378
  283. package/skills/workflow/references/workflow-loop.md +0 -92
  284. /package/skills/handoff/references/{templates.md → handoff-templates.md} +0 -0
  285. /package/skills/review-artifact/references/{reviewer-prompt.md → independent-artifact-reviewer-contract.md} +0 -0
@@ -2,17 +2,6 @@ import { execFile } from "node:child_process";
2
2
  import { appendFile, mkdir, readFile, rm, writeFile } from "node:fs/promises";
3
3
  import { dirname, isAbsolute, join, resolve } from "node:path";
4
4
  import { promisify } from "node:util";
5
- import {
6
- DEFAULT_OBSERVED_ROUTING_CONFIG,
7
- DEFAULT_OUTPUT_ROUTER_ENABLED,
8
- DEFAULT_POST_TOOL_ROUTING,
9
- DEFAULT_ROUTER_THRESHOLDS,
10
- DEFAULT_SCRIPT_TRANSFORM_CONFIG,
11
- DEFAULT_STORAGE_POLICY,
12
- DEFAULT_VAULT_RETENTION,
13
- DEFAULT_VAULT_ROOT,
14
- normalizeFreeflowConfig,
15
- } from "../../../router/dist/index.js";
16
5
  import {
17
6
  handleModeCommand,
18
7
  readCapabilityState,
@@ -25,13 +14,12 @@ import {
25
14
  VALID_MODES,
26
15
  } from "../runtime/runtime-context.js";
27
16
  import { PiSettingsComponent } from "./settings-tui.js";
28
- const POST_TOOL_ROUTING_VALUES = ["off", "safety-net", "strict"];
29
- const STORAGE_POLICY_VALUES = ["hybrid-dedupe", "store-everything"];
30
- const OBSERVED_PERSISTENCE_VALUES = ["none", "metadata-only", "exact"];
31
- const SCRIPT_LANGUAGES = ["javascript", "python", "jq"];
17
+ import { isPiFlowHost } from "../runtime/runtime-identity.js";
32
18
  const DEFAULT_FREEFLOW_ENABLED = true;
33
19
  const DEFAULT_INTERACTION_CONTRACT_ENABLED = true;
34
20
  const DEFAULT_SKILLS_ENABLED = true;
21
+ const DEFAULT_CONTEXT_VIRTUALIZATION_ENABLED = false;
22
+ const DEFAULT_CONVERSATION_HISTORY_ENABLED = false;
35
23
  const MODE_VALUES = ["conversation", "workflow", "strict-workflow"];
36
24
  const MODE_LABELS = {
37
25
  default: "Use configured default",
@@ -114,288 +102,9 @@ function setConfigValue(config, item, value) {
114
102
  }
115
103
  setPath(config, item.path, value);
116
104
  }
117
- function parseStringList(text) {
118
- return text
119
- .split(",")
120
- .map((item) => item.trim())
121
- .filter(Boolean);
122
- }
123
- function parsePositiveInteger(text) {
124
- const value = Number(text.trim());
125
- if (!Number.isInteger(value) || value <= 0) {
126
- throw new Error("Expected a positive integer.");
127
- }
128
- return value;
129
- }
130
- function parseJsonObject(text) {
131
- const trimmed = text.trim();
132
- if (!trimmed) return {};
133
- let parsed;
134
- try {
135
- parsed = JSON.parse(trimmed);
136
- } catch (error) {
137
- throw new Error(`Expected valid JSON: ${error instanceof Error ? error.message : String(error)}`);
138
- }
139
- if (!isRecord(parsed)) {
140
- throw new Error("Expected a JSON object.");
141
- }
142
- return parsed;
143
- }
144
- function formatList(value) {
145
- return Array.isArray(value) ? value.join(", ") : "";
146
- }
147
- function formatJson(value) {
148
- return JSON.stringify(isRecord(value) ? value : {});
149
- }
150
105
  function booleanValue(value) {
151
106
  return value === true ? "enabled" : "disabled";
152
107
  }
153
- function normalizedSettingsConfig(rawConfig) {
154
- const rawRouter = isRecord(rawConfig.outputRouter) ? rawConfig.outputRouter : {};
155
- const normalized = normalizeFreeflowConfig({
156
- ...rawConfig,
157
- outputRouter: { ...rawRouter, enabled: true },
158
- });
159
- return normalized.config;
160
- }
161
- function rawValue(rawConfig, path, fallback) {
162
- const value = getPath(rawConfig, path);
163
- return value === undefined ? fallback : value;
164
- }
165
- function outputRouterItems(rawConfig, freeflowInactive = false) {
166
- const effective = normalizedSettingsConfig(rawConfig);
167
- const routerEnabled = normalizeFreeflowConfig(rawConfig).config.outputRouter.enabled;
168
- const router = effective.outputRouter;
169
- const scriptTransform = effective.scriptTransform;
170
- const observedRouting = effective.observedRouting;
171
- const inactive = freeflowInactive || !routerEnabled;
172
- return [
173
- {
174
- id: "outputRouter.enabled",
175
- label: "Output Router",
176
- description: "Master switch for Freeflow routed evidence tools and router runtime guidance.",
177
- path: ["outputRouter", "enabled"],
178
- kind: "boolean",
179
- value: routerEnabled,
180
- defaultValue: DEFAULT_OUTPUT_ROUTER_ENABLED,
181
- inactive: freeflowInactive,
182
- },
183
- {
184
- id: "outputRouter.postToolRouting",
185
- label: "Native safety net",
186
- description:
187
- "Post-process large/noisy native read/bash output. safety-net can replace oversized native output with vaulted routed evidence; strict is stronger.",
188
- path: ["outputRouter", "postToolRouting"],
189
- kind: "enum",
190
- value: router.postToolRouting,
191
- values: [...POST_TOOL_ROUTING_VALUES],
192
- valueDescriptions: {
193
- off: "Leave native tool output unchanged",
194
- "safety-net": "Route oversized or noisy native output",
195
- strict: "Apply stronger native output routing",
196
- },
197
- defaultValue: DEFAULT_POST_TOOL_ROUTING,
198
- inactive,
199
- },
200
- {
201
- id: "outputRouter.storagePolicy",
202
- label: "Storage policy",
203
- description: "Vault storage behavior for exactness-sensitive and noisy output.",
204
- path: ["outputRouter", "storagePolicy"],
205
- kind: "enum",
206
- value: router.storagePolicy,
207
- values: [...STORAGE_POLICY_VALUES],
208
- defaultValue: DEFAULT_STORAGE_POLICY,
209
- inactive,
210
- },
211
- {
212
- id: "outputRouter.thresholds.largeOutputBytes",
213
- label: "Large output bytes",
214
- description: "Byte threshold for treating command/tool output as large.",
215
- path: ["outputRouter", "thresholds", "largeOutputBytes"],
216
- kind: "integer",
217
- value: router.thresholds.largeOutputBytes,
218
- defaultValue: DEFAULT_ROUTER_THRESHOLDS.largeOutputBytes,
219
- parse: parsePositiveInteger,
220
- inactive,
221
- },
222
- {
223
- id: "outputRouter.thresholds.largeOutputLines",
224
- label: "Large output lines",
225
- description: "Line threshold for treating command/tool output as large.",
226
- path: ["outputRouter", "thresholds", "largeOutputLines"],
227
- kind: "integer",
228
- value: router.thresholds.largeOutputLines,
229
- defaultValue: DEFAULT_ROUTER_THRESHOLDS.largeOutputLines,
230
- parse: parsePositiveInteger,
231
- inactive,
232
- },
233
- {
234
- id: "outputRouter.vault.root",
235
- label: "Vault root",
236
- description: "Root directory for recoverable routed output.",
237
- path: ["outputRouter", "vault", "root"],
238
- kind: "string",
239
- value: router.vault.root,
240
- defaultValue: DEFAULT_VAULT_ROOT,
241
- inactive,
242
- },
243
- {
244
- id: "outputRouter.vault.retention.ttlDays",
245
- label: "Vault retention days",
246
- description: "TTL retention for vaulted output. Set a positive integer; default is 7 days.",
247
- path: ["outputRouter", "vault", "retention", "ttlDays"],
248
- kind: "integer",
249
- value:
250
- router.vault.retention?.strategy === "ttl" ? router.vault.retention.ttlDays : DEFAULT_VAULT_RETENTION.ttlDays,
251
- defaultValue: DEFAULT_VAULT_RETENTION.ttlDays,
252
- parse: parsePositiveInteger,
253
- inactive,
254
- },
255
- {
256
- id: "outputRouter.generatedPaths",
257
- label: "Generated paths",
258
- description:
259
- "Comma-separated globs for generated repo paths that should be treated as noisy or lower-priority evidence.",
260
- path: ["outputRouter", "hints", "generatedPathGlobs"],
261
- kind: "list",
262
- value: rawValue(
263
- rawConfig,
264
- ["outputRouter", "hints", "generatedPathGlobs"],
265
- rawValue(rawConfig, ["outputRouter", "generatedPaths"], router.hints?.generatedPathGlobs ?? []),
266
- ),
267
- format: formatList,
268
- parse: parseStringList,
269
- inactive,
270
- },
271
- {
272
- id: "outputRouter.noisyCommandHints",
273
- label: "Noisy command hints",
274
- description: "Comma-separated command patterns that are likely to produce noisy output.",
275
- path: ["outputRouter", "hints", "noisyCommandPatterns"],
276
- kind: "list",
277
- value: rawValue(
278
- rawConfig,
279
- ["outputRouter", "hints", "noisyCommandPatterns"],
280
- rawValue(rawConfig, ["outputRouter", "noisyCommandHints"], router.hints?.noisyCommandPatterns ?? []),
281
- ),
282
- format: formatList,
283
- parse: parseStringList,
284
- inactive,
285
- },
286
- {
287
- id: "outputRouter.scriptTransform.enabled",
288
- label: "Script transform",
289
- description:
290
- "Enable sandboxed script transforms/producers. Requires proof-backed adapters; no unsandboxed fallback.",
291
- path: ["outputRouter", "scriptTransform", "enabled"],
292
- kind: "boolean",
293
- value: scriptTransform.enabled,
294
- defaultValue: DEFAULT_SCRIPT_TRANSFORM_CONFIG.enabled,
295
- inactive,
296
- },
297
- {
298
- id: "outputRouter.scriptTransform.languages",
299
- label: "Script languages",
300
- description: "Comma-separated sandbox languages to allow: javascript, python, jq.",
301
- path: ["outputRouter", "scriptTransform", "languages"],
302
- kind: "list",
303
- value: scriptTransform.languages,
304
- defaultValue: DEFAULT_SCRIPT_TRANSFORM_CONFIG.languages,
305
- format: formatList,
306
- parse: (text) => parseStringList(text).filter((value) => SCRIPT_LANGUAGES.includes(value)),
307
- inactive,
308
- },
309
- {
310
- id: "outputRouter.scriptTransform.limits.timeoutMs",
311
- label: "Script timeout ms",
312
- description: "Maximum sandboxed script execution time.",
313
- path: ["outputRouter", "scriptTransform", "limits", "timeoutMs"],
314
- kind: "integer",
315
- value: scriptTransform.limits.timeoutMs,
316
- defaultValue: DEFAULT_SCRIPT_TRANSFORM_CONFIG.limits.timeoutMs,
317
- parse: parsePositiveInteger,
318
- inactive,
319
- },
320
- {
321
- id: "outputRouter.scriptTransform.limits.maxInputBytes",
322
- label: "Script max input bytes",
323
- description: "Maximum input size for sandboxed scripts.",
324
- path: ["outputRouter", "scriptTransform", "limits", "maxInputBytes"],
325
- kind: "integer",
326
- value: scriptTransform.limits.maxInputBytes,
327
- defaultValue: DEFAULT_SCRIPT_TRANSFORM_CONFIG.limits.maxInputBytes,
328
- parse: parsePositiveInteger,
329
- inactive,
330
- },
331
- {
332
- id: "outputRouter.scriptTransform.limits.maxOutputBytes",
333
- label: "Script max output bytes",
334
- description: "Maximum output size for sandboxed scripts.",
335
- path: ["outputRouter", "scriptTransform", "limits", "maxOutputBytes"],
336
- kind: "integer",
337
- value: scriptTransform.limits.maxOutputBytes,
338
- defaultValue: DEFAULT_SCRIPT_TRANSFORM_CONFIG.limits.maxOutputBytes,
339
- parse: parsePositiveInteger,
340
- inactive,
341
- },
342
- {
343
- id: "outputRouter.observedRouting.enabled",
344
- label: "Observed routing",
345
- description: "Route configured host tool results after they run, e.g. MCP/web/fetch/code-search output.",
346
- path: ["outputRouter", "observedRouting", "enabled"],
347
- kind: "boolean",
348
- value: observedRouting.enabled,
349
- defaultValue: DEFAULT_OBSERVED_ROUTING_CONFIG.enabled,
350
- inactive,
351
- },
352
- ...observedProducerItems("web", "Web search", observedRouting.web, inactive),
353
- ...observedProducerItems("fetch", "Fetch content", observedRouting.fetch, inactive),
354
- ...observedProducerItems("codeSearch", "Code search", observedRouting.codeSearch, inactive),
355
- {
356
- id: "outputRouter.observedRouting.mcp.servers",
357
- label: "MCP servers JSON",
358
- description: 'JSON object keyed by MCP server id. Example: {"github":{"enabled":true,"persistence":"exact"}}',
359
- path: ["outputRouter", "observedRouting", "mcp", "servers"],
360
- kind: "json",
361
- value: observedRouting.mcp?.servers ?? {},
362
- defaultValue: DEFAULT_OBSERVED_ROUTING_CONFIG.mcp.servers,
363
- format: formatJson,
364
- parse: parseJsonObject,
365
- inactive,
366
- },
367
- ];
368
- }
369
- function observedProducerItems(id, label, value, inactive) {
370
- return [
371
- {
372
- id: `outputRouter.observedRouting.${id}.enabled`,
373
- label: `${label} observed`,
374
- description: `Enable observed routing for ${label.toLowerCase()} output.`,
375
- path: ["outputRouter", "observedRouting", id, "enabled"],
376
- kind: "boolean",
377
- value: value?.enabled === true,
378
- defaultValue: false,
379
- inactive,
380
- },
381
- {
382
- id: `outputRouter.observedRouting.${id}.persistence`,
383
- label: `${label} persistence`,
384
- description: "Persistence mode for this observed producer: none, metadata-only, or exact.",
385
- path: ["outputRouter", "observedRouting", id, "persistence"],
386
- kind: "enum",
387
- value: value?.persistence ?? "none",
388
- values: [...OBSERVED_PERSISTENCE_VALUES],
389
- valueDescriptions: {
390
- none: "Do not persist observed output",
391
- "metadata-only": "Persist metadata without raw output",
392
- exact: "Persist exact recoverable output",
393
- },
394
- defaultValue: value?.enabled === true ? undefined : "none",
395
- inactive,
396
- },
397
- ];
398
- }
399
108
  function configValueForChoice(item, value) {
400
109
  const key = String(value);
401
110
  if (item.configValues && Object.hasOwn(item.configValues, key)) {
@@ -404,10 +113,15 @@ function configValueForChoice(item, value) {
404
113
  return value;
405
114
  }
406
115
  function effectiveItemValue(item) {
407
- return item.effectiveValue !== undefined ? item.effectiveValue : item.value;
116
+ return item.effectiveValue === undefined ? item.value : item.effectiveValue;
408
117
  }
409
118
  function formatCoreValue(value) {
410
- return typeof value === "boolean" ? booleanValue(value) : String(value ?? "");
119
+ if (typeof value === "boolean") return booleanValue(value);
120
+ if (isRecord(value) && typeof value.provider === "string" && typeof value.model === "string") {
121
+ const effort = typeof value.thinkingLevel === "string" ? ` · ${value.thinkingLevel}` : "";
122
+ return `${value.provider}/${value.model}${effort}`;
123
+ }
124
+ return String(value ?? "");
411
125
  }
412
126
  function coreDisplaySuffix(item, inactive = false) {
413
127
  const source = item.effectiveSource ?? "builtin";
@@ -558,14 +272,19 @@ function resolveSettingsCoreView(rawConfig, layers) {
558
272
  const fallbackCore = {
559
273
  enabled: getPath(rawConfig, ["enabled"]) !== false,
560
274
  interactionContract: getPath(rawConfig, ["interactionContract"]) !== false,
275
+ contextVirtualization: getPath(rawConfig, ["contextVirtualization"]) === true,
276
+ conversationHistory: getPath(rawConfig, ["conversationHistory"]) === true,
561
277
  skills: { enabled: getPath(repositorySkills, ["enabled"]) !== false },
562
278
  defaultMode: validModeOrUndefined(rawConfig.defaultMode) ?? "workflow",
563
279
  };
564
280
  const fallbackSources = {
565
281
  enabled: typeof getPath(rawConfig, ["enabled"]) === "boolean" ? "repository" : "builtin",
566
282
  interactionContract: typeof getPath(rawConfig, ["interactionContract"]) === "boolean" ? "repository" : "builtin",
283
+ contextVirtualization:
284
+ typeof getPath(rawConfig, ["contextVirtualization"]) === "boolean" ? "repository" : "builtin",
285
+ conversationHistory: typeof getPath(rawConfig, ["conversationHistory"]) === "boolean" ? "repository" : "builtin",
567
286
  skillsEnabled: typeof getPath(repositorySkills, ["enabled"]) === "boolean" ? "repository" : "builtin",
568
- defaultMode: validModeOrUndefined(rawConfig.defaultMode) !== undefined ? "repository" : "builtin",
287
+ defaultMode: validModeOrUndefined(rawConfig.defaultMode) === undefined ? "builtin" : "repository",
569
288
  };
570
289
  return {
571
290
  localConfig,
@@ -573,9 +292,6 @@ function resolveSettingsCoreView(rawConfig, layers) {
573
292
  sources: layers?.sources ?? fallbackSources,
574
293
  };
575
294
  }
576
- function groupEnabled(items, id) {
577
- return items.find((item) => item.id === id)?.value === true;
578
- }
579
295
  function createSessionBooleanItem(options) {
580
296
  const override = options.sessionOverrides[options.key];
581
297
  const item = {
@@ -613,7 +329,7 @@ function createSessionBooleanItem(options) {
613
329
  item.displaySuffix = coreDisplaySuffix(item);
614
330
  return item;
615
331
  }
616
- function sessionFreeflowItems(state, modeState) {
332
+ function sessionFreeflowItems(state, modeState, cognitiveRoutingController) {
617
333
  const sessionOverrides = state.sessionOverrides;
618
334
  const configured = state.configuredCoreConfig;
619
335
  const configuredSources = state.configuredSources;
@@ -651,10 +367,34 @@ function sessionFreeflowItems(state, modeState) {
651
367
  effectiveSource: effectiveSources.skillsEnabled,
652
368
  sessionOverrides,
653
369
  });
370
+ const contextVirtualizationItem = createSessionBooleanItem({
371
+ id: "freeflow.contextVirtualization",
372
+ label: "Context Virtualization",
373
+ description: "Temporary Context Virtualization override for this Pi session.",
374
+ key: "contextVirtualization",
375
+ inheritedValue: configured.contextVirtualization,
376
+ inheritedSource: configuredSources.contextVirtualization,
377
+ effectiveValue: state.contextVirtualization.enabled,
378
+ effectiveSource: effectiveSources.contextVirtualization,
379
+ sessionOverrides,
380
+ });
381
+ const conversationHistoryItem = createSessionBooleanItem({
382
+ id: "freeflow.conversationHistory",
383
+ label: "Conversation History",
384
+ description: "Temporary Conversation History override for this Pi session.",
385
+ key: "conversationHistory",
386
+ inheritedValue: configured.conversationHistory,
387
+ inheritedSource: configuredSources.conversationHistory,
388
+ effectiveValue: state.conversationHistory.enabled,
389
+ effectiveSource: effectiveSources.conversationHistory,
390
+ sessionOverrides,
391
+ });
654
392
  const freeflowInactive = !state.enabled;
655
393
  const skillsEnabled = state.skills.enabled;
656
394
  interactionItem.inactive = freeflowInactive;
657
395
  skillsItem.inactive = freeflowInactive;
396
+ contextVirtualizationItem.inactive = freeflowInactive || !skillsEnabled;
397
+ conversationHistoryItem.inactive = freeflowInactive || !skillsEnabled;
658
398
  const sessionMode = modeState.currentMode ?? "default";
659
399
  const sessionModeItem = {
660
400
  id: "freeflow.sessionMode",
@@ -681,15 +421,47 @@ function sessionFreeflowItems(state, modeState) {
681
421
  inheritedValue: modeState.defaultMode,
682
422
  inheritedSource: modeState.defaultModeSource,
683
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 = 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: !state.cognitiveRouting.effective || cognitiveRoutingController === undefined,
450
+ runtimeInactive: state.cognitiveRouting.blockingReason?.code === "runtime_disabled",
451
+ displaySuffix: cognitiveRoutingState?.effective ? cognitiveRoutingProfile : "unavailable",
452
+ }
453
+ : undefined;
684
454
  return [
685
455
  freeflowItem,
686
456
  interactionItem,
687
457
  skillsItem,
688
458
  sessionModeItem,
459
+ ...(cognitiveRoutingItem ? [cognitiveRoutingItem] : []),
689
460
  {
690
461
  id: "freeflow.session.reset",
691
462
  label: "Reset session overrides",
692
- description: "Clear Freeflow, Interaction Contract, Skills, and mode overrides for this Pi session.",
463
+ description:
464
+ "Clear Freeflow, Interaction Contract, Skills, Context Virtualization, Conversation History, and mode overrides for this Pi session.",
693
465
  kind: "enum",
694
466
  value: "available",
695
467
  values: ["reset"],
@@ -698,7 +470,295 @@ function sessionFreeflowItems(state, modeState) {
698
470
  format: () => "available",
699
471
  transient: true,
700
472
  },
473
+ {
474
+ id: "freeflow.context",
475
+ label: "Freeflow Context",
476
+ description: "Choose which context projection and conversation-history operations are available to the model.",
477
+ kind: "group",
478
+ value: contextVirtualizationItem.effectiveValue === true || conversationHistoryItem.effectiveValue === true,
479
+ displaySuffix: `${[contextVirtualizationItem, conversationHistoryItem].filter((item) => item.effectiveValue === true).length}/2 enabled`,
480
+ children: [contextVirtualizationItem, conversationHistoryItem],
481
+ },
482
+ ];
483
+ }
484
+ const COGNITIVE_ROUTING_THINKING_LEVELS = ["off", "minimal", "low", "medium", "high", "xhigh", "max"];
485
+ const COGNITIVE_ROUTING_EFFORT_DESCRIPTIONS = {
486
+ off: "No reasoning",
487
+ minimal: "Very brief reasoning",
488
+ low: "Light reasoning",
489
+ medium: "Moderate reasoning",
490
+ high: "Deep reasoning",
491
+ xhigh: "Extra-high reasoning",
492
+ max: "Maximum reasoning",
493
+ };
494
+ function cognitiveRoutingModelKey(provider, model) {
495
+ return `${provider}/${model}`;
496
+ }
497
+ function isCognitiveRoutingProfile(value) {
498
+ return (
499
+ isRecord(value) &&
500
+ typeof value.provider === "string" &&
501
+ typeof value.model === "string" &&
502
+ typeof value.thinkingLevel === "string" &&
503
+ COGNITIVE_ROUTING_THINKING_LEVELS.includes(value.thinkingLevel)
504
+ );
505
+ }
506
+ function cognitiveRoutingThinkingLevels(model, registry) {
507
+ if (typeof registry?.clampThinkingLevel !== "function") {
508
+ return model?.reasoning === true ? [...COGNITIVE_ROUTING_THINKING_LEVELS] : ["off"];
509
+ }
510
+ return COGNITIVE_ROUTING_THINKING_LEVELS.filter((level) => {
511
+ try {
512
+ return registry.clampThinkingLevel(model, level) === level;
513
+ } catch {
514
+ return false;
515
+ }
516
+ });
517
+ }
518
+ function cognitiveRoutingModelOptions(ctx) {
519
+ const registry = ctx?.modelRegistry;
520
+ if (typeof registry?.getAvailable !== "function") return [];
521
+ let models;
522
+ try {
523
+ models = registry.getAvailable();
524
+ } catch {
525
+ return [];
526
+ }
527
+ if (!Array.isArray(models)) return [];
528
+ return models
529
+ .filter((model) => typeof model?.provider === "string" && typeof model?.id === "string")
530
+ .map((model) => {
531
+ const thinkingLevels = cognitiveRoutingThinkingLevels(model, registry);
532
+ const provider = model.provider;
533
+ const modelId = model.id;
534
+ const key = cognitiveRoutingModelKey(provider, modelId);
535
+ const displayName = typeof model.name === "string" && model.name !== modelId ? model.name : undefined;
536
+ return {
537
+ key,
538
+ provider,
539
+ model: modelId,
540
+ label: key,
541
+ description: displayName ? `${displayName} · ${thinkingLevels.join(", ")}` : thinkingLevels.join(", "),
542
+ thinkingLevels,
543
+ };
544
+ })
545
+ .filter((model) => model.thinkingLevels.length > 0)
546
+ .sort((a, b) => a.label.localeCompare(b.label));
547
+ }
548
+ function cognitiveRoutingProfileDisplay(value) {
549
+ return isCognitiveRoutingProfile(value) ? formatCoreValue(value) : "not configured";
550
+ }
551
+ function cognitiveRoutingSettingsSource(source) {
552
+ if (source === "repository") return "repository";
553
+ if (source === "personal" || source === "local") return "local";
554
+ return "builtin";
555
+ }
556
+ function cognitiveRoutingProfileDisplaySuffix(source, scope) {
557
+ if (!source) return undefined;
558
+ if (source === "personal" && scope === "repository") return "(effective personal)";
559
+ return `(${source})`;
560
+ }
561
+ function cognitiveRoutingConfirmStep(summary) {
562
+ return {
563
+ title: "Confirm preset",
564
+ choices: [
565
+ {
566
+ key: "__save__",
567
+ value: "save",
568
+ label: `Save ${summary}`,
569
+ description: "Write the complete profile atomically.",
570
+ },
571
+ { key: "__cancel__", value: "cancel", label: "Cancel", description: "Leave the previous preset unchanged." },
572
+ ],
573
+ selectedKey: "__save__",
574
+ };
575
+ }
576
+ function createCognitiveRoutingProfileWizard(currentValue, models, allowInherit) {
577
+ const currentProfile = isCognitiveRoutingProfile(currentValue) ? currentValue : undefined;
578
+ const currentModelKey = currentProfile
579
+ ? cognitiveRoutingModelKey(currentProfile.provider, currentProfile.model)
580
+ : undefined;
581
+ const firstChoices = [
582
+ ...(allowInherit
583
+ ? [
584
+ {
585
+ key: LOCAL_INHERIT,
586
+ value: LOCAL_INHERIT,
587
+ label: "Inherit repository preset",
588
+ description: "Remove the personal override without changing the repository preset.",
589
+ },
590
+ ]
591
+ : []),
592
+ ...models.map((model) => ({
593
+ key: model.key,
594
+ value: model,
595
+ label: model.label,
596
+ description: model.description,
597
+ })),
701
598
  ];
599
+ if (firstChoices.length === 0) {
600
+ firstChoices.push({
601
+ key: "__cancel__",
602
+ value: "cancel",
603
+ label: "No authenticated models available",
604
+ description: "Authenticate a provider before configuring this preset.",
605
+ });
606
+ }
607
+ return {
608
+ firstStep: () => ({
609
+ title: "Choose model",
610
+ choices: firstChoices,
611
+ selectedKey:
612
+ currentValue === LOCAL_INHERIT
613
+ ? LOCAL_INHERIT
614
+ : firstChoices.some((choice) => choice.key === currentModelKey)
615
+ ? currentModelKey
616
+ : firstChoices[0]?.key,
617
+ }),
618
+ nextStep: (selectedValues) => {
619
+ if (selectedValues.length === 1) {
620
+ const selected = selectedValues[0];
621
+ if (selected === LOCAL_INHERIT) {
622
+ return cognitiveRoutingConfirmStep("inherit repository preset");
623
+ }
624
+ const model = selected;
625
+ if (!model?.key || model.thinkingLevels.length === 0) return undefined;
626
+ const currentEffort =
627
+ currentProfile && currentModelKey === model.key ? currentProfile.thinkingLevel : undefined;
628
+ return {
629
+ title: `Choose effort · ${model.label}`,
630
+ choices: model.thinkingLevels.map((level) => ({
631
+ key: level,
632
+ value: level,
633
+ label: level,
634
+ description: COGNITIVE_ROUTING_EFFORT_DESCRIPTIONS[level],
635
+ })),
636
+ selectedKey:
637
+ currentEffort && model.thinkingLevels.includes(currentEffort) ? currentEffort : model.thinkingLevels[0],
638
+ };
639
+ }
640
+ if (selectedValues.length === 2) {
641
+ const model = selectedValues[0];
642
+ const effort = selectedValues[1];
643
+ return cognitiveRoutingConfirmStep(`${model.label} · ${effort}`);
644
+ }
645
+ return undefined;
646
+ },
647
+ valueFromSelections: (selectedValues) => {
648
+ if (selectedValues[0] === LOCAL_INHERIT) return undefined;
649
+ const model = selectedValues[0];
650
+ return {
651
+ provider: model.provider,
652
+ model: model.model,
653
+ thinkingLevel: selectedValues[1],
654
+ };
655
+ },
656
+ };
657
+ }
658
+ function cognitiveRoutingProfileItem(options) {
659
+ const path = ["cognitiveRouting", "profiles", options.name];
660
+ const repositoryProfile = getPath(options.rawConfig, path);
661
+ const localProfile = getPath(options.localConfig, path);
662
+ const effectiveProfile = options.capabilityState?.profiles?.[options.name] ?? repositoryProfile;
663
+ const localValue = isCognitiveRoutingProfile(localProfile) ? localProfile : LOCAL_INHERIT;
664
+ const value = options.scope === "local" ? localValue : repositoryProfile;
665
+ const source =
666
+ options.capabilityState?.profileSources?.[options.name] ??
667
+ (isCognitiveRoutingProfile(repositoryProfile) ? "repository" : undefined);
668
+ const models = cognitiveRoutingModelOptions(options.ctx);
669
+ const inactive = options.scope === "local" && models.length === 0 && localValue !== LOCAL_INHERIT;
670
+ return {
671
+ id: `freeflow.cognitiveRouting.${options.name}`,
672
+ label: `${options.name.charAt(0).toUpperCase()}${options.name.slice(1)} preset`,
673
+ description:
674
+ "Choose an authenticated available model and one effort supported by that model. Confirming writes the complete preset; cancel leaves it unchanged.",
675
+ path,
676
+ kind: "string",
677
+ value,
678
+ format: cognitiveRoutingProfileDisplay,
679
+ configScope: options.scope,
680
+ effectiveValue: effectiveProfile,
681
+ effectiveSource: cognitiveRoutingSettingsSource(source),
682
+ inheritedValue: repositoryProfile,
683
+ inheritedSource: "repository",
684
+ inactive,
685
+ displaySuffix: cognitiveRoutingProfileDisplaySuffix(source, options.scope),
686
+ wizard: () => createCognitiveRoutingProfileWizard(value, models, options.scope === "local"),
687
+ };
688
+ }
689
+ function cognitiveRoutingSessionStartItem(options) {
690
+ const path = ["cognitiveRouting", "sessionStart", options.setting];
691
+ const defaultValue = options.setting === "control" ? "automatic" : "standard";
692
+ const values = options.setting === "control" ? ["automatic", "manual"] : ["standard", "reasoning"];
693
+ const labels = Object.fromEntries(values.map((value) => [value, value]));
694
+ const descriptions =
695
+ options.setting === "control"
696
+ ? {
697
+ automatic: "Let Cognitive Routing manage profile transitions for the new session.",
698
+ manual: "Hold the selected profile for the new session until the user changes it.",
699
+ }
700
+ : {
701
+ standard: "Start a new session with the configured Standard preset.",
702
+ reasoning: "Start a new session with the configured Reasoning preset.",
703
+ };
704
+ const repositoryValue = getPath(options.rawConfig, path);
705
+ const localValue = getPath(options.localConfig, path);
706
+ const isValidValue = (value) => values.includes(value);
707
+ const repositorySetting = isValidValue(repositoryValue) ? repositoryValue : undefined;
708
+ const localSetting = isValidValue(localValue) ? localValue : undefined;
709
+ const inheritedValue = repositorySetting ?? defaultValue;
710
+ const inheritedSource = repositorySetting ? "repository" : "builtin";
711
+ const configuredSource =
712
+ options.capabilityState?.sessionStartSources?.[options.setting] ?? (repositorySetting ? "repository" : undefined);
713
+ const effectiveValue = options.capabilityState?.sessionStart?.[options.setting] ?? inheritedValue;
714
+ const effectiveSource = cognitiveRoutingSettingsSource(configuredSource);
715
+ const label = options.setting === "control" ? "Session start control" : "Session start profile";
716
+ const description =
717
+ options.setting === "control"
718
+ ? "Choose automatic or manual control for new sessions only; changing this does not alter the active session."
719
+ : "Choose the initial profile for new sessions only; changing this does not alter the active session.";
720
+ const item =
721
+ options.scope === "local"
722
+ ? {
723
+ id: `freeflow.cognitiveRouting.sessionStart.${options.setting}`,
724
+ label,
725
+ description: `${description} Choose inherit to use the repository value; use /freeflow settings repo to edit shared defaults.`,
726
+ path,
727
+ kind: "enum",
728
+ value: localSetting ?? LOCAL_INHERIT,
729
+ values: [LOCAL_INHERIT, ...values],
730
+ valueLabels: { inherit: "Inherit repository", ...labels },
731
+ valueDescriptions: {
732
+ inherit: `Use ${inheritedValue} from ${inheritedSource}.`,
733
+ ...descriptions,
734
+ },
735
+ format: (value) => String(value),
736
+ configScope: "local",
737
+ configValues: { inherit: undefined },
738
+ effectiveValue,
739
+ effectiveSource,
740
+ inheritedValue,
741
+ inheritedSource,
742
+ }
743
+ : {
744
+ id: `freeflow.cognitiveRouting.sessionStart.${options.setting}`,
745
+ label,
746
+ description: `${description} This edits shared .freeflow/config.json.`,
747
+ path,
748
+ kind: "enum",
749
+ value: inheritedValue,
750
+ values,
751
+ valueLabels: labels,
752
+ valueDescriptions: descriptions,
753
+ format: (value) => String(value),
754
+ defaultValue,
755
+ configScope: "repository",
756
+ effectiveValue,
757
+ effectiveSource,
758
+ localOverrideValue: localSetting,
759
+ };
760
+ item.displaySuffix = coreDisplaySuffix(item);
761
+ return item;
702
762
  }
703
763
  function freeflowItems(rawConfig, modeState, options = {}) {
704
764
  const scope = options.scope ?? "repository";
@@ -734,12 +794,36 @@ function freeflowItems(rawConfig, modeState, options = {}) {
734
794
  localConfig,
735
795
  id: "freeflow.skills.enabled",
736
796
  label: "Skills",
737
- description: "Expose Freeflow skills and load Workflow once on the first turn.",
797
+ description: "Expose Freeflow skills and discover Workflow before consequential or mutating work.",
738
798
  path: ["skills", "enabled"],
739
799
  effectiveValue: core.skills.enabled,
740
800
  effectiveSource: sources.skillsEnabled,
741
801
  defaultValue: DEFAULT_SKILLS_ENABLED,
742
802
  });
803
+ const contextVirtualizationItem = createScopedBooleanItem({
804
+ scope,
805
+ rawConfig,
806
+ localConfig,
807
+ id: "freeflow.contextVirtualization",
808
+ label: "Context Virtualization",
809
+ description: "Let the model archive consumed tool results from future context while preserving session history.",
810
+ path: ["contextVirtualization"],
811
+ effectiveValue: core.contextVirtualization,
812
+ effectiveSource: sources.contextVirtualization,
813
+ defaultValue: DEFAULT_CONTEXT_VIRTUALIZATION_ENABLED,
814
+ });
815
+ const conversationHistoryItem = createScopedBooleanItem({
816
+ scope,
817
+ rawConfig,
818
+ localConfig,
819
+ id: "freeflow.conversationHistory",
820
+ label: "Conversation History",
821
+ description: "Let the model search and retrieve hidden conversation history on the active branch.",
822
+ path: ["conversationHistory"],
823
+ effectiveValue: core.conversationHistory,
824
+ effectiveSource: sources.conversationHistory,
825
+ defaultValue: DEFAULT_CONVERSATION_HISTORY_ENABLED,
826
+ });
743
827
  const repositoryModeValue = validModeOrUndefined(rawConfig.defaultMode);
744
828
  const repositoryDefaultMode = repositoryModeValue ?? "workflow";
745
829
  const repositoryDefaultSource = repositoryModeValue ? "repository" : "builtin";
@@ -756,6 +840,8 @@ function freeflowItems(rawConfig, modeState, options = {}) {
756
840
  const skillsEnabled = core.skills.enabled;
757
841
  interactionItem.inactive = freeflowInactive;
758
842
  skillsItem.inactive = freeflowInactive;
843
+ contextVirtualizationItem.inactive = freeflowInactive || !skillsEnabled;
844
+ conversationHistoryItem.inactive = freeflowInactive || !skillsEnabled;
759
845
  defaultModeItem.inactive = freeflowInactive;
760
846
  defaultModeItem.displaySuffix = coreDisplaySuffix(defaultModeItem, freeflowInactive || !skillsEnabled);
761
847
  const sessionMode = modeState?.currentMode ?? "default";
@@ -783,175 +869,100 @@ function freeflowItems(rawConfig, modeState, options = {}) {
783
869
  freeflowInactive || !skillsEnabled,
784
870
  ),
785
871
  };
786
- const routerItems = outputRouterItems(rawConfig, freeflowInactive);
787
- const routerEnabled = groupEnabled(routerItems, "outputRouter.enabled");
872
+ const cognitiveRoutingState = options.cognitiveRouting;
873
+ const cognitiveRoutingRuntimeDisabled = !isPiFlowHost(options.hostInfo);
874
+ const cognitiveRoutingGroup = (() => {
875
+ const cognitiveRoutingEnabledItem = createScopedBooleanItem({
876
+ scope,
877
+ rawConfig,
878
+ localConfig,
879
+ id: "freeflow.cognitiveRouting.enabled",
880
+ label: "Enabled",
881
+ description: cognitiveRoutingRuntimeDisabled
882
+ ? "Configured for PiFlow, but disabled in normal Pi."
883
+ : "Allow Cognitive Routing to own the session model lease and switch the configured profiles.",
884
+ path: ["cognitiveRouting", "enabled"],
885
+ effectiveValue: cognitiveRoutingState?.enabled ?? false,
886
+ effectiveSource: cognitiveRoutingSettingsSource(cognitiveRoutingState?.enabledSource),
887
+ defaultValue: false,
888
+ });
889
+ cognitiveRoutingEnabledItem.inactive = freeflowInactive || !skillsEnabled || cognitiveRoutingRuntimeDisabled;
890
+ cognitiveRoutingEnabledItem.runtimeInactive = cognitiveRoutingRuntimeDisabled;
891
+ const cognitiveRoutingProfiles = ["standard", "reasoning"].map((name) =>
892
+ cognitiveRoutingProfileItem({
893
+ name: name,
894
+ scope,
895
+ rawConfig,
896
+ localConfig,
897
+ capabilityState: cognitiveRoutingState,
898
+ ctx: options.ctx,
899
+ }),
900
+ );
901
+ const cognitiveRoutingSessionStart = ["control", "profile"].map((setting) =>
902
+ cognitiveRoutingSessionStartItem({
903
+ setting,
904
+ scope,
905
+ rawConfig,
906
+ localConfig,
907
+ capabilityState: cognitiveRoutingState,
908
+ }),
909
+ );
910
+ for (const item of [...cognitiveRoutingProfiles, ...cognitiveRoutingSessionStart]) {
911
+ item.inactive ||= freeflowInactive || !skillsEnabled || cognitiveRoutingRuntimeDisabled;
912
+ item.runtimeInactive = cognitiveRoutingRuntimeDisabled;
913
+ }
914
+ const cognitiveRoutingStatus = cognitiveRoutingRuntimeDisabled
915
+ ? "disabled · PiFlow only"
916
+ : cognitiveRoutingState
917
+ ? cognitiveRoutingState.effective
918
+ ? "active"
919
+ : cognitiveRoutingState.blockingReason?.code === "profile_missing"
920
+ ? "not configured"
921
+ : (cognitiveRoutingState.blockingReason?.code ?? "inactive")
922
+ : "unavailable";
923
+ return {
924
+ id: "freeflow.cognitiveRouting",
925
+ label: "Cognitive Routing",
926
+ description: cognitiveRoutingRuntimeDisabled
927
+ ? "Cognitive Routing configuration is visible for inspection but can only run in PiFlow."
928
+ : "Configure the automatic standard/reasoning profiles and choose whether Freeflow may manage model state for this repository.",
929
+ kind: "group",
930
+ value: cognitiveRoutingRuntimeDisabled ? false : (cognitiveRoutingState?.enabled ?? false),
931
+ inactive: freeflowInactive || !skillsEnabled,
932
+ displaySuffix: cognitiveRoutingStatus,
933
+ children: [cognitiveRoutingEnabledItem, ...cognitiveRoutingProfiles, ...cognitiveRoutingSessionStart],
934
+ };
935
+ })();
788
936
  return [
789
937
  freeflowItem,
790
938
  interactionItem,
791
939
  skillsItem,
792
940
  sessionModeItem,
793
941
  defaultModeItem,
942
+ ...(cognitiveRoutingGroup ? [cognitiveRoutingGroup] : []),
794
943
  {
795
- id: "outputRouter.group",
796
- label: "Output Router",
797
- description:
798
- "Shared repository settings for routed evidence tools, native output safety net, vault storage, script transforms, and observed tool routing.",
944
+ id: "freeflow.context",
945
+ label: "Freeflow Context",
946
+ description: "Choose which context projection and conversation-history operations are available to the model.",
799
947
  kind: "group",
800
- value: routerEnabled,
801
- inactive: freeflowInactive,
802
- displaySuffix: "(repository)",
803
- children: routerItems,
948
+ value: contextVirtualizationItem.effectiveValue === true || conversationHistoryItem.effectiveValue === true,
949
+ displaySuffix: `${[contextVirtualizationItem, conversationHistoryItem].filter((item) => item.effectiveValue === true).length}/2 enabled`,
950
+ children: [contextVirtualizationItem, conversationHistoryItem],
804
951
  },
805
952
  ];
806
953
  }
807
- function migrateLegacyRouterConfig(config) {
808
- const outputRouter = isRecord(config.outputRouter) ? config.outputRouter : undefined;
809
- if (!outputRouter) return;
810
- if (config.scriptTransform !== undefined && outputRouter.scriptTransform === undefined) {
811
- outputRouter.scriptTransform = config.scriptTransform;
812
- delete config.scriptTransform;
813
- }
814
- if (config.observedRouting !== undefined && outputRouter.observedRouting === undefined) {
815
- outputRouter.observedRouting = config.observedRouting;
816
- delete config.observedRouting;
817
- }
818
- const thresholds = isRecord(outputRouter.thresholds) ? outputRouter.thresholds : {};
819
- if (outputRouter.largeOutputBytes !== undefined && thresholds.largeOutputBytes === undefined) {
820
- thresholds.largeOutputBytes = outputRouter.largeOutputBytes;
821
- outputRouter.thresholds = thresholds;
822
- delete outputRouter.largeOutputBytes;
823
- }
824
- if (outputRouter.largeOutputLines !== undefined && thresholds.largeOutputLines === undefined) {
825
- thresholds.largeOutputLines = outputRouter.largeOutputLines;
826
- outputRouter.thresholds = thresholds;
827
- delete outputRouter.largeOutputLines;
828
- }
829
- const vault = isRecord(outputRouter.vault) ? outputRouter.vault : {};
830
- if (outputRouter.vaultRoot !== undefined && vault.root === undefined) {
831
- vault.root = outputRouter.vaultRoot;
832
- outputRouter.vault = vault;
833
- delete outputRouter.vaultRoot;
834
- }
835
- if (outputRouter.vaultRetentionDays !== undefined && !isRecord(vault.retention)) {
836
- vault.retention = {
837
- strategy: "ttl",
838
- ttlDays: outputRouter.vaultRetentionDays,
839
- };
840
- outputRouter.vault = vault;
841
- delete outputRouter.vaultRetentionDays;
842
- }
843
- const hints = isRecord(outputRouter.hints) ? outputRouter.hints : {};
844
- if (outputRouter.generatedPaths !== undefined && hints.generatedPathGlobs === undefined) {
845
- hints.generatedPathGlobs = outputRouter.generatedPaths;
846
- outputRouter.hints = hints;
847
- delete outputRouter.generatedPaths;
848
- }
849
- if (outputRouter.noisyCommandHints !== undefined && hints.noisyCommandPatterns === undefined) {
850
- hints.noisyCommandPatterns = outputRouter.noisyCommandHints;
851
- outputRouter.hints = hints;
852
- delete outputRouter.noisyCommandHints;
853
- }
854
- }
855
954
  function pruneKnownDefaults(config) {
856
955
  const defaultPaths = [
857
956
  { path: ["enabled"], value: DEFAULT_FREEFLOW_ENABLED },
858
- {
859
- path: ["interactionContract"],
860
- value: DEFAULT_INTERACTION_CONTRACT_ENABLED,
861
- },
957
+ { path: ["interactionContract"], value: DEFAULT_INTERACTION_CONTRACT_ENABLED },
862
958
  { path: ["skills", "enabled"], value: DEFAULT_SKILLS_ENABLED },
863
- { path: ["outputRouter", "enabled"], value: DEFAULT_OUTPUT_ROUTER_ENABLED },
864
- {
865
- path: ["outputRouter", "postToolRouting"],
866
- value: DEFAULT_POST_TOOL_ROUTING,
867
- },
868
- { path: ["outputRouter", "storagePolicy"], value: DEFAULT_STORAGE_POLICY },
869
- {
870
- path: ["outputRouter", "thresholds", "largeOutputBytes"],
871
- value: DEFAULT_ROUTER_THRESHOLDS.largeOutputBytes,
872
- },
873
- {
874
- path: ["outputRouter", "thresholds", "largeOutputLines"],
875
- value: DEFAULT_ROUTER_THRESHOLDS.largeOutputLines,
876
- },
877
- { path: ["outputRouter", "vault", "root"], value: DEFAULT_VAULT_ROOT },
878
- {
879
- path: ["outputRouter", "vault", "retention", "ttlDays"],
880
- value: DEFAULT_VAULT_RETENTION.ttlDays,
881
- },
882
- {
883
- path: ["outputRouter", "scriptTransform", "enabled"],
884
- value: DEFAULT_SCRIPT_TRANSFORM_CONFIG.enabled,
885
- },
886
- {
887
- path: ["outputRouter", "scriptTransform", "sandbox"],
888
- value: DEFAULT_SCRIPT_TRANSFORM_CONFIG.sandbox,
889
- },
890
- {
891
- path: ["outputRouter", "scriptTransform", "languages"],
892
- value: DEFAULT_SCRIPT_TRANSFORM_CONFIG.languages,
893
- },
894
- {
895
- path: ["outputRouter", "scriptTransform", "network"],
896
- value: DEFAULT_SCRIPT_TRANSFORM_CONFIG.network,
897
- },
898
- {
899
- path: ["outputRouter", "scriptTransform", "rawScriptPersistence"],
900
- value: DEFAULT_SCRIPT_TRANSFORM_CONFIG.rawScriptPersistence,
901
- },
902
- {
903
- path: ["outputRouter", "scriptTransform", "limits", "timeoutMs"],
904
- value: DEFAULT_SCRIPT_TRANSFORM_CONFIG.limits.timeoutMs,
905
- },
906
- {
907
- path: ["outputRouter", "scriptTransform", "limits", "maxInputBytes"],
908
- value: DEFAULT_SCRIPT_TRANSFORM_CONFIG.limits.maxInputBytes,
909
- },
910
- {
911
- path: ["outputRouter", "scriptTransform", "limits", "maxOutputBytes"],
912
- value: DEFAULT_SCRIPT_TRANSFORM_CONFIG.limits.maxOutputBytes,
913
- },
914
- {
915
- path: ["outputRouter", "observedRouting", "enabled"],
916
- value: DEFAULT_OBSERVED_ROUTING_CONFIG.enabled,
917
- },
918
- {
919
- path: ["outputRouter", "observedRouting", "onRoutingFailure"],
920
- value: DEFAULT_OBSERVED_ROUTING_CONFIG.onRoutingFailure,
921
- },
922
- {
923
- path: ["outputRouter", "observedRouting", "mcp", "servers"],
924
- value: DEFAULT_OBSERVED_ROUTING_CONFIG.mcp.servers,
925
- },
926
- {
927
- path: ["outputRouter", "observedRouting", "web", "enabled"],
928
- value: DEFAULT_OBSERVED_ROUTING_CONFIG.web.enabled,
929
- },
930
- {
931
- path: ["outputRouter", "observedRouting", "fetch", "enabled"],
932
- value: DEFAULT_OBSERVED_ROUTING_CONFIG.fetch.enabled,
933
- },
934
- {
935
- path: ["outputRouter", "observedRouting", "codeSearch", "enabled"],
936
- value: DEFAULT_OBSERVED_ROUTING_CONFIG.codeSearch.enabled,
937
- },
959
+ { path: ["conversationHistory"], value: DEFAULT_CONVERSATION_HISTORY_ENABLED },
938
960
  ];
939
961
  for (const item of defaultPaths) {
940
962
  if (valuesEqual(getPath(config, item.path), item.value)) {
941
963
  deletePath(config, item.path);
942
964
  }
943
965
  }
944
- for (const producer of ["web", "fetch", "codeSearch"]) {
945
- const enabledPath = ["outputRouter", "observedRouting", producer, "enabled"];
946
- const persistencePath = ["outputRouter", "observedRouting", producer, "persistence"];
947
- if (getPath(config, enabledPath) !== true && getPath(config, persistencePath) === "none") {
948
- deletePath(config, persistencePath);
949
- }
950
- }
951
- const servers = getPath(config, ["outputRouter", "observedRouting", "mcp", "servers"]);
952
- if (isRecord(servers) && Object.keys(servers).length === 0) {
953
- deletePath(config, ["outputRouter", "observedRouting", "mcp", "servers"]);
954
- }
955
966
  }
956
967
  async function ensureLocalConfigIgnored(cwd) {
957
968
  let gitPath;
@@ -1014,14 +1025,18 @@ async function updateConfig(cwd, item, value, scope = item.configScope ?? "repos
1014
1025
  const current = await readFreeflowConfig(cwd);
1015
1026
  const next = cloneJson(current);
1016
1027
  setConfigValue(next, item, value);
1017
- migrateLegacyRouterConfig(next);
1018
1028
  pruneKnownDefaults(next);
1019
1029
  await mkdir(join(cwd, ".freeflow"), { recursive: true });
1020
1030
  await writeFile(join(cwd, ".freeflow/config.json"), `${JSON.stringify(next, null, 2)}\n`, "utf8");
1021
1031
  }
1032
+ function isCognitiveRoutingProfileItem(item) {
1033
+ return item.id === "freeflow.cognitiveRouting.standard" || item.id === "freeflow.cognitiveRouting.reasoning";
1034
+ }
1022
1035
  function valueForDisplay(item) {
1023
1036
  let value;
1024
- if (item.configScope === "local" || item.configScope === "session") {
1037
+ if (isCognitiveRoutingProfileItem(item) && item.format) {
1038
+ value = item.format(effectiveItemValue(item));
1039
+ } else if (item.configScope === "local" || item.configScope === "session") {
1025
1040
  value = formatCoreValue(effectiveItemValue(item));
1026
1041
  } else if (item.kind === "boolean") {
1027
1042
  value = booleanValue(item.value);
@@ -1055,10 +1070,14 @@ function refreshSettingsDerivedState(items) {
1055
1070
  const freeflowInactive = freeflowItem ? effectiveItemValue(freeflowItem) !== true : false;
1056
1071
  const skillsItem = findSettingsItem(items, "freeflow.skills.enabled");
1057
1072
  const skillsEnabled = skillsItem ? effectiveItemValue(skillsItem) === true : true;
1058
- const routerEnabled = findSettingsItem(items, "outputRouter.enabled")?.value === true;
1059
1073
  const sessionModeItem = findSettingsItem(items, "freeflow.sessionMode");
1060
1074
  const defaultModeItem = findSettingsItem(items, "freeflow.defaultMode");
1061
- const routerGroup = findSettingsItem(items, "outputRouter.group");
1075
+ const cognitiveRoutingGroup = findSettingsItem(items, "freeflow.cognitiveRouting");
1076
+ const cognitiveRoutingEnabledItem = findSettingsItem(items, "freeflow.cognitiveRouting.enabled");
1077
+ const cognitiveRoutingProfiles = [
1078
+ findSettingsItem(items, "freeflow.cognitiveRouting.standard"),
1079
+ findSettingsItem(items, "freeflow.cognitiveRouting.reasoning"),
1080
+ ];
1062
1081
  if (sessionModeItem) {
1063
1082
  const defaultMode = String(
1064
1083
  defaultModeItem ? effectiveItemValue(defaultModeItem) : (sessionModeItem.inheritedValue ?? "workflow"),
@@ -1076,28 +1095,57 @@ function refreshSettingsDerivedState(items) {
1076
1095
  ...MODE_DESCRIPTIONS,
1077
1096
  };
1078
1097
  }
1079
- if (routerGroup) {
1080
- routerGroup.value = routerEnabled;
1081
- routerGroup.inactive = freeflowInactive;
1098
+ if (cognitiveRoutingGroup && cognitiveRoutingEnabledItem) {
1099
+ const enabled = effectiveItemValue(cognitiveRoutingEnabledItem) === true;
1100
+ const profilesConfigured = cognitiveRoutingProfiles.every((item) =>
1101
+ item ? isCognitiveRoutingProfile(effectiveItemValue(item)) : false,
1102
+ );
1103
+ cognitiveRoutingGroup.value = enabled;
1104
+ cognitiveRoutingGroup.displaySuffix = enabled
1105
+ ? profilesConfigured
1106
+ ? cognitiveRoutingGroup.displaySuffix === "active"
1107
+ ? "active"
1108
+ : "configured"
1109
+ : "not configured"
1110
+ : "disabled";
1111
+ cognitiveRoutingGroup.inactive = freeflowInactive || !skillsEnabled;
1112
+ }
1113
+ const contextGroup = findSettingsItem(items, "freeflow.context");
1114
+ if (contextGroup?.children?.length) {
1115
+ const enabledCount = contextGroup.children.filter((item) => effectiveItemValue(item) === true).length;
1116
+ contextGroup.value = enabledCount > 0;
1117
+ contextGroup.displaySuffix = `${enabledCount}/${contextGroup.children.length} enabled`;
1082
1118
  }
1083
1119
  walkSettingsItems(items, (candidate) => {
1084
1120
  if (candidate.id === "freeflow.session.reset") {
1085
1121
  candidate.inactive = false;
1086
1122
  } else if (candidate.configScope) {
1087
- const inactive = candidate.id === "freeflow.enabled" ? false : freeflowInactive;
1123
+ const inactive =
1124
+ candidate.id === "freeflow.enabled"
1125
+ ? false
1126
+ : candidate.runtimeInactive === true ||
1127
+ freeflowInactive ||
1128
+ (!skillsEnabled &&
1129
+ [
1130
+ "freeflow.cognitiveRouting",
1131
+ "freeflow.cognitiveRouting.enabled",
1132
+ "freeflow.cognitiveRouting.standard",
1133
+ "freeflow.cognitiveRouting.reasoning",
1134
+ "freeflow.cognitiveRouting.sessionStart.control",
1135
+ "freeflow.cognitiveRouting.sessionStart.profile",
1136
+ "freeflow.contextVirtualization",
1137
+ "freeflow.conversationHistory",
1138
+ ].includes(candidate.id));
1088
1139
  const displayInactive = candidate.id === "freeflow.defaultMode" ? freeflowInactive || !skillsEnabled : inactive;
1089
1140
  candidate.inactive = inactive;
1090
1141
  candidate.displaySuffix = coreDisplaySuffix(candidate, displayInactive);
1091
1142
  } else if (candidate.id === "freeflow.sessionMode") {
1092
- candidate.inactive = freeflowInactive || !skillsEnabled;
1093
- } else if (candidate.id === "outputRouter.group") {
1094
- candidate.inactive = freeflowInactive;
1095
- } else if (candidate.id === "outputRouter.enabled") {
1096
- candidate.inactive = freeflowInactive;
1097
- } else if (candidate.id.startsWith("outputRouter.")) {
1098
- candidate.inactive = freeflowInactive || !routerEnabled;
1143
+ candidate.inactive = candidate.runtimeInactive === true || freeflowInactive || !skillsEnabled;
1099
1144
  } else {
1100
- candidate.inactive = freeflowInactive;
1145
+ candidate.inactive =
1146
+ candidate.runtimeInactive === true ||
1147
+ freeflowInactive ||
1148
+ (!skillsEnabled && candidate.id === "freeflow.cognitiveRouting");
1101
1149
  }
1102
1150
  });
1103
1151
  }
@@ -1137,6 +1185,7 @@ function settingsEntries(items, rootItems, onChange) {
1137
1185
  currentChoiceKey: () => String(item.value),
1138
1186
  choices: settingsChoices(item),
1139
1187
  children: item.children ? () => settingsEntries(item.children, rootItems, onChange) : undefined,
1188
+ wizard: item.wizard ? () => item.wizard() : undefined,
1140
1189
  edit:
1141
1190
  !item.children && !["boolean", "enum", "group"].includes(item.kind)
1142
1191
  ? {
@@ -1191,42 +1240,44 @@ async function openSettings(options) {
1191
1240
  await component?.waitForWrites();
1192
1241
  return component?.sessionResult() ?? { changed: false, configChanged: false, failed: false };
1193
1242
  }
1194
- function outputRouterStatusText(rawConfig) {
1195
- const settingsConfig = normalizedSettingsConfig(rawConfig);
1196
- const actual = normalizeFreeflowConfig(rawConfig).config;
1197
- const freeflowEnabled = rawConfig.enabled !== false;
1198
- const router = actual.outputRouter;
1199
- const configured = settingsConfig;
1200
- const generatedPaths = router.hints?.generatedPathGlobs?.length ?? 0;
1201
- const noisyHints = router.hints?.noisyCommandPatterns?.length ?? 0;
1202
- return [
1203
- `Output Router: ${freeflowEnabled && router.enabled ? "enabled" : "disabled"}${freeflowEnabled ? "" : " (inactive: Freeflow off)"}`,
1204
- `script transform: ${freeflowEnabled && configured.scriptTransform.enabled ? "enabled" : "disabled"}${freeflowEnabled && router.enabled ? "" : " (inactive)"}`,
1205
- `observed routing: ${freeflowEnabled && configured.observedRouting.enabled ? "enabled" : "disabled"}${freeflowEnabled && router.enabled ? "" : " (inactive)"}`,
1206
- `native safety net: ${freeflowEnabled ? router.postToolRouting : "off"}`,
1207
- `storage: ${router.storagePolicy}`,
1208
- `generated paths: ${generatedPaths}`,
1209
- `noisy command hints: ${noisyHints}`,
1210
- ].join("; ");
1211
- }
1212
- function freeflowStatusText(state) {
1243
+ function freeflowStatusText(state, cognitiveRoutingController) {
1213
1244
  if (!state.configured) {
1214
1245
  return state.configExists
1215
1246
  ? `Freeflow: inactive (invalid config: ${state.parseError ?? "unknown parse error"}); run /setup-freeflow or fix .freeflow/config.json`
1216
1247
  : "Freeflow: inactive (repo not set up); run /setup-freeflow";
1217
1248
  }
1218
1249
  const sessionSuffix = (source) => (source === "session" ? " (session override)" : "");
1250
+ const routingState = cognitiveRoutingController?.state();
1251
+ const cognitiveRouting = state.cognitiveRouting;
1252
+ const cognitiveRoutingStatus = cognitiveRouting
1253
+ ? cognitiveRouting.blockingReason?.code === "runtime_disabled"
1254
+ ? "disabled (PiFlow only)"
1255
+ : cognitiveRouting.effective
1256
+ ? routingState?.effective
1257
+ ? `active (${routingState.activeProfile ?? "unknown"}, ${routingState.controlMode})`
1258
+ : "effective (inactive)"
1259
+ : cognitiveRouting.enabled
1260
+ ? `blocked (${cognitiveRouting.blockingReason.code})`
1261
+ : "disabled"
1262
+ : undefined;
1263
+ const contextEnabled = state.contextVirtualization?.effective || state.conversationHistory?.effective;
1219
1264
  return [
1220
1265
  `Freeflow: ${state.enabled ? "enabled" : "disabled"}${sessionSuffix(state.configSources.enabled)}`,
1221
1266
  `interaction contract: ${state.interactionContract.effective ? "enabled" : "disabled"}${sessionSuffix(state.configSources.interactionContract)}`,
1222
1267
  `skills: ${state.skills.effective ? "enabled" : "disabled (workflow modes inactive)"}${sessionSuffix(state.configSources.skillsEnabled)}`,
1223
- `output router: ${state.outputRouter.enabled ? "enabled" : "disabled"}`,
1268
+ `context: ${contextEnabled ? "enabled" : "disabled"} (virtualization ${state.contextVirtualization?.effective ? "enabled" : "disabled"}, history ${state.conversationHistory?.effective ? "enabled" : "disabled"})`,
1269
+ ...(cognitiveRoutingStatus ? [`cognitive routing: ${cognitiveRoutingStatus}`] : []),
1224
1270
  ].join("; ");
1225
1271
  }
1226
1272
  const NON_TUI_SETTINGS_GUIDANCE =
1227
1273
  "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>.";
1228
1274
  const NON_TUI_MODE_GUIDANCE =
1229
1275
  "Freeflow mode selector requires Pi TUI mode. Use /freeflow mode conversation, /freeflow mode workflow, /freeflow mode strict-workflow, or /freeflow mode reset.";
1276
+ function ensureSettingsIdle(ctx) {
1277
+ if (typeof ctx?.isIdle !== "function" || ctx.isIdle()) return true;
1278
+ ctx.ui?.notify?.("Freeflow settings and profile changes are available only while Pi is idle.", "warning");
1279
+ return false;
1280
+ }
1230
1281
  function nonTuiGuidance(ctx, message) {
1231
1282
  if (ctx?.hasUI === false) throw new Error(message);
1232
1283
  ctx?.ui?.notify?.(message, "warning");
@@ -1238,9 +1289,10 @@ async function finalizeSettingsSession(
1238
1289
  afterChange,
1239
1290
  savedMessage,
1240
1291
  reloadWarning = "Run /reload for Freeflow changes to fully apply.",
1292
+ afterChangeOptions = {},
1241
1293
  ) {
1242
1294
  if (!session.configChanged) return false;
1243
- await afterChange(true);
1295
+ await afterChange(true, afterChangeOptions);
1244
1296
  if (session.failed) return false;
1245
1297
  ctx.ui.notify(savedMessage, "info");
1246
1298
  if (typeof ctx.reload !== "function") {
@@ -1266,29 +1318,7 @@ async function finalizeSessionSettings(session, ctx, afterChange) {
1266
1318
  await ctx.reload();
1267
1319
  return true;
1268
1320
  }
1269
- function actionIsMutation(action) {
1270
- return action === "" || action === "settings" || ["enable", "on", "true", "disable", "off", "false"].includes(action);
1271
- }
1272
- async function maybeBlockLayerMutation(action, ctx, layerName) {
1273
- if (!actionIsMutation(action)) return false;
1274
- const state = await readCapabilityState(ctx.cwd);
1275
- if (!state.configured) {
1276
- ctx.ui.notify(
1277
- `Freeflow is installed but this repo is not set up. Run /setup-freeflow before configuring ${layerName}.`,
1278
- "warning",
1279
- );
1280
- return true;
1281
- }
1282
- if (!state.enabled) {
1283
- ctx.ui.notify(
1284
- `Freeflow is disabled for this repo. Use /freeflow enable before configuring ${layerName}.`,
1285
- "warning",
1286
- );
1287
- return true;
1288
- }
1289
- return false;
1290
- }
1291
- export async function handleFreeflowCommand(args, ctx, afterChange, pi) {
1321
+ export async function handleFreeflowCommand(args, ctx, afterChange, pi, cognitiveRoutingController) {
1292
1322
  const input = (args ?? "settings").trim().toLowerCase() || "settings";
1293
1323
  const [action, ...rest] = input.split(/\s+/);
1294
1324
  const actionValue = rest.join(" ");
@@ -1304,17 +1334,17 @@ export async function handleFreeflowCommand(args, ctx, afterChange, pi) {
1304
1334
  }
1305
1335
  const [layers, state, modeState] = await Promise.all([
1306
1336
  readFreeflowConfigLayers(ctx.cwd),
1307
- readCapabilityState(ctx.cwd),
1337
+ readCapabilityState(ctx.cwd, ctx, pi?.host),
1308
1338
  readModeState(ctx.cwd),
1309
1339
  ]);
1310
1340
  const configState = layers.repository;
1311
1341
  const raw = configState.valid ? configState.parsed : {};
1312
1342
  if (action === "status") {
1313
- ctx.ui.notify(freeflowStatusText(state), "info");
1343
+ ctx.ui.notify(freeflowStatusText(state, cognitiveRoutingController), "info");
1314
1344
  return { changed: false, reloaded: false };
1315
1345
  }
1316
1346
  if (!configState.valid) {
1317
- ctx.ui.notify(freeflowStatusText(state), "warning");
1347
+ ctx.ui.notify(freeflowStatusText(state, cognitiveRoutingController), "warning");
1318
1348
  return { changed: false, reloaded: false, error: "not_configured" };
1319
1349
  }
1320
1350
  if (layers.local.exists && !layers.local.valid) {
@@ -1325,6 +1355,7 @@ export async function handleFreeflowCommand(args, ctx, afterChange, pi) {
1325
1355
  return { changed: false, reloaded: false, error: "invalid_local_config" };
1326
1356
  }
1327
1357
  if (action === "mode") {
1358
+ if (!ensureSettingsIdle(ctx)) return { changed: false, reloaded: false, error: "busy" };
1328
1359
  if (actionValue) {
1329
1360
  const result = await handleModeCommand(actionValue, ctx, pi);
1330
1361
  return { changed: result.changed, reloaded: false, error: result.error };
@@ -1340,6 +1371,9 @@ export async function handleFreeflowCommand(args, ctx, afterChange, pi) {
1340
1371
  const item = freeflowItems(raw, modeState, {
1341
1372
  scope: "local",
1342
1373
  layers,
1374
+ cognitiveRouting: state.cognitiveRouting,
1375
+ ctx,
1376
+ hostInfo: pi?.host,
1343
1377
  }).find((candidate) => candidate.id === "freeflow.sessionMode");
1344
1378
  const session = await openSettings({
1345
1379
  title: "Freeflow Mode",
@@ -1354,10 +1388,14 @@ export async function handleFreeflowCommand(args, ctx, afterChange, pi) {
1354
1388
  return { changed: session.changed, reloaded: false, error: session.failed ? "write_failed" : undefined };
1355
1389
  }
1356
1390
  if (["enable", "on", "true", "disable", "off", "false"].includes(action)) {
1391
+ if (!ensureSettingsIdle(ctx)) return { changed: false, reloaded: false, error: "busy" };
1357
1392
  const enabled = ["enable", "on", "true"].includes(action);
1358
1393
  const item = freeflowItems(raw, modeState, {
1359
1394
  scope: "repository",
1360
1395
  layers,
1396
+ cognitiveRouting: state.cognitiveRouting,
1397
+ ctx,
1398
+ hostInfo: pi?.host,
1361
1399
  }).find((candidate) => candidate.id === "freeflow.enabled");
1362
1400
  await updateConfig(ctx.cwd, item, enabled, "repository");
1363
1401
  await afterChange(true);
@@ -1388,10 +1426,18 @@ export async function handleFreeflowCommand(args, ctx, afterChange, pi) {
1388
1426
  );
1389
1427
  return { changed: false, reloaded: false, error: "invalid_scope" };
1390
1428
  }
1429
+ if (!ensureSettingsIdle(ctx)) return { changed: false, reloaded: false, error: "busy" };
1430
+ let reconcileCognitiveRouting = settingsScope === "session";
1391
1431
  const items =
1392
1432
  settingsScope === "session"
1393
- ? sessionFreeflowItems(state, modeState)
1394
- : freeflowItems(raw, modeState, { scope: settingsScope, layers });
1433
+ ? sessionFreeflowItems(state, modeState, cognitiveRoutingController)
1434
+ : freeflowItems(raw, modeState, {
1435
+ scope: settingsScope,
1436
+ layers,
1437
+ cognitiveRouting: state.cognitiveRouting,
1438
+ ctx,
1439
+ hostInfo: pi?.host,
1440
+ });
1395
1441
  const session = await openSettings({
1396
1442
  title:
1397
1443
  settingsScope === "session"
@@ -1402,10 +1448,34 @@ export async function handleFreeflowCommand(args, ctx, afterChange, pi) {
1402
1448
  items,
1403
1449
  ctx,
1404
1450
  onChange: async (item, value) => {
1451
+ if (!ensureSettingsIdle(ctx)) return { changed: false, reloadRequired: false };
1452
+ if (!isPiFlowHost(pi?.host) && item.id.startsWith("freeflow.cognitiveRouting.")) {
1453
+ ctx.ui.notify("Cognitive Routing is available only in PiFlow.", "warning");
1454
+ return { changed: false, reloadRequired: false };
1455
+ }
1405
1456
  if (item.id === "freeflow.sessionMode") {
1406
1457
  const result = await handleModeCommand(String(value), ctx, pi);
1407
1458
  return { changed: result.changed, reloadRequired: false };
1408
1459
  }
1460
+ if (item.id === "freeflow.cognitiveRouting.profile") {
1461
+ if (!cognitiveRoutingController) {
1462
+ ctx.ui.notify("Cognitive Routing is unavailable for this session.", "warning");
1463
+ return { changed: false, reloadRequired: false };
1464
+ }
1465
+ const result =
1466
+ value === "auto"
1467
+ ? await cognitiveRoutingController.setAutomaticControl("profile-settings")
1468
+ : await cognitiveRoutingController.setManualProfile(value, "profile-settings");
1469
+ if ((result.status !== "automatic" && result.status !== "active") || result.reason) {
1470
+ ctx.ui.notify(
1471
+ `Cognitive Routing settings could not be applied: ${result.reason ?? result.status}.`,
1472
+ "warning",
1473
+ );
1474
+ return { changed: false, reloadRequired: false };
1475
+ }
1476
+ await afterChange(false);
1477
+ return { changed: true, reloadRequired: false };
1478
+ }
1409
1479
  if (item.id === "freeflow.session.reset") {
1410
1480
  const result = await resetSessionOverrides(ctx, pi);
1411
1481
  return { changed: result.changed, reloadRequired: result.reloadRequired };
@@ -1415,6 +1485,8 @@ export async function handleFreeflowCommand(args, ctx, afterChange, pi) {
1415
1485
  "freeflow.enabled": "enabled",
1416
1486
  "freeflow.interactionContract": "interactionContract",
1417
1487
  "freeflow.skills.enabled": "skillsEnabled",
1488
+ "freeflow.contextVirtualization": "contextVirtualization",
1489
+ "freeflow.conversationHistory": "conversationHistory",
1418
1490
  };
1419
1491
  const key = keyById[item.id];
1420
1492
  const override = value === LOCAL_INHERIT ? null : value === "true";
@@ -1422,6 +1494,9 @@ export async function handleFreeflowCommand(args, ctx, afterChange, pi) {
1422
1494
  return { changed: result.changed, reloadRequired: result.reloadRequired === true };
1423
1495
  }
1424
1496
  await updateConfig(ctx.cwd, item, value, item.configScope ?? "repository");
1497
+ if (!item.id.startsWith("freeflow.cognitiveRouting.sessionStart.")) {
1498
+ reconcileCognitiveRouting = true;
1499
+ }
1425
1500
  return { changed: true, reloadRequired: true };
1426
1501
  },
1427
1502
  });
@@ -1435,53 +1510,8 @@ export async function handleFreeflowCommand(args, ctx, afterChange, pi) {
1435
1510
  ctx,
1436
1511
  afterChange,
1437
1512
  `Freeflow ${savedTarget} saved. Reloading Freeflow runtime...`,
1438
- );
1439
- return { changed: session.changed, reloaded, error: session.failed ? "write_failed" : undefined };
1440
- }
1441
- export async function handleOutputRouterCommand(args, ctx, afterChange) {
1442
- const action = (args ?? "").trim().toLowerCase();
1443
- const raw = await readFreeflowConfig(ctx.cwd);
1444
- if (await maybeBlockLayerMutation(action, ctx, "Output Router")) {
1445
- return { changed: false, reloaded: false, error: "freeflow_inactive" };
1446
- }
1447
- if (action === "status") {
1448
- const state = await readCapabilityState(ctx.cwd);
1449
- const prefix = !state.configured || !state.enabled ? `${freeflowStatusText(state)}; ` : "";
1450
- ctx.ui.notify(`${prefix}${outputRouterStatusText(raw)}`, !state.configured || !state.enabled ? "warning" : "info");
1451
- return { changed: false, reloaded: false };
1452
- }
1453
- if (["enable", "on", "true", "disable", "off", "false"].includes(action)) {
1454
- const enabled = ["enable", "on", "true"].includes(action);
1455
- const item = outputRouterItems(raw).find((candidate) => candidate.id === "outputRouter.enabled");
1456
- await updateConfig(ctx.cwd, item, enabled);
1457
- await afterChange(true);
1458
- ctx.ui.notify(`Output Router ${enabled ? "enabled" : "disabled"}. Reloading Freeflow runtime...`, "info");
1459
- if (typeof ctx.reload === "function") {
1460
- await ctx.reload();
1461
- return { changed: true, reloaded: true };
1462
- }
1463
- ctx.ui.notify("Run /reload for Output Router changes to fully apply.", "warning");
1464
- return { changed: true, reloaded: false };
1465
- }
1466
- if (action && action !== "settings") {
1467
- ctx.ui.notify("Usage: /output-router, /output-router settings, or /output-router status", "warning");
1468
- return { changed: false, reloaded: false, error: "invalid_action" };
1469
- }
1470
- const session = await openSettings({
1471
- title: "Output Router Settings",
1472
- items: outputRouterItems(raw),
1473
- ctx,
1474
- onChange: async (item, value) => {
1475
- await updateConfig(ctx.cwd, item, value);
1476
- return { changed: true, reloadRequired: true };
1477
- },
1478
- });
1479
- const reloaded = await finalizeSettingsSession(
1480
- session,
1481
- ctx,
1482
- afterChange,
1483
- "Output Router settings saved. Reloading Freeflow runtime...",
1484
- "Run /reload for Output Router changes to fully apply.",
1513
+ "Run /reload for Freeflow changes to fully apply.",
1514
+ { reconcileCognitiveRouting },
1485
1515
  );
1486
1516
  return { changed: session.changed, reloaded, error: session.failed ? "write_failed" : undefined };
1487
1517
  }