@hassangameryt/freeflow 0.7.3 → 0.8.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 (276) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/.codex-plugin/plugin.json +1 -1
  4. package/.cursor-plugin/plugin.json +1 -1
  5. package/NOTICE +14 -0
  6. package/README.md +26 -31
  7. package/capabilities/cognitive-routing/SKILL.md +106 -161
  8. package/capabilities/cognitive-routing/references/both-mode.md +36 -0
  9. package/capabilities/cognitive-routing/references/executor-mode.md +48 -0
  10. package/capabilities/cognitive-routing/references/helper-mode.md +47 -0
  11. package/capabilities/compaction/SKILL.md +49 -0
  12. package/capabilities/compaction/references/summary-format.md +239 -0
  13. package/command-surface.json +4 -0
  14. package/gemini-extension.json +1 -1
  15. package/hooks/shared/runtime-context.mjs +28 -30
  16. package/licenses/Apache-2.0.txt +55 -0
  17. package/package.json +22 -9
  18. package/pi-extension/README.md +41 -0
  19. package/pi-extension/dist/cognitive-routing/assembler.js +393 -0
  20. package/pi-extension/dist/{cognitive-routing-v2 → cognitive-routing}/config.js +5 -2
  21. package/pi-extension/dist/cognitive-routing/economics.js +45 -0
  22. package/pi-extension/dist/cognitive-routing/event-schema.js +388 -0
  23. package/pi-extension/dist/{session-sources/events.js → cognitive-routing/event-store.js} +103 -40
  24. package/pi-extension/dist/cognitive-routing/gate.js +241 -0
  25. package/pi-extension/dist/cognitive-routing/handlers.js +899 -0
  26. package/pi-extension/dist/cognitive-routing/handoffs.js +350 -0
  27. package/pi-extension/dist/cognitive-routing/model-control.js +467 -0
  28. package/pi-extension/dist/{cognitive-routing-v2 → cognitive-routing}/projection.js +229 -56
  29. package/pi-extension/dist/cognitive-routing/provenance.js +81 -0
  30. package/pi-extension/dist/cognitive-routing/runtime.js +396 -0
  31. package/pi-extension/dist/{cognitive-routing-v2 → cognitive-routing}/schemas.js +43 -7
  32. package/pi-extension/dist/cognitive-routing/session.js +221 -0
  33. package/pi-extension/dist/{session-sources → cognitive-routing}/sources.js +99 -85
  34. package/pi-extension/dist/{cognitive-routing-v2 → cognitive-routing}/state.js +37 -378
  35. package/pi-extension/dist/cognitive-routing/status.js +80 -0
  36. package/pi-extension/dist/{cognitive-routing-v2 → cognitive-routing}/tools.js +12 -12
  37. package/pi-extension/dist/{cognitive-routing-v2 → cognitive-routing}/types.js +5 -11
  38. package/pi-extension/dist/compaction/carry.js +191 -0
  39. package/pi-extension/dist/compaction/config.js +22 -0
  40. package/pi-extension/dist/compaction/controller.js +471 -0
  41. package/pi-extension/dist/compaction/harness.js +77 -0
  42. package/pi-extension/dist/compaction/results.js +158 -0
  43. package/pi-extension/dist/compaction/thresholds.js +35 -0
  44. package/pi-extension/dist/compaction/tool.js +57 -0
  45. package/pi-extension/dist/host/branch.js +13 -0
  46. package/pi-extension/dist/host/canonical.js +12 -0
  47. package/pi-extension/dist/host/catalog.js +77 -0
  48. package/pi-extension/dist/host/config.js +374 -0
  49. package/pi-extension/dist/host/projection-tags.js +60 -0
  50. package/pi-extension/dist/host/prompts.js +82 -0
  51. package/pi-extension/dist/{session-sources → host}/read-only-session.js +112 -1
  52. package/pi-extension/dist/{runtime → host}/request-history.js +121 -39
  53. package/pi-extension/dist/host/runtime-state.js +189 -0
  54. package/pi-extension/dist/{settings/settings-ui.js → host/settings/freeflow-command.js} +620 -410
  55. package/pi-extension/dist/{settings/settings-tui.js → host/settings/settings-view.js} +186 -73
  56. package/pi-extension/dist/host/staging.js +28 -0
  57. package/pi-extension/dist/host/status.js +101 -0
  58. package/pi-extension/dist/index.js +423 -173
  59. package/pi-extension/dist/provider-support/cache/anchor.js +21 -0
  60. package/pi-extension/dist/provider-support/cache/anthropic.js +102 -0
  61. package/pi-extension/dist/provider-support/cache/health.js +73 -0
  62. package/pi-extension/dist/provider-support/cache/index.js +58 -0
  63. package/pi-extension/dist/provider-support/cache/keepalive.js +210 -0
  64. package/pi-extension/dist/provider-support/cache/monitor.js +17 -0
  65. package/pi-extension/dist/provider-support/cache/openai-responses.js +48 -0
  66. package/pi-extension/dist/provider-support/effort.js +13 -0
  67. package/pi-extension/dist/provider-support/index.js +44 -3
  68. package/pi-extension/dist/provider-support/{astra → openai}/adapter.js +46 -29
  69. package/pi-extension/dist/provider-support/{astra → openai}/history.js +10 -8
  70. package/pi-extension/dist/provider-support/{astra → openai}/index.js +8 -4
  71. package/pi-extension/dist/provider-support/{astra → openai}/session-state.js +29 -38
  72. package/pi-extension/dist/provider-support/routes.js +17 -0
  73. package/pi-extension/dist/tool-execution/apply-patch/grammar.js +23 -0
  74. package/pi-extension/dist/tool-execution/apply-patch/match.js +38 -0
  75. package/pi-extension/dist/tool-execution/apply-patch/parser.js +177 -0
  76. package/pi-extension/dist/tool-execution/apply-patch/plan.js +201 -0
  77. package/pi-extension/dist/tool-execution/apply-patch/tool.js +84 -0
  78. package/pi-extension/dist/tool-execution/background.js +238 -0
  79. package/pi-extension/dist/tool-execution/bash-guard.js +23 -0
  80. package/pi-extension/dist/tool-execution/config.js +22 -0
  81. package/pi-extension/dist/tool-execution/edit-messages.js +120 -0
  82. package/pi-extension/dist/tool-execution/file-state.js +138 -0
  83. package/pi-extension/dist/tool-execution/file-tracking.js +143 -0
  84. package/pi-extension/dist/tool-execution/prompt.js +54 -0
  85. package/pi-extension/dist/tool-execution/tools.js +16 -0
  86. package/pi-extension/freeflow/README.md +9 -0
  87. package/plugin.json +1 -1
  88. package/runtime/prompts/cognitive-routing.md +4 -4
  89. package/runtime/prompts/core.md +22 -75
  90. package/runtime/prompts/interaction-contract.md +15 -8
  91. package/runtime/prompts/tool-execution.md +38 -0
  92. package/runtime/prompts/working-method.md +108 -0
  93. package/skills/action-selection/SKILL.md +39 -47
  94. package/skills/action-selection/references/trajectory-stalls.md +2 -0
  95. package/skills/bypass/SKILL.md +2 -2
  96. package/skills/commit-work/SKILL.md +31 -83
  97. package/skills/commit-work/references/git-state-edges.md +1 -1
  98. package/skills/commit-work/references/simple-push.md +2 -0
  99. package/skills/commit-work/references/staging-decisions.md +1 -1
  100. package/skills/decision-gate/SKILL.md +31 -50
  101. package/skills/design-for-depth/SKILL.md +44 -53
  102. package/skills/design-for-depth/references/design-pressure-signals.md +46 -144
  103. package/skills/design-for-depth/references/implementation-decisions.md +1 -1
  104. package/skills/design-for-depth/references/interface-design-loop.md +5 -36
  105. package/skills/design-for-depth/references/module-and-dependency-design.md +2 -32
  106. package/skills/design-for-depth/references/state-and-failure-boundaries.md +2 -13
  107. package/skills/diagnose-failure/SKILL.md +39 -24
  108. package/skills/diagnose-failure/references/diagnostic-loop-catalog.md +2 -0
  109. package/skills/diagnose-failure/references/flaky-and-performance-diagnosis.md +2 -2
  110. package/skills/discuss/SKILL.md +35 -11
  111. package/skills/discuss/references/discussion-continuity.md +7 -27
  112. package/skills/evaluate-skill/SKILL.md +49 -80
  113. package/skills/evaluate-skill/references/evaluation-definition-schema.md +35 -5
  114. package/skills/evaluate-skill/references/evaluation-design.md +14 -14
  115. package/skills/evaluate-skill/references/execution-and-evidence.md +26 -5
  116. package/skills/evaluate-skill/references/review-and-revision.md +3 -1
  117. package/skills/evaluate-skill/scripts/lib/definitions.mjs +49 -8
  118. package/skills/evaluate-skill/scripts/lib/pi.mjs +14 -7
  119. package/skills/evaluate-skill/scripts/lib/review.mjs +196 -0
  120. package/skills/evaluate-skill/scripts/lib/runner.mjs +45 -8
  121. package/skills/evaluate-skill/scripts/lib/sandbox.mjs +2 -0
  122. package/skills/evaluate-skill/scripts/lib/trials.mjs +103 -0
  123. package/skills/evaluate-skill/scripts/lib/view.mjs +67 -1
  124. package/skills/evaluate-skill/scripts/pi-command-tool.mjs +144 -0
  125. package/skills/evaluate-skill/scripts/pi-guard.mjs +30 -13
  126. package/skills/evaluate-skill/scripts/skill-eval.mjs +114 -34
  127. package/skills/execute-work/SKILL.md +41 -9
  128. package/skills/execute-work/references/code-practices.md +2 -0
  129. package/skills/execute-work/references/execute-work-edges.md +11 -1
  130. package/skills/handoff/SKILL.md +1 -1
  131. package/skills/handoff/references/handoff-templates.md +1 -1
  132. package/skills/review-artifact/SKILL.md +42 -41
  133. package/skills/review-artifact/references/adjudicate-artifact-review.md +1 -1
  134. package/skills/review-artifact/references/independent-artifact-reviewer-contract.md +1 -1
  135. package/skills/review-work/SKILL.md +38 -32
  136. package/skills/review-work/references/adjudicate-work-review.md +1 -1
  137. package/skills/review-work/references/independent-work-reviewer-contract.md +1 -1
  138. package/skills/review-work/references/security-risk-lens.md +2 -4
  139. package/skills/setup-freeflow/SKILL.md +3 -1
  140. package/skills/setup-freeflow/references/activation-contract.md +7 -13
  141. package/skills/setup-freeflow/references/host-setup.md +3 -3
  142. package/skills/track-work/SKILL.md +113 -161
  143. package/skills/track-work/references/working-record-format.md +189 -350
  144. package/skills/track-work/scripts/lib/document.mjs +62 -6
  145. package/skills/track-work/scripts/lib/format.mjs +16 -1
  146. package/skills/track-work/scripts/lib/store.mjs +9 -8
  147. package/skills/track-work/scripts/lib/transitions.mjs +79 -4
  148. package/skills/track-work/scripts/working-record.mjs +34 -12
  149. package/skills/verify-work/SKILL.md +29 -20
  150. package/skills/verify-work/references/browser-runtime-evidence.md +1 -5
  151. package/skills/verify-work/references/integration-evidence.md +2 -0
  152. package/skills/verify-work/references/performance-evidence.md +3 -1
  153. package/skills/verify-work/references/test-design.md +3 -1
  154. package/skills/workflow/SKILL.md +65 -21
  155. package/skills/workflow/references/domain-skill-composition.md +5 -3
  156. package/skills/write-docs/SKILL.md +83 -0
  157. package/skills/write-docs/references/documentation-shapes.md +119 -0
  158. package/skills/write-docs/scripts/docs-check.mjs +139 -0
  159. package/skills/write-plan/SKILL.md +40 -40
  160. package/skills/write-plan/references/plan-shapes.md +1 -1
  161. package/skills/write-skill/SKILL.md +107 -147
  162. package/skills/write-skill/references/activation-boundaries.md +2 -0
  163. package/skills/write-skill/references/agent-first-instructions.md +22 -10
  164. package/skills/write-skill/references/progressive-disclosure.md +4 -2
  165. package/skills/write-skill/references/skill-dependency-graphs.md +2 -0
  166. package/skills/write-skill/scripts/lib/similarity.mjs +56 -0
  167. package/skills/write-skill/scripts/skill-author.mjs +18 -2
  168. package/skills/write-spec/SKILL.md +34 -41
  169. package/skills/write-spec/references/artifact-standards.md +1 -1
  170. package/skills/write-spec/references/decision-records.md +2 -0
  171. package/skills/write-spec/references/spec-shapes.md +1 -1
  172. package/capabilities/context-virtualization/SKILL.md +0 -103
  173. package/capabilities/conversation-history/SKILL.md +0 -53
  174. package/hooks/tests/check-host-adapters.test.mjs +0 -82
  175. package/hooks/tests/check-runtime-context-hook.sh +0 -212
  176. package/pi-extension/dist/cognitive-routing-v2/runtime.js +0 -2181
  177. package/pi-extension/dist/context-virtualization/commands.js +0 -89
  178. package/pi-extension/dist/context-virtualization/projector.js +0 -42
  179. package/pi-extension/dist/context-virtualization/runtime.js +0 -378
  180. package/pi-extension/dist/context-virtualization/state.js +0 -57
  181. package/pi-extension/dist/context-virtualization/types.js +0 -10
  182. package/pi-extension/dist/conversation-history/passages.js +0 -66
  183. package/pi-extension/dist/conversation-history/ranking.js +0 -105
  184. package/pi-extension/dist/conversation-history/runtime.js +0 -226
  185. package/pi-extension/dist/conversation-history/search.js +0 -56
  186. package/pi-extension/dist/conversation-history/types.js +0 -1
  187. package/pi-extension/dist/freeflow-context/resolver.js +0 -120
  188. package/pi-extension/dist/freeflow-context/runtime.js +0 -80
  189. package/pi-extension/dist/freeflow-context/source-projector.js +0 -95
  190. package/pi-extension/dist/freeflow-context/tool.js +0 -314
  191. package/pi-extension/dist/freeflow-context/types.js +0 -12
  192. package/pi-extension/dist/freeflow-context/visibility.js +0 -28
  193. package/pi-extension/dist/integrations/mcp-config.js +0 -29
  194. package/pi-extension/dist/runtime/runtime-context.js +0 -754
  195. package/pi-extension/dist/runtime/runtime-identity.js +0 -5
  196. package/pi-extension/dist/session-sources/provenance.js +0 -32
  197. package/pi-extension/dist/ui/text-component.js +0 -57
  198. package/pi-extension/src/cognitive-routing-v2/budget.ts +0 -42
  199. package/pi-extension/src/cognitive-routing-v2/config.ts +0 -173
  200. package/pi-extension/src/cognitive-routing-v2/projection.ts +0 -375
  201. package/pi-extension/src/cognitive-routing-v2/render.ts +0 -277
  202. package/pi-extension/src/cognitive-routing-v2/runtime.ts +0 -2241
  203. package/pi-extension/src/cognitive-routing-v2/schemas.ts +0 -180
  204. package/pi-extension/src/cognitive-routing-v2/state.ts +0 -839
  205. package/pi-extension/src/cognitive-routing-v2/tools.ts +0 -62
  206. package/pi-extension/src/cognitive-routing-v2/types.ts +0 -254
  207. package/pi-extension/src/context-virtualization/commands.ts +0 -111
  208. package/pi-extension/src/context-virtualization/projector.ts +0 -62
  209. package/pi-extension/src/context-virtualization/runtime.ts +0 -428
  210. package/pi-extension/src/context-virtualization/state.ts +0 -79
  211. package/pi-extension/src/context-virtualization/types.ts +0 -92
  212. package/pi-extension/src/conversation-history/passages.ts +0 -81
  213. package/pi-extension/src/conversation-history/ranking.ts +0 -155
  214. package/pi-extension/src/conversation-history/runtime.ts +0 -250
  215. package/pi-extension/src/conversation-history/search.ts +0 -79
  216. package/pi-extension/src/conversation-history/types.ts +0 -31
  217. package/pi-extension/src/freeflow-context/resolver.ts +0 -139
  218. package/pi-extension/src/freeflow-context/runtime.ts +0 -100
  219. package/pi-extension/src/freeflow-context/source-projector.ts +0 -106
  220. package/pi-extension/src/freeflow-context/tool.ts +0 -366
  221. package/pi-extension/src/freeflow-context/types.ts +0 -70
  222. package/pi-extension/src/freeflow-context/visibility.ts +0 -53
  223. package/pi-extension/src/index.ts +0 -390
  224. package/pi-extension/src/integrations/mcp-config.ts +0 -32
  225. package/pi-extension/src/provider-support/astra/adapter.ts +0 -108
  226. package/pi-extension/src/provider-support/astra/history.ts +0 -145
  227. package/pi-extension/src/provider-support/astra/index.ts +0 -12
  228. package/pi-extension/src/provider-support/astra/session-state.ts +0 -113
  229. package/pi-extension/src/provider-support/index.ts +0 -5
  230. package/pi-extension/src/runtime/request-history.ts +0 -243
  231. package/pi-extension/src/runtime/runtime-context.ts +0 -860
  232. package/pi-extension/src/runtime/runtime-identity.ts +0 -8
  233. package/pi-extension/src/session-sources/events.ts +0 -230
  234. package/pi-extension/src/session-sources/provenance.ts +0 -39
  235. package/pi-extension/src/session-sources/read-only-session.ts +0 -170
  236. package/pi-extension/src/session-sources/sources.ts +0 -334
  237. package/pi-extension/src/settings/settings-tui.ts +0 -501
  238. package/pi-extension/src/settings/settings-ui.ts +0 -1652
  239. package/pi-extension/src/ui/text-component.ts +0 -66
  240. package/pi-extension/tests/cache-reuse/history.test.js +0 -117
  241. package/pi-extension/tests/cache-reuse/native.test.js +0 -91
  242. package/pi-extension/tests/cache-reuse/settings.test.js +0 -211
  243. package/pi-extension/tests/cognitive-routing-v2/communication.test.js +0 -577
  244. package/pi-extension/tests/cognitive-routing-v2/compaction-input.test.js +0 -183
  245. package/pi-extension/tests/cognitive-routing-v2/delegation.test.js +0 -557
  246. package/pi-extension/tests/cognitive-routing-v2/native.test.js +0 -474
  247. package/pi-extension/tests/cognitive-routing-v2/performance.test.js +0 -87
  248. package/pi-extension/tests/cognitive-routing-v2/persistence.test.js +0 -300
  249. package/pi-extension/tests/cognitive-routing-v2/projection.test.js +0 -241
  250. package/pi-extension/tests/cognitive-routing-v2/recovery.test.js +0 -1034
  251. package/pi-extension/tests/cognitive-routing-v2/render.test.js +0 -244
  252. package/pi-extension/tests/cognitive-routing-v2/runtime.test.js +0 -423
  253. package/pi-extension/tests/cognitive-routing-v2/stabilization.test.js +0 -521
  254. package/pi-extension/tests/cognitive-routing-v2/state.test.js +0 -586
  255. package/pi-extension/tests/context-virtualization/extension.test.js +0 -775
  256. package/pi-extension/tests/context-virtualization/projector.test.js +0 -110
  257. package/pi-extension/tests/context-virtualization/runtime.test.js +0 -230
  258. package/pi-extension/tests/conversation-history/runtime.test.js +0 -98
  259. package/pi-extension/tests/conversation-history/search.test.js +0 -97
  260. package/pi-extension/tests/fixtures/pi-host.js +0 -4
  261. package/pi-extension/tests/fixtures/routing-native.js +0 -191
  262. package/pi-extension/tests/integration/mode-free-core.test.js +0 -220
  263. package/pi-extension/tests/integration/pi-extension.test.js +0 -1447
  264. package/pi-extension/tests/integration/prompt-architecture.test.js +0 -447
  265. package/pi-extension/tests/integration/unaffected-regressions.test.js +0 -478
  266. package/pi-extension/tests/provider-support/astra-boundaries.test.js +0 -75
  267. package/pi-extension/tests/provider-support/astra.test.js +0 -306
  268. package/pi-extension/tests/provider-support/cli.test.js +0 -92
  269. package/pi-extension/tests/provider-support/native.test.js +0 -183
  270. package/pi-extension/tests/settings/settings-tui.test.js +0 -243
  271. package/pi-extension/tsconfig.json +0 -16
  272. package/runtime/prompts/context-virtualization.md +0 -3
  273. package/runtime/prompts/conversation-history.md +0 -3
  274. package/skills/design-for-depth/references/software-design-philosophy.md +0 -202
  275. /package/pi-extension/dist/{cognitive-routing-v2 → cognitive-routing}/budget.js +0 -0
  276. /package/pi-extension/dist/{cognitive-routing-v2 → cognitive-routing}/render.js +0 -0
@@ -9,14 +9,26 @@ import {
9
9
  readFreeflowLocalConfig,
10
10
  resetSessionOverrides,
11
11
  setSessionCoreOverride,
12
- } from "../runtime/runtime-context.js";
13
- import { PiSettingsComponent } from "./settings-tui.js";
14
- import { isPiFlowHost } from "../runtime/runtime-identity.js";
15
- import { isWorkerProfile, workersForDelegation } from "../cognitive-routing-v2/types.js";
12
+ } from "../config.js";
13
+ import { freeflowStatusText } from "../status.js";
14
+ import { PiSettingsComponent } from "./settings-view.js";
15
+ import { workersForDelegation } from "../../cognitive-routing/types.js";
16
+ import { DEFAULT_TOOL_EXECUTION_CONFIG } from "../../tool-execution/config.js";
17
+ import { DEFAULT_COMPACTION_CARRY, DEFAULT_COMPACTION_ENABLED } from "../../compaction/config.js";
18
+ /*
19
+ * The /freeflow command and the settings model it edits: every Freeflow setting with its repository, personal, and
20
+ * session scopes, the routing profile wizard, config file writes, and reload after saving. settings-view.ts draws the
21
+ * screen; this module decides what is on it.
22
+ */
16
23
  const DEFAULT_FREEFLOW_ENABLED = true;
17
- const DEFAULT_CONTEXT_VIRTUALIZATION_ENABLED = false;
18
- const DEFAULT_CONVERSATION_HISTORY_ENABLED = false;
19
24
  const LOCAL_INHERIT = "inherit";
25
+ /** How a value's source reads in the settings screen. */
26
+ const SOURCE_LABELS = {
27
+ builtin: "default",
28
+ local: "personal",
29
+ repository: "repository",
30
+ session: "session",
31
+ };
20
32
  const execFileAsync = promisify(execFile);
21
33
  function isRecord(value) {
22
34
  return typeof value === "object" && value !== null && !Array.isArray(value);
@@ -76,7 +88,7 @@ function setConfigValue(config, item, value) {
76
88
  if (!item.path?.length) {
77
89
  throw new Error(`${item.label} is a settings group, not a writable setting.`);
78
90
  }
79
- if (item.defaultValue !== undefined && valuesEqual(value, item.defaultValue)) {
91
+ if (value === undefined || (item.defaultValue !== undefined && valuesEqual(value, item.defaultValue))) {
80
92
  deletePath(config, item.path);
81
93
  return;
82
94
  }
@@ -118,7 +130,8 @@ function coreDisplaySuffix(item, inactive = false) {
118
130
  }
119
131
  function updateScopedItemState(item, value) {
120
132
  if (!item.configScope) return;
121
- const configValue = configValueForChoice(item, value);
133
+ const selectedValue = configValueForChoice(item, value);
134
+ const configValue = selectedValue === undefined || isEmptyValue(selectedValue) ? undefined : selectedValue;
122
135
  if (item.configScope === "session" || item.configScope === "local") {
123
136
  item.effectiveValue = configValue === undefined ? item.inheritedValue : configValue;
124
137
  item.effectiveSource = configValue === undefined ? (item.inheritedSource ?? "builtin") : item.configScope;
@@ -144,20 +157,16 @@ function createScopedBooleanItem(options) {
144
157
  item = {
145
158
  id: options.id,
146
159
  label: options.label,
147
- description: `${options.description} Choose inherit to use the repository value; use /freeflow settings repo to edit shared defaults.`,
160
+ description: `${options.description} Inherit uses the repository value.`,
148
161
  path: options.path,
149
162
  kind: "enum",
150
163
  value: selectedValue,
151
164
  values: [LOCAL_INHERIT, "true", "false"],
152
- valueLabels: {
153
- inherit: "Inherit repository",
154
- true: "enabled",
155
- false: "disabled",
156
- },
165
+ valueLabels: { inherit: "inherit", true: "enabled", false: "disabled" },
157
166
  valueDescriptions: {
158
- inherit: `Use ${formatCoreValue(inheritedValue)} from ${inheritedSource}.`,
159
- true: "Set a personal enabled override.",
160
- false: "Set a personal disabled override.",
167
+ inherit: `Use ${formatCoreValue(inheritedValue)} from ${SOURCE_LABELS[inheritedSource]}.`,
168
+ true: "Enabled for you in this checkout.",
169
+ false: "Disabled for you in this checkout.",
161
170
  },
162
171
  format: (value) => {
163
172
  if (value === LOCAL_INHERIT) return LOCAL_INHERIT;
@@ -178,7 +187,7 @@ function createScopedBooleanItem(options) {
178
187
  item = {
179
188
  id: options.id,
180
189
  label: options.label,
181
- description: `${options.description} This edits shared .freeflow/config.json.`,
190
+ description: options.description,
182
191
  path: options.path,
183
192
  kind: "boolean",
184
193
  value: inheritedValue,
@@ -207,7 +216,7 @@ function createScopedDelegationItem(options) {
207
216
  options.scope === "local"
208
217
  ? {
209
218
  id: "freeflow.cognitiveRouting.delegation",
210
- label: "Delegation method",
219
+ label: "Delegation",
211
220
  description:
212
221
  "Choose enabled workers for automatic routing. Inherit uses the repository value; this setting does not rank model presets.",
213
222
  path,
@@ -215,13 +224,13 @@ function createScopedDelegationItem(options) {
215
224
  value: ["executor", "helper", "both"].includes(localValue) ? localValue : LOCAL_INHERIT,
216
225
  values: [LOCAL_INHERIT, "executor", "helper", "both"],
217
226
  valueLabels: {
218
- inherit: "Inherit repository",
227
+ inherit: "inherit",
219
228
  executor: "Executor only",
220
229
  helper: "Helper only",
221
230
  both: "Helper and Executor",
222
231
  },
223
232
  valueDescriptions: {
224
- inherit: `Use ${inheritedValue} from ${inheritedSource}.`,
233
+ inherit: `Use ${inheritedValue} from ${SOURCE_LABELS[inheritedSource]}.`,
225
234
  ...descriptions,
226
235
  },
227
236
  configScope: "local",
@@ -233,7 +242,7 @@ function createScopedDelegationItem(options) {
233
242
  }
234
243
  : {
235
244
  id: "freeflow.cognitiveRouting.delegation",
236
- label: "Delegation method",
245
+ label: "Delegation",
237
246
  description: "Choose enabled workers for automatic routing. This edits shared .freeflow/config.json.",
238
247
  path,
239
248
  kind: "enum",
@@ -256,16 +265,9 @@ function createScopedDelegationItem(options) {
256
265
  }
257
266
  function resolveSettingsCoreView(rawConfig, layers) {
258
267
  const localConfig = layers?.local.valid && isRecord(layers.local.parsed) ? layers.local.parsed : {};
259
- const fallbackCore = {
260
- enabled: getPath(rawConfig, ["enabled"]) !== false,
261
- contextVirtualization: getPath(rawConfig, ["contextVirtualization"]) === true,
262
- conversationHistory: getPath(rawConfig, ["conversationHistory"]) === true,
263
- };
268
+ const fallbackCore = { enabled: getPath(rawConfig, ["enabled"]) !== false };
264
269
  const fallbackSources = {
265
270
  enabled: typeof getPath(rawConfig, ["enabled"]) === "boolean" ? "repository" : "builtin",
266
- contextVirtualization:
267
- typeof getPath(rawConfig, ["contextVirtualization"]) === "boolean" ? "repository" : "builtin",
268
- conversationHistory: typeof getPath(rawConfig, ["conversationHistory"]) === "boolean" ? "repository" : "builtin",
269
271
  };
270
272
  return {
271
273
  localConfig,
@@ -275,166 +277,258 @@ function resolveSettingsCoreView(rawConfig, layers) {
275
277
  }
276
278
  function createSessionBooleanItem(options) {
277
279
  const override = options.sessionOverrides[options.key];
278
- const item = {
280
+ return {
279
281
  id: options.id,
280
282
  label: options.label,
281
- description: options.description,
283
+ description: `${options.description} Inherit uses the personal or repository value.`,
284
+ path: options.key.split("."),
282
285
  kind: "enum",
283
286
  value: typeof override === "boolean" ? String(override) : LOCAL_INHERIT,
284
287
  values: [LOCAL_INHERIT, "true", "false"],
285
- valueLabels: {
286
- inherit: "Inherit configured value",
287
- true: "Enabled for this session",
288
- false: "Disabled for this session",
289
- },
288
+ valueLabels: { inherit: "inherit", true: "enabled", false: "disabled" },
290
289
  valueDescriptions: {
291
- inherit: `Use ${formatCoreValue(options.inheritedValue)} from ${options.inheritedSource}.`,
292
- true: "Temporarily enable this setting for the current Pi session.",
293
- false: "Temporarily disable this setting for the current Pi session.",
294
- },
295
- format: (value) => {
296
- if (value === LOCAL_INHERIT) return LOCAL_INHERIT;
297
- return booleanValue(value === "true");
290
+ inherit: `Use ${formatCoreValue(options.inheritedValue)} from ${SOURCE_LABELS[options.inheritedSource]}.`,
291
+ true: "Enabled for this Pi session only.",
292
+ false: "Disabled for this Pi session only.",
298
293
  },
299
294
  configScope: "session",
300
- configValues: {
301
- inherit: undefined,
302
- true: true,
303
- false: false,
304
- },
295
+ configValues: { inherit: undefined, true: true, false: false },
305
296
  effectiveValue: options.effectiveValue,
306
- effectiveSource: options.effectiveSource,
297
+ effectiveSource: typeof override === "boolean" ? "session" : options.inheritedSource,
307
298
  inheritedValue: options.inheritedValue,
308
299
  inheritedSource: options.inheritedSource,
309
300
  };
310
- item.displaySuffix = coreDisplaySuffix(item);
311
- return item;
312
301
  }
313
- function sessionFreeflowItems(state, cognitiveRoutingController, ctx) {
302
+ /** A setting's configured value and source without session overrides: personal, then repository, then the default. */
303
+ function configuredSetting(rawConfig, localConfig, path, defaultValue) {
304
+ const local = getPath(localConfig, path);
305
+ if (typeof local === "boolean") return { value: local, source: "local" };
306
+ const repository = getPath(rawConfig, path);
307
+ if (typeof repository === "boolean") return { value: repository, source: "repository" };
308
+ return { value: defaultValue, source: "builtin" };
309
+ }
310
+ function sectionItem(id, label, description, status, children) {
311
+ return { id, label, description, kind: "group", value: true, displaySuffix: status, children };
312
+ }
313
+ function capabilityStatus(state) {
314
+ return state?.effective ? "active" : state?.enabled ? "inactive" : "disabled";
315
+ }
316
+ function sessionFreeflowItems(state, cognitiveRoutingController, ctx, rawConfig, localConfig) {
314
317
  const sessionOverrides = state.sessionOverrides;
315
318
  const configured = state.configuredCoreConfig;
316
319
  const configuredSources = state.configuredSources;
317
- const effectiveSources = state.configSources;
320
+ const freeflowInactive = !state.enabled;
321
+ const sessionSwitch = (id, label, description, key, effective, fallback) => {
322
+ const inherited = configuredSetting(rawConfig, localConfig, key.split("."), fallback);
323
+ const item = createSessionBooleanItem({
324
+ id,
325
+ label,
326
+ description,
327
+ key,
328
+ inheritedValue: inherited.value,
329
+ inheritedSource: inherited.source,
330
+ effectiveValue: effective,
331
+ sessionOverrides,
332
+ });
333
+ item.inactive = freeflowInactive;
334
+ return item;
335
+ };
318
336
  const freeflowItem = createSessionBooleanItem({
319
337
  id: "freeflow.enabled",
320
338
  label: "Freeflow",
321
- description: "Temporary master override for this Pi session.",
339
+ description: "Master switch for Freeflow in this Pi session.",
322
340
  key: "enabled",
323
341
  inheritedValue: configured.enabled,
324
342
  inheritedSource: configuredSources.enabled,
325
343
  effectiveValue: state.enabled,
326
- effectiveSource: effectiveSources.enabled,
327
- sessionOverrides,
328
- });
329
- const contextVirtualizationItem = createSessionBooleanItem({
330
- id: "freeflow.contextVirtualization",
331
- label: "Context Virtualization",
332
- description: "Temporary Context Virtualization override for this Pi session.",
333
- key: "contextVirtualization",
334
- inheritedValue: configured.contextVirtualization,
335
- inheritedSource: configuredSources.contextVirtualization,
336
- effectiveValue: state.contextVirtualization.enabled,
337
- effectiveSource: effectiveSources.contextVirtualization,
338
- sessionOverrides,
339
- });
340
- const conversationHistoryItem = createSessionBooleanItem({
341
- id: "freeflow.conversationHistory",
342
- label: "Conversation History",
343
- description: "Temporary Conversation History override for this Pi session.",
344
- key: "conversationHistory",
345
- inheritedValue: configured.conversationHistory,
346
- inheritedSource: configuredSources.conversationHistory,
347
- effectiveValue: state.conversationHistory.enabled,
348
- effectiveSource: effectiveSources.conversationHistory,
349
344
  sessionOverrides,
350
345
  });
351
- const freeflowInactive = !state.enabled;
352
- contextVirtualizationItem.inactive = freeflowInactive;
353
- conversationHistoryItem.inactive = freeflowInactive;
346
+ const routing = state.cognitiveRouting;
347
+ const routingUnavailable = cognitiveRoutingController === undefined;
354
348
  const cognitiveRoutingState = cognitiveRoutingController?.state();
355
- const delegation = state.cognitiveRouting?.delegation ?? "executor";
349
+ const manualProfiles = ["coordinator", "helper", "executor"];
356
350
  const heldProfile = cognitiveRoutingState?.controlMode.startsWith("manual-")
357
351
  ? cognitiveRoutingState.activeProfile
358
352
  : undefined;
359
- const availableProfiles = ["coordinator", ...workersForDelegation(delegation)];
360
- if (heldProfile === "coordinator" || isWorkerProfile(heldProfile)) availableProfiles.push(heldProfile);
361
- const manualProfiles = [...new Set(availableProfiles)];
362
- const cognitiveRoutingProfile = heldProfile && manualProfiles.includes(heldProfile) ? heldProfile : "auto";
363
- const cognitiveRoutingItem = state.cognitiveRouting
364
- ? {
353
+ const control = heldProfile && manualProfiles.includes(heldProfile) ? heldProfile : "auto";
354
+ const routingInactive = freeflowInactive || !routing?.effective || routingUnavailable;
355
+ const routingItems = [];
356
+ if (routing) {
357
+ const enabledItem = sessionSwitch(
358
+ "freeflow.cognitiveRouting.enabled",
359
+ "Enabled",
360
+ "Use Cognitive Routing in this session.",
361
+ "cognitiveRouting.enabled",
362
+ routing.enabled,
363
+ false,
364
+ );
365
+ enabledItem.inactive = freeflowInactive || routingUnavailable;
366
+ const projectionItem = sessionSwitch(
367
+ "freeflow.cognitiveRouting.projection",
368
+ "Context projection",
369
+ "Show Coordinator the worker evidence selected for it instead of the whole history.",
370
+ "cognitiveRouting.projection",
371
+ routing.projection,
372
+ false,
373
+ );
374
+ projectionItem.inactive = routingInactive;
375
+ const delegationOverride = cognitiveRoutingController?.sessionDelegationOverride() ?? LOCAL_INHERIT;
376
+ const delegation = cognitiveRoutingState?.delegation ?? routing.delegation;
377
+ routingItems.push(
378
+ enabledItem,
379
+ {
365
380
  id: "freeflow.cognitiveRouting.profile",
366
- label: "Cognitive Routing",
367
- description: "Hold an enabled profile manually or release the hold for automatic model control.",
381
+ label: "Control",
382
+ description: "Automatic routing, or hold one profile until you release it.",
368
383
  kind: "enum",
369
- value: cognitiveRoutingProfile,
384
+ value: control,
370
385
  values: ["auto", ...manualProfiles],
371
386
  valueLabels: Object.fromEntries([
372
387
  ["auto", "automatic"],
373
- ...manualProfiles.map((profile) => [profile, `manual · ${profile}`]),
388
+ ...manualProfiles.map((profile) => [profile, `hold ${profile}`]),
374
389
  ]),
375
390
  valueDescriptions: Object.fromEntries([
376
- ["auto", "Release the manual hold and return to Coordinator reconciliation."],
377
- ...manualProfiles.map((profile) => [profile, `Hold the ${profile} profile until /freeflow profile auto.`]),
391
+ ["auto", "Release any hold; Coordinator places the work."],
392
+ ...manualProfiles.map((profile) => [
393
+ profile,
394
+ `Hold ${profile} when delegation enables it; release with /freeflow profile auto.`,
395
+ ]),
378
396
  ]),
379
- inactive: freeflowInactive || !state.cognitiveRouting.effective || cognitiveRoutingController === undefined,
380
- runtimeInactive: state.cognitiveRouting.blockingReason?.code === "runtime_disabled",
381
- displaySuffix: cognitiveRoutingState?.effective ? cognitiveRoutingProfile : "unavailable",
382
- }
383
- : undefined;
384
- const sessionProfiles = cognitiveRoutingController?.sessionProfileOverrides();
385
- const profileItems =
386
- state.cognitiveRouting && cognitiveRoutingController
387
- ? manualProfiles.map((name) =>
388
- cognitiveRoutingProfileItem({
389
- name,
390
- scope: "session",
391
- rawConfig: {},
392
- localConfig: {},
393
- capabilityState: state.cognitiveRouting,
394
- ctx,
395
- sessionProfiles,
396
- }),
397
- )
398
- : [];
399
- const cognitiveRoutingPresets = profileItems.length
400
- ? {
401
- id: "freeflow.cognitiveRouting.presets",
402
- label: "Cognitive Routing presets",
403
- description: "Temporarily choose complete model/effort pairs for profiles enabled in this Pi session.",
404
- kind: "group",
405
- value: profileItems.some((item) => item.value !== LOCAL_INHERIT && item.value !== undefined),
406
- displaySuffix: `${profileItems.filter((item) => item.value !== LOCAL_INHERIT && item.value !== undefined).length}/${profileItems.length} session overrides`,
407
- inactive: freeflowInactive || !state.cognitiveRouting?.effective || cognitiveRoutingController === undefined,
408
- children: profileItems,
397
+ inactive: routingInactive,
398
+ runtimeInactive: routing.blockingReason?.code === "runtime_disabled",
399
+ },
400
+ {
401
+ id: "freeflow.cognitiveRouting.delegation",
402
+ label: "Delegation",
403
+ description: "Which workers Coordinator may use in this session.",
404
+ kind: "enum",
405
+ value: delegationOverride,
406
+ values: [LOCAL_INHERIT, "executor", "helper", "both"],
407
+ valueLabels: {
408
+ inherit: "inherit",
409
+ executor: "Executor only",
410
+ helper: "Helper only",
411
+ both: "Helper and Executor",
412
+ },
413
+ valueDescriptions: {
414
+ inherit: `Use ${routing.delegation} from ${SOURCE_LABELS[cognitiveRoutingSettingsSource(routing.delegationSource)]}.`,
415
+ executor: "Coordinator delegates assignments to Executor.",
416
+ helper: "Coordinator delegates supporting assignments to Helper and keeps production implementation.",
417
+ both: "Coordinator chooses Helper or Executor for each assignment.",
418
+ },
419
+ configScope: "session",
420
+ effectiveValue: delegation,
421
+ effectiveSource:
422
+ delegationOverride === LOCAL_INHERIT ? cognitiveRoutingSettingsSource(routing.delegationSource) : "session",
423
+ inheritedValue: routing.delegation,
424
+ inheritedSource: cognitiveRoutingSettingsSource(routing.delegationSource),
425
+ inactive: routingInactive,
426
+ },
427
+ projectionItem,
428
+ );
429
+ if (cognitiveRoutingController) {
430
+ const sessionProfiles = cognitiveRoutingController.sessionProfileOverrides();
431
+ for (const name of manualProfiles) {
432
+ const item = cognitiveRoutingProfileItem({
433
+ name,
434
+ scope: "session",
435
+ rawConfig: {},
436
+ localConfig: {},
437
+ capabilityState: routing,
438
+ ctx,
439
+ sessionProfiles,
440
+ });
441
+ item.inactive ||= routingInactive;
442
+ routingItems.push(item);
409
443
  }
410
- : undefined;
411
- return [
444
+ }
445
+ }
446
+ const toolItem = sessionSwitch(
447
+ "freeflow.toolExecution.enabled",
448
+ "Enabled",
449
+ "Use Tool Execution in this session.",
450
+ "toolExecution.enabled",
451
+ state.toolExecution?.enabled ?? false,
452
+ DEFAULT_TOOL_EXECUTION_CONFIG.enabled,
453
+ );
454
+ const compactionItem = sessionSwitch(
455
+ "freeflow.compaction.enabled",
456
+ "Enabled",
457
+ "Use Freeflow compaction in this session.",
458
+ "compaction.enabled",
459
+ state.compaction?.enabled ?? DEFAULT_COMPACTION_ENABLED,
460
+ DEFAULT_COMPACTION_ENABLED,
461
+ );
462
+ const carryItem = sessionSwitch(
463
+ "freeflow.compaction.carry",
464
+ "Context reuse",
465
+ "Let the agent carry selected files and tool results into the next cycle.",
466
+ "compaction.carry",
467
+ state.compaction?.carry ?? DEFAULT_COMPACTION_CARRY,
468
+ DEFAULT_COMPACTION_CARRY,
469
+ );
470
+ const items = [
412
471
  freeflowItem,
413
- ...(cognitiveRoutingItem ? [cognitiveRoutingItem] : []),
414
- ...(cognitiveRoutingPresets ? [cognitiveRoutingPresets] : []),
415
- {
416
- id: "freeflow.session.reset",
417
- label: "Reset session overrides",
418
- description:
419
- "Clear Freeflow, Context Virtualization, Conversation History, and routing profile overrides for this Pi session.",
420
- kind: "enum",
421
- value: "available",
422
- values: ["reset"],
423
- valueLabels: { reset: "Reset all session overrides" },
424
- valueDescriptions: { reset: "Return every session setting and routing preset to its configured value." },
425
- format: () => "available",
426
- transient: true,
427
- },
428
- {
429
- id: "freeflow.context",
430
- label: "Freeflow Context",
431
- description: "Choose which context projection and conversation-history operations are available to the model.",
432
- kind: "group",
433
- value: contextVirtualizationItem.effectiveValue === true || conversationHistoryItem.effectiveValue === true,
434
- displaySuffix: `${[contextVirtualizationItem, conversationHistoryItem].filter((item) => item.effectiveValue === true).length}/2 enabled`,
435
- children: [contextVirtualizationItem, conversationHistoryItem],
436
- },
472
+ ...(routing
473
+ ? [
474
+ sectionItem(
475
+ "freeflow.cognitiveRouting",
476
+ "Cognitive Routing",
477
+ "Routing for this session.",
478
+ routingUnavailable
479
+ ? "unavailable"
480
+ : cognitiveRoutingState?.effective
481
+ ? `active · ${control === "auto" ? "automatic" : `hold ${control}`}`
482
+ : capabilityStatus(routing),
483
+ routingItems,
484
+ ),
485
+ ]
486
+ : []),
487
+ sectionItem(
488
+ "freeflow.toolExecution",
489
+ "Tool Execution",
490
+ "Tool Execution for this session.",
491
+ capabilityStatus(state.toolExecution),
492
+ [toolItem],
493
+ ),
494
+ sectionItem(
495
+ "freeflow.compaction",
496
+ "Compaction",
497
+ "Compaction for this session.",
498
+ capabilityStatus(state.compaction),
499
+ [compactionItem, carryItem],
500
+ ),
501
+ sectionItem("freeflow.session", "Session", "Session overrides.", "", [
502
+ {
503
+ id: "freeflow.session.reset",
504
+ label: "Reset overrides",
505
+ description:
506
+ "Clear every session override above, so the session follows personal and repository settings again.",
507
+ kind: "enum",
508
+ value: "available",
509
+ values: ["reset"],
510
+ valueLabels: { reset: "Reset all session overrides" },
511
+ valueDescriptions: {
512
+ reset: "Return every session setting, delegation, and routing preset to its configured value.",
513
+ },
514
+ // Counted when shown, so it follows changes made on this screen.
515
+ format: () => {
516
+ let count = 0;
517
+ walkSettingsItems(items, (candidate) => {
518
+ if (
519
+ candidate.configScope === "session" &&
520
+ candidate.value !== LOCAL_INHERIT &&
521
+ candidate.value !== undefined
522
+ )
523
+ count += 1;
524
+ });
525
+ return count ? `${count} active` : "none active";
526
+ },
527
+ transient: true,
528
+ },
529
+ ]),
437
530
  ];
531
+ return items;
438
532
  }
439
533
  const COGNITIVE_ROUTING_THINKING_LEVELS = ["off", "minimal", "low", "medium", "high", "xhigh", "max"];
440
534
  const COGNITIVE_ROUTING_EFFORT_DESCRIPTIONS = {
@@ -633,7 +727,7 @@ function cognitiveRoutingProfileItem(options) {
633
727
  const localValue = isCognitiveRoutingProfile(localProfile) ? localProfile : LOCAL_INHERIT;
634
728
  const value =
635
729
  options.scope === "session"
636
- ? (sessionProfile ?? LOCAL_INHERIT)
730
+ ? (sessionProfile ?? (configuredProfile ? LOCAL_INHERIT : undefined))
637
731
  : options.scope === "local"
638
732
  ? localValue
639
733
  : repositoryProfile;
@@ -665,7 +759,9 @@ function cognitiveRoutingProfileItem(options) {
665
759
  const inherits = current === LOCAL_INHERIT || (options.scope === "session" && current === undefined);
666
760
  return inherits
667
761
  ? options.scope === "session"
668
- ? "inherit configured preset"
762
+ ? configuredProfile
763
+ ? "inherit configured preset"
764
+ : "not configured"
669
765
  : "inherit repository preset"
670
766
  : cognitiveRoutingProfileDisplay(current);
671
767
  },
@@ -676,12 +772,17 @@ function cognitiveRoutingProfileItem(options) {
676
772
  inheritedSource: cognitiveRoutingSettingsSource(configuredSource ?? "builtin"),
677
773
  inactive,
678
774
  displaySuffix: cognitiveRoutingProfileDisplaySuffix(source, options.scope),
679
- wizard: () => createCognitiveRoutingProfileWizard(value, models, options.scope !== "repository", inheritChoice),
775
+ wizard: () =>
776
+ createCognitiveRoutingProfileWizard(
777
+ value,
778
+ models,
779
+ options.scope !== "repository" && (options.scope !== "session" || configuredProfile !== undefined),
780
+ inheritChoice,
781
+ ),
680
782
  };
681
783
  }
682
784
  function isCognitiveRoutingRuntimeAvailable(pi) {
683
785
  return (
684
- !isPiFlowHost(pi?.host) &&
685
786
  typeof pi?.appendEntry === "function" &&
686
787
  typeof pi?.setModel === "function" &&
687
788
  typeof pi?.setThinkingLevel === "function"
@@ -703,33 +804,7 @@ function freeflowItems(rawConfig, options = {}) {
703
804
  effectiveSource: sources.enabled,
704
805
  defaultValue: DEFAULT_FREEFLOW_ENABLED,
705
806
  });
706
- const contextVirtualizationItem = createScopedBooleanItem({
707
- scope,
708
- rawConfig,
709
- localConfig,
710
- id: "freeflow.contextVirtualization",
711
- label: "Context Virtualization",
712
- description: "Let the model archive consumed tool results from future context while preserving session history.",
713
- path: ["contextVirtualization"],
714
- effectiveValue: core.contextVirtualization,
715
- effectiveSource: sources.contextVirtualization,
716
- defaultValue: DEFAULT_CONTEXT_VIRTUALIZATION_ENABLED,
717
- });
718
- const conversationHistoryItem = createScopedBooleanItem({
719
- scope,
720
- rawConfig,
721
- localConfig,
722
- id: "freeflow.conversationHistory",
723
- label: "Conversation History",
724
- description: "Let the model search and retrieve hidden conversation history on the active branch.",
725
- path: ["conversationHistory"],
726
- effectiveValue: core.conversationHistory,
727
- effectiveSource: sources.conversationHistory,
728
- defaultValue: DEFAULT_CONVERSATION_HISTORY_ENABLED,
729
- });
730
807
  const freeflowInactive = !core.enabled;
731
- contextVirtualizationItem.inactive = freeflowInactive;
732
- conversationHistoryItem.inactive = freeflowInactive;
733
808
  const cognitiveRoutingState = options.cognitiveRouting;
734
809
  const cognitiveRoutingRuntimeDisabled = options.runtimeAvailable !== true;
735
810
  const cognitiveRoutingGroup = (() => {
@@ -814,26 +889,86 @@ function freeflowItems(rawConfig, options = {}) {
814
889
  ],
815
890
  };
816
891
  })();
817
- return [
818
- freeflowItem,
819
- ...(cognitiveRoutingGroup ? [cognitiveRoutingGroup] : []),
820
- {
821
- id: "freeflow.context",
822
- label: "Freeflow Context",
823
- description: "Choose which context projection and conversation-history operations are available to the model.",
824
- kind: "group",
825
- value: contextVirtualizationItem.effectiveValue === true || conversationHistoryItem.effectiveValue === true,
826
- displaySuffix: `${[contextVirtualizationItem, conversationHistoryItem].filter((item) => item.effectiveValue === true).length}/2 enabled`,
827
- children: [contextVirtualizationItem, conversationHistoryItem],
828
- },
829
- ];
892
+ const toolExecutionState = options.toolExecution;
893
+ const toolSource = (path) => {
894
+ if (getPath(localConfig, path) !== undefined) return "local";
895
+ if (getPath(rawConfig, path) !== undefined) return "repository";
896
+ return "builtin";
897
+ };
898
+ const toolExecutionEnabledItem = createScopedBooleanItem({
899
+ scope,
900
+ rawConfig,
901
+ localConfig,
902
+ id: "freeflow.toolExecution.enabled",
903
+ label: "Enabled",
904
+ description: "Enable the Tool Execution capability.",
905
+ path: ["toolExecution", "enabled"],
906
+ effectiveValue: toolExecutionState?.enabled ?? false,
907
+ effectiveSource: toolSource(["toolExecution", "enabled"]),
908
+ defaultValue: false,
909
+ });
910
+ const toolExecutionItems = [toolExecutionEnabledItem];
911
+ walkSettingsItems(toolExecutionItems, (item) => {
912
+ item.inactive = freeflowInactive;
913
+ });
914
+ const toolExecutionGroup = {
915
+ id: "freeflow.toolExecution",
916
+ label: "Tool Execution",
917
+ description: "Configure Freeflow's execution layer around Pi's tools.",
918
+ kind: "group",
919
+ value: toolExecutionState?.enabled ?? false,
920
+ inactive: freeflowInactive,
921
+ displaySuffix: toolExecutionState?.effective ? "active" : toolExecutionState?.enabled ? "inactive" : "disabled",
922
+ children: toolExecutionItems,
923
+ };
924
+ const compactionState = options.compaction;
925
+ const compactionEnabledItem = createScopedBooleanItem({
926
+ scope,
927
+ rawConfig,
928
+ localConfig,
929
+ id: "freeflow.compaction.enabled",
930
+ label: "Enabled",
931
+ description: "Warn before Pi's compaction limit and let the agent compact itself and recover in the same run.",
932
+ path: ["compaction", "enabled"],
933
+ effectiveValue: compactionState?.enabled ?? DEFAULT_COMPACTION_ENABLED,
934
+ effectiveSource: toolSource(["compaction", "enabled"]),
935
+ defaultValue: DEFAULT_COMPACTION_ENABLED,
936
+ });
937
+ const compactionCarryItem = createScopedBooleanItem({
938
+ scope,
939
+ rawConfig,
940
+ localConfig,
941
+ id: "freeflow.compaction.carry",
942
+ label: "Context reuse",
943
+ description: "Let the agent carry selected files and tool results into the next cycle.",
944
+ path: ["compaction", "carry"],
945
+ effectiveValue: compactionState?.carry ?? DEFAULT_COMPACTION_CARRY,
946
+ effectiveSource: toolSource(["compaction", "carry"]),
947
+ defaultValue: DEFAULT_COMPACTION_CARRY,
948
+ });
949
+ const compactionItems = [compactionEnabledItem, compactionCarryItem];
950
+ walkSettingsItems(compactionItems, (item) => {
951
+ item.inactive = freeflowInactive;
952
+ });
953
+ const compactionGroup = {
954
+ id: "freeflow.compaction",
955
+ label: "Compaction",
956
+ description: "Configure Freeflow compaction; Pi's own compaction stays as the fallback.",
957
+ kind: "group",
958
+ value: compactionState?.enabled ?? DEFAULT_COMPACTION_ENABLED,
959
+ inactive: freeflowInactive,
960
+ displaySuffix: compactionState?.effective ? "active" : compactionState?.enabled ? "inactive" : "disabled",
961
+ children: compactionItems,
962
+ };
963
+ return [freeflowItem, ...(cognitiveRoutingGroup ? [cognitiveRoutingGroup] : []), toolExecutionGroup, compactionGroup];
830
964
  }
831
965
  function pruneKnownDefaults(config) {
832
966
  const defaultPaths = [
833
967
  { path: ["enabled"], value: DEFAULT_FREEFLOW_ENABLED },
834
- { path: ["contextVirtualization"], value: DEFAULT_CONTEXT_VIRTUALIZATION_ENABLED },
835
- { path: ["conversationHistory"], value: DEFAULT_CONVERSATION_HISTORY_ENABLED },
836
968
  { path: ["cognitiveRouting", "delegation"], value: "executor" },
969
+ { path: ["toolExecution", "enabled"], value: DEFAULT_TOOL_EXECUTION_CONFIG.enabled },
970
+ { path: ["compaction", "enabled"], value: DEFAULT_COMPACTION_ENABLED },
971
+ { path: ["compaction", "carry"], value: DEFAULT_COMPACTION_CARRY },
837
972
  ];
838
973
  for (const item of defaultPaths) {
839
974
  if (valuesEqual(getPath(config, item.path), item.value)) {
@@ -909,25 +1044,34 @@ async function updateConfig(cwd, item, value, scope = item.configScope ?? "repos
909
1044
  function isCognitiveRoutingProfileItem(item) {
910
1045
  return ["coordinator", "helper", "executor"].some((profile) => item.id === `freeflow.cognitiveRouting.${profile}`);
911
1046
  }
912
- function valueForDisplay(item) {
913
- let value;
914
- if (isCognitiveRoutingProfileItem(item) && item.format) {
915
- value = item.format(effectiveItemValue(item));
916
- } else if (item.configScope === "local" || item.configScope === "session") {
917
- value = formatCoreValue(effectiveItemValue(item));
918
- } else if (item.kind === "boolean") {
919
- value = booleanValue(item.value);
920
- } else if (item.kind === "group") {
921
- const status = typeof item.value === "boolean" ? booleanValue(item.value) : String(item.value ?? "");
922
- const count = item.children?.length ?? 0;
923
- value = count > 0 ? `${status} (${count})` : status;
924
- } else if (item.format) {
925
- value = item.format(item.value);
926
- } else {
927
- value = String(item.value ?? "");
1047
+ /** A value as the settings screen shows it, e.g. "enabled · personal" or "inherit → enabled (repository)". */
1048
+ function formatSettingValue(item, value) {
1049
+ if (isCognitiveRoutingProfileItem(item)) return cognitiveRoutingProfileDisplay(value);
1050
+ if (typeof value === "boolean") return booleanValue(value);
1051
+ const key = String(value ?? "");
1052
+ return item.valueLabels?.[key] && key !== LOCAL_INHERIT ? item.valueLabels[key] : formatCoreValue(value);
1053
+ }
1054
+ function displayValue(item) {
1055
+ if (item.kind === "group") return item.displaySuffix ?? "";
1056
+ if (item.transient && item.format) return item.format(item.value);
1057
+ if (item.id === "freeflow.cognitiveRouting.profile")
1058
+ return item.valueLabels?.[String(item.value)] ?? String(item.value);
1059
+ if (item.configScope === "local" || item.configScope === "session") {
1060
+ const inherits = item.value === LOCAL_INHERIT || item.value === undefined;
1061
+ if (inherits) {
1062
+ if (item.inheritedValue === undefined) return "not configured";
1063
+ return `inherit → ${formatSettingValue(item, item.inheritedValue)} (${SOURCE_LABELS[item.inheritedSource ?? "builtin"]})`;
1064
+ }
1065
+ return `${formatSettingValue(item, effectiveItemValue(item))} · ${SOURCE_LABELS[item.configScope]}`;
928
1066
  }
929
- return item.displaySuffix ? `${value} ${item.displaySuffix}` : value;
1067
+ const value = item.value === undefined && isCognitiveRoutingProfileItem(item) ? undefined : item.value;
1068
+ const shown = formatSettingValue(item, value);
1069
+ if (item.localOverrideValue !== undefined && !valuesEqual(item.localOverrideValue, item.value))
1070
+ return `${shown} · personal override: ${formatSettingValue(item, item.localOverrideValue)}`;
1071
+ return item.effectiveSource === "builtin" && item.defaultValue !== undefined ? `${shown} · default` : shown;
930
1072
  }
1073
+ /** Small choices change in place; holding a profile switches models and a reset clears everything, so those ask. */
1074
+ const PICKED_CHOICES = new Set(["freeflow.cognitiveRouting.profile", "freeflow.session.reset"]);
931
1075
  function walkSettingsItems(items, visitor) {
932
1076
  for (const item of items) {
933
1077
  visitor(item);
@@ -973,6 +1117,24 @@ function refreshSettingsDerivedState(items) {
973
1117
  contextGroup.value = enabledCount > 0;
974
1118
  contextGroup.displaySuffix = `${enabledCount}/${contextGroup.children.length} enabled`;
975
1119
  }
1120
+ const toolExecutionGroup = findSettingsItem(items, "freeflow.toolExecution");
1121
+ const toolExecutionEnabled = findSettingsItem(items, "freeflow.toolExecution.enabled");
1122
+ if (toolExecutionGroup && toolExecutionEnabled) {
1123
+ const enabled = !freeflowInactive && effectiveItemValue(toolExecutionEnabled) === true;
1124
+ toolExecutionGroup.value = effectiveItemValue(toolExecutionEnabled) === true;
1125
+ toolExecutionGroup.displaySuffix = enabled
1126
+ ? "active"
1127
+ : effectiveItemValue(toolExecutionEnabled) === true
1128
+ ? "inactive"
1129
+ : "disabled";
1130
+ }
1131
+ const compactionGroup = findSettingsItem(items, "freeflow.compaction");
1132
+ const compactionEnabled = findSettingsItem(items, "freeflow.compaction.enabled");
1133
+ if (compactionGroup && compactionEnabled) {
1134
+ const on = effectiveItemValue(compactionEnabled) === true;
1135
+ compactionGroup.value = on;
1136
+ compactionGroup.displaySuffix = on && !freeflowInactive ? "active" : on ? "inactive" : "disabled";
1137
+ }
976
1138
  walkSettingsItems(items, (candidate) => {
977
1139
  if (candidate.id === "freeflow.session.reset") {
978
1140
  candidate.inactive = false;
@@ -989,8 +1151,8 @@ function refreshSettingsDerivedState(items) {
989
1151
  function settingsChoices(item) {
990
1152
  if (item.kind === "boolean") {
991
1153
  return [
992
- { key: "true", value: true, label: "enabled", description: "Enable this setting." },
993
- { key: "false", value: false, label: "disabled", description: "Disable this setting." },
1154
+ { key: "true", value: true, label: "enabled" },
1155
+ { key: "false", value: false, label: "disabled" },
994
1156
  ];
995
1157
  }
996
1158
  if (item.kind !== "enum") return undefined;
@@ -1013,41 +1175,51 @@ function resetSessionItemState(items) {
1013
1175
  });
1014
1176
  }
1015
1177
  function settingsEntries(items, rootItems, onChange) {
1016
- return items.map((item) => ({
1017
- id: item.id,
1018
- label: item.label,
1019
- description: item.description,
1020
- currentValue: () => valueForDisplay(item),
1021
- inactive: () => item.inactive === true,
1022
- currentChoiceKey: () => String(item.value),
1023
- choices: settingsChoices(item),
1024
- children: item.children ? () => settingsEntries(item.children, rootItems, onChange) : undefined,
1025
- wizard: item.wizard ? () => item.wizard() : undefined,
1026
- edit:
1027
- !item.children && !["boolean", "enum", "group"].includes(item.kind)
1028
- ? {
1029
- initialValue: () => valueForDisplay(item),
1030
- parse: (text) => (item.parse ? item.parse(text) : text),
1031
- }
1032
- : undefined,
1033
- commit:
1034
- item.kind === "group"
1035
- ? undefined
1036
- : async (value) => {
1037
- if (valuesEqual(item.value, value)) return { changed: false, reloadRequired: false };
1038
- const outcome = await onChange(item, value);
1039
- if (outcome.changed) {
1040
- updateScopedItemState(item, value);
1041
- if (item.id === "freeflow.session.reset") {
1042
- resetSessionItemState(rootItems);
1043
- } else if (!item.transient) {
1044
- item.value = value;
1178
+ return items.flatMap((item) => {
1179
+ if (item.kind === "group" && item.children) {
1180
+ const section = {
1181
+ id: item.id,
1182
+ label: item.label,
1183
+ description: item.description,
1184
+ section: true,
1185
+ currentValue: () => displayValue(item),
1186
+ inactive: () => item.inactive === true,
1187
+ };
1188
+ return [section, ...settingsEntries(item.children, rootItems, onChange)];
1189
+ }
1190
+ const choices = settingsChoices(item);
1191
+ return [
1192
+ {
1193
+ id: item.id,
1194
+ label: item.label,
1195
+ description: item.description,
1196
+ currentValue: () => displayValue(item),
1197
+ inactive: () => item.inactive === true,
1198
+ currentChoiceKey: () => String(item.value),
1199
+ choices,
1200
+ cycle: Boolean(choices?.length) && !PICKED_CHOICES.has(item.id),
1201
+ wizard: item.wizard ? () => item.wizard() : undefined,
1202
+ edit:
1203
+ !["boolean", "enum", "group"].includes(item.kind) && !item.wizard
1204
+ ? {
1205
+ initialValue: () => item.editInitialValue?.() ?? displayValue(item),
1206
+ parse: (text) => (item.parse ? item.parse(text) : text),
1045
1207
  }
1046
- refreshSettingsDerivedState(rootItems);
1047
- }
1048
- return outcome;
1049
- },
1050
- }));
1208
+ : undefined,
1209
+ commit: async (value) => {
1210
+ if (valuesEqual(item.value, value)) return { changed: false, reloadRequired: false };
1211
+ const outcome = await onChange(item, value);
1212
+ if (outcome.changed) {
1213
+ updateScopedItemState(item, value);
1214
+ if (item.id === "freeflow.session.reset") resetSessionItemState(rootItems);
1215
+ else if (!item.transient) item.value = value;
1216
+ refreshSettingsDerivedState(rootItems);
1217
+ }
1218
+ return outcome;
1219
+ },
1220
+ },
1221
+ ];
1222
+ });
1051
1223
  }
1052
1224
  async function openSettings(options) {
1053
1225
  if (typeof options.ctx?.ui?.custom !== "function") {
@@ -1057,16 +1229,12 @@ async function openSettings(options) {
1057
1229
  );
1058
1230
  return { changed: false, configChanged: false, failed: false };
1059
1231
  }
1060
- const entries = settingsEntries(options.items, options.items, options.onChange);
1061
- const initialChoice = options.initialChoice
1062
- ? entries.find((entry) => entry.id === options.initialChoice?.id)
1063
- : undefined;
1064
1232
  let component;
1065
1233
  await options.ctx.ui.custom((tui, theme, _keybindings, done) => {
1066
1234
  component = new PiSettingsComponent({
1067
1235
  title: options.title,
1068
- entries,
1069
- initialChoice,
1236
+ scopes: options.scopes,
1237
+ initialScope: options.initialScope,
1070
1238
  theme: theme ?? {},
1071
1239
  requestRender: () => tui.requestRender(),
1072
1240
  notify: (message, level) => options.ctx?.ui?.notify?.(message, level),
@@ -1077,33 +1245,24 @@ async function openSettings(options) {
1077
1245
  await component?.waitForWrites();
1078
1246
  return component?.sessionResult() ?? { changed: false, configChanged: false, failed: false };
1079
1247
  }
1080
- function freeflowStatusText(state, cognitiveRoutingController) {
1081
- if (!state.configured) {
1082
- return state.configExists
1083
- ? `Freeflow: inactive (invalid config: ${state.parseError ?? "unknown parse error"}); run /setup-freeflow or fix .freeflow/config.json`
1084
- : "Freeflow: inactive (repo not set up); run /setup-freeflow";
1085
- }
1086
- const sessionSuffix = (source) => (source === "session" ? " (session override)" : "");
1087
- const routingState = cognitiveRoutingController?.state();
1088
- const cognitiveRouting = state.cognitiveRouting;
1089
- const cognitiveRoutingStatus = cognitiveRouting
1090
- ? cognitiveRouting.blockingReason?.code === "runtime_disabled"
1091
- ? "unavailable (host unsupported)"
1092
- : cognitiveRouting.effective
1093
- ? routingState?.effective
1094
- ? `active (${routingState.activeProfile ?? "unknown"}, ${routingState.controlMode})`
1095
- : "effective (inactive)"
1096
- : cognitiveRouting.enabled
1097
- ? `blocked (${cognitiveRouting.blockingReason.code})`
1098
- : "disabled"
1099
- : undefined;
1100
- const contextEnabled = state.contextVirtualization?.effective || state.conversationHistory?.effective;
1101
- return [
1102
- `Freeflow: ${state.enabled ? "enabled" : "disabled"}${sessionSuffix(state.configSources.enabled)}`,
1103
- `context: ${contextEnabled ? "enabled" : "disabled"} (virtualization ${state.contextVirtualization?.effective ? "enabled" : "disabled"}, history ${state.conversationHistory?.effective ? "enabled" : "disabled"})`,
1104
- ...(cognitiveRoutingStatus ? [`cognitive routing: ${cognitiveRoutingStatus}`] : []),
1105
- ].join("; ");
1106
- }
1248
+ /** Where each scope's values live and whom they affect, shown under the tabs. */
1249
+ const SCOPES = [
1250
+ {
1251
+ id: "session",
1252
+ label: "Session",
1253
+ summary: "This Pi session only, not saved to a file. Overrides personal and repository values.",
1254
+ },
1255
+ {
1256
+ id: "local",
1257
+ label: "Personal",
1258
+ summary: ".freeflow/local.json: only you, in this checkout. Overrides the repository.",
1259
+ },
1260
+ {
1261
+ id: "repository",
1262
+ label: "Repository",
1263
+ summary: ".freeflow/config.json: shared with everyone who uses this repository.",
1264
+ },
1265
+ ];
1107
1266
  const NON_TUI_SETTINGS_GUIDANCE =
1108
1267
  "Freeflow settings require Pi TUI mode. Use /freeflow status to inspect current state; supported non-TUI changes are /freeflow enable and /freeflow disable.";
1109
1268
  function ensureSettingsIdle(ctx) {
@@ -1151,7 +1310,7 @@ async function finalizeSessionSettings(session, ctx, afterChange) {
1151
1310
  await ctx.reload();
1152
1311
  return true;
1153
1312
  }
1154
- export async function handleFreeflowCommand(args, ctx, afterChange, pi, cognitiveRoutingController) {
1313
+ export async function handleFreeflowCommand(args, ctx, afterChange, pi, cognitiveRoutingController, diagnostics) {
1155
1314
  const input = (args ?? "settings").trim().toLowerCase() || "settings";
1156
1315
  const [action, ...rest] = input.split(/\s+/);
1157
1316
  const actionValue = rest.join(" ");
@@ -1162,14 +1321,11 @@ export async function handleFreeflowCommand(args, ctx, afterChange, pi, cognitiv
1162
1321
  if (nonTui && settingsSelector) {
1163
1322
  return nonTuiGuidance(ctx, NON_TUI_SETTINGS_GUIDANCE);
1164
1323
  }
1165
- const [layers, state] = await Promise.all([
1166
- readFreeflowConfigLayers(ctx.cwd),
1167
- readCapabilityState(ctx.cwd, ctx, pi?.host),
1168
- ]);
1324
+ const [layers, state] = await Promise.all([readFreeflowConfigLayers(ctx.cwd), readCapabilityState(ctx.cwd, ctx)]);
1169
1325
  const configState = layers.repository;
1170
1326
  const raw = configState.valid ? configState.parsed : {};
1171
1327
  if (action === "status") {
1172
- ctx.ui.notify(freeflowStatusText(state, cognitiveRoutingController), "info");
1328
+ ctx.ui.notify(freeflowStatusText(state, cognitiveRoutingController, diagnostics?.status()), "info");
1173
1329
  return { changed: false, reloaded: false };
1174
1330
  }
1175
1331
  if (!configState.valid) {
@@ -1190,6 +1346,8 @@ export async function handleFreeflowCommand(args, ctx, afterChange, pi, cognitiv
1190
1346
  scope: "repository",
1191
1347
  layers,
1192
1348
  cognitiveRouting: state.cognitiveRouting,
1349
+ toolExecution: state.toolExecution,
1350
+ compaction: state.compaction,
1193
1351
  ctx,
1194
1352
  runtimeAvailable: isCognitiveRoutingRuntimeAvailable(pi),
1195
1353
  }).find((candidate) => candidate.id === "freeflow.enabled");
@@ -1210,11 +1368,11 @@ export async function handleFreeflowCommand(args, ctx, afterChange, pi, cognitiv
1210
1368
  );
1211
1369
  return { changed: false, reloaded: false, error: "invalid_action" };
1212
1370
  }
1213
- let settingsScope = "local";
1371
+ let initialScope = "local";
1214
1372
  if (actionValue === "session") {
1215
- settingsScope = "session";
1373
+ initialScope = "session";
1216
1374
  } else if (["repo", "repository", "shared"].includes(actionValue)) {
1217
- settingsScope = "repository";
1375
+ initialScope = "repository";
1218
1376
  } else if (actionValue && !["local", "personal"].includes(actionValue)) {
1219
1377
  ctx.ui.notify(
1220
1378
  "Usage: /freeflow settings, /freeflow settings session, /freeflow settings local, or /freeflow settings repo",
@@ -1223,138 +1381,190 @@ export async function handleFreeflowCommand(args, ctx, afterChange, pi, cognitiv
1223
1381
  return { changed: false, reloaded: false, error: "invalid_scope" };
1224
1382
  }
1225
1383
  if (!ensureSettingsIdle(ctx)) return { changed: false, reloaded: false, error: "busy" };
1226
- let reconcileCognitiveRouting = settingsScope === "session";
1227
- const items =
1228
- settingsScope === "session"
1229
- ? sessionFreeflowItems(state, cognitiveRoutingController, ctx)
1230
- : freeflowItems(raw, {
1231
- scope: settingsScope,
1232
- layers,
1233
- cognitiveRouting: state.cognitiveRouting,
1234
- ctx,
1235
- runtimeAvailable: isCognitiveRoutingRuntimeAvailable(pi),
1236
- });
1237
- const session = await openSettings({
1238
- title:
1239
- settingsScope === "session"
1240
- ? "Freeflow Settings · Session overrides"
1241
- : settingsScope === "local"
1242
- ? "Freeflow Settings · Personal overrides"
1243
- : "Freeflow Repository Settings · modifies .freeflow/config.json",
1244
- items,
1245
- ctx,
1246
- onChange: async (item, value) => {
1247
- if (!ensureSettingsIdle(ctx)) return { changed: false, reloadRequired: false };
1248
- if (!isCognitiveRoutingRuntimeAvailable(pi) && item.id.startsWith("freeflow.cognitiveRouting.")) {
1249
- ctx.ui.notify("Cognitive Routing is unavailable because this host lacks model-state controls.", "warning");
1384
+ // Each scope is read fresh when shown, so a change in one scope shows up in the others' inherited values.
1385
+ const touched = new Set();
1386
+ const loadItems = async (scope) => {
1387
+ const [layersNow, stateNow] = await Promise.all([
1388
+ readFreeflowConfigLayers(ctx.cwd),
1389
+ readCapabilityState(ctx.cwd, ctx),
1390
+ ]);
1391
+ const rawNow = layersNow.repository.valid ? layersNow.repository.parsed : {};
1392
+ if (scope === "session") {
1393
+ const localNow = layersNow.local.valid && isRecord(layersNow.local.parsed) ? layersNow.local.parsed : {};
1394
+ return sessionFreeflowItems(stateNow, cognitiveRoutingController, ctx, rawNow, localNow);
1395
+ }
1396
+ return freeflowItems(rawNow, {
1397
+ scope,
1398
+ layers: layersNow,
1399
+ cognitiveRouting: stateNow.cognitiveRouting,
1400
+ toolExecution: stateNow.toolExecution,
1401
+ compaction: stateNow.compaction,
1402
+ ctx,
1403
+ runtimeAvailable: isCognitiveRoutingRuntimeAvailable(pi),
1404
+ });
1405
+ };
1406
+ const scopes = SCOPES.map((scope) => ({
1407
+ ...scope,
1408
+ load: async () => {
1409
+ const items = await loadItems(scope.id);
1410
+ return settingsEntries(items, items, async (item, value) => {
1411
+ const outcome = await changeSetting(scope.id, item, value);
1412
+ if (outcome.changed) touched.add(scope.id);
1413
+ return outcome;
1414
+ });
1415
+ },
1416
+ }));
1417
+ const changeSetting = async (settingsScope, item, value) => {
1418
+ if (!ensureSettingsIdle(ctx)) return { changed: false, reloadRequired: false };
1419
+ if (!isCognitiveRoutingRuntimeAvailable(pi) && item.id.startsWith("freeflow.cognitiveRouting.")) {
1420
+ ctx.ui.notify("Cognitive Routing is unavailable because this host lacks model-state controls.", "warning");
1421
+ return { changed: false, reloadRequired: false };
1422
+ }
1423
+ if (item.id === "freeflow.cognitiveRouting.profile") {
1424
+ if (!cognitiveRoutingController) {
1425
+ ctx.ui.notify("Cognitive Routing is unavailable for this session.", "warning");
1250
1426
  return { changed: false, reloadRequired: false };
1251
1427
  }
1252
- if (item.id === "freeflow.cognitiveRouting.profile") {
1253
- if (!cognitiveRoutingController) {
1254
- ctx.ui.notify("Cognitive Routing is unavailable for this session.", "warning");
1255
- return { changed: false, reloadRequired: false };
1256
- }
1257
- const result =
1258
- value === "auto"
1259
- ? await cognitiveRoutingController.setAutomaticControl("profile-settings")
1260
- : await cognitiveRoutingController.setManualProfile(value, "profile-settings");
1261
- if ((result.status !== "automatic" && result.status !== "active") || result.reason) {
1262
- ctx.ui.notify(
1263
- `Cognitive Routing settings could not be applied: ${result.reason ?? result.status}.`,
1264
- "warning",
1265
- );
1266
- return { changed: false, reloadRequired: false };
1267
- }
1268
- await afterChange(false);
1269
- return { changed: true, reloadRequired: false };
1428
+ const result =
1429
+ value === "auto"
1430
+ ? await cognitiveRoutingController.setAutomaticControl("profile-settings")
1431
+ : await cognitiveRoutingController.setManualProfile(value, "profile-settings");
1432
+ if ((result.status !== "automatic" && result.status !== "active") || result.reason) {
1433
+ ctx.ui.notify(`Cognitive Routing settings could not be applied: ${result.reason ?? result.status}.`, "warning");
1434
+ return { changed: false, reloadRequired: false };
1270
1435
  }
1271
- if (item.id === "freeflow.session.reset") {
1272
- const routingResult = cognitiveRoutingController
1273
- ? await cognitiveRoutingController.resetSessionProfileOverrides("Reset session overrides")
1274
- : { status: "unchanged" };
1275
- if (routingResult.status === "blocked" || routingResult.reason) {
1276
- ctx.ui.notify(
1277
- `Cognitive Routing session presets could not be reset: ${routingResult.reason ?? routingResult.status}.`,
1278
- "warning",
1279
- );
1280
- return { changed: false, reloadRequired: false };
1281
- }
1282
- try {
1283
- const result = await resetSessionOverrides(ctx, pi);
1284
- return {
1285
- changed: result.changed || routingResult.status !== "unchanged",
1286
- reloadRequired: result.reloadRequired,
1287
- };
1288
- } catch (error) {
1289
- ctx.ui.notify(
1290
- `Routing presets were reset, but core/context session overrides could not be reset: ${error instanceof Error ? error.message : String(error)}`,
1291
- "warning",
1292
- );
1293
- return {
1294
- changed: routingResult.status !== "unchanged",
1295
- reloadRequired: false,
1296
- };
1297
- }
1436
+ await afterChange(false);
1437
+ return { changed: true, reloadRequired: false };
1438
+ }
1439
+ if (settingsScope === "session" && item.id === "freeflow.cognitiveRouting.delegation") {
1440
+ if (!cognitiveRoutingController) return { changed: false, reloadRequired: false };
1441
+ const result = await cognitiveRoutingController.setSessionDelegationOverride(
1442
+ value === LOCAL_INHERIT ? null : value,
1443
+ "Session delegation settings",
1444
+ );
1445
+ if (result.status === "blocked" || result.reason) {
1446
+ ctx.ui.notify(
1447
+ `Cognitive Routing delegation mode could not be applied: ${result.reason ?? result.status}.`,
1448
+ "warning",
1449
+ );
1450
+ return { changed: false, reloadRequired: false };
1298
1451
  }
1299
- if (item.configScope === "session" && isCognitiveRoutingProfileItem(item)) {
1300
- if (!cognitiveRoutingController) {
1301
- ctx.ui.notify("Cognitive Routing is unavailable for this session.", "warning");
1302
- return { changed: false, reloadRequired: false };
1303
- }
1304
- const profile = item.id.split(".").at(-1);
1305
- const override =
1306
- value === undefined || value === LOCAL_INHERIT
1307
- ? null
1308
- : {
1309
- provider: value.provider,
1310
- modelId: value.model,
1311
- thinking: value.thinking,
1312
- };
1313
- const result = await cognitiveRoutingController.setSessionProfileOverride(
1314
- profile,
1315
- override,
1316
- "Session profile settings",
1452
+ await afterChange(false);
1453
+ return { changed: result.status !== "unchanged", reloadRequired: false };
1454
+ }
1455
+ if (item.id === "freeflow.session.reset") {
1456
+ const previousDelegation = cognitiveRoutingController?.sessionDelegationOverride() ?? null;
1457
+ const delegationResult = cognitiveRoutingController
1458
+ ? await cognitiveRoutingController.setSessionDelegationOverride(null, "Reset session delegation mode")
1459
+ : { status: "unchanged" };
1460
+ if (delegationResult.status === "blocked" || delegationResult.reason) {
1461
+ ctx.ui.notify(
1462
+ `Cognitive Routing session mode could not be reset: ${delegationResult.reason ?? delegationResult.status}.`,
1463
+ "warning",
1317
1464
  );
1318
- if (result.status === "blocked" || result.reason) {
1319
- ctx.ui.notify(
1320
- `Cognitive Routing session preset could not be applied: ${result.reason ?? result.status}.`,
1321
- "warning",
1465
+ return { changed: false, reloadRequired: false };
1466
+ }
1467
+ const routingResult = cognitiveRoutingController
1468
+ ? await cognitiveRoutingController.resetSessionProfileOverrides("Reset session overrides")
1469
+ : { status: "unchanged" };
1470
+ if (routingResult.status === "blocked" || routingResult.reason) {
1471
+ if (delegationResult.status !== "unchanged") {
1472
+ const restored = await cognitiveRoutingController.setSessionDelegationOverride(
1473
+ previousDelegation,
1474
+ "Session reset failed; prior delegation mode retained",
1322
1475
  );
1323
- return { changed: false, reloadRequired: false };
1476
+ if (restored.status === "blocked" || restored.reason) {
1477
+ await afterChange(false);
1478
+ ctx.ui.notify(
1479
+ `Session mode was reset but could not be restored: ${restored.reason ?? restored.status}. Reopen settings to see the current mode.`,
1480
+ "warning",
1481
+ );
1482
+ }
1324
1483
  }
1325
- await afterChange(false);
1326
- return { changed: result.status !== "unchanged", reloadRequired: false };
1484
+ ctx.ui.notify(
1485
+ `Cognitive Routing session presets could not be reset: ${routingResult.reason ?? routingResult.status}.`,
1486
+ "warning",
1487
+ );
1488
+ return { changed: false, reloadRequired: false };
1327
1489
  }
1328
- if (item.configScope === "session") {
1329
- const keyById = {
1330
- "freeflow.enabled": "enabled",
1331
- "freeflow.contextVirtualization": "contextVirtualization",
1332
- "freeflow.conversationHistory": "conversationHistory",
1490
+ try {
1491
+ const result = await resetSessionOverrides(ctx, pi);
1492
+ return {
1493
+ changed: result.changed || routingResult.status !== "unchanged" || delegationResult.status !== "unchanged",
1494
+ reloadRequired: result.reloadRequired,
1495
+ };
1496
+ } catch (error) {
1497
+ ctx.ui.notify(
1498
+ `Routing presets were reset, but core/context session overrides could not be reset: ${error instanceof Error ? error.message : String(error)}`,
1499
+ "warning",
1500
+ );
1501
+ return {
1502
+ changed: routingResult.status !== "unchanged" || delegationResult.status !== "unchanged",
1503
+ reloadRequired: false,
1333
1504
  };
1334
- const key = keyById[item.id];
1335
- const override = value === LOCAL_INHERIT ? null : value === "true";
1336
- const result = await setSessionCoreOverride(key, override, ctx, pi);
1337
- return { changed: result.changed, reloadRequired: result.reloadRequired === true };
1338
1505
  }
1339
- await updateConfig(ctx.cwd, item, value, item.configScope ?? "repository");
1340
- if (!item.id.startsWith("freeflow.cognitiveRouting.sessionStart.")) {
1341
- reconcileCognitiveRouting = true;
1506
+ }
1507
+ if (item.configScope === "session" && isCognitiveRoutingProfileItem(item)) {
1508
+ if (!cognitiveRoutingController) {
1509
+ ctx.ui.notify("Cognitive Routing is unavailable for this session.", "warning");
1510
+ return { changed: false, reloadRequired: false };
1342
1511
  }
1343
- return { changed: true, reloadRequired: true };
1344
- },
1345
- });
1346
- if (settingsScope === "session") {
1347
- const reloaded = await finalizeSessionSettings(session, ctx, afterChange);
1512
+ const profile = item.id.split(".").at(-1);
1513
+ const override =
1514
+ value === undefined || value === LOCAL_INHERIT
1515
+ ? null
1516
+ : {
1517
+ provider: value.provider,
1518
+ modelId: value.model,
1519
+ thinking: value.thinking,
1520
+ };
1521
+ const result = await cognitiveRoutingController.setSessionProfileOverride(
1522
+ profile,
1523
+ override,
1524
+ "Session profile settings",
1525
+ );
1526
+ if (result.status === "blocked" || result.reason) {
1527
+ ctx.ui.notify(
1528
+ `Cognitive Routing session preset could not be applied: ${result.reason ?? result.status}.`,
1529
+ "warning",
1530
+ );
1531
+ return { changed: false, reloadRequired: false };
1532
+ }
1533
+ await afterChange(false);
1534
+ return { changed: result.status !== "unchanged", reloadRequired: false };
1535
+ }
1536
+ if (item.configScope === "session") {
1537
+ const key = item.path.join(".");
1538
+ const override = value === LOCAL_INHERIT ? null : value === "true";
1539
+ const result = await setSessionCoreOverride(key, override, ctx, pi);
1540
+ return { changed: result.changed, reloadRequired: result.reloadRequired === true };
1541
+ }
1542
+ await updateConfig(ctx.cwd, item, value, item.configScope ?? "repository");
1543
+ return { changed: true, reloadRequired: true };
1544
+ };
1545
+ // The first scope opens loaded; the others load when Tab reaches them.
1546
+ const first = scopes.find((scope) => scope.id === initialScope);
1547
+ const firstEntries = await first.load();
1548
+ let preloaded = true;
1549
+ const firstLoad = first.load;
1550
+ first.load = () => {
1551
+ if (!preloaded) return firstLoad();
1552
+ preloaded = false;
1553
+ return firstEntries;
1554
+ };
1555
+ const session = await openSettings({ title: "Freeflow settings", scopes, initialScope, ctx });
1556
+ // A file write reloads Freeflow; session-only changes reload only when they switch a capability.
1557
+ if (touched.has("local") || touched.has("repository")) {
1558
+ const reloaded = await finalizeSettingsSession(
1559
+ session,
1560
+ ctx,
1561
+ afterChange,
1562
+ "Freeflow settings saved. Reloading Freeflow runtime...",
1563
+ "Run /reload for Freeflow changes to fully apply.",
1564
+ { reconcileCognitiveRouting: true },
1565
+ );
1348
1566
  return { changed: session.changed, reloaded, error: session.failed ? "write_failed" : undefined };
1349
1567
  }
1350
- const savedTarget = settingsScope === "local" ? "personal overrides" : "repository settings";
1351
- const reloaded = await finalizeSettingsSession(
1352
- session,
1353
- ctx,
1354
- afterChange,
1355
- `Freeflow ${savedTarget} saved. Reloading Freeflow runtime...`,
1356
- "Run /reload for Freeflow changes to fully apply.",
1357
- { reconcileCognitiveRouting },
1358
- );
1568
+ const reloaded = await finalizeSessionSettings(session, ctx, afterChange);
1359
1569
  return { changed: session.changed, reloaded, error: session.failed ? "write_failed" : undefined };
1360
1570
  }