@hassangameryt/freeflow 0.7.2 → 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 (267) 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 +151 -136
  7. package/capabilities/cognitive-routing/SKILL.md +110 -96
  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 +40 -21
  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/cognitive-routing/event-store.js +280 -0
  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/projection.js +503 -0
  29. package/pi-extension/dist/cognitive-routing/provenance.js +81 -0
  30. package/pi-extension/dist/{cognitive-routing-v2 → cognitive-routing}/render.js +31 -10
  31. package/pi-extension/dist/cognitive-routing/runtime.js +396 -0
  32. package/pi-extension/dist/cognitive-routing/schemas.js +209 -0
  33. package/pi-extension/dist/cognitive-routing/session.js +221 -0
  34. package/pi-extension/dist/cognitive-routing/sources.js +291 -0
  35. package/pi-extension/dist/cognitive-routing/state.js +487 -0
  36. package/pi-extension/dist/cognitive-routing/status.js +80 -0
  37. package/pi-extension/dist/cognitive-routing/tools.js +60 -0
  38. package/pi-extension/dist/{cognitive-routing-v2 → cognitive-routing}/types.js +10 -12
  39. package/pi-extension/dist/compaction/carry.js +191 -0
  40. package/pi-extension/dist/compaction/config.js +22 -0
  41. package/pi-extension/dist/compaction/controller.js +471 -0
  42. package/pi-extension/dist/compaction/harness.js +77 -0
  43. package/pi-extension/dist/compaction/results.js +158 -0
  44. package/pi-extension/dist/compaction/thresholds.js +35 -0
  45. package/pi-extension/dist/compaction/tool.js +57 -0
  46. package/pi-extension/dist/host/branch.js +13 -0
  47. package/pi-extension/dist/host/canonical.js +12 -0
  48. package/pi-extension/dist/host/catalog.js +77 -0
  49. package/pi-extension/dist/host/config.js +374 -0
  50. package/pi-extension/dist/host/projection-tags.js +60 -0
  51. package/pi-extension/dist/host/prompts.js +82 -0
  52. package/pi-extension/dist/{session-sources → host}/read-only-session.js +112 -1
  53. package/pi-extension/dist/host/request-history.js +298 -0
  54. package/pi-extension/dist/host/runtime-state.js +189 -0
  55. package/pi-extension/dist/host/settings/freeflow-command.js +1570 -0
  56. package/pi-extension/dist/{settings/settings-tui.js → host/settings/settings-view.js} +186 -73
  57. package/pi-extension/dist/host/staging.js +28 -0
  58. package/pi-extension/dist/host/status.js +101 -0
  59. package/pi-extension/dist/index.js +441 -170
  60. package/pi-extension/dist/provider-support/cache/anchor.js +21 -0
  61. package/pi-extension/dist/provider-support/cache/anthropic.js +102 -0
  62. package/pi-extension/dist/provider-support/cache/health.js +73 -0
  63. package/pi-extension/dist/provider-support/cache/index.js +58 -0
  64. package/pi-extension/dist/provider-support/cache/keepalive.js +210 -0
  65. package/pi-extension/dist/provider-support/cache/monitor.js +17 -0
  66. package/pi-extension/dist/provider-support/cache/openai-responses.js +48 -0
  67. package/pi-extension/dist/provider-support/effort.js +13 -0
  68. package/pi-extension/dist/provider-support/index.js +45 -0
  69. package/pi-extension/dist/provider-support/openai/adapter.js +127 -0
  70. package/pi-extension/dist/provider-support/openai/history.js +127 -0
  71. package/pi-extension/dist/provider-support/openai/index.js +15 -0
  72. package/pi-extension/dist/provider-support/openai/session-state.js +105 -0
  73. package/pi-extension/dist/provider-support/routes.js +17 -0
  74. package/pi-extension/dist/tool-execution/apply-patch/grammar.js +23 -0
  75. package/pi-extension/dist/tool-execution/apply-patch/match.js +38 -0
  76. package/pi-extension/dist/tool-execution/apply-patch/parser.js +177 -0
  77. package/pi-extension/dist/tool-execution/apply-patch/plan.js +201 -0
  78. package/pi-extension/dist/tool-execution/apply-patch/tool.js +84 -0
  79. package/pi-extension/dist/tool-execution/background.js +238 -0
  80. package/pi-extension/dist/tool-execution/bash-guard.js +23 -0
  81. package/pi-extension/dist/tool-execution/config.js +22 -0
  82. package/pi-extension/dist/tool-execution/edit-messages.js +120 -0
  83. package/pi-extension/dist/tool-execution/file-state.js +138 -0
  84. package/pi-extension/dist/tool-execution/file-tracking.js +143 -0
  85. package/pi-extension/dist/tool-execution/prompt.js +54 -0
  86. package/pi-extension/dist/tool-execution/tools.js +16 -0
  87. package/pi-extension/freeflow/README.md +9 -0
  88. package/plugin.json +1 -1
  89. package/runtime/prompts/cognitive-routing.md +4 -4
  90. package/runtime/prompts/core.md +22 -75
  91. package/runtime/prompts/interaction-contract.md +15 -8
  92. package/runtime/prompts/tool-execution.md +38 -0
  93. package/runtime/prompts/working-method.md +108 -0
  94. package/skills/action-selection/SKILL.md +39 -47
  95. package/skills/action-selection/references/trajectory-stalls.md +2 -0
  96. package/skills/bypass/SKILL.md +2 -2
  97. package/skills/commit-work/SKILL.md +31 -83
  98. package/skills/commit-work/references/git-state-edges.md +1 -1
  99. package/skills/commit-work/references/simple-push.md +2 -0
  100. package/skills/commit-work/references/staging-decisions.md +1 -1
  101. package/skills/decision-gate/SKILL.md +31 -50
  102. package/skills/design-for-depth/SKILL.md +44 -53
  103. package/skills/design-for-depth/references/design-pressure-signals.md +46 -144
  104. package/skills/design-for-depth/references/implementation-decisions.md +4 -4
  105. package/skills/design-for-depth/references/interface-design-loop.md +5 -36
  106. package/skills/design-for-depth/references/module-and-dependency-design.md +2 -32
  107. package/skills/design-for-depth/references/state-and-failure-boundaries.md +2 -13
  108. package/skills/diagnose-failure/SKILL.md +39 -24
  109. package/skills/diagnose-failure/references/diagnostic-loop-catalog.md +2 -0
  110. package/skills/diagnose-failure/references/flaky-and-performance-diagnosis.md +2 -2
  111. package/skills/discuss/SKILL.md +37 -13
  112. package/skills/discuss/references/discussion-continuity.md +7 -27
  113. package/skills/evaluate-skill/SKILL.md +49 -80
  114. package/skills/evaluate-skill/references/evaluation-definition-schema.md +35 -5
  115. package/skills/evaluate-skill/references/evaluation-design.md +14 -14
  116. package/skills/evaluate-skill/references/execution-and-evidence.md +26 -5
  117. package/skills/evaluate-skill/references/review-and-revision.md +3 -1
  118. package/skills/evaluate-skill/scripts/lib/definitions.mjs +49 -8
  119. package/skills/evaluate-skill/scripts/lib/pi.mjs +14 -7
  120. package/skills/evaluate-skill/scripts/lib/review.mjs +196 -0
  121. package/skills/evaluate-skill/scripts/lib/runner.mjs +45 -8
  122. package/skills/evaluate-skill/scripts/lib/sandbox.mjs +2 -0
  123. package/skills/evaluate-skill/scripts/lib/trials.mjs +103 -0
  124. package/skills/evaluate-skill/scripts/lib/view.mjs +67 -1
  125. package/skills/evaluate-skill/scripts/pi-command-tool.mjs +144 -0
  126. package/skills/evaluate-skill/scripts/pi-guard.mjs +30 -13
  127. package/skills/evaluate-skill/scripts/skill-eval.mjs +114 -34
  128. package/skills/execute-work/SKILL.md +41 -9
  129. package/skills/execute-work/references/code-practices.md +2 -0
  130. package/skills/execute-work/references/execute-work-edges.md +11 -1
  131. package/skills/handoff/SKILL.md +1 -1
  132. package/skills/handoff/references/handoff-templates.md +1 -1
  133. package/skills/review-artifact/SKILL.md +42 -41
  134. package/skills/review-artifact/references/adjudicate-artifact-review.md +1 -1
  135. package/skills/review-artifact/references/independent-artifact-reviewer-contract.md +1 -1
  136. package/skills/review-work/SKILL.md +38 -32
  137. package/skills/review-work/references/adjudicate-work-review.md +1 -1
  138. package/skills/review-work/references/independent-work-reviewer-contract.md +1 -1
  139. package/skills/review-work/references/security-risk-lens.md +2 -4
  140. package/skills/setup-freeflow/SKILL.md +3 -1
  141. package/skills/setup-freeflow/references/activation-contract.md +7 -13
  142. package/skills/setup-freeflow/references/host-setup.md +3 -3
  143. package/skills/track-work/SKILL.md +113 -161
  144. package/skills/track-work/references/working-record-format.md +189 -350
  145. package/skills/track-work/scripts/lib/document.mjs +62 -6
  146. package/skills/track-work/scripts/lib/format.mjs +16 -1
  147. package/skills/track-work/scripts/lib/store.mjs +9 -8
  148. package/skills/track-work/scripts/lib/transitions.mjs +79 -4
  149. package/skills/track-work/scripts/working-record.mjs +34 -12
  150. package/skills/verify-work/SKILL.md +29 -20
  151. package/skills/verify-work/references/browser-runtime-evidence.md +1 -5
  152. package/skills/verify-work/references/integration-evidence.md +2 -0
  153. package/skills/verify-work/references/performance-evidence.md +3 -1
  154. package/skills/verify-work/references/test-design.md +3 -1
  155. package/skills/workflow/SKILL.md +65 -21
  156. package/skills/workflow/references/domain-skill-composition.md +5 -3
  157. package/skills/write-docs/SKILL.md +83 -0
  158. package/skills/write-docs/references/documentation-shapes.md +119 -0
  159. package/skills/write-docs/scripts/docs-check.mjs +139 -0
  160. package/skills/write-plan/SKILL.md +40 -40
  161. package/skills/write-plan/references/plan-shapes.md +1 -1
  162. package/skills/write-skill/SKILL.md +107 -147
  163. package/skills/write-skill/references/activation-boundaries.md +2 -0
  164. package/skills/write-skill/references/agent-first-instructions.md +22 -10
  165. package/skills/write-skill/references/progressive-disclosure.md +4 -2
  166. package/skills/write-skill/references/skill-dependency-graphs.md +2 -0
  167. package/skills/write-skill/scripts/lib/similarity.mjs +56 -0
  168. package/skills/write-skill/scripts/skill-author.mjs +18 -2
  169. package/skills/write-spec/SKILL.md +34 -41
  170. package/skills/write-spec/references/artifact-standards.md +1 -1
  171. package/skills/write-spec/references/decision-records.md +2 -0
  172. package/skills/write-spec/references/spec-shapes.md +1 -1
  173. package/capabilities/context-virtualization/SKILL.md +0 -103
  174. package/capabilities/conversation-history/SKILL.md +0 -53
  175. package/hooks/tests/check-host-adapters.test.mjs +0 -82
  176. package/hooks/tests/check-runtime-context-hook.sh +0 -212
  177. package/pi-extension/dist/cognitive-routing-v2/projection.js +0 -321
  178. package/pi-extension/dist/cognitive-routing-v2/runtime.js +0 -1585
  179. package/pi-extension/dist/cognitive-routing-v2/schemas.js +0 -107
  180. package/pi-extension/dist/cognitive-routing-v2/state.js +0 -612
  181. package/pi-extension/dist/cognitive-routing-v2/tools.js +0 -91
  182. package/pi-extension/dist/context-virtualization/commands.js +0 -89
  183. package/pi-extension/dist/context-virtualization/projector.js +0 -42
  184. package/pi-extension/dist/context-virtualization/runtime.js +0 -356
  185. package/pi-extension/dist/context-virtualization/state.js +0 -57
  186. package/pi-extension/dist/context-virtualization/types.js +0 -10
  187. package/pi-extension/dist/conversation-history/passages.js +0 -66
  188. package/pi-extension/dist/conversation-history/ranking.js +0 -105
  189. package/pi-extension/dist/conversation-history/runtime.js +0 -226
  190. package/pi-extension/dist/conversation-history/search.js +0 -56
  191. package/pi-extension/dist/conversation-history/types.js +0 -1
  192. package/pi-extension/dist/freeflow-context/resolver.js +0 -120
  193. package/pi-extension/dist/freeflow-context/runtime.js +0 -80
  194. package/pi-extension/dist/freeflow-context/source-projector.js +0 -95
  195. package/pi-extension/dist/freeflow-context/tool.js +0 -313
  196. package/pi-extension/dist/freeflow-context/types.js +0 -12
  197. package/pi-extension/dist/freeflow-context/visibility.js +0 -28
  198. package/pi-extension/dist/integrations/mcp-config.js +0 -29
  199. package/pi-extension/dist/runtime/runtime-context.js +0 -735
  200. package/pi-extension/dist/runtime/runtime-identity.js +0 -5
  201. package/pi-extension/dist/session-sources/events.js +0 -195
  202. package/pi-extension/dist/session-sources/sources.js +0 -233
  203. package/pi-extension/dist/settings/settings-ui.js +0 -1160
  204. package/pi-extension/dist/ui/text-component.js +0 -57
  205. package/pi-extension/src/cognitive-routing-v2/budget.ts +0 -42
  206. package/pi-extension/src/cognitive-routing-v2/config.ts +0 -143
  207. package/pi-extension/src/cognitive-routing-v2/projection.ts +0 -365
  208. package/pi-extension/src/cognitive-routing-v2/render.ts +0 -256
  209. package/pi-extension/src/cognitive-routing-v2/runtime.ts +0 -1634
  210. package/pi-extension/src/cognitive-routing-v2/schemas.ts +0 -109
  211. package/pi-extension/src/cognitive-routing-v2/state.ts +0 -622
  212. package/pi-extension/src/cognitive-routing-v2/tools.ts +0 -93
  213. package/pi-extension/src/cognitive-routing-v2/types.ts +0 -222
  214. package/pi-extension/src/context-virtualization/commands.ts +0 -111
  215. package/pi-extension/src/context-virtualization/projector.ts +0 -62
  216. package/pi-extension/src/context-virtualization/runtime.ts +0 -406
  217. package/pi-extension/src/context-virtualization/state.ts +0 -79
  218. package/pi-extension/src/context-virtualization/types.ts +0 -92
  219. package/pi-extension/src/conversation-history/passages.ts +0 -81
  220. package/pi-extension/src/conversation-history/ranking.ts +0 -155
  221. package/pi-extension/src/conversation-history/runtime.ts +0 -250
  222. package/pi-extension/src/conversation-history/search.ts +0 -79
  223. package/pi-extension/src/conversation-history/types.ts +0 -31
  224. package/pi-extension/src/freeflow-context/resolver.ts +0 -139
  225. package/pi-extension/src/freeflow-context/runtime.ts +0 -100
  226. package/pi-extension/src/freeflow-context/source-projector.ts +0 -106
  227. package/pi-extension/src/freeflow-context/tool.ts +0 -365
  228. package/pi-extension/src/freeflow-context/types.ts +0 -70
  229. package/pi-extension/src/freeflow-context/visibility.ts +0 -53
  230. package/pi-extension/src/index.ts +0 -368
  231. package/pi-extension/src/integrations/mcp-config.ts +0 -32
  232. package/pi-extension/src/runtime/runtime-context.ts +0 -838
  233. package/pi-extension/src/runtime/runtime-identity.ts +0 -8
  234. package/pi-extension/src/session-sources/events.ts +0 -208
  235. package/pi-extension/src/session-sources/read-only-session.ts +0 -170
  236. package/pi-extension/src/session-sources/sources.ts +0 -263
  237. package/pi-extension/src/settings/settings-tui.ts +0 -501
  238. package/pi-extension/src/settings/settings-ui.ts +0 -1422
  239. package/pi-extension/src/ui/text-component.ts +0 -66
  240. package/pi-extension/tests/cognitive-routing-v2/communication.test.js +0 -484
  241. package/pi-extension/tests/cognitive-routing-v2/compaction-input.test.js +0 -183
  242. package/pi-extension/tests/cognitive-routing-v2/native.test.js +0 -441
  243. package/pi-extension/tests/cognitive-routing-v2/performance.test.js +0 -87
  244. package/pi-extension/tests/cognitive-routing-v2/persistence.test.js +0 -245
  245. package/pi-extension/tests/cognitive-routing-v2/projection.test.js +0 -241
  246. package/pi-extension/tests/cognitive-routing-v2/recovery.test.js +0 -236
  247. package/pi-extension/tests/cognitive-routing-v2/render.test.js +0 -230
  248. package/pi-extension/tests/cognitive-routing-v2/runtime.test.js +0 -153
  249. package/pi-extension/tests/cognitive-routing-v2/stabilization.test.js +0 -521
  250. package/pi-extension/tests/cognitive-routing-v2/state.test.js +0 -275
  251. package/pi-extension/tests/context-virtualization/extension.test.js +0 -765
  252. package/pi-extension/tests/context-virtualization/projector.test.js +0 -110
  253. package/pi-extension/tests/context-virtualization/runtime.test.js +0 -216
  254. package/pi-extension/tests/conversation-history/runtime.test.js +0 -98
  255. package/pi-extension/tests/conversation-history/search.test.js +0 -97
  256. package/pi-extension/tests/fixtures/pi-host.js +0 -4
  257. package/pi-extension/tests/fixtures/routing-native.js +0 -188
  258. package/pi-extension/tests/integration/mode-free-core.test.js +0 -219
  259. package/pi-extension/tests/integration/pi-extension.test.js +0 -968
  260. package/pi-extension/tests/integration/prompt-architecture.test.js +0 -431
  261. package/pi-extension/tests/integration/unaffected-regressions.test.js +0 -478
  262. package/pi-extension/tests/settings/settings-tui.test.js +0 -243
  263. package/pi-extension/tsconfig.json +0 -16
  264. package/runtime/prompts/context-virtualization.md +0 -3
  265. package/runtime/prompts/conversation-history.md +0 -3
  266. package/skills/design-for-depth/references/software-design-philosophy.md +0 -202
  267. /package/pi-extension/dist/{cognitive-routing-v2 → cognitive-routing}/budget.js +0 -0
@@ -0,0 +1,1570 @@
1
+ import { execFile } from "node:child_process";
2
+ import { appendFile, mkdir, readFile, rm, writeFile } from "node:fs/promises";
3
+ import { dirname, isAbsolute, join, resolve } from "node:path";
4
+ import { promisify } from "node:util";
5
+ import {
6
+ readCapabilityState,
7
+ readFreeflowConfig,
8
+ readFreeflowConfigLayers,
9
+ readFreeflowLocalConfig,
10
+ resetSessionOverrides,
11
+ setSessionCoreOverride,
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
+ */
23
+ const DEFAULT_FREEFLOW_ENABLED = true;
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
+ };
32
+ const execFileAsync = promisify(execFile);
33
+ function isRecord(value) {
34
+ return typeof value === "object" && value !== null && !Array.isArray(value);
35
+ }
36
+ function cloneJson(value) {
37
+ if (value === undefined) return value;
38
+ try {
39
+ return JSON.parse(JSON.stringify(value));
40
+ } catch (error) {
41
+ throw new Error(`Could not clone settings value: ${error instanceof Error ? error.message : String(error)}`);
42
+ }
43
+ }
44
+ function getPath(source, path) {
45
+ let current = source;
46
+ for (const key of path) {
47
+ if (!isRecord(current)) return undefined;
48
+ current = current[key];
49
+ }
50
+ return current;
51
+ }
52
+ function setPath(target, path, value) {
53
+ let current = target;
54
+ for (const key of path.slice(0, -1)) {
55
+ const next = current[key];
56
+ if (!isRecord(next)) {
57
+ current[key] = {};
58
+ }
59
+ current = current[key];
60
+ }
61
+ current[path[path.length - 1]] = value;
62
+ }
63
+ function deletePath(target, path) {
64
+ let current = target;
65
+ const parents = [];
66
+ for (const key of path.slice(0, -1)) {
67
+ if (!isRecord(current)) return;
68
+ parents.push({ object: current, key });
69
+ current = current[key];
70
+ }
71
+ if (!isRecord(current)) return;
72
+ delete current[path[path.length - 1]];
73
+ for (let index = parents.length - 1; index >= 0; index--) {
74
+ const { object, key } = parents[index];
75
+ const child = object[key];
76
+ if (isRecord(child) && Object.keys(child).length === 0) {
77
+ delete object[key];
78
+ }
79
+ }
80
+ }
81
+ function valuesEqual(a, b) {
82
+ return JSON.stringify(a) === JSON.stringify(b);
83
+ }
84
+ function isEmptyValue(value) {
85
+ return Array.isArray(value) && value.length === 0;
86
+ }
87
+ function setConfigValue(config, item, value) {
88
+ if (!item.path?.length) {
89
+ throw new Error(`${item.label} is a settings group, not a writable setting.`);
90
+ }
91
+ if (value === undefined || (item.defaultValue !== undefined && valuesEqual(value, item.defaultValue))) {
92
+ deletePath(config, item.path);
93
+ return;
94
+ }
95
+ if (isEmptyValue(value)) {
96
+ deletePath(config, item.path);
97
+ return;
98
+ }
99
+ setPath(config, item.path, value);
100
+ }
101
+ function booleanValue(value) {
102
+ return value === true ? "enabled" : "disabled";
103
+ }
104
+ function configValueForChoice(item, value) {
105
+ const key = String(value);
106
+ if (item.configValues && Object.hasOwn(item.configValues, key)) {
107
+ return item.configValues[key];
108
+ }
109
+ return value;
110
+ }
111
+ function effectiveItemValue(item) {
112
+ return item.effectiveValue === undefined ? item.value : item.effectiveValue;
113
+ }
114
+ function formatCoreValue(value) {
115
+ if (typeof value === "boolean") return booleanValue(value);
116
+ if (isRecord(value) && typeof value.provider === "string" && typeof value.model === "string") {
117
+ const effort = typeof value.thinking === "string" ? ` · ${value.thinking}` : "";
118
+ return `${value.provider}/${value.model}${effort}`;
119
+ }
120
+ return String(value ?? "");
121
+ }
122
+ function coreDisplaySuffix(item, inactive = false) {
123
+ const source = item.effectiveSource ?? "builtin";
124
+ const parts =
125
+ item.configScope === "repository" && source === "local"
126
+ ? [`effective ${formatCoreValue(effectiveItemValue(item))}`, source]
127
+ : [source];
128
+ if (inactive) parts.push("inactive");
129
+ return `(${parts.join(" · ")})`;
130
+ }
131
+ function updateScopedItemState(item, value) {
132
+ if (!item.configScope) return;
133
+ const selectedValue = configValueForChoice(item, value);
134
+ const configValue = selectedValue === undefined || isEmptyValue(selectedValue) ? undefined : selectedValue;
135
+ if (item.configScope === "session" || item.configScope === "local") {
136
+ item.effectiveValue = configValue === undefined ? item.inheritedValue : configValue;
137
+ item.effectiveSource = configValue === undefined ? (item.inheritedSource ?? "builtin") : item.configScope;
138
+ return;
139
+ }
140
+ if (item.localOverrideValue !== undefined) {
141
+ item.effectiveValue = item.localOverrideValue;
142
+ item.effectiveSource = "local";
143
+ return;
144
+ }
145
+ item.effectiveValue = configValue;
146
+ item.effectiveSource =
147
+ item.defaultValue !== undefined && valuesEqual(configValue, item.defaultValue) ? "builtin" : "repository";
148
+ }
149
+ function createScopedBooleanItem(options) {
150
+ const repositoryValue = getPath(options.rawConfig, options.path);
151
+ const inheritedValue = typeof repositoryValue === "boolean" ? repositoryValue : options.defaultValue;
152
+ const inheritedSource = typeof repositoryValue === "boolean" ? "repository" : "builtin";
153
+ const localValue = getPath(options.localConfig, options.path);
154
+ let item;
155
+ if (options.scope === "local") {
156
+ const selectedValue = typeof localValue === "boolean" ? String(localValue) : LOCAL_INHERIT;
157
+ item = {
158
+ id: options.id,
159
+ label: options.label,
160
+ description: `${options.description} Inherit uses the repository value.`,
161
+ path: options.path,
162
+ kind: "enum",
163
+ value: selectedValue,
164
+ values: [LOCAL_INHERIT, "true", "false"],
165
+ valueLabels: { inherit: "inherit", true: "enabled", false: "disabled" },
166
+ valueDescriptions: {
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.",
170
+ },
171
+ format: (value) => {
172
+ if (value === LOCAL_INHERIT) return LOCAL_INHERIT;
173
+ return booleanValue(value === "true");
174
+ },
175
+ configScope: "local",
176
+ configValues: {
177
+ inherit: undefined,
178
+ true: true,
179
+ false: false,
180
+ },
181
+ effectiveValue: options.effectiveValue,
182
+ effectiveSource: options.effectiveSource,
183
+ inheritedValue,
184
+ inheritedSource,
185
+ };
186
+ } else {
187
+ item = {
188
+ id: options.id,
189
+ label: options.label,
190
+ description: options.description,
191
+ path: options.path,
192
+ kind: "boolean",
193
+ value: inheritedValue,
194
+ defaultValue: options.defaultValue,
195
+ configScope: "repository",
196
+ effectiveValue: options.effectiveValue,
197
+ effectiveSource: options.effectiveSource,
198
+ localOverrideValue: typeof localValue === "boolean" ? localValue : undefined,
199
+ };
200
+ }
201
+ item.displaySuffix = coreDisplaySuffix(item);
202
+ return item;
203
+ }
204
+ function createScopedDelegationItem(options) {
205
+ const path = ["cognitiveRouting", "delegation"];
206
+ const repositoryValue = getPath(options.rawConfig, path);
207
+ const inheritedValue = ["executor", "helper", "both"].includes(repositoryValue) ? repositoryValue : "executor";
208
+ const inheritedSource = repositoryValue === undefined ? "builtin" : "repository";
209
+ const localValue = getPath(options.localConfig, path);
210
+ const descriptions = {
211
+ executor: "Coordinator delegates assignments to Executor.",
212
+ helper: "Coordinator delegates supporting assignments to Helper and keeps production implementation.",
213
+ both: "Coordinator chooses Helper or Executor for each assignment.",
214
+ };
215
+ const item =
216
+ options.scope === "local"
217
+ ? {
218
+ id: "freeflow.cognitiveRouting.delegation",
219
+ label: "Delegation",
220
+ description:
221
+ "Choose enabled workers for automatic routing. Inherit uses the repository value; this setting does not rank model presets.",
222
+ path,
223
+ kind: "enum",
224
+ value: ["executor", "helper", "both"].includes(localValue) ? localValue : LOCAL_INHERIT,
225
+ values: [LOCAL_INHERIT, "executor", "helper", "both"],
226
+ valueLabels: {
227
+ inherit: "inherit",
228
+ executor: "Executor only",
229
+ helper: "Helper only",
230
+ both: "Helper and Executor",
231
+ },
232
+ valueDescriptions: {
233
+ inherit: `Use ${inheritedValue} from ${SOURCE_LABELS[inheritedSource]}.`,
234
+ ...descriptions,
235
+ },
236
+ configScope: "local",
237
+ configValues: { inherit: undefined, executor: "executor", helper: "helper", both: "both" },
238
+ effectiveValue: options.effectiveValue,
239
+ effectiveSource: options.effectiveSource,
240
+ inheritedValue,
241
+ inheritedSource,
242
+ }
243
+ : {
244
+ id: "freeflow.cognitiveRouting.delegation",
245
+ label: "Delegation",
246
+ description: "Choose enabled workers for automatic routing. This edits shared .freeflow/config.json.",
247
+ path,
248
+ kind: "enum",
249
+ value: inheritedValue,
250
+ values: ["executor", "helper", "both"],
251
+ valueLabels: {
252
+ executor: "Executor only",
253
+ helper: "Helper only",
254
+ both: "Helper and Executor",
255
+ },
256
+ valueDescriptions: descriptions,
257
+ defaultValue: "executor",
258
+ configScope: "repository",
259
+ effectiveValue: options.effectiveValue,
260
+ effectiveSource: options.effectiveSource,
261
+ localOverrideValue: ["executor", "helper", "both"].includes(localValue) ? localValue : undefined,
262
+ };
263
+ item.displaySuffix = coreDisplaySuffix(item);
264
+ return item;
265
+ }
266
+ function resolveSettingsCoreView(rawConfig, layers) {
267
+ const localConfig = layers?.local.valid && isRecord(layers.local.parsed) ? layers.local.parsed : {};
268
+ const fallbackCore = { enabled: getPath(rawConfig, ["enabled"]) !== false };
269
+ const fallbackSources = {
270
+ enabled: typeof getPath(rawConfig, ["enabled"]) === "boolean" ? "repository" : "builtin",
271
+ };
272
+ return {
273
+ localConfig,
274
+ core: layers?.coreConfig ?? fallbackCore,
275
+ sources: layers?.sources ?? fallbackSources,
276
+ };
277
+ }
278
+ function createSessionBooleanItem(options) {
279
+ const override = options.sessionOverrides[options.key];
280
+ return {
281
+ id: options.id,
282
+ label: options.label,
283
+ description: `${options.description} Inherit uses the personal or repository value.`,
284
+ path: options.key.split("."),
285
+ kind: "enum",
286
+ value: typeof override === "boolean" ? String(override) : LOCAL_INHERIT,
287
+ values: [LOCAL_INHERIT, "true", "false"],
288
+ valueLabels: { inherit: "inherit", true: "enabled", false: "disabled" },
289
+ valueDescriptions: {
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.",
293
+ },
294
+ configScope: "session",
295
+ configValues: { inherit: undefined, true: true, false: false },
296
+ effectiveValue: options.effectiveValue,
297
+ effectiveSource: typeof override === "boolean" ? "session" : options.inheritedSource,
298
+ inheritedValue: options.inheritedValue,
299
+ inheritedSource: options.inheritedSource,
300
+ };
301
+ }
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) {
317
+ const sessionOverrides = state.sessionOverrides;
318
+ const configured = state.configuredCoreConfig;
319
+ const configuredSources = state.configuredSources;
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
+ };
336
+ const freeflowItem = createSessionBooleanItem({
337
+ id: "freeflow.enabled",
338
+ label: "Freeflow",
339
+ description: "Master switch for Freeflow in this Pi session.",
340
+ key: "enabled",
341
+ inheritedValue: configured.enabled,
342
+ inheritedSource: configuredSources.enabled,
343
+ effectiveValue: state.enabled,
344
+ sessionOverrides,
345
+ });
346
+ const routing = state.cognitiveRouting;
347
+ const routingUnavailable = cognitiveRoutingController === undefined;
348
+ const cognitiveRoutingState = cognitiveRoutingController?.state();
349
+ const manualProfiles = ["coordinator", "helper", "executor"];
350
+ const heldProfile = cognitiveRoutingState?.controlMode.startsWith("manual-")
351
+ ? cognitiveRoutingState.activeProfile
352
+ : undefined;
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
+ {
380
+ id: "freeflow.cognitiveRouting.profile",
381
+ label: "Control",
382
+ description: "Automatic routing, or hold one profile until you release it.",
383
+ kind: "enum",
384
+ value: control,
385
+ values: ["auto", ...manualProfiles],
386
+ valueLabels: Object.fromEntries([
387
+ ["auto", "automatic"],
388
+ ...manualProfiles.map((profile) => [profile, `hold ${profile}`]),
389
+ ]),
390
+ valueDescriptions: Object.fromEntries([
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
+ ]),
396
+ ]),
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);
443
+ }
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 = [
471
+ freeflowItem,
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
+ ]),
530
+ ];
531
+ return items;
532
+ }
533
+ const COGNITIVE_ROUTING_THINKING_LEVELS = ["off", "minimal", "low", "medium", "high", "xhigh", "max"];
534
+ const COGNITIVE_ROUTING_EFFORT_DESCRIPTIONS = {
535
+ off: "No reasoning",
536
+ minimal: "Very brief reasoning",
537
+ low: "Light reasoning",
538
+ medium: "Moderate reasoning",
539
+ high: "Deep reasoning",
540
+ xhigh: "Extra-high reasoning",
541
+ max: "Maximum reasoning",
542
+ };
543
+ function cognitiveRoutingModelKey(provider, model) {
544
+ return `${provider}/${model}`;
545
+ }
546
+ function isCognitiveRoutingProfile(value) {
547
+ return (
548
+ isRecord(value) &&
549
+ typeof value.provider === "string" &&
550
+ typeof value.model === "string" &&
551
+ typeof value.thinking === "string" &&
552
+ COGNITIVE_ROUTING_THINKING_LEVELS.includes(value.thinking)
553
+ );
554
+ }
555
+ function cognitiveRoutingThinkingLevels(model, registry) {
556
+ if (typeof registry?.clampThinkingLevel !== "function") {
557
+ return model?.reasoning === true ? [...COGNITIVE_ROUTING_THINKING_LEVELS] : ["off"];
558
+ }
559
+ return COGNITIVE_ROUTING_THINKING_LEVELS.filter((level) => {
560
+ try {
561
+ return registry.clampThinkingLevel(model, level) === level;
562
+ } catch {
563
+ return false;
564
+ }
565
+ });
566
+ }
567
+ function cognitiveRoutingModelOptions(ctx) {
568
+ const registry = ctx?.modelRegistry;
569
+ if (typeof registry?.getAvailable !== "function") return [];
570
+ let models;
571
+ try {
572
+ models = registry.getAvailable();
573
+ } catch {
574
+ return [];
575
+ }
576
+ if (!Array.isArray(models)) return [];
577
+ return models
578
+ .filter((model) => typeof model?.provider === "string" && typeof model?.id === "string")
579
+ .map((model) => {
580
+ const thinkingLevels = cognitiveRoutingThinkingLevels(model, registry);
581
+ const provider = model.provider;
582
+ const modelId = model.id;
583
+ const key = cognitiveRoutingModelKey(provider, modelId);
584
+ const displayName = typeof model.name === "string" && model.name !== modelId ? model.name : undefined;
585
+ return {
586
+ key,
587
+ provider,
588
+ model: modelId,
589
+ label: key,
590
+ description: displayName ? `${displayName} · ${thinkingLevels.join(", ")}` : thinkingLevels.join(", "),
591
+ thinkingLevels,
592
+ };
593
+ })
594
+ .filter((model) => model.thinkingLevels.length > 0)
595
+ .sort((a, b) => a.label.localeCompare(b.label));
596
+ }
597
+ function cognitiveRoutingProfileDisplay(value) {
598
+ return isCognitiveRoutingProfile(value) ? formatCoreValue(value) : "not configured";
599
+ }
600
+ function cognitiveRoutingSettingsSource(source) {
601
+ if (source === "session") return "session";
602
+ if (source === "repository") return "repository";
603
+ if (source === "personal" || source === "local") return "local";
604
+ return "builtin";
605
+ }
606
+ function cognitiveRoutingProfileDisplaySuffix(source, scope) {
607
+ if (!source) return undefined;
608
+ if (source === "session") return "(session override)";
609
+ if (source === "personal" && scope === "repository") return "(effective personal)";
610
+ return `(${source})`;
611
+ }
612
+ function cognitiveRoutingConfirmStep(summary) {
613
+ return {
614
+ title: "Confirm preset",
615
+ choices: [
616
+ {
617
+ key: "__save__",
618
+ value: "save",
619
+ label: `Save ${summary}`,
620
+ description: "Write the complete profile atomically.",
621
+ },
622
+ { key: "__cancel__", value: "cancel", label: "Cancel", description: "Leave the previous preset unchanged." },
623
+ ],
624
+ selectedKey: "__save__",
625
+ };
626
+ }
627
+ function createCognitiveRoutingProfileWizard(
628
+ currentValue,
629
+ models,
630
+ allowInherit,
631
+ inheritChoice = {
632
+ label: "Inherit repository preset",
633
+ description: "Remove the personal override without changing the repository preset.",
634
+ summary: "inherit repository preset",
635
+ },
636
+ ) {
637
+ const currentProfile = isCognitiveRoutingProfile(currentValue) ? currentValue : undefined;
638
+ const currentModelKey = currentProfile
639
+ ? cognitiveRoutingModelKey(currentProfile.provider, currentProfile.model)
640
+ : undefined;
641
+ const firstChoices = [
642
+ ...(allowInherit
643
+ ? [
644
+ {
645
+ key: LOCAL_INHERIT,
646
+ value: LOCAL_INHERIT,
647
+ label: inheritChoice.label,
648
+ description: inheritChoice.description,
649
+ },
650
+ ]
651
+ : []),
652
+ ...models.map((model) => ({
653
+ key: model.key,
654
+ value: model,
655
+ label: model.label,
656
+ description: model.description,
657
+ })),
658
+ ];
659
+ if (firstChoices.length === 0) {
660
+ firstChoices.push({
661
+ key: "__cancel__",
662
+ value: "cancel",
663
+ label: "No authenticated models available",
664
+ description: "Authenticate a provider before configuring this preset.",
665
+ });
666
+ }
667
+ return {
668
+ firstStep: () => ({
669
+ title: "Choose model",
670
+ choices: firstChoices,
671
+ selectedKey:
672
+ currentValue === LOCAL_INHERIT
673
+ ? LOCAL_INHERIT
674
+ : firstChoices.some((choice) => choice.key === currentModelKey)
675
+ ? currentModelKey
676
+ : firstChoices[0]?.key,
677
+ }),
678
+ nextStep: (selectedValues) => {
679
+ if (selectedValues.length === 1) {
680
+ const selected = selectedValues[0];
681
+ if (selected === LOCAL_INHERIT) {
682
+ return cognitiveRoutingConfirmStep(inheritChoice.summary);
683
+ }
684
+ const model = selected;
685
+ if (!model?.key || model.thinkingLevels.length === 0) return undefined;
686
+ const currentEffort = currentProfile && currentModelKey === model.key ? currentProfile.thinking : undefined;
687
+ return {
688
+ title: `Choose effort · ${model.label}`,
689
+ choices: model.thinkingLevels.map((level) => ({
690
+ key: level,
691
+ value: level,
692
+ label: level,
693
+ description: COGNITIVE_ROUTING_EFFORT_DESCRIPTIONS[level],
694
+ })),
695
+ selectedKey:
696
+ currentEffort && model.thinkingLevels.includes(currentEffort) ? currentEffort : model.thinkingLevels[0],
697
+ };
698
+ }
699
+ if (selectedValues.length === 2) {
700
+ const model = selectedValues[0];
701
+ const effort = selectedValues[1];
702
+ return cognitiveRoutingConfirmStep(`${model.label} · ${effort}`);
703
+ }
704
+ return undefined;
705
+ },
706
+ valueFromSelections: (selectedValues) => {
707
+ if (selectedValues[0] === LOCAL_INHERIT) return undefined;
708
+ const model = selectedValues[0];
709
+ return {
710
+ provider: model.provider,
711
+ model: model.model,
712
+ thinking: selectedValues[1],
713
+ };
714
+ },
715
+ };
716
+ }
717
+ function cognitiveRoutingProfileItem(options) {
718
+ const path = ["cognitiveRouting", "profiles", options.name];
719
+ const repositoryProfile = getPath(options.rawConfig, path);
720
+ const localProfile = getPath(options.localConfig, path);
721
+ const configuredProfile = options.capabilityState?.profiles?.[options.name] ?? repositoryProfile;
722
+ const sessionPair = options.sessionProfiles?.[options.name];
723
+ const sessionProfile = sessionPair
724
+ ? { provider: sessionPair.provider, model: sessionPair.modelId, thinking: sessionPair.thinking }
725
+ : undefined;
726
+ const effectiveProfile = sessionProfile ?? configuredProfile;
727
+ const localValue = isCognitiveRoutingProfile(localProfile) ? localProfile : LOCAL_INHERIT;
728
+ const value =
729
+ options.scope === "session"
730
+ ? (sessionProfile ?? (configuredProfile ? LOCAL_INHERIT : undefined))
731
+ : options.scope === "local"
732
+ ? localValue
733
+ : repositoryProfile;
734
+ const configuredSource =
735
+ options.capabilityState?.profileSources?.[options.name] ??
736
+ (isCognitiveRoutingProfile(repositoryProfile) ? "repository" : undefined);
737
+ const source = options.scope === "session" ? (sessionProfile ? "session" : configuredSource) : configuredSource;
738
+ const models = cognitiveRoutingModelOptions(options.ctx);
739
+ const inactive = options.scope !== "repository" && models.length === 0 && value !== LOCAL_INHERIT;
740
+ const inheritChoice =
741
+ options.scope === "session"
742
+ ? {
743
+ label: "Inherit configured preset",
744
+ description: "Remove the session override without changing local or repository configuration.",
745
+ summary: "inherit configured preset",
746
+ }
747
+ : undefined;
748
+ return {
749
+ id: `freeflow.cognitiveRouting.${options.name}`,
750
+ label: `${options.name.charAt(0).toUpperCase()}${options.name.slice(1)} preset`,
751
+ description:
752
+ options.scope === "session"
753
+ ? "Temporarily choose the model and effort for this profile in the current Pi session only."
754
+ : "Choose an authenticated available model and one effort supported by that model. Confirming writes the complete preset; cancel leaves it unchanged.",
755
+ path,
756
+ kind: "string",
757
+ value,
758
+ format: (current) => {
759
+ const inherits = current === LOCAL_INHERIT || (options.scope === "session" && current === undefined);
760
+ return inherits
761
+ ? options.scope === "session"
762
+ ? configuredProfile
763
+ ? "inherit configured preset"
764
+ : "not configured"
765
+ : "inherit repository preset"
766
+ : cognitiveRoutingProfileDisplay(current);
767
+ },
768
+ configScope: options.scope,
769
+ effectiveValue: effectiveProfile,
770
+ effectiveSource: cognitiveRoutingSettingsSource(source),
771
+ inheritedValue: options.scope === "session" ? configuredProfile : repositoryProfile,
772
+ inheritedSource: cognitiveRoutingSettingsSource(configuredSource ?? "builtin"),
773
+ inactive,
774
+ displaySuffix: cognitiveRoutingProfileDisplaySuffix(source, options.scope),
775
+ wizard: () =>
776
+ createCognitiveRoutingProfileWizard(
777
+ value,
778
+ models,
779
+ options.scope !== "repository" && (options.scope !== "session" || configuredProfile !== undefined),
780
+ inheritChoice,
781
+ ),
782
+ };
783
+ }
784
+ function isCognitiveRoutingRuntimeAvailable(pi) {
785
+ return (
786
+ typeof pi?.appendEntry === "function" &&
787
+ typeof pi?.setModel === "function" &&
788
+ typeof pi?.setThinkingLevel === "function"
789
+ );
790
+ }
791
+ function freeflowItems(rawConfig, options = {}) {
792
+ const scope = options.scope === "local" ? "local" : "repository";
793
+ const layers = options.layers;
794
+ const { localConfig, core, sources } = resolveSettingsCoreView(rawConfig, layers);
795
+ const freeflowItem = createScopedBooleanItem({
796
+ scope,
797
+ rawConfig,
798
+ localConfig,
799
+ id: "freeflow.enabled",
800
+ label: "Freeflow",
801
+ description: "Master switch for Freeflow's core guidance, skills, and optional capabilities in this checkout.",
802
+ path: ["enabled"],
803
+ effectiveValue: core.enabled,
804
+ effectiveSource: sources.enabled,
805
+ defaultValue: DEFAULT_FREEFLOW_ENABLED,
806
+ });
807
+ const freeflowInactive = !core.enabled;
808
+ const cognitiveRoutingState = options.cognitiveRouting;
809
+ const cognitiveRoutingRuntimeDisabled = options.runtimeAvailable !== true;
810
+ const cognitiveRoutingGroup = (() => {
811
+ const cognitiveRoutingEnabledItem = createScopedBooleanItem({
812
+ scope,
813
+ rawConfig,
814
+ localConfig,
815
+ id: "freeflow.cognitiveRouting.enabled",
816
+ label: "Enabled",
817
+ description: cognitiveRoutingRuntimeDisabled
818
+ ? "Requires a host model-state control API; configuration is read-only on this host."
819
+ : "Allow Cognitive Routing to manage explicit assignments and request native profile transitions.",
820
+ path: ["cognitiveRouting", "enabled"],
821
+ effectiveValue: cognitiveRoutingState?.enabled ?? false,
822
+ effectiveSource: cognitiveRoutingSettingsSource(cognitiveRoutingState?.enabledSource),
823
+ defaultValue: false,
824
+ });
825
+ cognitiveRoutingEnabledItem.inactive = freeflowInactive || cognitiveRoutingRuntimeDisabled;
826
+ cognitiveRoutingEnabledItem.runtimeInactive = cognitiveRoutingRuntimeDisabled;
827
+ const cognitiveRoutingDelegationItem = createScopedDelegationItem({
828
+ scope,
829
+ rawConfig,
830
+ localConfig,
831
+ effectiveValue: cognitiveRoutingState?.delegation ?? "executor",
832
+ effectiveSource: cognitiveRoutingSettingsSource(cognitiveRoutingState?.delegationSource),
833
+ });
834
+ cognitiveRoutingDelegationItem.inactive = freeflowInactive || cognitiveRoutingRuntimeDisabled;
835
+ cognitiveRoutingDelegationItem.runtimeInactive = cognitiveRoutingRuntimeDisabled;
836
+ const cognitiveRoutingProjectionItem = createScopedBooleanItem({
837
+ scope,
838
+ rawConfig,
839
+ localConfig,
840
+ id: "freeflow.cognitiveRouting.projection",
841
+ label: "Context projection",
842
+ description:
843
+ "Select worker evidence for Coordinator; disable projection to keep ordinary Pi context in all enabled profiles.",
844
+ path: ["cognitiveRouting", "projection"],
845
+ effectiveValue: cognitiveRoutingState?.projection ?? false,
846
+ effectiveSource: cognitiveRoutingSettingsSource(cognitiveRoutingState?.projectionSource),
847
+ defaultValue: false,
848
+ });
849
+ cognitiveRoutingProjectionItem.inactive = freeflowInactive || cognitiveRoutingRuntimeDisabled;
850
+ cognitiveRoutingProjectionItem.runtimeInactive = cognitiveRoutingRuntimeDisabled;
851
+ const cognitiveRoutingProfiles = ["coordinator", "helper", "executor"].map((name) =>
852
+ cognitiveRoutingProfileItem({
853
+ name: name,
854
+ scope,
855
+ rawConfig,
856
+ localConfig,
857
+ capabilityState: cognitiveRoutingState,
858
+ ctx: options.ctx,
859
+ }),
860
+ );
861
+ for (const item of cognitiveRoutingProfiles) {
862
+ item.inactive ||= freeflowInactive || cognitiveRoutingRuntimeDisabled;
863
+ item.runtimeInactive = cognitiveRoutingRuntimeDisabled;
864
+ }
865
+ const cognitiveRoutingStatus = cognitiveRoutingRuntimeDisabled
866
+ ? "unavailable · host unsupported"
867
+ : cognitiveRoutingState
868
+ ? cognitiveRoutingState.effective
869
+ ? "active"
870
+ : cognitiveRoutingState.blockingReason?.code === "profile_missing"
871
+ ? "not configured"
872
+ : (cognitiveRoutingState.blockingReason?.code ?? "inactive")
873
+ : "unavailable";
874
+ return {
875
+ id: "freeflow.cognitiveRouting",
876
+ label: "Cognitive Routing",
877
+ description: cognitiveRoutingRuntimeDisabled
878
+ ? "Cognitive Routing configuration is visible for inspection but requires a host model-state control API."
879
+ : "Configure Coordinator, Helper, and Executor profiles plus the workers available for delegation.",
880
+ kind: "group",
881
+ value: cognitiveRoutingRuntimeDisabled ? false : (cognitiveRoutingState?.enabled ?? false),
882
+ inactive: freeflowInactive,
883
+ displaySuffix: cognitiveRoutingStatus,
884
+ children: [
885
+ cognitiveRoutingEnabledItem,
886
+ cognitiveRoutingDelegationItem,
887
+ ...cognitiveRoutingProfiles,
888
+ cognitiveRoutingProjectionItem,
889
+ ],
890
+ };
891
+ })();
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];
964
+ }
965
+ function pruneKnownDefaults(config) {
966
+ const defaultPaths = [
967
+ { path: ["enabled"], value: DEFAULT_FREEFLOW_ENABLED },
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 },
972
+ ];
973
+ for (const item of defaultPaths) {
974
+ if (valuesEqual(getPath(config, item.path), item.value)) {
975
+ deletePath(config, item.path);
976
+ }
977
+ }
978
+ }
979
+ async function ensureLocalConfigIgnored(cwd) {
980
+ let gitPath;
981
+ try {
982
+ const result = await execFileAsync("git", ["-C", cwd, "rev-parse", "--git-path", "info/exclude"]);
983
+ gitPath = result.stdout.trim();
984
+ } catch {
985
+ return;
986
+ }
987
+ const tracked = await execFileAsync("git", ["-C", cwd, "ls-files", "--", ".freeflow/local.json"]);
988
+ if (tracked.stdout.trim()) {
989
+ throw new Error(
990
+ ".freeflow/local.json is tracked by git; remove it from the index before writing personal overrides.",
991
+ );
992
+ }
993
+ try {
994
+ await execFileAsync("git", ["-C", cwd, "check-ignore", "-q", "--", ".freeflow/local.json"]);
995
+ return;
996
+ } catch {
997
+ // Add a local exclude when the repository does not already ignore the file.
998
+ }
999
+ const excludePath = isAbsolute(gitPath) ? gitPath : resolve(cwd, gitPath);
1000
+ let existing = "";
1001
+ try {
1002
+ existing = await readFile(excludePath, "utf8");
1003
+ } catch {
1004
+ // The git metadata path may not exist yet in a minimal repository.
1005
+ }
1006
+ const rule = ".freeflow/local.json";
1007
+ if (existing.split(/\r?\n/).includes(rule)) return;
1008
+ await mkdir(dirname(excludePath), { recursive: true });
1009
+ const prefix = existing && !existing.endsWith("\n") ? "\n" : "";
1010
+ await appendFile(excludePath, `${prefix}${rule}\n`, "utf8");
1011
+ }
1012
+ async function updateConfig(cwd, item, value, scope = item.configScope ?? "repository") {
1013
+ if (scope === "local") {
1014
+ if (!item.path?.length) {
1015
+ throw new Error(`${item.label} is a settings group, not a writable setting.`);
1016
+ }
1017
+ const current = await readFreeflowLocalConfig(cwd);
1018
+ const next = cloneJson(current);
1019
+ const configValue = configValueForChoice(item, value);
1020
+ const previousValue = getPath(current, item.path);
1021
+ if (configValue === undefined || isEmptyValue(configValue)) {
1022
+ if (previousValue === undefined) return;
1023
+ deletePath(next, item.path);
1024
+ } else {
1025
+ setPath(next, item.path, configValue);
1026
+ }
1027
+ const path = join(cwd, ".freeflow/local.json");
1028
+ await ensureLocalConfigIgnored(cwd);
1029
+ if (Object.keys(next).length === 0) {
1030
+ await rm(path, { force: true });
1031
+ return;
1032
+ }
1033
+ await mkdir(join(cwd, ".freeflow"), { recursive: true });
1034
+ await writeFile(path, `${JSON.stringify(next, null, 2)}\n`, "utf8");
1035
+ return;
1036
+ }
1037
+ const current = await readFreeflowConfig(cwd);
1038
+ const next = cloneJson(current);
1039
+ setConfigValue(next, item, value);
1040
+ pruneKnownDefaults(next);
1041
+ await mkdir(join(cwd, ".freeflow"), { recursive: true });
1042
+ await writeFile(join(cwd, ".freeflow/config.json"), `${JSON.stringify(next, null, 2)}\n`, "utf8");
1043
+ }
1044
+ function isCognitiveRoutingProfileItem(item) {
1045
+ return ["coordinator", "helper", "executor"].some((profile) => item.id === `freeflow.cognitiveRouting.${profile}`);
1046
+ }
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]}`;
1066
+ }
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;
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"]);
1075
+ function walkSettingsItems(items, visitor) {
1076
+ for (const item of items) {
1077
+ visitor(item);
1078
+ if (item.children) walkSettingsItems(item.children, visitor);
1079
+ }
1080
+ }
1081
+ function findSettingsItem(items, id) {
1082
+ for (const item of items) {
1083
+ if (item.id === id) return item;
1084
+ const child = item.children ? findSettingsItem(item.children, id) : undefined;
1085
+ if (child) return child;
1086
+ }
1087
+ return undefined;
1088
+ }
1089
+ function refreshSettingsDerivedState(items) {
1090
+ const freeflowItem = findSettingsItem(items, "freeflow.enabled");
1091
+ const freeflowInactive = freeflowItem ? effectiveItemValue(freeflowItem) !== true : false;
1092
+ const cognitiveRoutingGroup = findSettingsItem(items, "freeflow.cognitiveRouting");
1093
+ const cognitiveRoutingEnabledItem = findSettingsItem(items, "freeflow.cognitiveRouting.enabled");
1094
+ const cognitiveRoutingDelegationItem = findSettingsItem(items, "freeflow.cognitiveRouting.delegation");
1095
+ const delegation = cognitiveRoutingDelegationItem ? effectiveItemValue(cognitiveRoutingDelegationItem) : "executor";
1096
+ const cognitiveRoutingProfiles = ["coordinator", ...workersForDelegation(delegation)].map((profile) =>
1097
+ findSettingsItem(items, `freeflow.cognitiveRouting.${profile}`),
1098
+ );
1099
+ if (cognitiveRoutingGroup && cognitiveRoutingEnabledItem) {
1100
+ const enabled = effectiveItemValue(cognitiveRoutingEnabledItem) === true;
1101
+ const profilesConfigured = cognitiveRoutingProfiles.every((item) =>
1102
+ item ? isCognitiveRoutingProfile(effectiveItemValue(item)) : false,
1103
+ );
1104
+ cognitiveRoutingGroup.value = enabled;
1105
+ cognitiveRoutingGroup.displaySuffix = enabled
1106
+ ? profilesConfigured
1107
+ ? cognitiveRoutingGroup.displaySuffix === "active"
1108
+ ? "active"
1109
+ : "configured"
1110
+ : "not configured"
1111
+ : "disabled";
1112
+ cognitiveRoutingGroup.inactive = freeflowInactive;
1113
+ }
1114
+ const contextGroup = findSettingsItem(items, "freeflow.context");
1115
+ if (contextGroup?.children?.length) {
1116
+ const enabledCount = contextGroup.children.filter((item) => effectiveItemValue(item) === true).length;
1117
+ contextGroup.value = enabledCount > 0;
1118
+ contextGroup.displaySuffix = `${enabledCount}/${contextGroup.children.length} enabled`;
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
+ }
1138
+ walkSettingsItems(items, (candidate) => {
1139
+ if (candidate.id === "freeflow.session.reset") {
1140
+ candidate.inactive = false;
1141
+ } else if (candidate.configScope) {
1142
+ const inactive =
1143
+ candidate.id === "freeflow.enabled" ? false : candidate.runtimeInactive === true || freeflowInactive;
1144
+ candidate.inactive = inactive;
1145
+ candidate.displaySuffix = coreDisplaySuffix(candidate, inactive);
1146
+ } else {
1147
+ candidate.inactive = candidate.runtimeInactive === true || freeflowInactive;
1148
+ }
1149
+ });
1150
+ }
1151
+ function settingsChoices(item) {
1152
+ if (item.kind === "boolean") {
1153
+ return [
1154
+ { key: "true", value: true, label: "enabled" },
1155
+ { key: "false", value: false, label: "disabled" },
1156
+ ];
1157
+ }
1158
+ if (item.kind !== "enum") return undefined;
1159
+ return (item.values ?? []).map((value) => ({
1160
+ key: value,
1161
+ value,
1162
+ label: item.valueLabels?.[value] ?? value,
1163
+ description: item.valueDescriptions?.[value],
1164
+ }));
1165
+ }
1166
+ function resetSessionItemState(items) {
1167
+ walkSettingsItems(items, (item) => {
1168
+ if (item.configScope === "session") {
1169
+ item.value = LOCAL_INHERIT;
1170
+ item.effectiveValue = item.inheritedValue;
1171
+ item.effectiveSource = item.inheritedSource ?? "builtin";
1172
+ } else if (item.id === "freeflow.sessionMode") {
1173
+ item.value = "default";
1174
+ }
1175
+ });
1176
+ }
1177
+ function settingsEntries(items, rootItems, onChange) {
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),
1207
+ }
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
+ });
1223
+ }
1224
+ async function openSettings(options) {
1225
+ if (typeof options.ctx?.ui?.custom !== "function") {
1226
+ options.ctx?.ui?.notify?.(
1227
+ `${options.title} requires Pi TUI mode. Use the status command for a compact summary.`,
1228
+ "warning",
1229
+ );
1230
+ return { changed: false, configChanged: false, failed: false };
1231
+ }
1232
+ let component;
1233
+ await options.ctx.ui.custom((tui, theme, _keybindings, done) => {
1234
+ component = new PiSettingsComponent({
1235
+ title: options.title,
1236
+ scopes: options.scopes,
1237
+ initialScope: options.initialScope,
1238
+ theme: theme ?? {},
1239
+ requestRender: () => tui.requestRender(),
1240
+ notify: (message, level) => options.ctx?.ui?.notify?.(message, level),
1241
+ done,
1242
+ });
1243
+ return component;
1244
+ });
1245
+ await component?.waitForWrites();
1246
+ return component?.sessionResult() ?? { changed: false, configChanged: false, failed: false };
1247
+ }
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
+ ];
1266
+ const NON_TUI_SETTINGS_GUIDANCE =
1267
+ "Freeflow settings require Pi TUI mode. Use /freeflow status to inspect current state; supported non-TUI changes are /freeflow enable and /freeflow disable.";
1268
+ function ensureSettingsIdle(ctx) {
1269
+ if (typeof ctx?.isIdle !== "function" || ctx.isIdle()) return true;
1270
+ ctx.ui?.notify?.("Freeflow settings and profile changes are available only while Pi is idle.", "warning");
1271
+ return false;
1272
+ }
1273
+ function nonTuiGuidance(ctx, message) {
1274
+ if (ctx?.hasUI === false) throw new Error(message);
1275
+ ctx?.ui?.notify?.(message, "warning");
1276
+ return { changed: false, reloaded: false, error: "tui_required" };
1277
+ }
1278
+ async function finalizeSettingsSession(
1279
+ session,
1280
+ ctx,
1281
+ afterChange,
1282
+ savedMessage,
1283
+ reloadWarning = "Run /reload for Freeflow changes to fully apply.",
1284
+ afterChangeOptions = {},
1285
+ ) {
1286
+ if (!session.configChanged) return false;
1287
+ await afterChange(true, afterChangeOptions);
1288
+ if (session.failed) return false;
1289
+ ctx.ui.notify(savedMessage, "info");
1290
+ if (typeof ctx.reload !== "function") {
1291
+ ctx.ui.notify(reloadWarning, "warning");
1292
+ return false;
1293
+ }
1294
+ await ctx.reload();
1295
+ return true;
1296
+ }
1297
+ async function finalizeSessionSettings(session, ctx, afterChange) {
1298
+ if (!session.changed) return false;
1299
+ await afterChange(true);
1300
+ if (session.failed) return false;
1301
+ if (!session.configChanged) {
1302
+ ctx.ui.notify("Freeflow session overrides updated.", "info");
1303
+ return false;
1304
+ }
1305
+ ctx.ui.notify("Freeflow session overrides updated. Reloading skills and resources...", "info");
1306
+ if (typeof ctx.reload !== "function") {
1307
+ ctx.ui.notify("Run /reload for Freeflow session overrides to fully apply.", "warning");
1308
+ return false;
1309
+ }
1310
+ await ctx.reload();
1311
+ return true;
1312
+ }
1313
+ export async function handleFreeflowCommand(args, ctx, afterChange, pi, cognitiveRoutingController, diagnostics) {
1314
+ const input = (args ?? "settings").trim().toLowerCase() || "settings";
1315
+ const [action, ...rest] = input.split(/\s+/);
1316
+ const actionValue = rest.join(" ");
1317
+ const nonTui = ctx?.mode && ctx.mode !== "tui";
1318
+ const settingsSelector =
1319
+ action === "settings" &&
1320
+ (!actionValue || ["session", "local", "personal", "repo", "repository", "shared"].includes(actionValue));
1321
+ if (nonTui && settingsSelector) {
1322
+ return nonTuiGuidance(ctx, NON_TUI_SETTINGS_GUIDANCE);
1323
+ }
1324
+ const [layers, state] = await Promise.all([readFreeflowConfigLayers(ctx.cwd), readCapabilityState(ctx.cwd, ctx)]);
1325
+ const configState = layers.repository;
1326
+ const raw = configState.valid ? configState.parsed : {};
1327
+ if (action === "status") {
1328
+ ctx.ui.notify(freeflowStatusText(state, cognitiveRoutingController, diagnostics?.status()), "info");
1329
+ return { changed: false, reloaded: false };
1330
+ }
1331
+ if (!configState.valid) {
1332
+ ctx.ui.notify(freeflowStatusText(state, cognitiveRoutingController), "warning");
1333
+ return { changed: false, reloaded: false, error: "not_configured" };
1334
+ }
1335
+ if (layers.local.exists && !layers.local.valid) {
1336
+ ctx.ui.notify(
1337
+ `.freeflow/local.json is invalid; repair or remove it before changing Freeflow settings. ${layers.local.parseError ?? ""}`.trim(),
1338
+ "warning",
1339
+ );
1340
+ return { changed: false, reloaded: false, error: "invalid_local_config" };
1341
+ }
1342
+ if (["enable", "on", "true", "disable", "off", "false"].includes(action)) {
1343
+ if (!ensureSettingsIdle(ctx)) return { changed: false, reloaded: false, error: "busy" };
1344
+ const enabled = ["enable", "on", "true"].includes(action);
1345
+ const item = freeflowItems(raw, {
1346
+ scope: "repository",
1347
+ layers,
1348
+ cognitiveRouting: state.cognitiveRouting,
1349
+ toolExecution: state.toolExecution,
1350
+ compaction: state.compaction,
1351
+ ctx,
1352
+ runtimeAvailable: isCognitiveRoutingRuntimeAvailable(pi),
1353
+ }).find((candidate) => candidate.id === "freeflow.enabled");
1354
+ await updateConfig(ctx.cwd, item, enabled, "repository");
1355
+ await afterChange(true);
1356
+ ctx.ui.notify(`Freeflow ${enabled ? "enabled" : "disabled"}. Reloading Freeflow runtime...`, "info");
1357
+ if (typeof ctx.reload === "function") {
1358
+ await ctx.reload();
1359
+ return { changed: true, reloaded: true };
1360
+ }
1361
+ ctx.ui.notify("Run /reload for Freeflow changes to fully apply.", "warning");
1362
+ return { changed: true, reloaded: false };
1363
+ }
1364
+ if (action && action !== "settings") {
1365
+ ctx.ui.notify(
1366
+ "Usage: /freeflow, /freeflow settings [local|repo], /freeflow status, /freeflow enable, or /freeflow disable",
1367
+ "warning",
1368
+ );
1369
+ return { changed: false, reloaded: false, error: "invalid_action" };
1370
+ }
1371
+ let initialScope = "local";
1372
+ if (actionValue === "session") {
1373
+ initialScope = "session";
1374
+ } else if (["repo", "repository", "shared"].includes(actionValue)) {
1375
+ initialScope = "repository";
1376
+ } else if (actionValue && !["local", "personal"].includes(actionValue)) {
1377
+ ctx.ui.notify(
1378
+ "Usage: /freeflow settings, /freeflow settings session, /freeflow settings local, or /freeflow settings repo",
1379
+ "warning",
1380
+ );
1381
+ return { changed: false, reloaded: false, error: "invalid_scope" };
1382
+ }
1383
+ if (!ensureSettingsIdle(ctx)) return { changed: false, reloaded: false, error: "busy" };
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");
1426
+ return { changed: false, reloadRequired: false };
1427
+ }
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 };
1435
+ }
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 };
1451
+ }
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",
1464
+ );
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",
1475
+ );
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
+ }
1483
+ }
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 };
1489
+ }
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,
1504
+ };
1505
+ }
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 };
1511
+ }
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
+ );
1566
+ return { changed: session.changed, reloaded, error: session.failed ? "write_failed" : undefined };
1567
+ }
1568
+ const reloaded = await finalizeSessionSettings(session, ctx, afterChange);
1569
+ return { changed: session.changed, reloaded, error: session.failed ? "write_failed" : undefined };
1570
+ }