@hassangameryt/freeflow 0.7.3 → 0.9.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 (426) hide show
  1. package/.agents/plugins/marketplace.json +2 -2
  2. package/.claude-plugin/marketplace.json +1 -1
  3. package/.claude-plugin/plugin.json +2 -2
  4. package/.codex-plugin/plugin.json +3 -3
  5. package/.cursor-plugin/plugin.json +1 -1
  6. package/NOTICE +14 -0
  7. package/README.md +101 -159
  8. package/capabilities/cognitive-routing/SKILL.md +47 -210
  9. package/capabilities/cognitive-routing/references/coordinator.md +163 -0
  10. package/capabilities/cognitive-routing/references/worker.md +38 -0
  11. package/capabilities/compaction/SKILL.md +49 -0
  12. package/capabilities/compaction/references/summary-format.md +250 -0
  13. package/capabilities/messaging/SKILL.md +49 -0
  14. package/capabilities/task-memory/SKILL.md +87 -0
  15. package/command-surface.json +8 -52
  16. package/gemini-extension.json +1 -1
  17. package/hooks/claude/hooks.json +13 -3
  18. package/hooks/codex/hooks.json +2 -1
  19. package/hooks/shared/runtime-context.mjs +73 -83
  20. package/licenses/Apache-2.0.txt +55 -0
  21. package/package.json +42 -12
  22. package/pi-extension/README.md +41 -0
  23. package/pi-extension/dist/ask-user/ask.js +108 -0
  24. package/pi-extension/dist/ask-user/form.js +250 -0
  25. package/pi-extension/dist/ask-user/questions.js +66 -0
  26. package/pi-extension/dist/ask-user/tool.js +124 -0
  27. package/pi-extension/dist/cognitive-routing/assembler.js +417 -0
  28. package/pi-extension/dist/cognitive-routing/bootstrap.js +90 -0
  29. package/pi-extension/dist/{cognitive-routing-v2 → cognitive-routing}/config.js +8 -6
  30. package/pi-extension/dist/cognitive-routing/economics.js +45 -0
  31. package/pi-extension/dist/cognitive-routing/event-schema.js +388 -0
  32. package/pi-extension/dist/{session-sources/events.js → cognitive-routing/event-store.js} +107 -41
  33. package/pi-extension/dist/cognitive-routing/gate.js +256 -0
  34. package/pi-extension/dist/cognitive-routing/handlers.js +901 -0
  35. package/pi-extension/dist/cognitive-routing/handoffs.js +350 -0
  36. package/pi-extension/dist/cognitive-routing/model-control.js +476 -0
  37. package/pi-extension/dist/cognitive-routing/projection.js +520 -0
  38. package/pi-extension/dist/cognitive-routing/provenance.js +83 -0
  39. package/pi-extension/dist/{cognitive-routing-v2 → cognitive-routing}/render.js +15 -15
  40. package/pi-extension/dist/cognitive-routing/runtime.js +398 -0
  41. package/pi-extension/dist/{cognitive-routing-v2 → cognitive-routing}/schemas.js +47 -11
  42. package/pi-extension/dist/cognitive-routing/session.js +223 -0
  43. package/pi-extension/dist/{session-sources → cognitive-routing}/sources.js +126 -87
  44. package/pi-extension/dist/{cognitive-routing-v2 → cognitive-routing}/state.js +55 -386
  45. package/pi-extension/dist/cognitive-routing/status.js +81 -0
  46. package/pi-extension/dist/cognitive-routing/tools.js +74 -0
  47. package/pi-extension/dist/{cognitive-routing-v2 → cognitive-routing}/types.js +14 -11
  48. package/pi-extension/dist/compaction/carry.js +205 -0
  49. package/pi-extension/dist/compaction/config.js +24 -0
  50. package/pi-extension/dist/compaction/controller.js +494 -0
  51. package/pi-extension/dist/compaction/harness.js +93 -0
  52. package/pi-extension/dist/compaction/results.js +159 -0
  53. package/pi-extension/dist/compaction/thresholds.js +35 -0
  54. package/pi-extension/dist/compaction/tool.js +57 -0
  55. package/pi-extension/dist/host/branch.js +13 -0
  56. package/pi-extension/dist/host/canonical.js +12 -0
  57. package/pi-extension/dist/host/catalog.js +62 -0
  58. package/pi-extension/dist/host/config-validation.js +233 -0
  59. package/pi-extension/dist/host/config.js +361 -0
  60. package/pi-extension/dist/host/inbound.js +142 -0
  61. package/pi-extension/dist/host/projection-tags.js +60 -0
  62. package/pi-extension/dist/host/prompts.js +93 -0
  63. package/pi-extension/dist/{session-sources → host}/read-only-session.js +117 -1
  64. package/pi-extension/dist/{runtime → host}/request-history.js +125 -39
  65. package/pi-extension/dist/host/rows.js +25 -0
  66. package/pi-extension/dist/host/runtime-state.js +204 -0
  67. package/pi-extension/dist/host/section-keeper.js +85 -0
  68. package/pi-extension/dist/host/settings/freeflow-command.js +2323 -0
  69. package/pi-extension/dist/{settings/settings-tui.js → host/settings/settings-view.js} +205 -73
  70. package/pi-extension/dist/host/staging.js +28 -0
  71. package/pi-extension/dist/host/status.js +115 -0
  72. package/pi-extension/dist/index.js +1004 -188
  73. package/pi-extension/dist/messaging/config.js +27 -0
  74. package/pi-extension/dist/messaging/messaging.js +398 -0
  75. package/pi-extension/dist/messaging/registry.js +57 -0
  76. package/pi-extension/dist/messaging/render.js +90 -0
  77. package/pi-extension/dist/messaging/tools.js +109 -0
  78. package/pi-extension/dist/messaging/transport.js +101 -0
  79. package/pi-extension/dist/provider-support/cache/anchor.js +21 -0
  80. package/pi-extension/dist/provider-support/cache/anthropic.js +102 -0
  81. package/pi-extension/dist/provider-support/cache/health.js +73 -0
  82. package/pi-extension/dist/provider-support/cache/index.js +58 -0
  83. package/pi-extension/dist/provider-support/cache/keepalive.js +222 -0
  84. package/pi-extension/dist/provider-support/cache/monitor.js +17 -0
  85. package/pi-extension/dist/provider-support/cache/openai-responses.js +48 -0
  86. package/pi-extension/dist/provider-support/effort.js +13 -0
  87. package/pi-extension/dist/provider-support/index.js +44 -3
  88. package/pi-extension/dist/provider-support/{astra → openai}/adapter.js +46 -29
  89. package/pi-extension/dist/provider-support/{astra → openai}/history.js +10 -8
  90. package/pi-extension/dist/provider-support/{astra → openai}/index.js +8 -4
  91. package/pi-extension/dist/provider-support/{astra → openai}/session-state.js +29 -38
  92. package/pi-extension/dist/provider-support/routes.js +17 -0
  93. package/pi-extension/dist/task-memory/config.js +49 -0
  94. package/pi-extension/dist/task-memory/registry.js +119 -0
  95. package/pi-extension/dist/task-memory/render.js +107 -0
  96. package/pi-extension/dist/task-memory/runtime.js +994 -0
  97. package/pi-extension/dist/task-memory/tool.js +233 -0
  98. package/pi-extension/dist/tool-execution/apply-patch/grammar.js +23 -0
  99. package/pi-extension/dist/tool-execution/apply-patch/match.js +38 -0
  100. package/pi-extension/dist/tool-execution/apply-patch/parser.js +177 -0
  101. package/pi-extension/dist/tool-execution/apply-patch/plan.js +201 -0
  102. package/pi-extension/dist/tool-execution/apply-patch/tool.js +84 -0
  103. package/pi-extension/dist/tool-execution/background.js +430 -0
  104. package/pi-extension/dist/tool-execution/bash-guard.js +23 -0
  105. package/pi-extension/dist/tool-execution/config.js +33 -0
  106. package/pi-extension/dist/tool-execution/edit-messages.js +120 -0
  107. package/pi-extension/dist/tool-execution/file-state.js +138 -0
  108. package/pi-extension/dist/tool-execution/file-tracking.js +163 -0
  109. package/pi-extension/dist/tool-execution/guidance-notices.js +124 -0
  110. package/pi-extension/dist/tool-execution/prompt.js +54 -0
  111. package/pi-extension/dist/tool-execution/skill-reads.js +143 -0
  112. package/pi-extension/dist/tool-execution/tools.js +16 -0
  113. package/pi-extension/freeflow/README.md +9 -0
  114. package/runtime/prompts/cognitive-routing.md +4 -4
  115. package/runtime/prompts/core.md +29 -72
  116. package/runtime/prompts/interaction-contract.md +18 -8
  117. package/runtime/prompts/tool-execution.md +25 -0
  118. package/runtime/prompts/working-method.md +25 -0
  119. package/skills/deliver-work/SKILL.md +76 -0
  120. package/skills/deliver-work/references/branch-closeout.md +134 -0
  121. package/skills/deliver-work/references/commit.md +83 -0
  122. package/skills/deliver-work/references/launch.md +94 -0
  123. package/skills/deliver-work/references/release.md +85 -0
  124. package/skills/deliver-work/scripts/commit-state.mjs +139 -0
  125. package/skills/deliver-work/scripts/delivery-state.mjs +107 -0
  126. package/skills/deliver-work/scripts/lib/git-state.mjs +178 -0
  127. package/skills/deliver-work/scripts/push-route.mjs +122 -0
  128. package/skills/deliver-work/scripts/release-preflight.mjs +177 -0
  129. package/skills/discuss-work/SKILL.md +148 -0
  130. package/skills/{discuss → discuss-work}/references/discussion-continuity.md +13 -50
  131. package/skills/discuss-work/references/learning-action.md +36 -0
  132. package/skills/discuss-work/scripts/choices.mjs +101 -0
  133. package/skills/discuss-work/scripts/lib/choices.mjs +159 -0
  134. package/skills/evaluate-behavior/SKILL.md +107 -0
  135. package/skills/evaluate-behavior/references/authoring.md +381 -0
  136. package/skills/evaluate-behavior/references/coding-tasks.md +145 -0
  137. package/skills/evaluate-behavior/references/conversations-pipelines-judges.md +177 -0
  138. package/skills/evaluate-behavior/references/design-and-measurement.md +273 -0
  139. package/skills/evaluate-behavior/references/running-and-evidence.md +245 -0
  140. package/skills/evaluate-behavior/scripts/claude-guard.mjs +76 -0
  141. package/skills/evaluate-behavior/scripts/guard-rules.mjs +309 -0
  142. package/skills/evaluate-behavior/scripts/lib/agreement.mjs +90 -0
  143. package/skills/evaluate-behavior/scripts/lib/arms.mjs +55 -0
  144. package/skills/evaluate-behavior/scripts/lib/budget.mjs +87 -0
  145. package/skills/evaluate-behavior/scripts/lib/build-source.mjs +287 -0
  146. package/skills/evaluate-behavior/scripts/lib/clear.mjs +74 -0
  147. package/skills/evaluate-behavior/scripts/lib/container.mjs +395 -0
  148. package/skills/evaluate-behavior/scripts/lib/decision.mjs +418 -0
  149. package/skills/evaluate-behavior/scripts/lib/definitions.mjs +1072 -0
  150. package/skills/evaluate-behavior/scripts/lib/freeflow.mjs +103 -0
  151. package/skills/evaluate-behavior/scripts/lib/grade-command.mjs +591 -0
  152. package/skills/evaluate-behavior/scripts/lib/grade-comparison.mjs +90 -0
  153. package/skills/{evaluate-skill → evaluate-behavior}/scripts/lib/grade-interaction.mjs +19 -18
  154. package/skills/{evaluate-skill → evaluate-behavior}/scripts/lib/grade-support.mjs +32 -5
  155. package/skills/{evaluate-skill → evaluate-behavior}/scripts/lib/grade-workspace.mjs +28 -12
  156. package/skills/evaluate-behavior/scripts/lib/grade.mjs +101 -0
  157. package/skills/evaluate-behavior/scripts/lib/hooks.mjs +98 -0
  158. package/skills/evaluate-behavior/scripts/lib/hosts/claude-code/confinement.mjs +196 -0
  159. package/skills/evaluate-behavior/scripts/lib/hosts/claude-code/constraints.mjs +111 -0
  160. package/skills/evaluate-behavior/scripts/lib/hosts/claude-code/index.mjs +48 -0
  161. package/skills/evaluate-behavior/scripts/lib/hosts/claude-code/isolation.mjs +74 -0
  162. package/skills/evaluate-behavior/scripts/lib/hosts/claude-code/launch.mjs +123 -0
  163. package/skills/evaluate-behavior/scripts/lib/hosts/claude-code/model-call.mjs +73 -0
  164. package/skills/evaluate-behavior/scripts/lib/hosts/claude-code/recorder.mjs +178 -0
  165. package/skills/evaluate-behavior/scripts/lib/hosts/claude-code/session.mjs +494 -0
  166. package/skills/evaluate-behavior/scripts/lib/hosts/claude-code/skills.mjs +81 -0
  167. package/skills/evaluate-behavior/scripts/lib/hosts/index.mjs +31 -0
  168. package/skills/evaluate-behavior/scripts/lib/hosts/names.mjs +3 -0
  169. package/skills/evaluate-behavior/scripts/lib/hosts/pi/confinement.mjs +173 -0
  170. package/skills/evaluate-behavior/scripts/lib/hosts/pi/host.mjs +148 -0
  171. package/skills/evaluate-behavior/scripts/lib/hosts/pi/index.mjs +70 -0
  172. package/skills/evaluate-behavior/scripts/lib/hosts/pi/isolation.mjs +81 -0
  173. package/skills/evaluate-behavior/scripts/lib/hosts/pi/model-call.mjs +79 -0
  174. package/skills/evaluate-behavior/scripts/lib/hosts/pi/process.mjs +241 -0
  175. package/skills/{evaluate-skill/scripts/lib → evaluate-behavior/scripts/lib/hosts/pi}/rpc.mjs +144 -26
  176. package/skills/evaluate-behavior/scripts/lib/identity.mjs +82 -0
  177. package/skills/evaluate-behavior/scripts/lib/judge.mjs +688 -0
  178. package/skills/evaluate-behavior/scripts/lib/metrics.mjs +332 -0
  179. package/skills/evaluate-behavior/scripts/lib/model-call.mjs +17 -0
  180. package/skills/evaluate-behavior/scripts/lib/participants.mjs +616 -0
  181. package/skills/{evaluate-skill → evaluate-behavior}/scripts/lib/process.mjs +22 -0
  182. package/skills/evaluate-behavior/scripts/lib/queue.mjs +54 -0
  183. package/skills/evaluate-behavior/scripts/lib/reference.mjs +343 -0
  184. package/skills/evaluate-behavior/scripts/lib/reuse.mjs +277 -0
  185. package/skills/evaluate-behavior/scripts/lib/review.mjs +933 -0
  186. package/skills/evaluate-behavior/scripts/lib/runner.mjs +732 -0
  187. package/skills/{evaluate-skill → evaluate-behavior}/scripts/lib/sandbox.mjs +121 -14
  188. package/skills/evaluate-behavior/scripts/lib/scan-shell.mjs +457 -0
  189. package/skills/evaluate-behavior/scripts/lib/scan.mjs +479 -0
  190. package/skills/evaluate-behavior/scripts/lib/seal.mjs +91 -0
  191. package/skills/evaluate-behavior/scripts/lib/shell.mjs +560 -0
  192. package/skills/evaluate-behavior/scripts/lib/source-override.mjs +64 -0
  193. package/skills/evaluate-behavior/scripts/lib/subject-file.mjs +88 -0
  194. package/skills/{evaluate-skill/scripts/lib/pi.mjs → evaluate-behavior/scripts/lib/subject.mjs} +239 -262
  195. package/skills/evaluate-behavior/scripts/lib/task-copy.mjs +124 -0
  196. package/skills/evaluate-behavior/scripts/lib/trials.mjs +524 -0
  197. package/skills/evaluate-behavior/scripts/lib/validity.mjs +68 -0
  198. package/skills/evaluate-behavior/scripts/lib/view-html.mjs +454 -0
  199. package/skills/{evaluate-skill → evaluate-behavior}/scripts/lib/view.mjs +218 -11
  200. package/skills/evaluate-behavior/scripts/pi-command-tool.mjs +159 -0
  201. package/skills/evaluate-behavior/scripts/pi-container-route.mjs +23 -0
  202. package/skills/evaluate-behavior/scripts/pi-guard.mjs +77 -0
  203. package/skills/evaluate-behavior/scripts/pi-login-refresh.mjs +31 -0
  204. package/skills/evaluate-behavior/scripts/skill-eval.mjs +586 -0
  205. package/skills/execute-work/SKILL.md +96 -72
  206. package/skills/execute-work/references/code-practices.md +27 -49
  207. package/skills/execute-work/references/design.md +271 -0
  208. package/skills/execute-work/references/diagnose.md +117 -0
  209. package/skills/execute-work/references/migration.md +108 -0
  210. package/skills/execute-work/references/simplify.md +65 -0
  211. package/skills/execute-work/scripts/self-review.mjs +422 -0
  212. package/skills/execute-work/scripts/touch-map.mjs +290 -0
  213. package/skills/review-work/SKILL.md +94 -64
  214. package/skills/review-work/references/adjudication.md +94 -0
  215. package/skills/review-work/references/artifact.md +198 -0
  216. package/skills/review-work/references/evals-and-judges.md +66 -0
  217. package/skills/review-work/references/work.md +177 -0
  218. package/skills/review-work/scripts/check-brief.mjs +128 -0
  219. package/skills/review-work/scripts/review-packet.mjs +233 -0
  220. package/skills/setup-freeflow/SKILL.md +69 -16
  221. package/skills/track-work/SKILL.md +135 -151
  222. package/skills/track-work/references/handoff.md +190 -0
  223. package/skills/track-work/references/projects.md +51 -0
  224. package/skills/track-work/references/working-record-format.md +201 -344
  225. package/skills/track-work/scripts/lib/document.mjs +79 -19
  226. package/skills/track-work/scripts/lib/format.mjs +18 -1
  227. package/skills/track-work/scripts/lib/recover.mjs +254 -0
  228. package/skills/track-work/scripts/lib/registry.mjs +656 -0
  229. package/skills/track-work/scripts/lib/search.mjs +40 -0
  230. package/skills/track-work/scripts/lib/store.mjs +53 -21
  231. package/skills/track-work/scripts/lib/transitions.mjs +79 -4
  232. package/skills/track-work/scripts/lib/views.mjs +50 -1
  233. package/skills/track-work/scripts/working-record.mjs +263 -22
  234. package/skills/verify-work/SKILL.md +64 -47
  235. package/skills/verify-work/references/feature-map.md +120 -0
  236. package/skills/verify-work/references/runtime-evidence.md +203 -0
  237. package/skills/verify-work/references/test-design.md +27 -6
  238. package/skills/verify-work/scripts/test-changes.mjs +246 -0
  239. package/skills/write-docs/SKILL.md +171 -0
  240. package/skills/write-docs/references/documentation-shapes.md +184 -0
  241. package/skills/write-docs/references/spec-and-plan-shapes.md +308 -0
  242. package/skills/write-docs/scripts/artifact-check.mjs +148 -0
  243. package/skills/write-docs/scripts/docs-check.mjs +229 -0
  244. package/skills/write-docs/scripts/plan-lint.mjs +123 -0
  245. package/skills/write-skill/SKILL.md +165 -165
  246. package/skills/write-skill/references/activation-and-wording.md +143 -0
  247. package/skills/write-skill/references/placement-and-dependencies.md +154 -0
  248. package/skills/write-skill/scripts/lib/compare.mjs +701 -0
  249. package/skills/write-skill/scripts/lib/similarity.mjs +56 -0
  250. package/skills/write-skill/scripts/lib/skill-author.mjs +197 -3
  251. package/skills/write-skill/scripts/skill-author.mjs +67 -8
  252. package/capabilities/context-virtualization/SKILL.md +0 -103
  253. package/capabilities/conversation-history/SKILL.md +0 -53
  254. package/hooks/tests/check-host-adapters.test.mjs +0 -82
  255. package/hooks/tests/check-runtime-context-hook.sh +0 -212
  256. package/pi-extension/dist/cognitive-routing-v2/projection.js +0 -330
  257. package/pi-extension/dist/cognitive-routing-v2/runtime.js +0 -2181
  258. package/pi-extension/dist/cognitive-routing-v2/tools.js +0 -60
  259. package/pi-extension/dist/context-virtualization/commands.js +0 -89
  260. package/pi-extension/dist/context-virtualization/projector.js +0 -42
  261. package/pi-extension/dist/context-virtualization/runtime.js +0 -378
  262. package/pi-extension/dist/context-virtualization/state.js +0 -57
  263. package/pi-extension/dist/context-virtualization/types.js +0 -10
  264. package/pi-extension/dist/conversation-history/passages.js +0 -66
  265. package/pi-extension/dist/conversation-history/ranking.js +0 -105
  266. package/pi-extension/dist/conversation-history/runtime.js +0 -226
  267. package/pi-extension/dist/conversation-history/search.js +0 -56
  268. package/pi-extension/dist/conversation-history/types.js +0 -1
  269. package/pi-extension/dist/freeflow-context/resolver.js +0 -120
  270. package/pi-extension/dist/freeflow-context/runtime.js +0 -80
  271. package/pi-extension/dist/freeflow-context/source-projector.js +0 -95
  272. package/pi-extension/dist/freeflow-context/tool.js +0 -314
  273. package/pi-extension/dist/freeflow-context/types.js +0 -12
  274. package/pi-extension/dist/freeflow-context/visibility.js +0 -28
  275. package/pi-extension/dist/integrations/mcp-config.js +0 -29
  276. package/pi-extension/dist/runtime/runtime-context.js +0 -754
  277. package/pi-extension/dist/runtime/runtime-identity.js +0 -5
  278. package/pi-extension/dist/session-sources/provenance.js +0 -32
  279. package/pi-extension/dist/settings/settings-ui.js +0 -1360
  280. package/pi-extension/dist/ui/text-component.js +0 -57
  281. package/pi-extension/src/cognitive-routing-v2/budget.ts +0 -42
  282. package/pi-extension/src/cognitive-routing-v2/config.ts +0 -173
  283. package/pi-extension/src/cognitive-routing-v2/projection.ts +0 -375
  284. package/pi-extension/src/cognitive-routing-v2/render.ts +0 -277
  285. package/pi-extension/src/cognitive-routing-v2/runtime.ts +0 -2241
  286. package/pi-extension/src/cognitive-routing-v2/schemas.ts +0 -180
  287. package/pi-extension/src/cognitive-routing-v2/state.ts +0 -839
  288. package/pi-extension/src/cognitive-routing-v2/tools.ts +0 -62
  289. package/pi-extension/src/cognitive-routing-v2/types.ts +0 -254
  290. package/pi-extension/src/context-virtualization/commands.ts +0 -111
  291. package/pi-extension/src/context-virtualization/projector.ts +0 -62
  292. package/pi-extension/src/context-virtualization/runtime.ts +0 -428
  293. package/pi-extension/src/context-virtualization/state.ts +0 -79
  294. package/pi-extension/src/context-virtualization/types.ts +0 -92
  295. package/pi-extension/src/conversation-history/passages.ts +0 -81
  296. package/pi-extension/src/conversation-history/ranking.ts +0 -155
  297. package/pi-extension/src/conversation-history/runtime.ts +0 -250
  298. package/pi-extension/src/conversation-history/search.ts +0 -79
  299. package/pi-extension/src/conversation-history/types.ts +0 -31
  300. package/pi-extension/src/freeflow-context/resolver.ts +0 -139
  301. package/pi-extension/src/freeflow-context/runtime.ts +0 -100
  302. package/pi-extension/src/freeflow-context/source-projector.ts +0 -106
  303. package/pi-extension/src/freeflow-context/tool.ts +0 -366
  304. package/pi-extension/src/freeflow-context/types.ts +0 -70
  305. package/pi-extension/src/freeflow-context/visibility.ts +0 -53
  306. package/pi-extension/src/index.ts +0 -390
  307. package/pi-extension/src/integrations/mcp-config.ts +0 -32
  308. package/pi-extension/src/provider-support/astra/adapter.ts +0 -108
  309. package/pi-extension/src/provider-support/astra/history.ts +0 -145
  310. package/pi-extension/src/provider-support/astra/index.ts +0 -12
  311. package/pi-extension/src/provider-support/astra/session-state.ts +0 -113
  312. package/pi-extension/src/provider-support/index.ts +0 -5
  313. package/pi-extension/src/runtime/request-history.ts +0 -243
  314. package/pi-extension/src/runtime/runtime-context.ts +0 -860
  315. package/pi-extension/src/runtime/runtime-identity.ts +0 -8
  316. package/pi-extension/src/session-sources/events.ts +0 -230
  317. package/pi-extension/src/session-sources/provenance.ts +0 -39
  318. package/pi-extension/src/session-sources/read-only-session.ts +0 -170
  319. package/pi-extension/src/session-sources/sources.ts +0 -334
  320. package/pi-extension/src/settings/settings-tui.ts +0 -501
  321. package/pi-extension/src/settings/settings-ui.ts +0 -1652
  322. package/pi-extension/src/ui/text-component.ts +0 -66
  323. package/pi-extension/tests/cache-reuse/history.test.js +0 -117
  324. package/pi-extension/tests/cache-reuse/native.test.js +0 -91
  325. package/pi-extension/tests/cache-reuse/settings.test.js +0 -211
  326. package/pi-extension/tests/cognitive-routing-v2/communication.test.js +0 -577
  327. package/pi-extension/tests/cognitive-routing-v2/compaction-input.test.js +0 -183
  328. package/pi-extension/tests/cognitive-routing-v2/delegation.test.js +0 -557
  329. package/pi-extension/tests/cognitive-routing-v2/native.test.js +0 -474
  330. package/pi-extension/tests/cognitive-routing-v2/performance.test.js +0 -87
  331. package/pi-extension/tests/cognitive-routing-v2/persistence.test.js +0 -300
  332. package/pi-extension/tests/cognitive-routing-v2/projection.test.js +0 -241
  333. package/pi-extension/tests/cognitive-routing-v2/recovery.test.js +0 -1034
  334. package/pi-extension/tests/cognitive-routing-v2/render.test.js +0 -244
  335. package/pi-extension/tests/cognitive-routing-v2/runtime.test.js +0 -423
  336. package/pi-extension/tests/cognitive-routing-v2/stabilization.test.js +0 -521
  337. package/pi-extension/tests/cognitive-routing-v2/state.test.js +0 -586
  338. package/pi-extension/tests/context-virtualization/extension.test.js +0 -775
  339. package/pi-extension/tests/context-virtualization/projector.test.js +0 -110
  340. package/pi-extension/tests/context-virtualization/runtime.test.js +0 -230
  341. package/pi-extension/tests/conversation-history/runtime.test.js +0 -98
  342. package/pi-extension/tests/conversation-history/search.test.js +0 -97
  343. package/pi-extension/tests/fixtures/pi-host.js +0 -4
  344. package/pi-extension/tests/fixtures/routing-native.js +0 -191
  345. package/pi-extension/tests/integration/mode-free-core.test.js +0 -220
  346. package/pi-extension/tests/integration/pi-extension.test.js +0 -1447
  347. package/pi-extension/tests/integration/prompt-architecture.test.js +0 -447
  348. package/pi-extension/tests/integration/unaffected-regressions.test.js +0 -478
  349. package/pi-extension/tests/provider-support/astra-boundaries.test.js +0 -75
  350. package/pi-extension/tests/provider-support/astra.test.js +0 -306
  351. package/pi-extension/tests/provider-support/cli.test.js +0 -92
  352. package/pi-extension/tests/provider-support/native.test.js +0 -183
  353. package/pi-extension/tests/settings/settings-tui.test.js +0 -243
  354. package/pi-extension/tsconfig.json +0 -16
  355. package/plugin.json +0 -33
  356. package/runtime/prompts/context-virtualization.md +0 -3
  357. package/runtime/prompts/conversation-history.md +0 -3
  358. package/skills/action-selection/SKILL.md +0 -100
  359. package/skills/action-selection/references/trajectory-stalls.md +0 -71
  360. package/skills/bypass/SKILL.md +0 -153
  361. package/skills/commit-work/SKILL.md +0 -145
  362. package/skills/commit-work/references/git-state-edges.md +0 -94
  363. package/skills/commit-work/references/simple-push.md +0 -92
  364. package/skills/commit-work/references/staging-decisions.md +0 -106
  365. package/skills/decision-gate/SKILL.md +0 -106
  366. package/skills/design-for-depth/SKILL.md +0 -117
  367. package/skills/design-for-depth/references/design-pressure-signals.md +0 -182
  368. package/skills/design-for-depth/references/implementation-decisions.md +0 -76
  369. package/skills/design-for-depth/references/interface-design-loop.md +0 -136
  370. package/skills/design-for-depth/references/module-and-dependency-design.md +0 -147
  371. package/skills/design-for-depth/references/software-design-philosophy.md +0 -202
  372. package/skills/design-for-depth/references/state-and-failure-boundaries.md +0 -136
  373. package/skills/diagnose-failure/SKILL.md +0 -125
  374. package/skills/diagnose-failure/references/diagnostic-loop-catalog.md +0 -48
  375. package/skills/diagnose-failure/references/flaky-and-performance-diagnosis.md +0 -51
  376. package/skills/discuss/SKILL.md +0 -116
  377. package/skills/evaluate-skill/SKILL.md +0 -219
  378. package/skills/evaluate-skill/references/evaluation-definition-schema.md +0 -385
  379. package/skills/evaluate-skill/references/evaluation-design.md +0 -112
  380. package/skills/evaluate-skill/references/execution-and-evidence.md +0 -90
  381. package/skills/evaluate-skill/references/review-and-revision.md +0 -92
  382. package/skills/evaluate-skill/scripts/lib/definitions.mjs +0 -407
  383. package/skills/evaluate-skill/scripts/lib/grade-comparison.mjs +0 -60
  384. package/skills/evaluate-skill/scripts/lib/grade.mjs +0 -64
  385. package/skills/evaluate-skill/scripts/lib/runner.mjs +0 -368
  386. package/skills/evaluate-skill/scripts/pi-guard.mjs +0 -164
  387. package/skills/evaluate-skill/scripts/skill-eval.mjs +0 -102
  388. package/skills/execute-work/references/execute-work-edges.md +0 -68
  389. package/skills/finish-branch/SKILL.md +0 -117
  390. package/skills/finish-branch/references/integration-options.md +0 -80
  391. package/skills/handoff/SKILL.md +0 -82
  392. package/skills/handoff/references/handoff-templates.md +0 -150
  393. package/skills/launch-work/SKILL.md +0 -116
  394. package/skills/launch-work/references/launch-readiness.md +0 -85
  395. package/skills/migration-work/SKILL.md +0 -115
  396. package/skills/migration-work/references/migration-lifecycle.md +0 -82
  397. package/skills/release-work/SKILL.md +0 -118
  398. package/skills/release-work/references/release-evidence.md +0 -78
  399. package/skills/review-artifact/SKILL.md +0 -142
  400. package/skills/review-artifact/references/adjudicate-artifact-review.md +0 -101
  401. package/skills/review-artifact/references/independent-artifact-reviewer-contract.md +0 -159
  402. package/skills/review-work/references/adjudicate-work-review.md +0 -97
  403. package/skills/review-work/references/independent-work-reviewer-contract.md +0 -139
  404. package/skills/review-work/references/security-risk-lens.md +0 -69
  405. package/skills/setup-freeflow/references/activation-contract.md +0 -109
  406. package/skills/setup-freeflow/references/host-setup.md +0 -67
  407. package/skills/simplify-code/SKILL.md +0 -85
  408. package/skills/simplify-code/references/simplification-patterns.md +0 -70
  409. package/skills/verify-work/references/browser-runtime-evidence.md +0 -55
  410. package/skills/verify-work/references/integration-evidence.md +0 -47
  411. package/skills/verify-work/references/performance-evidence.md +0 -57
  412. package/skills/workflow/SKILL.md +0 -127
  413. package/skills/workflow/references/domain-skill-composition.md +0 -43
  414. package/skills/write-plan/SKILL.md +0 -92
  415. package/skills/write-plan/references/plan-shapes.md +0 -156
  416. package/skills/write-skill/references/activation-boundaries.md +0 -104
  417. package/skills/write-skill/references/agent-first-instructions.md +0 -121
  418. package/skills/write-skill/references/progressive-disclosure.md +0 -127
  419. package/skills/write-skill/references/skill-dependency-graphs.md +0 -190
  420. package/skills/write-spec/SKILL.md +0 -92
  421. package/skills/write-spec/references/artifact-standards.md +0 -86
  422. package/skills/write-spec/references/decision-records.md +0 -78
  423. package/skills/write-spec/references/spec-shapes.md +0 -143
  424. /package/pi-extension/dist/{cognitive-routing-v2 → cognitive-routing}/budget.js +0 -0
  425. /package/skills/{evaluate-skill → evaluate-behavior}/scripts/lib/evidence.mjs +0 -0
  426. /package/skills/{evaluate-skill → evaluate-behavior}/scripts/pi-observer.mjs +0 -0
@@ -0,0 +1,2323 @@
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 {
14
+ REVIEW_MODES,
15
+ RUNTIME_PROMPTS,
16
+ SWITCHABLE_SKILLS,
17
+ resolveReviewSettings,
18
+ sectionEnabled,
19
+ } from "../config-validation.js";
20
+ import { freeflowStatusText } from "../status.js";
21
+ import { PiSettingsComponent } from "./settings-view.js";
22
+ import { workersForDelegation } from "../../cognitive-routing/types.js";
23
+ import { DEFAULT_TOOL_EXECUTION_CONFIG } from "../../tool-execution/config.js";
24
+ import { DEFAULT_COMPACTION_CARRY, DEFAULT_COMPACTION_ENABLED } from "../../compaction/config.js";
25
+ import { AUTO_APPROVE_GROUPS, DEFAULT_TASK_MEMORY_ENABLED } from "../../task-memory/config.js";
26
+ import { DEFAULT_INBOUND_POLICY, DEFAULT_MESSAGING_ENABLED, INBOUND_POLICIES } from "../../messaging/config.js";
27
+ /*
28
+ * The /freeflow command and the settings model it edits: every Freeflow setting with its repository, personal, and
29
+ * session scopes, the routing profile wizard, config file writes, and reload after saving. settings-view.ts draws the
30
+ * screen; this module decides what is on it.
31
+ */
32
+ const DEFAULT_FREEFLOW_ENABLED = true;
33
+ const LOCAL_INHERIT = "inherit";
34
+ /** How a value's source reads in the settings screen. */
35
+ const SOURCE_LABELS = {
36
+ builtin: "default",
37
+ local: "personal",
38
+ repository: "repository",
39
+ session: "session",
40
+ };
41
+ const execFileAsync = promisify(execFile);
42
+ function isRecord(value) {
43
+ return typeof value === "object" && value !== null && !Array.isArray(value);
44
+ }
45
+ function cloneJson(value) {
46
+ if (value === undefined) return value;
47
+ try {
48
+ return JSON.parse(JSON.stringify(value));
49
+ } catch (error) {
50
+ throw new Error(`Could not clone settings value: ${error instanceof Error ? error.message : String(error)}`);
51
+ }
52
+ }
53
+ function getPath(source, path) {
54
+ let current = source;
55
+ for (const key of path) {
56
+ if (!isRecord(current)) return undefined;
57
+ current = current[key];
58
+ }
59
+ return current;
60
+ }
61
+ function setPath(target, path, value) {
62
+ let current = target;
63
+ for (const key of path.slice(0, -1)) {
64
+ const next = current[key];
65
+ if (!isRecord(next)) {
66
+ current[key] = {};
67
+ }
68
+ current = current[key];
69
+ }
70
+ current[path[path.length - 1]] = value;
71
+ }
72
+ function deletePath(target, path) {
73
+ let current = target;
74
+ const parents = [];
75
+ for (const key of path.slice(0, -1)) {
76
+ if (!isRecord(current)) return;
77
+ parents.push({ object: current, key });
78
+ current = current[key];
79
+ }
80
+ if (!isRecord(current)) return;
81
+ delete current[path[path.length - 1]];
82
+ for (let index = parents.length - 1; index >= 0; index--) {
83
+ const { object, key } = parents[index];
84
+ const child = object[key];
85
+ if (isRecord(child) && Object.keys(child).length === 0) {
86
+ delete object[key];
87
+ }
88
+ }
89
+ }
90
+ function valuesEqual(a, b) {
91
+ return JSON.stringify(a) === JSON.stringify(b);
92
+ }
93
+ function isEmptyValue(value) {
94
+ return Array.isArray(value) && value.length === 0;
95
+ }
96
+ function setConfigValue(config, item, value) {
97
+ if (!item.path?.length) {
98
+ throw new Error(`${item.label} is a settings group, not a writable setting.`);
99
+ }
100
+ if (value === undefined || (item.defaultValue !== undefined && valuesEqual(value, item.defaultValue))) {
101
+ deletePath(config, item.path);
102
+ return;
103
+ }
104
+ if (isEmptyValue(value)) {
105
+ deletePath(config, item.path);
106
+ return;
107
+ }
108
+ setPath(config, item.path, value);
109
+ }
110
+ /** What choosing inherit means, for the help line: "Follow the default (on)." */
111
+ function inheritText(value, source) {
112
+ return source === "default" ? `Inherit Freeflow's default (${value}).` : `Inherit the ${source} setting (${value}).`;
113
+ }
114
+ function booleanValue(value) {
115
+ return value === true ? "on" : "off";
116
+ }
117
+ function configValueForChoice(item, value) {
118
+ const key = String(value);
119
+ if (item.configValues && Object.hasOwn(item.configValues, key)) {
120
+ return item.configValues[key];
121
+ }
122
+ return value;
123
+ }
124
+ function effectiveItemValue(item) {
125
+ return item.effectiveValue === undefined ? item.value : item.effectiveValue;
126
+ }
127
+ function formatCoreValue(value) {
128
+ if (typeof value === "boolean") return booleanValue(value);
129
+ if (isRecord(value) && typeof value.provider === "string" && typeof value.model === "string") {
130
+ const effort = typeof value.thinking === "string" ? ` · ${value.thinking}` : "";
131
+ return `${value.provider}/${value.model}${effort}`;
132
+ }
133
+ return String(value ?? "");
134
+ }
135
+ function coreDisplaySuffix(item, inactive = false) {
136
+ const source = item.effectiveSource ?? "builtin";
137
+ const parts =
138
+ item.configScope === "repository" && source === "local"
139
+ ? [`effective ${formatCoreValue(effectiveItemValue(item))}`, source]
140
+ : [source];
141
+ if (inactive) parts.push("inactive");
142
+ return `(${parts.join(" · ")})`;
143
+ }
144
+ function updateScopedItemState(item, value) {
145
+ if (!item.configScope) return;
146
+ const selectedValue = configValueForChoice(item, value);
147
+ const configValue = selectedValue === undefined || isEmptyValue(selectedValue) ? undefined : selectedValue;
148
+ if (item.configScope === "session" || item.configScope === "local") {
149
+ item.effectiveValue = configValue === undefined ? item.inheritedValue : configValue;
150
+ item.effectiveSource = configValue === undefined ? (item.inheritedSource ?? "builtin") : item.configScope;
151
+ return;
152
+ }
153
+ if (item.localOverrideValue !== undefined) {
154
+ item.effectiveValue = item.localOverrideValue;
155
+ item.effectiveSource = "local";
156
+ return;
157
+ }
158
+ item.effectiveValue = configValue;
159
+ item.effectiveSource =
160
+ item.defaultValue !== undefined && valuesEqual(configValue, item.defaultValue) ? "builtin" : "repository";
161
+ }
162
+ function createScopedBooleanItem(options) {
163
+ const repositoryValue = getPath(options.rawConfig, options.path);
164
+ const inheritedValue = typeof repositoryValue === "boolean" ? repositoryValue : options.defaultValue;
165
+ const inheritedSource = typeof repositoryValue === "boolean" ? "repository" : "builtin";
166
+ const localValue = getPath(options.localConfig, options.path);
167
+ let item;
168
+ if (options.scope === "local") {
169
+ const selectedValue = typeof localValue === "boolean" ? String(localValue) : LOCAL_INHERIT;
170
+ item = {
171
+ id: options.id,
172
+ label: options.label,
173
+ description: options.description,
174
+ path: options.path,
175
+ kind: "enum",
176
+ value: selectedValue,
177
+ values: [LOCAL_INHERIT, "true", "false"],
178
+ valueLabels: { inherit: "inherit", true: "on", false: "off" },
179
+ valueDescriptions: {
180
+ inherit: inheritText(formatCoreValue(inheritedValue), SOURCE_LABELS[inheritedSource]),
181
+ true: "Enabled for you in this checkout.",
182
+ false: "Disabled for you in this checkout.",
183
+ },
184
+ format: (value) => {
185
+ if (value === LOCAL_INHERIT) return LOCAL_INHERIT;
186
+ return booleanValue(value === "true");
187
+ },
188
+ configScope: "local",
189
+ configValues: {
190
+ inherit: undefined,
191
+ true: true,
192
+ false: false,
193
+ },
194
+ effectiveValue: options.effectiveValue,
195
+ effectiveSource: options.effectiveSource,
196
+ inheritedValue,
197
+ inheritedSource,
198
+ };
199
+ } else {
200
+ item = {
201
+ id: options.id,
202
+ label: options.label,
203
+ description: options.description,
204
+ path: options.path,
205
+ kind: "boolean",
206
+ value: inheritedValue,
207
+ defaultValue: options.defaultValue,
208
+ configScope: "repository",
209
+ repositoryUnset: typeof repositoryValue !== "boolean",
210
+ effectiveValue: options.effectiveValue,
211
+ effectiveSource: options.effectiveSource,
212
+ localOverrideValue: typeof localValue === "boolean" ? localValue : undefined,
213
+ };
214
+ }
215
+ item.displaySuffix = coreDisplaySuffix(item);
216
+ return item;
217
+ }
218
+ /** A choice among fixed values, in the Personal or Repository scope; Personal can inherit the repository's value. */
219
+ function createScopedChoiceItem(options) {
220
+ const repositoryValue = getPath(options.rawConfig, options.path);
221
+ const inheritedValue = options.values.includes(repositoryValue) ? repositoryValue : options.defaultValue;
222
+ const inheritedSource = repositoryValue === undefined ? "builtin" : "repository";
223
+ const localValue = getPath(options.localConfig, options.path);
224
+ const common = {
225
+ id: options.id,
226
+ label: options.label,
227
+ path: options.path,
228
+ kind: "enum",
229
+ effectiveValue: options.effectiveValue,
230
+ effectiveSource: options.effectiveSource,
231
+ };
232
+ return options.scope === "local"
233
+ ? {
234
+ ...common,
235
+ description: options.description,
236
+ value: options.values.includes(localValue) ? localValue : LOCAL_INHERIT,
237
+ values: [LOCAL_INHERIT, ...options.values],
238
+ valueLabels: { inherit: "inherit", ...options.valueLabels },
239
+ valueDescriptions: {
240
+ inherit: inheritText(options.valueLabels[inheritedValue] ?? inheritedValue, SOURCE_LABELS[inheritedSource]),
241
+ ...options.valueDescriptions,
242
+ },
243
+ configScope: "local",
244
+ configValues: Object.fromEntries([["inherit", undefined], ...options.values.map((value) => [value, value])]),
245
+ inheritedValue,
246
+ inheritedSource,
247
+ }
248
+ : {
249
+ ...common,
250
+ description: `${options.description} This edits shared .freeflow/config.json.`,
251
+ value: inheritedValue,
252
+ values: [...options.values],
253
+ valueLabels: options.valueLabels,
254
+ valueDescriptions: options.valueDescriptions,
255
+ defaultValue: options.defaultValue,
256
+ configScope: "repository",
257
+ repositoryUnset: repositoryValue === undefined,
258
+ localOverrideValue: options.values.includes(localValue) ? localValue : undefined,
259
+ };
260
+ }
261
+ function createScopedDelegationItem(options) {
262
+ const path = ["cognitiveRouting", "delegation"];
263
+ const repositoryValue = getPath(options.rawConfig, path);
264
+ const inheritedValue = ["executor", "helper", "both"].includes(repositoryValue) ? repositoryValue : "executor";
265
+ const inheritedSource = repositoryValue === undefined ? "builtin" : "repository";
266
+ const localValue = getPath(options.localConfig, path);
267
+ const descriptions = {
268
+ executor: "Coordinator delegates assignments to Executor.",
269
+ helper: "Coordinator delegates supporting assignments to Helper and keeps production implementation.",
270
+ both: "Coordinator chooses Helper or Executor for each assignment.",
271
+ };
272
+ const item =
273
+ options.scope === "local"
274
+ ? {
275
+ id: "freeflow.cognitiveRouting.delegation",
276
+ label: "Delegation",
277
+ description: "Choose enabled workers for automatic routing; this setting does not rank model presets.",
278
+ path,
279
+ kind: "enum",
280
+ value: ["executor", "helper", "both"].includes(localValue) ? localValue : LOCAL_INHERIT,
281
+ values: [LOCAL_INHERIT, "executor", "helper", "both"],
282
+ valueLabels: {
283
+ inherit: "inherit",
284
+ executor: "Executor only",
285
+ helper: "Helper only",
286
+ both: "Helper and Executor",
287
+ },
288
+ valueDescriptions: {
289
+ inherit: inheritText(inheritedValue, SOURCE_LABELS[inheritedSource]),
290
+ ...descriptions,
291
+ },
292
+ configScope: "local",
293
+ configValues: { inherit: undefined, executor: "executor", helper: "helper", both: "both" },
294
+ effectiveValue: options.effectiveValue,
295
+ effectiveSource: options.effectiveSource,
296
+ inheritedValue,
297
+ inheritedSource,
298
+ }
299
+ : {
300
+ id: "freeflow.cognitiveRouting.delegation",
301
+ label: "Delegation",
302
+ description: "Choose enabled workers for automatic routing. This edits shared .freeflow/config.json.",
303
+ path,
304
+ kind: "enum",
305
+ value: inheritedValue,
306
+ values: ["executor", "helper", "both"],
307
+ valueLabels: {
308
+ executor: "Executor only",
309
+ helper: "Helper only",
310
+ both: "Helper and Executor",
311
+ },
312
+ valueDescriptions: descriptions,
313
+ defaultValue: "executor",
314
+ configScope: "repository",
315
+ repositoryUnset: repositoryValue === undefined,
316
+ effectiveValue: options.effectiveValue,
317
+ effectiveSource: options.effectiveSource,
318
+ localOverrideValue: ["executor", "helper", "both"].includes(localValue) ? localValue : undefined,
319
+ };
320
+ item.displaySuffix = coreDisplaySuffix(item);
321
+ return item;
322
+ }
323
+ function resolveSettingsCoreView(rawConfig, layers) {
324
+ const localConfig = layers?.local.valid && isRecord(layers.local.parsed) ? layers.local.parsed : {};
325
+ const fallbackCore = { enabled: getPath(rawConfig, ["enabled"]) !== false };
326
+ const fallbackSources = {
327
+ enabled: typeof getPath(rawConfig, ["enabled"]) === "boolean" ? "repository" : "builtin",
328
+ };
329
+ return {
330
+ localConfig,
331
+ core: layers?.coreConfig ?? fallbackCore,
332
+ sources: layers?.sources ?? fallbackSources,
333
+ };
334
+ }
335
+ function createSessionBooleanItem(options) {
336
+ const override = options.sessionOverrides[options.key];
337
+ return {
338
+ id: options.id,
339
+ label: options.label,
340
+ description: options.description,
341
+ path: options.key.split("."),
342
+ kind: "enum",
343
+ value: typeof override === "boolean" ? String(override) : LOCAL_INHERIT,
344
+ values: [LOCAL_INHERIT, "true", "false"],
345
+ valueLabels: { inherit: "inherit", true: "on", false: "off" },
346
+ valueDescriptions: {
347
+ inherit: inheritText(formatCoreValue(options.inheritedValue), SOURCE_LABELS[options.inheritedSource]),
348
+ true: "Enabled for this Pi session only.",
349
+ false: "Disabled for this Pi session only.",
350
+ },
351
+ configScope: "session",
352
+ configValues: { inherit: undefined, true: true, false: false },
353
+ effectiveValue: options.effectiveValue,
354
+ effectiveSource: typeof override === "boolean" ? "session" : options.inheritedSource,
355
+ inheritedValue: options.inheritedValue,
356
+ inheritedSource: options.inheritedSource,
357
+ };
358
+ }
359
+ const AUTO_APPROVE_LABELS = {
360
+ attach: ["Attach", "attaching a record, and making a held record writable"],
361
+ create: ["Create", "creating a task or a child task"],
362
+ env: ["Directories", "changing a task's directories"],
363
+ fanout: ["Fan-out", "a child task with its own Git worktree and branch"],
364
+ projects: ["Projects", "creating or linking a project, and importing a record into one"],
365
+ };
366
+ const INBOUND_LABELS = { accept: "never", project: "outside the project", hold: "every message", refuse: "refuse all" };
367
+ const INBOUND_DESCRIPTIONS = {
368
+ accept: "Deliver every message to the agent.",
369
+ project: "Deliver messages from sessions in this repository's Task Memory project; ask you about the rest.",
370
+ hold: "Ask you before delivering any message.",
371
+ refuse: "Refuse every message.",
372
+ };
373
+ const REVIEW_DESCRIPTIONS = {
374
+ auto: "Review Work's risk check decides.",
375
+ off: "Never on its own; for a risky change the agent proposes a review, with its reason and cost, and waits for you.",
376
+ always: "Review every finished task.",
377
+ };
378
+ /** A choice among fixed values in the Session scope; inherit follows the personal and repository settings. */
379
+ function createSessionChoiceItem(options) {
380
+ const override = options.sessionOverrides[options.key];
381
+ const set = typeof override === "string" && options.values.includes(override);
382
+ return {
383
+ id: options.id,
384
+ label: options.label,
385
+ description: options.description,
386
+ path: options.key.split("."),
387
+ kind: "enum",
388
+ value: set ? override : LOCAL_INHERIT,
389
+ values: [LOCAL_INHERIT, ...options.values],
390
+ valueLabels: { inherit: "inherit", ...options.valueLabels },
391
+ valueDescriptions: {
392
+ inherit: inheritText(
393
+ options.valueLabels[options.inheritedValue] ?? options.inheritedValue,
394
+ SOURCE_LABELS[options.inheritedSource],
395
+ ),
396
+ ...options.valueDescriptions,
397
+ },
398
+ configScope: "session",
399
+ configValues: Object.fromEntries([["inherit", undefined], ...options.values.map((value) => [value, value])]),
400
+ effectiveValue: options.effectiveValue,
401
+ effectiveSource: set ? "session" : options.inheritedSource,
402
+ inheritedValue: options.inheritedValue,
403
+ inheritedSource: options.inheritedSource,
404
+ };
405
+ }
406
+ /** A setting's configured value and source without session overrides: personal, then repository, then the default. */
407
+ function configuredSetting(rawConfig, localConfig, path, defaultValue) {
408
+ const local = getPath(localConfig, path);
409
+ if (typeof local === typeof defaultValue) return { value: local, source: "local" };
410
+ const repository = getPath(rawConfig, path);
411
+ if (typeof repository === typeof defaultValue) return { value: repository, source: "repository" };
412
+ return { value: defaultValue, source: "builtin" };
413
+ }
414
+ const PROMPT_LABELS = {
415
+ core: ["Core", "How Freeflow works: the cycle, what authorizes work, and where it stops."],
416
+ interactionContract: ["Interaction Contract", "How the agent talks with you: questions, reports and stopping."],
417
+ workingMethod: ["Working Method", "How the agent works: small output, named decisions, diagnosing before fixing."],
418
+ };
419
+ /** A skill's name as the screen shows it: discuss-work as Discuss Work. */
420
+ function skillLabel(name) {
421
+ return name
422
+ .split("-")
423
+ .map((word) => word.charAt(0).toUpperCase() + word.slice(1))
424
+ .join(" ");
425
+ }
426
+ /** One row that opens a list of switches on a screen of its own, as Auto-approve does. */
427
+ function nestedGroup(id, label, description, children) {
428
+ return { id, label, description, kind: "group", nested: true, value: true, children };
429
+ }
430
+ /** Sections from everyday to experimental (Task 005): Freeflow, then the capabilities, then session tools. */
431
+ const SECTION_ORDER = [
432
+ "freeflow.enabled",
433
+ "freeflow.prompts",
434
+ "freeflow.skills",
435
+ "freeflow.compaction",
436
+ "freeflow.toolExecution",
437
+ "freeflow.taskMemory",
438
+ "freeflow.messaging",
439
+ "freeflow.cognitiveRouting",
440
+ "freeflow.review",
441
+ "freeflow.session",
442
+ ];
443
+ function inSectionOrder(items) {
444
+ const rank = (item) => {
445
+ const at = SECTION_ORDER.indexOf(item.id);
446
+ return at < 0 ? SECTION_ORDER.length : at;
447
+ };
448
+ return [...items].sort((a, b) => rank(a) - rank(b));
449
+ }
450
+ function sectionItem(id, label, description, status, children) {
451
+ return { id, label, description, kind: "group", value: true, displaySuffix: status, children };
452
+ }
453
+ function capabilityStatus(state) {
454
+ return state?.effective ? "active" : state?.enabled ? "inactive" : "disabled";
455
+ }
456
+ function sessionFreeflowItems(state, cognitiveRoutingController, ctx, rawConfig, localConfig) {
457
+ const sessionOverrides = state.sessionOverrides;
458
+ const configured = state.configuredCoreConfig;
459
+ const configuredSources = state.configuredSources;
460
+ const freeflowInactive = !state.enabled;
461
+ const sessionSwitch = (id, label, description, key, effective, fallback) => {
462
+ const inherited = configuredSetting(rawConfig, localConfig, key.split("."), fallback);
463
+ const item = createSessionBooleanItem({
464
+ id,
465
+ label,
466
+ description,
467
+ key,
468
+ inheritedValue: inherited.value,
469
+ inheritedSource: inherited.source,
470
+ effectiveValue: effective,
471
+ sessionOverrides,
472
+ });
473
+ item.inactive = freeflowInactive;
474
+ return item;
475
+ };
476
+ const freeflowItem = createSessionBooleanItem({
477
+ id: "freeflow.enabled",
478
+ label: "Freeflow",
479
+ description: "Master switch for Freeflow in this Pi session.",
480
+ key: "enabled",
481
+ inheritedValue: configured.enabled,
482
+ inheritedSource: configuredSources.enabled,
483
+ effectiveValue: state.enabled,
484
+ sessionOverrides,
485
+ });
486
+ const routing = state.cognitiveRouting;
487
+ const routingUnavailable = cognitiveRoutingController === undefined;
488
+ const cognitiveRoutingState = cognitiveRoutingController?.state();
489
+ const manualProfiles = ["coordinator", "helper", "executor"];
490
+ const heldProfile = cognitiveRoutingState?.controlMode.startsWith("manual-")
491
+ ? cognitiveRoutingState.activeProfile
492
+ : undefined;
493
+ const control = heldProfile && manualProfiles.includes(heldProfile) ? heldProfile : "auto";
494
+ const routingInactive = freeflowInactive || !routing?.effective || routingUnavailable;
495
+ const routingItems = [];
496
+ if (routing) {
497
+ const enabledItem = sessionSwitch(
498
+ "freeflow.cognitiveRouting.enabled",
499
+ "Enabled",
500
+ "Use Cognitive Routing in this session.",
501
+ "cognitiveRouting.enabled",
502
+ routing.enabled,
503
+ false,
504
+ );
505
+ enabledItem.inactive = freeflowInactive || routingUnavailable;
506
+ enabledItem.runtimeInactive = routingUnavailable;
507
+ const projectionItem = sessionSwitch(
508
+ "freeflow.cognitiveRouting.projection",
509
+ "Context projection",
510
+ "Show Coordinator the worker evidence selected for it instead of the whole history.",
511
+ "cognitiveRouting.projection",
512
+ routing.projection,
513
+ true,
514
+ );
515
+ // Delegation, projection and the presets can be set while routing is not running: setting a missing preset is how
516
+ // it starts. Only Control needs routing running, since holding a profile switches the model at once.
517
+ projectionItem.inactive = freeflowInactive || routingUnavailable;
518
+ projectionItem.runtimeInactive = routingUnavailable;
519
+ const delegationOverride = cognitiveRoutingController?.sessionDelegationOverride() ?? LOCAL_INHERIT;
520
+ const delegation = cognitiveRoutingState?.delegation ?? routing.delegation;
521
+ routingItems.push(
522
+ enabledItem,
523
+ {
524
+ id: "freeflow.cognitiveRouting.profile",
525
+ label: "Control",
526
+ description: "Automatic routing, or hold one profile until you release it.",
527
+ kind: "enum",
528
+ value: control,
529
+ values: ["auto", ...manualProfiles],
530
+ valueLabels: Object.fromEntries([
531
+ ["auto", "automatic"],
532
+ ...manualProfiles.map((profile) => [profile, `hold ${profile}`]),
533
+ ]),
534
+ valueDescriptions: Object.fromEntries([
535
+ ["auto", "Release any hold; Coordinator places the work."],
536
+ ...manualProfiles.map((profile) => [
537
+ profile,
538
+ `Hold ${profile} when delegation enables it; release with /freeflow profile auto.`,
539
+ ]),
540
+ ]),
541
+ inactive: routingInactive,
542
+ // Holding a profile switches the model at once, so it needs routing running, not only configured.
543
+ runtimeInactive: routingInactive,
544
+ },
545
+ {
546
+ id: "freeflow.cognitiveRouting.delegation",
547
+ label: "Delegation",
548
+ description: "Which workers Coordinator may use in this session.",
549
+ kind: "enum",
550
+ value: delegationOverride,
551
+ values: [LOCAL_INHERIT, "executor", "helper", "both"],
552
+ valueLabels: {
553
+ inherit: "inherit",
554
+ executor: "Executor only",
555
+ helper: "Helper only",
556
+ both: "Helper and Executor",
557
+ },
558
+ valueDescriptions: {
559
+ inherit: inheritText(
560
+ routing.delegation,
561
+ SOURCE_LABELS[cognitiveRoutingSettingsSource(routing.delegationSource)],
562
+ ),
563
+ executor: "Coordinator delegates assignments to Executor.",
564
+ helper: "Coordinator delegates supporting assignments to Helper and keeps production implementation.",
565
+ both: "Coordinator chooses Helper or Executor for each assignment.",
566
+ },
567
+ configScope: "session",
568
+ effectiveValue: delegation,
569
+ effectiveSource:
570
+ delegationOverride === LOCAL_INHERIT ? cognitiveRoutingSettingsSource(routing.delegationSource) : "session",
571
+ inheritedValue: routing.delegation,
572
+ inheritedSource: cognitiveRoutingSettingsSource(routing.delegationSource),
573
+ inactive: freeflowInactive || routingUnavailable,
574
+ runtimeInactive: routingUnavailable,
575
+ },
576
+ projectionItem,
577
+ );
578
+ if (cognitiveRoutingController) {
579
+ const sessionProfiles = cognitiveRoutingController.sessionProfileOverrides();
580
+ for (const name of manualProfiles) {
581
+ const item = cognitiveRoutingProfileItem({
582
+ name,
583
+ scope: "session",
584
+ rawConfig: {},
585
+ localConfig: {},
586
+ capabilityState: routing,
587
+ ctx,
588
+ sessionProfiles,
589
+ });
590
+ item.inactive ||= freeflowInactive || routingUnavailable;
591
+ item.runtimeInactive ||= routingUnavailable;
592
+ routingItems.push(item);
593
+ }
594
+ }
595
+ }
596
+ const toolItem = sessionSwitch(
597
+ "freeflow.toolExecution.enabled",
598
+ "Enabled",
599
+ "Use Tool Execution in this session.",
600
+ "toolExecution.enabled",
601
+ state.toolExecution?.enabled ?? DEFAULT_TOOL_EXECUTION_CONFIG.enabled,
602
+ DEFAULT_TOOL_EXECUTION_CONFIG.enabled,
603
+ );
604
+ const noticesItem = sessionSwitch(
605
+ "freeflow.toolExecution.guidanceNotices",
606
+ "Guidance notices",
607
+ "Add a skill's key lines to the result of the tool call that needs them, in this session.",
608
+ "toolExecution.guidanceNotices",
609
+ state.toolExecution?.guidanceNotices ?? DEFAULT_TOOL_EXECUTION_CONFIG.guidanceNotices,
610
+ DEFAULT_TOOL_EXECUTION_CONFIG.guidanceNotices,
611
+ );
612
+ const compactionItem = sessionSwitch(
613
+ "freeflow.compaction.enabled",
614
+ "Enabled",
615
+ "Use Freeflow compaction in this session.",
616
+ "compaction.enabled",
617
+ state.compaction?.enabled ?? DEFAULT_COMPACTION_ENABLED,
618
+ DEFAULT_COMPACTION_ENABLED,
619
+ );
620
+ const carryItem = sessionSwitch(
621
+ "freeflow.compaction.carry",
622
+ "Context reuse",
623
+ "Let the agent carry selected files and tool results into the next cycle.",
624
+ "compaction.carry",
625
+ state.compaction?.carry ?? DEFAULT_COMPACTION_CARRY,
626
+ DEFAULT_COMPACTION_CARRY,
627
+ );
628
+ const autoApproveItems = Object.keys(AUTO_APPROVE_GROUPS).map((group) =>
629
+ sessionSwitch(
630
+ `freeflow.taskMemory.autoApprove.${group}`,
631
+ AUTO_APPROVE_LABELS[group][0],
632
+ `Let the agent do this without asking you first, in this session: ${AUTO_APPROVE_LABELS[group][1]}.`,
633
+ `taskMemory.autoApprove.${group}`,
634
+ state.taskMemory?.autoApprove?.[group] ?? false,
635
+ false,
636
+ ),
637
+ );
638
+ const inboundInherited = configuredSetting(rawConfig, localConfig, ["messaging", "inbound"], DEFAULT_INBOUND_POLICY);
639
+ const inboundItem = createSessionChoiceItem({
640
+ id: "freeflow.messaging.inbound",
641
+ label: "Ask before delivering",
642
+ description: "Whether a message from another session waits for you to accept it, in this session.",
643
+ key: "messaging.inbound",
644
+ values: INBOUND_POLICIES,
645
+ valueLabels: INBOUND_LABELS,
646
+ valueDescriptions: INBOUND_DESCRIPTIONS,
647
+ inheritedValue: inboundInherited.value,
648
+ inheritedSource: inboundInherited.source,
649
+ effectiveValue: state.messaging?.inbound ?? DEFAULT_INBOUND_POLICY,
650
+ sessionOverrides,
651
+ });
652
+ inboundItem.inactive = freeflowInactive;
653
+ const reviewInherited = configuredSetting(rawConfig, localConfig, ["review", "mode"], "auto");
654
+ const reviewModeItem = createSessionChoiceItem({
655
+ id: "freeflow.review.mode",
656
+ label: "Independent review",
657
+ description: "When finished work is reviewed by a separate reviewer, in this session.",
658
+ key: "review.mode",
659
+ values: REVIEW_MODES,
660
+ valueLabels: { auto: "auto", off: "off", always: "always" },
661
+ valueDescriptions: REVIEW_DESCRIPTIONS,
662
+ inheritedValue: reviewInherited.value,
663
+ inheritedSource: reviewInherited.source,
664
+ effectiveValue: state.review?.mode ?? "auto",
665
+ sessionOverrides,
666
+ });
667
+ reviewModeItem.inactive = freeflowInactive;
668
+ reviewModeItem.heading = true;
669
+ const reviewModelItem = reviewerModelItem({
670
+ scope: "session",
671
+ rawConfig,
672
+ localConfig,
673
+ effectiveValue: state.review?.model,
674
+ effectiveSource: typeof sessionOverrides["review.model"] === "string" ? "session" : "builtin",
675
+ ctx,
676
+ sessionValue: typeof sessionOverrides["review.model"] === "string" ? sessionOverrides["review.model"] : undefined,
677
+ });
678
+ reviewModelItem.inactive = freeflowInactive;
679
+ const promptsMaster = sessionSwitch(
680
+ "freeflow.prompts.enabled",
681
+ "Enabled",
682
+ "Add Freeflow's runtime prompts in this session. Off leaves all three out; Freeflow stays on.",
683
+ "prompts.enabled",
684
+ state.promptsEnabled ?? true,
685
+ true,
686
+ );
687
+ const skillsMaster = sessionSwitch(
688
+ "freeflow.skills.enabled",
689
+ "Enabled",
690
+ "Give the agent Freeflow's skills in this session. Off turns every skill but Setup Freeflow off.",
691
+ "skills.enabled",
692
+ state.skills?.enabled ?? true,
693
+ true,
694
+ );
695
+ const promptItems = RUNTIME_PROMPTS.map((prompt) =>
696
+ sessionSwitch(
697
+ `freeflow.prompts.${prompt}`,
698
+ PROMPT_LABELS[prompt][0],
699
+ `${PROMPT_LABELS[prompt][1]} In this session.`,
700
+ `prompts.${prompt}`,
701
+ configuredSetting(rawConfig, localConfig, ["prompts", prompt], true).value,
702
+ true,
703
+ ),
704
+ );
705
+ const skillItems = SWITCHABLE_SKILLS.map((skill) =>
706
+ sessionSwitch(
707
+ `freeflow.skills.${skill}`,
708
+ skillLabel(skill),
709
+ `Give the agent ${skill} in this session.`,
710
+ `skills.${skill}`,
711
+ configuredSetting(rawConfig, localConfig, ["skills", skill], true).value,
712
+ true,
713
+ ),
714
+ );
715
+ const taskMemoryItem = sessionSwitch(
716
+ "freeflow.taskMemory.enabled",
717
+ "Enabled",
718
+ "Use task memory in this session.",
719
+ "taskMemory.enabled",
720
+ state.taskMemory?.enabled ?? DEFAULT_TASK_MEMORY_ENABLED,
721
+ DEFAULT_TASK_MEMORY_ENABLED,
722
+ );
723
+ const messagingItem = sessionSwitch(
724
+ "freeflow.messaging.enabled",
725
+ "Enabled",
726
+ "Exchange messages with other Pi sessions on this machine.",
727
+ "messaging.enabled",
728
+ state.messaging?.enabled ?? DEFAULT_MESSAGING_ENABLED,
729
+ DEFAULT_MESSAGING_ENABLED,
730
+ );
731
+ const items = [
732
+ freeflowItem,
733
+ sectionItem("freeflow.prompts", "Runtime prompts", "Runtime prompts for this session.", "", [
734
+ promptsMaster,
735
+ nestedGroup("freeflow.prompts.each", "Each prompt", "One switch per prompt, in this session.", promptItems),
736
+ ]),
737
+ sectionItem("freeflow.skills", "Skills", "Skills for this session; Setup Freeflow always stays.", "", [
738
+ skillsMaster,
739
+ nestedGroup("freeflow.skills.each", "Each skill", "One switch per skill, in this session.", skillItems),
740
+ ]),
741
+ ...(routing
742
+ ? [
743
+ sectionItem(
744
+ "freeflow.cognitiveRouting",
745
+ "Cognitive Routing",
746
+ "Routing for this session.",
747
+ routingUnavailable
748
+ ? "unavailable"
749
+ : cognitiveRoutingState?.effective
750
+ ? `active · ${control === "auto" ? "automatic" : `hold ${control}`}`
751
+ : capabilityStatus(routing),
752
+ routingItems,
753
+ ),
754
+ ]
755
+ : []),
756
+ sectionItem(
757
+ "freeflow.toolExecution",
758
+ "Tool Execution",
759
+ "Tool Execution for this session.",
760
+ capabilityStatus(state.toolExecution),
761
+ [toolItem, noticesItem],
762
+ ),
763
+ sectionItem(
764
+ "freeflow.compaction",
765
+ "Compaction",
766
+ "Compaction for this session.",
767
+ capabilityStatus(state.compaction),
768
+ [compactionItem, carryItem],
769
+ ),
770
+ sectionItem(
771
+ "freeflow.taskMemory",
772
+ "Task Memory",
773
+ "Task memory for this session.",
774
+ capabilityStatus(state.taskMemory),
775
+ [
776
+ taskMemoryItem,
777
+ nestedGroup(
778
+ "freeflow.taskMemory.autoApprove",
779
+ "Auto-approve",
780
+ "Task memory actions the agent may take without asking you first, in this session. Enter opens the five switches; Esc returns.",
781
+ autoApproveItems,
782
+ ),
783
+ ],
784
+ ),
785
+ sectionItem("freeflow.messaging", "Messaging", "Messaging for this session.", capabilityStatus(state.messaging), [
786
+ messagingItem,
787
+ inboundItem,
788
+ ]),
789
+ sectionItem("freeflow.review", "Review", "Review Work for this session.", "", [reviewModeItem, reviewModelItem]),
790
+ sectionItem("freeflow.session", "Session", "Session overrides.", "", [
791
+ {
792
+ id: "freeflow.session.reset",
793
+ label: "Reset overrides",
794
+ description:
795
+ "Clear every session override above, so the session follows personal and repository settings again.",
796
+ kind: "enum",
797
+ value: "available",
798
+ values: ["reset"],
799
+ valueLabels: { reset: "Reset all session overrides" },
800
+ valueDescriptions: {
801
+ reset: "Return every session setting, delegation, and routing preset to its configured value.",
802
+ },
803
+ // Counted when shown, so it follows changes made on this screen.
804
+ format: () => {
805
+ let count = 0;
806
+ walkSettingsItems(items, (candidate) => {
807
+ if (
808
+ candidate.configScope === "session" &&
809
+ candidate.value !== LOCAL_INHERIT &&
810
+ candidate.value !== undefined
811
+ )
812
+ count += 1;
813
+ });
814
+ return count ? `${count} active` : "none active";
815
+ },
816
+ transient: true,
817
+ },
818
+ ]),
819
+ ];
820
+ return inSectionOrder(items);
821
+ }
822
+ const COGNITIVE_ROUTING_THINKING_LEVELS = ["off", "minimal", "low", "medium", "high", "xhigh", "max"];
823
+ const COGNITIVE_ROUTING_EFFORT_DESCRIPTIONS = {
824
+ off: "No reasoning",
825
+ minimal: "Very brief reasoning",
826
+ low: "Light reasoning",
827
+ medium: "Moderate reasoning",
828
+ high: "Deep reasoning",
829
+ xhigh: "Extra-high reasoning",
830
+ max: "Maximum reasoning",
831
+ };
832
+ function cognitiveRoutingModelKey(provider, model) {
833
+ return `${provider}/${model}`;
834
+ }
835
+ function isCognitiveRoutingProfile(value) {
836
+ return (
837
+ isRecord(value) &&
838
+ typeof value.provider === "string" &&
839
+ typeof value.model === "string" &&
840
+ typeof value.thinking === "string" &&
841
+ COGNITIVE_ROUTING_THINKING_LEVELS.includes(value.thinking)
842
+ );
843
+ }
844
+ /**
845
+ * The thinking levels a model supports, by Pi's own rule (pi-ai's getSupportedThinkingLevels): a model without
846
+ * reasoning has only off; a level its thinkingLevelMap maps to null is unsupported; xhigh and max need a mapping.
847
+ * Pi's model registry has no such method, so the rule reads the model's data directly.
848
+ */
849
+ function cognitiveRoutingThinkingLevels(model) {
850
+ if (model?.reasoning !== true) return ["off"];
851
+ return COGNITIVE_ROUTING_THINKING_LEVELS.filter((level) => {
852
+ const mapped = model.thinkingLevelMap?.[level];
853
+ if (mapped === null) return false;
854
+ return level === "xhigh" || level === "max" ? mapped !== undefined : true;
855
+ });
856
+ }
857
+ function cognitiveRoutingModelOptions(ctx) {
858
+ const registry = ctx?.modelRegistry;
859
+ if (typeof registry?.getAvailable !== "function") return [];
860
+ let models;
861
+ try {
862
+ models = registry.getAvailable();
863
+ } catch {
864
+ return [];
865
+ }
866
+ if (!Array.isArray(models)) return [];
867
+ return models
868
+ .filter((model) => typeof model?.provider === "string" && typeof model?.id === "string")
869
+ .map((model) => {
870
+ const thinkingLevels = cognitiveRoutingThinkingLevels(model);
871
+ const provider = model.provider;
872
+ const modelId = model.id;
873
+ const key = cognitiveRoutingModelKey(provider, modelId);
874
+ const displayName = typeof model.name === "string" && model.name !== modelId ? model.name : undefined;
875
+ return {
876
+ key,
877
+ provider,
878
+ model: modelId,
879
+ label: key,
880
+ description: displayName ? `${displayName} · ${thinkingLevels.join(", ")}` : thinkingLevels.join(", "),
881
+ thinkingLevels,
882
+ };
883
+ })
884
+ .filter((model) => model.thinkingLevels.length > 0)
885
+ .sort((a, b) => a.label.localeCompare(b.label));
886
+ }
887
+ function cognitiveRoutingProfileDisplay(value) {
888
+ return isCognitiveRoutingProfile(value) ? formatCoreValue(value) : "not configured";
889
+ }
890
+ function cognitiveRoutingSettingsSource(source) {
891
+ if (source === "session") return "session";
892
+ if (source === "repository") return "repository";
893
+ if (source === "personal" || source === "local") return "local";
894
+ return "builtin";
895
+ }
896
+ function cognitiveRoutingProfileDisplaySuffix(source, scope) {
897
+ if (!source) return undefined;
898
+ if (source === "session") return "(session override)";
899
+ if (source === "personal" && scope === "repository") return "(effective personal)";
900
+ return `(${source})`;
901
+ }
902
+ function cognitiveRoutingConfirmStep(summary) {
903
+ return {
904
+ title: "Confirm preset",
905
+ choices: [
906
+ {
907
+ key: "__save__",
908
+ value: "save",
909
+ label: `Save ${summary}`,
910
+ description: "Write the complete profile atomically.",
911
+ },
912
+ { key: "__cancel__", value: "cancel", label: "Cancel", description: "Leave the previous preset unchanged." },
913
+ ],
914
+ selectedKey: "__save__",
915
+ };
916
+ }
917
+ /**
918
+ * The reviewer model: Freeflow passes the name on as written (Review Work), so the picker offers this host's
919
+ * authenticated models by `provider/id`, keeps a name set elsewhere, and clears the setting with None or inherit.
920
+ */
921
+ function reviewerModelItem(options) {
922
+ const path = ["review", "model"];
923
+ const name = (value) => (typeof value === "string" ? value : undefined);
924
+ const repositoryModel = name(getPath(options.rawConfig, path));
925
+ const configuredModel = name(getPath(options.localConfig, path)) ?? repositoryModel;
926
+ const session = options.scope === "session";
927
+ const local = options.scope === "local";
928
+ const value = session ? options.sessionValue : local ? name(getPath(options.localConfig, path)) : repositoryModel;
929
+ const inherited = session ? configuredModel : repositoryModel;
930
+ const clear = session
931
+ ? { label: "Inherit configured setting", description: "Remove this session's reviewer model." }
932
+ : local
933
+ ? { label: "Inherit repository setting", description: "Remove your personal reviewer model." }
934
+ : { label: "None", description: "Review Work chooses the reviewer's model." };
935
+ const models = cognitiveRoutingModelOptions(options.ctx).map((model) => model.key);
936
+ const names = value && !models.includes(value) ? [value, ...models] : models;
937
+ return {
938
+ id: "freeflow.review.model",
939
+ label: "Reviewer model",
940
+ description: session
941
+ ? "The model a reviewer runs on, in this Pi session only."
942
+ : local
943
+ ? "The model a reviewer runs on, for you in this checkout."
944
+ : "The model a reviewer runs on, where the host lets the agent choose one. This edits shared .freeflow/config.json.",
945
+ path,
946
+ kind: "string",
947
+ value,
948
+ format: (current) => (typeof current === "string" ? current : "none"),
949
+ configScope: options.scope,
950
+ effectiveValue: options.effectiveValue,
951
+ effectiveSource: options.effectiveSource,
952
+ inheritedValue: session || local ? inherited : undefined,
953
+ inheritedSource:
954
+ session && name(getPath(options.localConfig, path))
955
+ ? "local"
956
+ : (session || local) && repositoryModel
957
+ ? "repository"
958
+ : "builtin",
959
+ wizard: () => ({
960
+ firstStep: () => ({
961
+ title: "Choose reviewer model",
962
+ choices: [
963
+ { key: LOCAL_INHERIT, value: LOCAL_INHERIT, label: clear.label, description: clear.description },
964
+ ...names.map((key) => ({
965
+ key,
966
+ value: key,
967
+ label: key,
968
+ description: models.includes(key) ? "" : "Set in the file; not an authenticated model here.",
969
+ })),
970
+ ],
971
+ selectedKey: value ?? LOCAL_INHERIT,
972
+ }),
973
+ nextStep: () => undefined,
974
+ valueFromSelections: (selected) => (selected[0] === LOCAL_INHERIT ? undefined : selected[0]),
975
+ }),
976
+ };
977
+ }
978
+ function createCognitiveRoutingProfileWizard(
979
+ currentValue,
980
+ models,
981
+ allowInherit,
982
+ inheritChoice = {
983
+ label: "Inherit repository preset",
984
+ description: "Remove the personal override without changing the repository preset.",
985
+ summary: "inherit repository preset",
986
+ },
987
+ ) {
988
+ const currentProfile = isCognitiveRoutingProfile(currentValue) ? currentValue : undefined;
989
+ const currentModelKey = currentProfile
990
+ ? cognitiveRoutingModelKey(currentProfile.provider, currentProfile.model)
991
+ : undefined;
992
+ const firstChoices = [
993
+ ...(allowInherit
994
+ ? [
995
+ {
996
+ key: LOCAL_INHERIT,
997
+ value: LOCAL_INHERIT,
998
+ label: inheritChoice.label,
999
+ description: inheritChoice.description,
1000
+ },
1001
+ ]
1002
+ : []),
1003
+ ...models.map((model) => ({
1004
+ key: model.key,
1005
+ value: model,
1006
+ label: model.label,
1007
+ description: model.description,
1008
+ })),
1009
+ ];
1010
+ if (firstChoices.length === 0) {
1011
+ firstChoices.push({
1012
+ key: "__cancel__",
1013
+ value: "cancel",
1014
+ label: "No authenticated models available",
1015
+ description: "Authenticate a provider before configuring this preset.",
1016
+ });
1017
+ }
1018
+ return {
1019
+ firstStep: () => ({
1020
+ title: "Choose model",
1021
+ choices: firstChoices,
1022
+ selectedKey:
1023
+ currentValue === LOCAL_INHERIT
1024
+ ? LOCAL_INHERIT
1025
+ : firstChoices.some((choice) => choice.key === currentModelKey)
1026
+ ? currentModelKey
1027
+ : firstChoices[0]?.key,
1028
+ }),
1029
+ nextStep: (selectedValues) => {
1030
+ if (selectedValues.length === 1) {
1031
+ const selected = selectedValues[0];
1032
+ if (selected === LOCAL_INHERIT) {
1033
+ return cognitiveRoutingConfirmStep(inheritChoice.summary);
1034
+ }
1035
+ const model = selected;
1036
+ if (!model?.key || model.thinkingLevels.length === 0) return undefined;
1037
+ const currentEffort = currentProfile && currentModelKey === model.key ? currentProfile.thinking : undefined;
1038
+ return {
1039
+ title: `Choose effort · ${model.label}`,
1040
+ choices: model.thinkingLevels.map((level) => ({
1041
+ key: level,
1042
+ value: level,
1043
+ label: level,
1044
+ description: COGNITIVE_ROUTING_EFFORT_DESCRIPTIONS[level],
1045
+ })),
1046
+ selectedKey:
1047
+ currentEffort && model.thinkingLevels.includes(currentEffort) ? currentEffort : model.thinkingLevels[0],
1048
+ };
1049
+ }
1050
+ if (selectedValues.length === 2) {
1051
+ const model = selectedValues[0];
1052
+ const effort = selectedValues[1];
1053
+ return cognitiveRoutingConfirmStep(`${model.label} · ${effort}`);
1054
+ }
1055
+ return undefined;
1056
+ },
1057
+ valueFromSelections: (selectedValues) => {
1058
+ if (selectedValues[0] === LOCAL_INHERIT) return undefined;
1059
+ const model = selectedValues[0];
1060
+ return {
1061
+ provider: model.provider,
1062
+ model: model.model,
1063
+ thinking: selectedValues[1],
1064
+ };
1065
+ },
1066
+ };
1067
+ }
1068
+ function cognitiveRoutingProfileItem(options) {
1069
+ const path = ["cognitiveRouting", "profiles", options.name];
1070
+ const repositoryProfile = getPath(options.rawConfig, path);
1071
+ const localProfile = getPath(options.localConfig, path);
1072
+ const configuredProfile = options.capabilityState?.profiles?.[options.name] ?? repositoryProfile;
1073
+ const sessionPair = options.sessionProfiles?.[options.name];
1074
+ const sessionProfile = sessionPair
1075
+ ? { provider: sessionPair.provider, model: sessionPair.modelId, thinking: sessionPair.thinking }
1076
+ : undefined;
1077
+ const effectiveProfile = sessionProfile ?? configuredProfile;
1078
+ const localValue = isCognitiveRoutingProfile(localProfile) ? localProfile : LOCAL_INHERIT;
1079
+ const value =
1080
+ options.scope === "session"
1081
+ ? (sessionProfile ?? (configuredProfile ? LOCAL_INHERIT : undefined))
1082
+ : options.scope === "local"
1083
+ ? localValue
1084
+ : repositoryProfile;
1085
+ const configuredSource =
1086
+ options.capabilityState?.profileSources?.[options.name] ??
1087
+ (isCognitiveRoutingProfile(repositoryProfile) ? "repository" : undefined);
1088
+ const source = options.scope === "session" ? (sessionProfile ? "session" : configuredSource) : configuredSource;
1089
+ const models = cognitiveRoutingModelOptions(options.ctx);
1090
+ const inactive = options.scope !== "repository" && models.length === 0 && value !== LOCAL_INHERIT;
1091
+ const inheritChoice =
1092
+ options.scope === "session"
1093
+ ? {
1094
+ label: "Inherit configured preset",
1095
+ description: "Remove the session override without changing local or repository configuration.",
1096
+ summary: "inherit configured preset",
1097
+ }
1098
+ : undefined;
1099
+ return {
1100
+ id: `freeflow.cognitiveRouting.${options.name}`,
1101
+ label: `${options.name.charAt(0).toUpperCase()}${options.name.slice(1)} preset`,
1102
+ description:
1103
+ options.scope === "session"
1104
+ ? "Temporarily choose the model and effort for this profile in the current Pi session only."
1105
+ : "Choose an authenticated available model and one effort supported by that model. Confirming writes the complete preset; cancel leaves it unchanged.",
1106
+ path,
1107
+ kind: "string",
1108
+ value,
1109
+ format: (current) => {
1110
+ const inherits = current === LOCAL_INHERIT || (options.scope === "session" && current === undefined);
1111
+ return inherits
1112
+ ? options.scope === "session"
1113
+ ? configuredProfile
1114
+ ? "inherit configured preset"
1115
+ : "not configured"
1116
+ : "inherit repository preset"
1117
+ : cognitiveRoutingProfileDisplay(current);
1118
+ },
1119
+ configScope: options.scope,
1120
+ effectiveValue: effectiveProfile,
1121
+ effectiveSource: cognitiveRoutingSettingsSource(source),
1122
+ inheritedValue: options.scope === "session" ? configuredProfile : repositoryProfile,
1123
+ inheritedSource: cognitiveRoutingSettingsSource(configuredSource ?? "builtin"),
1124
+ inactive,
1125
+ displaySuffix: cognitiveRoutingProfileDisplaySuffix(source, options.scope),
1126
+ wizard: () =>
1127
+ createCognitiveRoutingProfileWizard(
1128
+ value,
1129
+ models,
1130
+ options.scope !== "repository" && (options.scope !== "session" || configuredProfile !== undefined),
1131
+ inheritChoice,
1132
+ ),
1133
+ };
1134
+ }
1135
+ function isCognitiveRoutingRuntimeAvailable(pi) {
1136
+ return (
1137
+ typeof pi?.appendEntry === "function" &&
1138
+ typeof pi?.setModel === "function" &&
1139
+ typeof pi?.setThinkingLevel === "function"
1140
+ );
1141
+ }
1142
+ function freeflowItems(rawConfig, options = {}) {
1143
+ const scope = options.scope === "local" ? "local" : "repository";
1144
+ const layers = options.layers;
1145
+ const { localConfig, core, sources } = resolveSettingsCoreView(rawConfig, layers);
1146
+ const freeflowItem = createScopedBooleanItem({
1147
+ scope,
1148
+ rawConfig,
1149
+ localConfig,
1150
+ id: "freeflow.enabled",
1151
+ label: "Freeflow",
1152
+ description: "Master switch for Freeflow's core guidance, skills, and optional capabilities in this checkout.",
1153
+ path: ["enabled"],
1154
+ effectiveValue: core.enabled,
1155
+ effectiveSource: sources.enabled,
1156
+ defaultValue: DEFAULT_FREEFLOW_ENABLED,
1157
+ });
1158
+ const freeflowInactive = !core.enabled;
1159
+ const cognitiveRoutingState = options.cognitiveRouting;
1160
+ const cognitiveRoutingRuntimeDisabled = options.runtimeAvailable !== true;
1161
+ const cognitiveRoutingGroup = (() => {
1162
+ const cognitiveRoutingEnabledItem = createScopedBooleanItem({
1163
+ scope,
1164
+ rawConfig,
1165
+ localConfig,
1166
+ id: "freeflow.cognitiveRouting.enabled",
1167
+ label: "Enabled",
1168
+ description: cognitiveRoutingRuntimeDisabled
1169
+ ? "Requires a host model-state control API; configuration is read-only on this host."
1170
+ : "Allow Cognitive Routing to manage explicit assignments and request native profile transitions.",
1171
+ path: ["cognitiveRouting", "enabled"],
1172
+ effectiveValue: cognitiveRoutingState?.enabled ?? false,
1173
+ effectiveSource: cognitiveRoutingSettingsSource(cognitiveRoutingState?.enabledSource),
1174
+ defaultValue: false,
1175
+ });
1176
+ cognitiveRoutingEnabledItem.inactive = freeflowInactive || cognitiveRoutingRuntimeDisabled;
1177
+ cognitiveRoutingEnabledItem.runtimeInactive = cognitiveRoutingRuntimeDisabled;
1178
+ const cognitiveRoutingDelegationItem = createScopedDelegationItem({
1179
+ scope,
1180
+ rawConfig,
1181
+ localConfig,
1182
+ effectiveValue: cognitiveRoutingState?.delegation ?? "executor",
1183
+ effectiveSource: cognitiveRoutingSettingsSource(cognitiveRoutingState?.delegationSource),
1184
+ });
1185
+ cognitiveRoutingDelegationItem.inactive = freeflowInactive || cognitiveRoutingRuntimeDisabled;
1186
+ cognitiveRoutingDelegationItem.runtimeInactive = cognitiveRoutingRuntimeDisabled;
1187
+ const cognitiveRoutingProjectionItem = createScopedBooleanItem({
1188
+ scope,
1189
+ rawConfig,
1190
+ localConfig,
1191
+ id: "freeflow.cognitiveRouting.projection",
1192
+ label: "Context projection",
1193
+ description:
1194
+ "Select worker evidence for Coordinator; disable projection to keep ordinary Pi context in all enabled profiles.",
1195
+ path: ["cognitiveRouting", "projection"],
1196
+ // On by default with routing, as cognitive-routing/config.ts resolves it.
1197
+ effectiveValue: cognitiveRoutingState?.projection ?? true,
1198
+ effectiveSource: cognitiveRoutingSettingsSource(cognitiveRoutingState?.projectionSource),
1199
+ defaultValue: true,
1200
+ });
1201
+ cognitiveRoutingProjectionItem.inactive = freeflowInactive || cognitiveRoutingRuntimeDisabled;
1202
+ cognitiveRoutingProjectionItem.runtimeInactive = cognitiveRoutingRuntimeDisabled;
1203
+ const cognitiveRoutingProfiles = ["coordinator", "helper", "executor"].map((name) =>
1204
+ cognitiveRoutingProfileItem({
1205
+ name: name,
1206
+ scope,
1207
+ rawConfig,
1208
+ localConfig,
1209
+ capabilityState: cognitiveRoutingState,
1210
+ ctx: options.ctx,
1211
+ }),
1212
+ );
1213
+ for (const item of cognitiveRoutingProfiles) {
1214
+ item.inactive ||= freeflowInactive || cognitiveRoutingRuntimeDisabled;
1215
+ item.runtimeInactive = cognitiveRoutingRuntimeDisabled;
1216
+ }
1217
+ const cognitiveRoutingStatus = cognitiveRoutingRuntimeDisabled
1218
+ ? "unavailable · host unsupported"
1219
+ : cognitiveRoutingState
1220
+ ? cognitiveRoutingState.effective
1221
+ ? "active"
1222
+ : cognitiveRoutingState.blockingReason?.code === "profile_missing"
1223
+ ? "not configured"
1224
+ : (cognitiveRoutingState.blockingReason?.code ?? "inactive")
1225
+ : "unavailable";
1226
+ return {
1227
+ id: "freeflow.cognitiveRouting",
1228
+ label: "Cognitive Routing",
1229
+ description: cognitiveRoutingRuntimeDisabled
1230
+ ? "Cognitive Routing configuration is visible for inspection but requires a host model-state control API."
1231
+ : "Configure Coordinator, Helper, and Executor profiles plus the workers available for delegation.",
1232
+ kind: "group",
1233
+ value: cognitiveRoutingRuntimeDisabled ? false : (cognitiveRoutingState?.enabled ?? false),
1234
+ inactive: freeflowInactive,
1235
+ displaySuffix: cognitiveRoutingStatus,
1236
+ children: [
1237
+ cognitiveRoutingEnabledItem,
1238
+ cognitiveRoutingDelegationItem,
1239
+ ...cognitiveRoutingProfiles,
1240
+ cognitiveRoutingProjectionItem,
1241
+ ],
1242
+ };
1243
+ })();
1244
+ const toolExecutionState = options.toolExecution;
1245
+ const toolSource = (path) => {
1246
+ if (getPath(localConfig, path) !== undefined) return "local";
1247
+ if (getPath(rawConfig, path) !== undefined) return "repository";
1248
+ return "builtin";
1249
+ };
1250
+ const toolExecutionEnabledItem = createScopedBooleanItem({
1251
+ scope,
1252
+ rawConfig,
1253
+ localConfig,
1254
+ id: "freeflow.toolExecution.enabled",
1255
+ label: "Enabled",
1256
+ description: "Enable the Tool Execution capability.",
1257
+ path: ["toolExecution", "enabled"],
1258
+ effectiveValue: toolExecutionState?.enabled ?? DEFAULT_TOOL_EXECUTION_CONFIG.enabled,
1259
+ effectiveSource: toolSource(["toolExecution", "enabled"]),
1260
+ defaultValue: DEFAULT_TOOL_EXECUTION_CONFIG.enabled,
1261
+ });
1262
+ const guidanceNoticesItem = createScopedBooleanItem({
1263
+ scope,
1264
+ rawConfig,
1265
+ localConfig,
1266
+ id: "freeflow.toolExecution.guidanceNotices",
1267
+ label: "Guidance notices",
1268
+ description:
1269
+ "Add a skill's key lines to the result of the tool call that needs them, such as a commit or a failing test.",
1270
+ path: ["toolExecution", "guidanceNotices"],
1271
+ effectiveValue: toolExecutionState?.guidanceNotices ?? DEFAULT_TOOL_EXECUTION_CONFIG.guidanceNotices,
1272
+ effectiveSource: toolSource(["toolExecution", "guidanceNotices"]),
1273
+ defaultValue: DEFAULT_TOOL_EXECUTION_CONFIG.guidanceNotices,
1274
+ });
1275
+ const toolExecutionItems = [toolExecutionEnabledItem, guidanceNoticesItem];
1276
+ walkSettingsItems(toolExecutionItems, (item) => {
1277
+ item.inactive = freeflowInactive;
1278
+ });
1279
+ const toolExecutionGroup = {
1280
+ id: "freeflow.toolExecution",
1281
+ label: "Tool Execution",
1282
+ description: "Configure Freeflow's execution layer around Pi's tools.",
1283
+ kind: "group",
1284
+ value: toolExecutionState?.enabled ?? DEFAULT_TOOL_EXECUTION_CONFIG.enabled,
1285
+ inactive: freeflowInactive,
1286
+ displaySuffix: toolExecutionState?.effective ? "active" : toolExecutionState?.enabled ? "inactive" : "disabled",
1287
+ children: toolExecutionItems,
1288
+ };
1289
+ const compactionState = options.compaction;
1290
+ const compactionEnabledItem = createScopedBooleanItem({
1291
+ scope,
1292
+ rawConfig,
1293
+ localConfig,
1294
+ id: "freeflow.compaction.enabled",
1295
+ label: "Enabled",
1296
+ description: "Warn before Pi's compaction limit and let the agent compact itself and recover in the same run.",
1297
+ path: ["compaction", "enabled"],
1298
+ effectiveValue: compactionState?.enabled ?? DEFAULT_COMPACTION_ENABLED,
1299
+ effectiveSource: toolSource(["compaction", "enabled"]),
1300
+ defaultValue: DEFAULT_COMPACTION_ENABLED,
1301
+ });
1302
+ const compactionCarryItem = createScopedBooleanItem({
1303
+ scope,
1304
+ rawConfig,
1305
+ localConfig,
1306
+ id: "freeflow.compaction.carry",
1307
+ label: "Context reuse",
1308
+ description: "Let the agent carry selected files and tool results into the next cycle.",
1309
+ path: ["compaction", "carry"],
1310
+ effectiveValue: compactionState?.carry ?? DEFAULT_COMPACTION_CARRY,
1311
+ effectiveSource: toolSource(["compaction", "carry"]),
1312
+ defaultValue: DEFAULT_COMPACTION_CARRY,
1313
+ });
1314
+ const compactionItems = [compactionEnabledItem, compactionCarryItem];
1315
+ walkSettingsItems(compactionItems, (item) => {
1316
+ item.inactive = freeflowInactive;
1317
+ });
1318
+ const compactionGroup = {
1319
+ id: "freeflow.compaction",
1320
+ label: "Compaction",
1321
+ description: "Configure Freeflow compaction; Pi's own compaction stays as the fallback.",
1322
+ kind: "group",
1323
+ value: compactionState?.enabled ?? DEFAULT_COMPACTION_ENABLED,
1324
+ inactive: freeflowInactive,
1325
+ displaySuffix: compactionState?.effective ? "active" : compactionState?.enabled ? "inactive" : "disabled",
1326
+ children: compactionItems,
1327
+ };
1328
+ const taskMemoryState = options.taskMemory;
1329
+ const taskMemoryEnabledItem = createScopedBooleanItem({
1330
+ scope,
1331
+ rawConfig,
1332
+ localConfig,
1333
+ id: "freeflow.taskMemory.enabled",
1334
+ label: "Enabled",
1335
+ description:
1336
+ "A preview: attach Working Records to the session, deliver the focused record after compaction, and refuse record writes to records not attached.",
1337
+ path: ["taskMemory", "enabled"],
1338
+ effectiveValue: taskMemoryState?.enabled ?? DEFAULT_TASK_MEMORY_ENABLED,
1339
+ effectiveSource: toolSource(["taskMemory", "enabled"]),
1340
+ defaultValue: DEFAULT_TASK_MEMORY_ENABLED,
1341
+ });
1342
+ taskMemoryEnabledItem.inactive = freeflowInactive;
1343
+ // Which proposals skip the confirmation dialog (Task 004 D-002).
1344
+ const approveLabels = AUTO_APPROVE_LABELS;
1345
+ const approveItems = Object.keys(AUTO_APPROVE_GROUPS).map((group) => {
1346
+ const item = createScopedBooleanItem({
1347
+ scope,
1348
+ rawConfig,
1349
+ localConfig,
1350
+ id: `freeflow.taskMemory.autoApprove.${group}`,
1351
+ label: approveLabels[group][0],
1352
+ description: `Let the agent do this without asking you first: ${approveLabels[group][1]}. Off asks every time.`,
1353
+ path: ["taskMemory", "autoApprove", group],
1354
+ effectiveValue: taskMemoryState?.autoApprove?.[group] ?? false,
1355
+ effectiveSource: toolSource(["taskMemory", "autoApprove", group]),
1356
+ defaultValue: false,
1357
+ });
1358
+ item.inactive = freeflowInactive;
1359
+ return item;
1360
+ });
1361
+ const taskMemoryGroup = {
1362
+ id: "freeflow.taskMemory",
1363
+ label: "Task Memory",
1364
+ description: "Configure task memory: Working Records the session knows, delivers and guards.",
1365
+ kind: "group",
1366
+ value: taskMemoryState?.enabled ?? DEFAULT_TASK_MEMORY_ENABLED,
1367
+ inactive: freeflowInactive,
1368
+ displaySuffix: taskMemoryState?.effective ? "active" : taskMemoryState?.enabled ? "inactive" : "disabled",
1369
+ children: [
1370
+ taskMemoryEnabledItem,
1371
+ {
1372
+ id: "freeflow.taskMemory.autoApprove",
1373
+ label: "Auto-approve",
1374
+ description:
1375
+ "Task memory actions the agent may take without asking you first. Enter opens the five switches; Esc returns.",
1376
+ kind: "group",
1377
+ nested: true,
1378
+ value: true,
1379
+ children: approveItems,
1380
+ },
1381
+ ],
1382
+ };
1383
+ const messagingState = options.messaging;
1384
+ const messagingEnabledItem = createScopedBooleanItem({
1385
+ scope,
1386
+ rawConfig,
1387
+ localConfig,
1388
+ id: "freeflow.messaging.enabled",
1389
+ label: "Enabled",
1390
+ description:
1391
+ "A preview: list this session for other Pi sessions on this machine and let the agent exchange messages with them.",
1392
+ path: ["messaging", "enabled"],
1393
+ effectiveValue: messagingState?.enabled ?? DEFAULT_MESSAGING_ENABLED,
1394
+ effectiveSource: toolSource(["messaging", "enabled"]),
1395
+ defaultValue: DEFAULT_MESSAGING_ENABLED,
1396
+ });
1397
+ messagingEnabledItem.inactive = freeflowInactive;
1398
+ // Messages are delivered unless the user asks to approve them (Task 002 D-008).
1399
+ const messagingInboundItem = createScopedChoiceItem({
1400
+ scope,
1401
+ rawConfig,
1402
+ localConfig,
1403
+ id: "freeflow.messaging.inbound",
1404
+ label: "Ask before delivering",
1405
+ description: "Whether a message from another session waits for you to accept it.",
1406
+ path: ["messaging", "inbound"],
1407
+ values: INBOUND_POLICIES,
1408
+ valueLabels: INBOUND_LABELS,
1409
+ valueDescriptions: INBOUND_DESCRIPTIONS,
1410
+ defaultValue: DEFAULT_INBOUND_POLICY,
1411
+ effectiveValue: messagingState?.inbound ?? DEFAULT_INBOUND_POLICY,
1412
+ effectiveSource: toolSource(["messaging", "inbound"]),
1413
+ });
1414
+ messagingInboundItem.inactive = freeflowInactive;
1415
+ const messagingGroup = {
1416
+ id: "freeflow.messaging",
1417
+ label: "Messaging",
1418
+ description: "Configure messaging between Pi sessions on this machine.",
1419
+ kind: "group",
1420
+ value: messagingState?.enabled ?? DEFAULT_MESSAGING_ENABLED,
1421
+ inactive: freeflowInactive,
1422
+ displaySuffix: messagingState?.effective ? "active" : messagingState?.enabled ? "inactive" : "disabled",
1423
+ children: [messagingEnabledItem, messagingInboundItem],
1424
+ };
1425
+ // The runtime prompts and skills, on every host (config-validation.ts).
1426
+ // Each a section whose row is its master switch, with one row opening a switch per item.
1427
+ const masterItem = (section, label, description) =>
1428
+ createScopedBooleanItem({
1429
+ scope,
1430
+ rawConfig,
1431
+ localConfig,
1432
+ id: `freeflow.${section}.enabled`,
1433
+ label,
1434
+ description,
1435
+ path: [section, "enabled"],
1436
+ effectiveValue: sectionEnabled(rawConfig, localConfig, section),
1437
+ effectiveSource: toolSource([section, "enabled"]),
1438
+ defaultValue: true,
1439
+ });
1440
+ const eachPrompt = nestedGroup(
1441
+ "freeflow.prompts.each",
1442
+ "Each prompt",
1443
+ "One switch per prompt. Enter opens them; Esc returns.",
1444
+ RUNTIME_PROMPTS.map((prompt) =>
1445
+ createScopedBooleanItem({
1446
+ scope,
1447
+ rawConfig,
1448
+ localConfig,
1449
+ id: `freeflow.prompts.${prompt}`,
1450
+ label: PROMPT_LABELS[prompt][0],
1451
+ description: PROMPT_LABELS[prompt][1],
1452
+ path: ["prompts", prompt],
1453
+ effectiveValue: configuredSetting(rawConfig, localConfig, ["prompts", prompt], true).value,
1454
+ effectiveSource: toolSource(["prompts", prompt]),
1455
+ defaultValue: true,
1456
+ }),
1457
+ ),
1458
+ );
1459
+ const promptsGroup = {
1460
+ id: "freeflow.prompts",
1461
+ label: "Runtime prompts",
1462
+ description: "The prompts Freeflow adds to every session: Core, the Interaction Contract and the Working Method.",
1463
+ kind: "group",
1464
+ value: true,
1465
+ displaySuffix: "",
1466
+ children: [
1467
+ masterItem("prompts", "Enabled", "Add Freeflow's runtime prompts. Off leaves all three out; Freeflow stays on."),
1468
+ eachPrompt,
1469
+ ],
1470
+ };
1471
+ const eachSkill = nestedGroup(
1472
+ "freeflow.skills.each",
1473
+ "Each skill",
1474
+ "One switch per skill. Enter opens them; Esc returns.",
1475
+ SWITCHABLE_SKILLS.map((skill) =>
1476
+ createScopedBooleanItem({
1477
+ scope,
1478
+ rawConfig,
1479
+ localConfig,
1480
+ id: `freeflow.skills.${skill}`,
1481
+ label: skillLabel(skill),
1482
+ description: `Give the agent ${skill}. Off, Pi does not load it and its command refuses; other hosts load it themselves, so there the agent is told not to use it.`,
1483
+ path: ["skills", skill],
1484
+ effectiveValue: configuredSetting(rawConfig, localConfig, ["skills", skill], true).value,
1485
+ effectiveSource: toolSource(["skills", skill]),
1486
+ defaultValue: true,
1487
+ }),
1488
+ ),
1489
+ );
1490
+ const skillsGroup = {
1491
+ id: "freeflow.skills",
1492
+ label: "Skills",
1493
+ description: "The skills Freeflow gives the agent; Setup Freeflow always stays.",
1494
+ kind: "group",
1495
+ value: true,
1496
+ displaySuffix: "",
1497
+ children: [
1498
+ masterItem(
1499
+ "skills",
1500
+ "Enabled",
1501
+ "Give the agent Freeflow's skills. Off turns every skill but Setup Freeflow off.",
1502
+ ),
1503
+ eachSkill,
1504
+ ],
1505
+ };
1506
+ walkSettingsItems([promptsGroup, skillsGroup], (item) => {
1507
+ item.inactive = freeflowInactive;
1508
+ });
1509
+ // Review Work's choice on every host (config-validation.ts).
1510
+ const review = resolveReviewSettings(rawConfig, localConfig);
1511
+ const reviewModeItem = createScopedChoiceItem({
1512
+ scope,
1513
+ rawConfig,
1514
+ localConfig,
1515
+ id: "freeflow.review.mode",
1516
+ label: "Independent review",
1517
+ description: "When finished work is reviewed by a separate reviewer; the agent's own self-review always runs.",
1518
+ path: ["review", "mode"],
1519
+ values: REVIEW_MODES,
1520
+ valueLabels: { auto: "auto", off: "off", always: "always" },
1521
+ valueDescriptions: REVIEW_DESCRIPTIONS,
1522
+ defaultValue: "auto",
1523
+ effectiveValue: review.mode,
1524
+ effectiveSource: toolSource(["review", "mode"]),
1525
+ });
1526
+ reviewModeItem.inactive = freeflowInactive;
1527
+ reviewModeItem.heading = true;
1528
+ const reviewModelItem = reviewerModelItem({
1529
+ scope,
1530
+ rawConfig,
1531
+ localConfig,
1532
+ effectiveValue: review.model,
1533
+ effectiveSource: toolSource(["review", "model"]),
1534
+ ctx: options.ctx,
1535
+ });
1536
+ reviewModelItem.inactive = freeflowInactive;
1537
+ const reviewGroup = {
1538
+ id: "freeflow.review",
1539
+ label: "Review",
1540
+ description: "Configure Review Work's independent review, on every host that reads these files.",
1541
+ kind: "group",
1542
+ value: true,
1543
+ inactive: freeflowInactive,
1544
+ displaySuffix: "",
1545
+ children: [reviewModeItem, reviewModelItem],
1546
+ };
1547
+ return inSectionOrder([
1548
+ freeflowItem,
1549
+ ...(cognitiveRoutingGroup ? [cognitiveRoutingGroup] : []),
1550
+ toolExecutionGroup,
1551
+ compactionGroup,
1552
+ taskMemoryGroup,
1553
+ messagingGroup,
1554
+ reviewGroup,
1555
+ promptsGroup,
1556
+ skillsGroup,
1557
+ ]);
1558
+ }
1559
+ function pruneKnownDefaults(config) {
1560
+ const defaultPaths = [
1561
+ { path: ["enabled"], value: DEFAULT_FREEFLOW_ENABLED },
1562
+ { path: ["cognitiveRouting", "delegation"], value: "executor" },
1563
+ { path: ["toolExecution", "enabled"], value: DEFAULT_TOOL_EXECUTION_CONFIG.enabled },
1564
+ { path: ["toolExecution", "guidanceNotices"], value: DEFAULT_TOOL_EXECUTION_CONFIG.guidanceNotices },
1565
+ { path: ["compaction", "enabled"], value: DEFAULT_COMPACTION_ENABLED },
1566
+ { path: ["compaction", "carry"], value: DEFAULT_COMPACTION_CARRY },
1567
+ { path: ["taskMemory", "enabled"], value: DEFAULT_TASK_MEMORY_ENABLED },
1568
+ ...Object.keys(AUTO_APPROVE_GROUPS).map((group) => ({
1569
+ path: ["taskMemory", "autoApprove", group],
1570
+ value: false,
1571
+ })),
1572
+ { path: ["messaging", "enabled"], value: DEFAULT_MESSAGING_ENABLED },
1573
+ { path: ["messaging", "inbound"], value: DEFAULT_INBOUND_POLICY },
1574
+ { path: ["prompts", "enabled"], value: true },
1575
+ ...RUNTIME_PROMPTS.map((prompt) => ({ path: ["prompts", prompt], value: true })),
1576
+ { path: ["skills", "enabled"], value: true },
1577
+ ...SWITCHABLE_SKILLS.map((skill) => ({ path: ["skills", skill], value: true })),
1578
+ ];
1579
+ for (const item of defaultPaths) {
1580
+ if (valuesEqual(getPath(config, item.path), item.value)) {
1581
+ deletePath(config, item.path);
1582
+ }
1583
+ }
1584
+ }
1585
+ async function ensureLocalConfigIgnored(cwd) {
1586
+ let gitPath;
1587
+ try {
1588
+ const result = await execFileAsync("git", ["-C", cwd, "rev-parse", "--git-path", "info/exclude"]);
1589
+ gitPath = result.stdout.trim();
1590
+ } catch {
1591
+ return;
1592
+ }
1593
+ const tracked = await execFileAsync("git", ["-C", cwd, "ls-files", "--", ".freeflow/local.json"]);
1594
+ if (tracked.stdout.trim()) {
1595
+ throw new Error(
1596
+ ".freeflow/local.json is tracked by git; remove it from the index before writing personal overrides.",
1597
+ );
1598
+ }
1599
+ try {
1600
+ await execFileAsync("git", ["-C", cwd, "check-ignore", "-q", "--", ".freeflow/local.json"]);
1601
+ return;
1602
+ } catch {
1603
+ // Add a local exclude when the repository does not already ignore the file.
1604
+ }
1605
+ const excludePath = isAbsolute(gitPath) ? gitPath : resolve(cwd, gitPath);
1606
+ let existing = "";
1607
+ try {
1608
+ existing = await readFile(excludePath, "utf8");
1609
+ } catch {
1610
+ // The git metadata path may not exist yet in a minimal repository.
1611
+ }
1612
+ const rule = ".freeflow/local.json";
1613
+ if (existing.split(/\r?\n/).includes(rule)) return;
1614
+ await mkdir(dirname(excludePath), { recursive: true });
1615
+ const prefix = existing && !existing.endsWith("\n") ? "\n" : "";
1616
+ await appendFile(excludePath, `${prefix}${rule}\n`, "utf8");
1617
+ }
1618
+ async function updateConfig(cwd, item, value, scope = item.configScope ?? "repository") {
1619
+ if (scope === "local") {
1620
+ if (!item.path?.length) {
1621
+ throw new Error(`${item.label} is a settings group, not a writable setting.`);
1622
+ }
1623
+ const current = await readFreeflowLocalConfig(cwd);
1624
+ const next = cloneJson(current);
1625
+ const configValue = configValueForChoice(item, value);
1626
+ const previousValue = getPath(current, item.path);
1627
+ if (configValue === undefined || isEmptyValue(configValue)) {
1628
+ if (previousValue === undefined) return;
1629
+ deletePath(next, item.path);
1630
+ } else {
1631
+ setPath(next, item.path, configValue);
1632
+ }
1633
+ const path = join(cwd, ".freeflow/local.json");
1634
+ await ensureLocalConfigIgnored(cwd);
1635
+ if (Object.keys(next).length === 0) {
1636
+ await rm(path, { force: true });
1637
+ return;
1638
+ }
1639
+ await mkdir(join(cwd, ".freeflow"), { recursive: true });
1640
+ await writeFile(path, `${JSON.stringify(next, null, 2)}\n`, "utf8");
1641
+ return;
1642
+ }
1643
+ const current = await readFreeflowConfig(cwd);
1644
+ const next = cloneJson(current);
1645
+ setConfigValue(next, item, value);
1646
+ pruneKnownDefaults(next);
1647
+ await mkdir(join(cwd, ".freeflow"), { recursive: true });
1648
+ await writeFile(join(cwd, ".freeflow/config.json"), `${JSON.stringify(next, null, 2)}\n`, "utf8");
1649
+ }
1650
+ function isCognitiveRoutingProfileItem(item) {
1651
+ return ["coordinator", "helper", "executor"].some((profile) => item.id === `freeflow.cognitiveRouting.${profile}`);
1652
+ }
1653
+ /** A value as the settings screen shows it, e.g. "enabled · personal" or "inherit → enabled (repository)". */
1654
+ function formatSettingValue(item, value) {
1655
+ if (isCognitiveRoutingProfileItem(item)) return cognitiveRoutingProfileDisplay(value);
1656
+ if (typeof value === "boolean") return booleanValue(value);
1657
+ const key = String(value ?? "");
1658
+ return item.valueLabels?.[key] && key !== LOCAL_INHERIT ? item.valueLabels[key] : formatCoreValue(value);
1659
+ }
1660
+ const ORIGINS = {
1661
+ builtin: "Inherited: not set in any file, so Freeflow's default applies.",
1662
+ repository: "Inherited from the repository's .freeflow/config.json.",
1663
+ local: "Inherited from your .freeflow/local.json.",
1664
+ session: "Inherited from this session.",
1665
+ };
1666
+ function displayParts(item) {
1667
+ const tone = (value) => (value === true ? { tone: "on" } : value === false ? { tone: "off" } : {});
1668
+ if (item.kind === "group") return { value: item.displaySuffix ?? "" };
1669
+ if (item.transient && item.format) return { value: item.format(item.value) };
1670
+ if (item.id === "freeflow.cognitiveRouting.profile")
1671
+ return { value: item.valueLabels?.[String(item.value)] ?? String(item.value) };
1672
+ if (item.configScope === "local" || item.configScope === "session") {
1673
+ const inherits = item.value === LOCAL_INHERIT || item.value === undefined;
1674
+ if (inherits) {
1675
+ if (item.inheritedValue === undefined) return { value: "not configured" };
1676
+ return {
1677
+ value: formatSettingValue(item, item.inheritedValue),
1678
+ note: "inherited",
1679
+ origin: ORIGINS[item.inheritedSource ?? "builtin"],
1680
+ ...tone(item.inheritedValue),
1681
+ };
1682
+ }
1683
+ // The value this tab sets, not the effective one: a session override or a switch above it does not change it.
1684
+ const value =
1685
+ item.configValues && String(item.value) in item.configValues ? item.configValues[String(item.value)] : item.value;
1686
+ return { value: formatSettingValue(item, value), ...tone(value) };
1687
+ }
1688
+ const value = item.value === undefined && isCognitiveRoutingProfileItem(item) ? undefined : item.value;
1689
+ const shown = { value: formatSettingValue(item, value), ...tone(value) };
1690
+ const unset = item.repositoryUnset ?? (item.effectiveSource === "builtin" && item.defaultValue !== undefined);
1691
+ const notes = [
1692
+ ...(unset ? ["inherited"] : []),
1693
+ ...(item.localOverrideValue !== undefined && !valuesEqual(item.localOverrideValue, item.value)
1694
+ ? [`personal: ${formatSettingValue(item, item.localOverrideValue)}`]
1695
+ : []),
1696
+ ];
1697
+ return {
1698
+ ...shown,
1699
+ ...(notes.length ? { note: notes.join(" · ") } : {}),
1700
+ ...(unset ? { origin: "Inherited: not set in .freeflow/config.json, so Freeflow's default applies." } : {}),
1701
+ };
1702
+ }
1703
+ function joinParts(parts) {
1704
+ return parts.note ? `${parts.value} (${parts.note})` : parts.value;
1705
+ }
1706
+ function displayValue(item) {
1707
+ return joinParts(displayParts(item));
1708
+ }
1709
+ /** Small choices change in place; holding a profile switches models and a reset clears everything, so those ask. */
1710
+ const PICKED_CHOICES = new Set(["freeflow.cognitiveRouting.profile", "freeflow.session.reset"]);
1711
+ function walkSettingsItems(items, visitor) {
1712
+ for (const item of items) {
1713
+ visitor(item);
1714
+ if (item.children) walkSettingsItems(item.children, visitor);
1715
+ }
1716
+ }
1717
+ function findSettingsItem(items, id) {
1718
+ for (const item of items) {
1719
+ if (item.id === id) return item;
1720
+ const child = item.children ? findSettingsItem(item.children, id) : undefined;
1721
+ if (child) return child;
1722
+ }
1723
+ return undefined;
1724
+ }
1725
+ function refreshSettingsDerivedState(items) {
1726
+ const freeflowItem = findSettingsItem(items, "freeflow.enabled");
1727
+ const freeflowInactive = freeflowItem ? effectiveItemValue(freeflowItem) !== true : false;
1728
+ const cognitiveRoutingGroup = findSettingsItem(items, "freeflow.cognitiveRouting");
1729
+ const cognitiveRoutingEnabledItem = findSettingsItem(items, "freeflow.cognitiveRouting.enabled");
1730
+ const cognitiveRoutingDelegationItem = findSettingsItem(items, "freeflow.cognitiveRouting.delegation");
1731
+ const delegation = cognitiveRoutingDelegationItem ? effectiveItemValue(cognitiveRoutingDelegationItem) : "executor";
1732
+ const cognitiveRoutingProfiles = ["coordinator", ...workersForDelegation(delegation)].map((profile) =>
1733
+ findSettingsItem(items, `freeflow.cognitiveRouting.${profile}`),
1734
+ );
1735
+ if (cognitiveRoutingGroup && cognitiveRoutingEnabledItem) {
1736
+ const enabled = effectiveItemValue(cognitiveRoutingEnabledItem) === true;
1737
+ const profilesConfigured = cognitiveRoutingProfiles.every((item) =>
1738
+ item ? isCognitiveRoutingProfile(effectiveItemValue(item)) : false,
1739
+ );
1740
+ cognitiveRoutingGroup.value = enabled;
1741
+ // A status the runtime reported (active with its control mode, a blocking reason, host unsupported) stays until
1742
+ // the switch goes off; only the configuration statuses are worked out again here.
1743
+ const current = cognitiveRoutingGroup.displaySuffix ?? "";
1744
+ if (!enabled) cognitiveRoutingGroup.displaySuffix = "disabled";
1745
+ else if (["", "disabled", "configured", "not configured"].includes(current))
1746
+ cognitiveRoutingGroup.displaySuffix = profilesConfigured ? "configured" : "not configured";
1747
+ cognitiveRoutingGroup.inactive = freeflowInactive;
1748
+ }
1749
+ const contextGroup = findSettingsItem(items, "freeflow.context");
1750
+ if (contextGroup?.children?.length) {
1751
+ const enabledCount = contextGroup.children.filter((item) => effectiveItemValue(item) === true).length;
1752
+ contextGroup.value = enabledCount > 0;
1753
+ contextGroup.displaySuffix = `${enabledCount}/${contextGroup.children.length} enabled`;
1754
+ }
1755
+ const toolExecutionGroup = findSettingsItem(items, "freeflow.toolExecution");
1756
+ const toolExecutionEnabled = findSettingsItem(items, "freeflow.toolExecution.enabled");
1757
+ if (toolExecutionGroup && toolExecutionEnabled) {
1758
+ const enabled = !freeflowInactive && effectiveItemValue(toolExecutionEnabled) === true;
1759
+ toolExecutionGroup.value = effectiveItemValue(toolExecutionEnabled) === true;
1760
+ toolExecutionGroup.displaySuffix = enabled
1761
+ ? "active"
1762
+ : effectiveItemValue(toolExecutionEnabled) === true
1763
+ ? "inactive"
1764
+ : "disabled";
1765
+ }
1766
+ const compactionGroup = findSettingsItem(items, "freeflow.compaction");
1767
+ const compactionEnabled = findSettingsItem(items, "freeflow.compaction.enabled");
1768
+ if (compactionGroup && compactionEnabled) {
1769
+ const on = effectiveItemValue(compactionEnabled) === true;
1770
+ compactionGroup.value = on;
1771
+ compactionGroup.displaySuffix = on && !freeflowInactive ? "active" : on ? "inactive" : "disabled";
1772
+ }
1773
+ const taskMemoryGroup = findSettingsItem(items, "freeflow.taskMemory");
1774
+ const taskMemoryEnabled = findSettingsItem(items, "freeflow.taskMemory.enabled");
1775
+ if (taskMemoryGroup && taskMemoryEnabled) {
1776
+ const on = effectiveItemValue(taskMemoryEnabled) === true;
1777
+ taskMemoryGroup.value = on;
1778
+ taskMemoryGroup.displaySuffix = on && !freeflowInactive ? "active" : on ? "inactive" : "disabled";
1779
+ }
1780
+ const messagingGroup = findSettingsItem(items, "freeflow.messaging");
1781
+ const messagingEnabled = findSettingsItem(items, "freeflow.messaging.enabled");
1782
+ if (messagingGroup && messagingEnabled) {
1783
+ const on = effectiveItemValue(messagingEnabled) === true;
1784
+ messagingGroup.value = on;
1785
+ messagingGroup.displaySuffix = on && !freeflowInactive ? "active" : on ? "inactive" : "disabled";
1786
+ }
1787
+ walkSettingsItems(items, (candidate) => {
1788
+ if (candidate.id === "freeflow.session.reset") {
1789
+ candidate.inactive = false;
1790
+ } else if (candidate.configScope) {
1791
+ const inactive =
1792
+ candidate.id === "freeflow.enabled" ? false : candidate.runtimeInactive === true || freeflowInactive;
1793
+ candidate.inactive = inactive;
1794
+ candidate.displaySuffix = coreDisplaySuffix(candidate, inactive);
1795
+ } else {
1796
+ candidate.inactive = candidate.runtimeInactive === true || freeflowInactive;
1797
+ }
1798
+ });
1799
+ }
1800
+ function settingsChoices(item) {
1801
+ if (item.kind === "boolean") {
1802
+ return [
1803
+ { key: "true", value: true, label: "on" },
1804
+ { key: "false", value: false, label: "off" },
1805
+ ];
1806
+ }
1807
+ if (item.kind !== "enum") return undefined;
1808
+ return (item.values ?? []).map((value) => ({
1809
+ key: value,
1810
+ value,
1811
+ label: item.valueLabels?.[value] ?? value,
1812
+ description: item.valueDescriptions?.[value],
1813
+ }));
1814
+ }
1815
+ function resetSessionItemState(items) {
1816
+ walkSettingsItems(items, (item) => {
1817
+ if (item.configScope === "session") {
1818
+ item.value = LOCAL_INHERIT;
1819
+ item.effectiveValue = item.inheritedValue;
1820
+ item.effectiveSource = item.inheritedSource ?? "builtin";
1821
+ } else if (item.id === "freeflow.sessionMode") {
1822
+ item.value = "default";
1823
+ }
1824
+ });
1825
+ }
1826
+ function settingsEntries(items, rootItems, onChange, depth = 0, lockedBy) {
1827
+ // Freeflow's own switch locks everything else on the screen while it is off.
1828
+ if (depth === 0 && !lockedBy) {
1829
+ const master = rootItems.find((candidate) => candidate.id === "freeflow.enabled");
1830
+ const off = () =>
1831
+ master && effectiveItemValue(master) === false ? "Takes effect when Freeflow is on." : undefined;
1832
+ return items.flatMap((item) =>
1833
+ settingsEntries([item], rootItems, onChange, 0, item === master ? () => undefined : off),
1834
+ );
1835
+ }
1836
+ return items.flatMap((item) => {
1837
+ if (item.kind === "group" && item.children && item.nested) {
1838
+ // One row; Enter opens its settings on a screen of their own. Its value names the ones that are on.
1839
+ const children = item.children;
1840
+ const parts = () => {
1841
+ const isOn = (child) => displayParts(child).tone === "on";
1842
+ const on = children.filter(isOn).map((child) => child.label.toLowerCase());
1843
+ const off = children.filter((child) => !isOn(child)).map((child) => child.label.toLowerCase());
1844
+ // Name the shorter side: what is on, or what is off when most are on.
1845
+ return on.length === 0
1846
+ ? { value: "none", tone: "off" }
1847
+ : off.length === 0
1848
+ ? { value: "all", tone: "on" }
1849
+ : off.length < on.length
1850
+ ? { value: `all but ${off.join(", ")}` }
1851
+ : { value: on.join(", ") };
1852
+ };
1853
+ return [
1854
+ {
1855
+ id: item.id,
1856
+ label: item.label,
1857
+ description: item.description,
1858
+ depth,
1859
+ parts,
1860
+ currentValue: () => parts().value,
1861
+ inactive: () => false,
1862
+ ...(lockedBy ? { dormant: lockedBy } : {}),
1863
+ children: () => settingsEntries(children, rootItems, onChange, 0, lockedBy),
1864
+ },
1865
+ ];
1866
+ }
1867
+ if (item.kind === "group" && item.children) {
1868
+ // A capability's own switch is its section's row; while it is off, the section's other settings are locked.
1869
+ const toggle = item.children.find((child) => child.id === `${item.id}.enabled` || child.heading);
1870
+ if (toggle) {
1871
+ const header = settingEntry(toggle, rootItems, onChange, depth, lockedBy);
1872
+ // The section's status adds what the switch alone does not say: not active, unavailable, the routing control.
1873
+ const parts = () => {
1874
+ const shown = displayParts(toggle);
1875
+ const status = item.displaySuffix ?? "";
1876
+ const extra =
1877
+ status === "inactive"
1878
+ ? shown.tone === "on"
1879
+ ? "not active"
1880
+ : ""
1881
+ : ["", "active", "disabled", "configured"].includes(status)
1882
+ ? ""
1883
+ : status.replace(/^active · /, "");
1884
+ return extra ? { ...shown, note: shown.note ? `${shown.note} · ${extra}` : extra } : shown;
1885
+ };
1886
+ const off = () =>
1887
+ lockedBy?.() ?? (effectiveItemValue(toggle) === false ? `Takes effect when ${item.label} is on.` : undefined);
1888
+ return [
1889
+ { ...header, label: item.label, heading: true, parts, currentValue: () => joinParts(parts()) },
1890
+ ...settingsEntries(
1891
+ item.children.filter((child) => child !== toggle),
1892
+ rootItems,
1893
+ onChange,
1894
+ depth + 1,
1895
+ off,
1896
+ ),
1897
+ ];
1898
+ }
1899
+ const section = {
1900
+ id: item.id,
1901
+ label: item.label,
1902
+ description: item.description,
1903
+ section: true,
1904
+ depth,
1905
+ currentValue: () => displayValue(item),
1906
+ inactive: () => item.inactive === true,
1907
+ };
1908
+ return [section, ...settingsEntries(item.children, rootItems, onChange, depth + 1, lockedBy)];
1909
+ }
1910
+ return [settingEntry(item, rootItems, onChange, depth, lockedBy)];
1911
+ });
1912
+ }
1913
+ function settingEntry(item, rootItems, onChange, depth, lockedBy) {
1914
+ const choices = settingsChoices(item);
1915
+ return {
1916
+ id: item.id,
1917
+ label: item.label,
1918
+ description: item.description,
1919
+ depth,
1920
+ parts: () => displayParts(item),
1921
+ currentValue: () => displayValue(item),
1922
+ // A setting under an off switch stays changeable, so it can be set up before the switch is turned on; only what
1923
+ // the host cannot do (runtimeInactive), or what acts at once, blocks it.
1924
+ inactive: () => item.runtimeInactive === true || (item.inactive === true && lockedBy?.() === undefined),
1925
+ ...(lockedBy ? { dormant: lockedBy } : {}),
1926
+ currentChoiceKey: () => String(item.value),
1927
+ choices,
1928
+ cycle: Boolean(choices?.length) && !PICKED_CHOICES.has(item.id),
1929
+ wizard: item.wizard ? () => item.wizard() : undefined,
1930
+ edit:
1931
+ !["boolean", "enum", "group"].includes(item.kind) && !item.wizard
1932
+ ? {
1933
+ initialValue: () => item.editInitialValue?.() ?? displayValue(item),
1934
+ parse: (text) => (item.parse ? item.parse(text) : text),
1935
+ }
1936
+ : undefined,
1937
+ commit: async (value) => {
1938
+ if (valuesEqual(item.value, value)) return { changed: false, reloadRequired: false };
1939
+ const outcome = await onChange(item, value);
1940
+ if (outcome.changed) {
1941
+ updateScopedItemState(item, value);
1942
+ // The repository file now sets it, unless the value was its default, which setConfigValue leaves out of the file.
1943
+ if (item.configScope === "repository")
1944
+ item.repositoryUnset =
1945
+ value === undefined ||
1946
+ isEmptyValue(value) ||
1947
+ (item.defaultValue !== undefined && valuesEqual(value, item.defaultValue));
1948
+ if (item.id === "freeflow.session.reset") resetSessionItemState(rootItems);
1949
+ else if (!item.transient) item.value = value;
1950
+ refreshSettingsDerivedState(rootItems);
1951
+ }
1952
+ return outcome;
1953
+ },
1954
+ };
1955
+ }
1956
+ async function openSettings(options) {
1957
+ if (typeof options.ctx?.ui?.custom !== "function") {
1958
+ options.ctx?.ui?.notify?.(
1959
+ `${options.title} requires Pi TUI mode. Use the status command for a compact summary.`,
1960
+ "warning",
1961
+ );
1962
+ return { changed: false, configChanged: false, failed: false };
1963
+ }
1964
+ let component;
1965
+ await options.ctx.ui.custom((tui, theme, _keybindings, done) => {
1966
+ component = new PiSettingsComponent({
1967
+ title: options.title,
1968
+ scopes: options.scopes,
1969
+ initialScope: options.initialScope,
1970
+ theme: theme ?? {},
1971
+ requestRender: () => tui.requestRender(),
1972
+ notify: (message, level) => options.ctx?.ui?.notify?.(message, level),
1973
+ done,
1974
+ });
1975
+ return component;
1976
+ });
1977
+ await component?.waitForWrites();
1978
+ return component?.sessionResult() ?? { changed: false, configChanged: false, failed: false };
1979
+ }
1980
+ /** Where each scope's values live and whom they affect, shown under the tabs. */
1981
+ const SCOPES = [
1982
+ {
1983
+ id: "session",
1984
+ label: "Session",
1985
+ summary: "This Pi session only, not saved to a file. Overrides personal and repository values.",
1986
+ },
1987
+ {
1988
+ id: "local",
1989
+ label: "Personal",
1990
+ summary: ".freeflow/local.json: only you, in this checkout. Overrides the repository.",
1991
+ },
1992
+ {
1993
+ id: "repository",
1994
+ label: "Repository",
1995
+ summary: ".freeflow/config.json: shared with everyone who uses this repository.",
1996
+ },
1997
+ ];
1998
+ const NON_TUI_SETTINGS_GUIDANCE =
1999
+ "Freeflow settings require Pi TUI mode. Use /freeflow status to inspect current state; supported non-TUI changes are /freeflow enable and /freeflow disable.";
2000
+ function ensureSettingsIdle(ctx) {
2001
+ if (typeof ctx?.isIdle !== "function" || ctx.isIdle()) return true;
2002
+ ctx.ui?.notify?.("Freeflow settings and profile changes are available only while Pi is idle.", "warning");
2003
+ return false;
2004
+ }
2005
+ function nonTuiGuidance(ctx, message) {
2006
+ if (ctx?.hasUI === false) throw new Error(message);
2007
+ ctx?.ui?.notify?.(message, "warning");
2008
+ return { changed: false, reloaded: false, error: "tui_required" };
2009
+ }
2010
+ async function finalizeSettingsSession(
2011
+ session,
2012
+ ctx,
2013
+ afterChange,
2014
+ savedMessage,
2015
+ reloadWarning = "Run /reload for Freeflow changes to fully apply.",
2016
+ afterChangeOptions = {},
2017
+ ) {
2018
+ if (!session.configChanged) return false;
2019
+ await afterChange(true, afterChangeOptions);
2020
+ if (session.failed) return false;
2021
+ ctx.ui.notify(savedMessage, "info");
2022
+ if (typeof ctx.reload !== "function") {
2023
+ ctx.ui.notify(reloadWarning, "warning");
2024
+ return false;
2025
+ }
2026
+ await ctx.reload();
2027
+ return true;
2028
+ }
2029
+ async function finalizeSessionSettings(session, ctx, afterChange) {
2030
+ if (!session.changed) return false;
2031
+ await afterChange(true);
2032
+ if (session.failed) return false;
2033
+ if (!session.configChanged) {
2034
+ ctx.ui.notify("Freeflow session overrides updated.", "info");
2035
+ return false;
2036
+ }
2037
+ ctx.ui.notify("Freeflow session overrides updated. Reloading skills and resources...", "info");
2038
+ if (typeof ctx.reload !== "function") {
2039
+ ctx.ui.notify("Run /reload for Freeflow session overrides to fully apply.", "warning");
2040
+ return false;
2041
+ }
2042
+ await ctx.reload();
2043
+ return true;
2044
+ }
2045
+ export async function handleFreeflowCommand(args, ctx, afterChange, pi, cognitiveRoutingController, diagnostics) {
2046
+ const input = (args ?? "settings").trim().toLowerCase() || "settings";
2047
+ const [action, ...rest] = input.split(/\s+/);
2048
+ const actionValue = rest.join(" ");
2049
+ const nonTui = ctx?.mode && ctx.mode !== "tui";
2050
+ const settingsSelector =
2051
+ action === "settings" &&
2052
+ (!actionValue || ["session", "local", "personal", "repo", "repository", "shared"].includes(actionValue));
2053
+ if (nonTui && settingsSelector) {
2054
+ return nonTuiGuidance(ctx, NON_TUI_SETTINGS_GUIDANCE);
2055
+ }
2056
+ const [layers, state] = await Promise.all([readFreeflowConfigLayers(ctx.cwd), readCapabilityState(ctx.cwd, ctx)]);
2057
+ const configState = layers.repository;
2058
+ const raw = configState.valid ? configState.parsed : {};
2059
+ if (action === "status") {
2060
+ ctx.ui.notify(freeflowStatusText(state, cognitiveRoutingController, diagnostics?.status()), "info");
2061
+ return { changed: false, reloaded: false };
2062
+ }
2063
+ if (!configState.valid) {
2064
+ ctx.ui.notify(freeflowStatusText(state, cognitiveRoutingController), "warning");
2065
+ return { changed: false, reloaded: false, error: "not_configured" };
2066
+ }
2067
+ if (layers.local.exists && !layers.local.valid) {
2068
+ ctx.ui.notify(
2069
+ `.freeflow/local.json is invalid; repair or remove it before changing Freeflow settings. ${layers.local.parseError ?? ""}`.trim(),
2070
+ "warning",
2071
+ );
2072
+ return { changed: false, reloaded: false, error: "invalid_local_config" };
2073
+ }
2074
+ if (["enable", "on", "true", "disable", "off", "false"].includes(action)) {
2075
+ if (!ensureSettingsIdle(ctx)) return { changed: false, reloaded: false, error: "busy" };
2076
+ const enabled = ["enable", "on", "true"].includes(action);
2077
+ const item = freeflowItems(raw, {
2078
+ scope: "repository",
2079
+ layers,
2080
+ cognitiveRouting: state.cognitiveRouting,
2081
+ toolExecution: state.toolExecution,
2082
+ compaction: state.compaction,
2083
+ taskMemory: state.taskMemory,
2084
+ messaging: state.messaging,
2085
+ ctx,
2086
+ runtimeAvailable: isCognitiveRoutingRuntimeAvailable(pi),
2087
+ }).find((candidate) => candidate.id === "freeflow.enabled");
2088
+ await updateConfig(ctx.cwd, item, enabled, "repository");
2089
+ await afterChange(true);
2090
+ // The command writes the repository file; a personal file or a session override still wins over it.
2091
+ const after = await readCapabilityState(ctx.cwd, ctx);
2092
+ const winner = after?.configSources?.enabled;
2093
+ if (after?.enabled !== enabled && (winner === "local" || winner === "session")) {
2094
+ ctx.ui.notify(
2095
+ `The repository now ${enabled ? "enables" : "disables"} Freeflow, but ${winner === "session" ? "this session's override" : "your .freeflow/local.json"} still turns it ${enabled ? "off" : "on"}. Change it in /freeflow settings.`,
2096
+ "warning",
2097
+ );
2098
+ return { changed: true, reloaded: false };
2099
+ }
2100
+ ctx.ui.notify(`Freeflow ${enabled ? "enabled" : "disabled"}. Reloading Freeflow runtime...`, "info");
2101
+ if (typeof ctx.reload === "function") {
2102
+ await ctx.reload();
2103
+ return { changed: true, reloaded: true };
2104
+ }
2105
+ ctx.ui.notify("Run /reload for Freeflow changes to fully apply.", "warning");
2106
+ return { changed: true, reloaded: false };
2107
+ }
2108
+ if (action && action !== "settings") {
2109
+ ctx.ui.notify(
2110
+ "Usage: /freeflow, /freeflow settings [local|repo], /freeflow status, /freeflow enable, or /freeflow disable",
2111
+ "warning",
2112
+ );
2113
+ return { changed: false, reloaded: false, error: "invalid_action" };
2114
+ }
2115
+ let initialScope = "local";
2116
+ if (actionValue === "session") {
2117
+ initialScope = "session";
2118
+ } else if (["repo", "repository", "shared"].includes(actionValue)) {
2119
+ initialScope = "repository";
2120
+ } else if (actionValue && !["local", "personal"].includes(actionValue)) {
2121
+ ctx.ui.notify(
2122
+ "Usage: /freeflow settings, /freeflow settings session, /freeflow settings local, or /freeflow settings repo",
2123
+ "warning",
2124
+ );
2125
+ return { changed: false, reloaded: false, error: "invalid_scope" };
2126
+ }
2127
+ if (!ensureSettingsIdle(ctx)) return { changed: false, reloaded: false, error: "busy" };
2128
+ // Each scope is read fresh when shown, so a change in one scope shows up in the others' inherited values.
2129
+ const touched = new Set();
2130
+ const loadItems = async (scope) => {
2131
+ const [layersNow, stateNow] = await Promise.all([
2132
+ readFreeflowConfigLayers(ctx.cwd),
2133
+ readCapabilityState(ctx.cwd, ctx),
2134
+ ]);
2135
+ const rawNow = layersNow.repository.valid ? layersNow.repository.parsed : {};
2136
+ if (scope === "session") {
2137
+ const localNow = layersNow.local.valid && isRecord(layersNow.local.parsed) ? layersNow.local.parsed : {};
2138
+ return sessionFreeflowItems(stateNow, cognitiveRoutingController, ctx, rawNow, localNow);
2139
+ }
2140
+ return freeflowItems(rawNow, {
2141
+ scope,
2142
+ layers: layersNow,
2143
+ cognitiveRouting: stateNow.cognitiveRouting,
2144
+ toolExecution: stateNow.toolExecution,
2145
+ compaction: stateNow.compaction,
2146
+ taskMemory: stateNow.taskMemory,
2147
+ messaging: stateNow.messaging,
2148
+ ctx,
2149
+ runtimeAvailable: isCognitiveRoutingRuntimeAvailable(pi),
2150
+ });
2151
+ };
2152
+ const scopes = SCOPES.map((scope) => ({
2153
+ ...scope,
2154
+ load: async () => {
2155
+ const items = await loadItems(scope.id);
2156
+ return settingsEntries(items, items, async (item, value) => {
2157
+ const outcome = await changeSetting(scope.id, item, value);
2158
+ if (outcome.changed) touched.add(scope.id);
2159
+ return outcome;
2160
+ });
2161
+ },
2162
+ }));
2163
+ const changeSetting = async (settingsScope, item, value) => {
2164
+ if (!ensureSettingsIdle(ctx)) return { changed: false, reloadRequired: false };
2165
+ if (!isCognitiveRoutingRuntimeAvailable(pi) && item.id.startsWith("freeflow.cognitiveRouting.")) {
2166
+ ctx.ui.notify("Cognitive Routing is unavailable because this host lacks model-state controls.", "warning");
2167
+ return { changed: false, reloadRequired: false };
2168
+ }
2169
+ if (item.id === "freeflow.cognitiveRouting.profile") {
2170
+ if (!cognitiveRoutingController) {
2171
+ ctx.ui.notify("Cognitive Routing is unavailable for this session.", "warning");
2172
+ return { changed: false, reloadRequired: false };
2173
+ }
2174
+ const result =
2175
+ value === "auto"
2176
+ ? await cognitiveRoutingController.setAutomaticControl("profile-settings")
2177
+ : await cognitiveRoutingController.setManualProfile(value, "profile-settings");
2178
+ if ((result.status !== "automatic" && result.status !== "active") || result.reason) {
2179
+ ctx.ui.notify(`Cognitive Routing settings could not be applied: ${result.reason ?? result.status}.`, "warning");
2180
+ return { changed: false, reloadRequired: false };
2181
+ }
2182
+ await afterChange(false);
2183
+ return { changed: true, reloadRequired: false };
2184
+ }
2185
+ if (settingsScope === "session" && item.id === "freeflow.cognitiveRouting.delegation") {
2186
+ if (!cognitiveRoutingController) return { changed: false, reloadRequired: false };
2187
+ const result = await cognitiveRoutingController.setSessionDelegationOverride(
2188
+ value === LOCAL_INHERIT ? null : value,
2189
+ "Session delegation settings",
2190
+ );
2191
+ if (result.status === "blocked" || result.reason) {
2192
+ ctx.ui.notify(
2193
+ `Cognitive Routing delegation mode could not be applied: ${result.reason ?? result.status}.`,
2194
+ "warning",
2195
+ );
2196
+ return { changed: false, reloadRequired: false };
2197
+ }
2198
+ await afterChange(false);
2199
+ return { changed: result.status !== "unchanged", reloadRequired: false };
2200
+ }
2201
+ if (item.id === "freeflow.session.reset") {
2202
+ const previousDelegation = cognitiveRoutingController?.sessionDelegationOverride() ?? null;
2203
+ const delegationResult = cognitiveRoutingController
2204
+ ? await cognitiveRoutingController.setSessionDelegationOverride(null, "Reset session delegation mode")
2205
+ : { status: "unchanged" };
2206
+ if (delegationResult.status === "blocked" || delegationResult.reason) {
2207
+ ctx.ui.notify(
2208
+ `Cognitive Routing session mode could not be reset: ${delegationResult.reason ?? delegationResult.status}.`,
2209
+ "warning",
2210
+ );
2211
+ return { changed: false, reloadRequired: false };
2212
+ }
2213
+ const routingResult = cognitiveRoutingController
2214
+ ? await cognitiveRoutingController.resetSessionProfileOverrides("Reset session overrides")
2215
+ : { status: "unchanged" };
2216
+ if (routingResult.status === "blocked" || routingResult.reason) {
2217
+ if (delegationResult.status !== "unchanged") {
2218
+ const restored = await cognitiveRoutingController.setSessionDelegationOverride(
2219
+ previousDelegation,
2220
+ "Session reset failed; prior delegation mode retained",
2221
+ );
2222
+ if (restored.status === "blocked" || restored.reason) {
2223
+ await afterChange(false);
2224
+ ctx.ui.notify(
2225
+ `Session mode was reset but could not be restored: ${restored.reason ?? restored.status}. Reopen settings to see the current mode.`,
2226
+ "warning",
2227
+ );
2228
+ }
2229
+ }
2230
+ ctx.ui.notify(
2231
+ `Cognitive Routing session presets could not be reset: ${routingResult.reason ?? routingResult.status}.`,
2232
+ "warning",
2233
+ );
2234
+ return { changed: false, reloadRequired: false };
2235
+ }
2236
+ try {
2237
+ const result = await resetSessionOverrides(ctx, pi);
2238
+ return {
2239
+ changed: result.changed || routingResult.status !== "unchanged" || delegationResult.status !== "unchanged",
2240
+ reloadRequired: result.reloadRequired,
2241
+ };
2242
+ } catch (error) {
2243
+ ctx.ui.notify(
2244
+ `Routing presets were reset, but core/context session overrides could not be reset: ${error instanceof Error ? error.message : String(error)}`,
2245
+ "warning",
2246
+ );
2247
+ return {
2248
+ changed: routingResult.status !== "unchanged" || delegationResult.status !== "unchanged",
2249
+ reloadRequired: false,
2250
+ };
2251
+ }
2252
+ }
2253
+ if (item.configScope === "session" && isCognitiveRoutingProfileItem(item)) {
2254
+ if (!cognitiveRoutingController) {
2255
+ ctx.ui.notify("Cognitive Routing is unavailable for this session.", "warning");
2256
+ return { changed: false, reloadRequired: false };
2257
+ }
2258
+ const profile = item.id.split(".").at(-1);
2259
+ const override =
2260
+ value === undefined || value === LOCAL_INHERIT
2261
+ ? null
2262
+ : {
2263
+ provider: value.provider,
2264
+ modelId: value.model,
2265
+ thinking: value.thinking,
2266
+ };
2267
+ const result = await cognitiveRoutingController.setSessionProfileOverride(
2268
+ profile,
2269
+ override,
2270
+ "Session profile settings",
2271
+ );
2272
+ if (result.status === "blocked" || result.reason) {
2273
+ ctx.ui.notify(
2274
+ `Cognitive Routing session preset could not be applied: ${result.reason ?? result.status}.`,
2275
+ "warning",
2276
+ );
2277
+ return { changed: false, reloadRequired: false };
2278
+ }
2279
+ await afterChange(false);
2280
+ return { changed: result.status !== "unchanged", reloadRequired: false };
2281
+ }
2282
+ if (item.configScope === "session") {
2283
+ const key = item.path.join(".");
2284
+ const override =
2285
+ value === LOCAL_INHERIT || value === undefined
2286
+ ? null
2287
+ : value === "true"
2288
+ ? true
2289
+ : value === "false"
2290
+ ? false
2291
+ : value;
2292
+ const result = await setSessionCoreOverride(key, override, ctx, pi);
2293
+ return { changed: result.changed, reloadRequired: result.reloadRequired === true };
2294
+ }
2295
+ await updateConfig(ctx.cwd, item, value, item.configScope ?? "repository");
2296
+ return { changed: true, reloadRequired: true };
2297
+ };
2298
+ // The first scope opens loaded; the others load when Tab reaches them.
2299
+ const first = scopes.find((scope) => scope.id === initialScope);
2300
+ const firstEntries = await first.load();
2301
+ let preloaded = true;
2302
+ const firstLoad = first.load;
2303
+ first.load = () => {
2304
+ if (!preloaded) return firstLoad();
2305
+ preloaded = false;
2306
+ return firstEntries;
2307
+ };
2308
+ const session = await openSettings({ title: "Freeflow settings", scopes, initialScope, ctx });
2309
+ // A file write reloads Freeflow; session-only changes reload only when they switch a capability.
2310
+ if (touched.has("local") || touched.has("repository")) {
2311
+ const reloaded = await finalizeSettingsSession(
2312
+ session,
2313
+ ctx,
2314
+ afterChange,
2315
+ "Freeflow settings saved. Reloading Freeflow runtime...",
2316
+ "Run /reload for Freeflow changes to fully apply.",
2317
+ { reconcileCognitiveRouting: true },
2318
+ );
2319
+ return { changed: session.changed, reloaded, error: session.failed ? "write_failed" : undefined };
2320
+ }
2321
+ const reloaded = await finalizeSessionSettings(session, ctx, afterChange);
2322
+ return { changed: session.changed, reloaded, error: session.failed ? "write_failed" : undefined };
2323
+ }