@hassangameryt/freeflow 0.7.3 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (276) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/.codex-plugin/plugin.json +1 -1
  4. package/.cursor-plugin/plugin.json +1 -1
  5. package/NOTICE +14 -0
  6. package/README.md +26 -31
  7. package/capabilities/cognitive-routing/SKILL.md +106 -161
  8. package/capabilities/cognitive-routing/references/both-mode.md +36 -0
  9. package/capabilities/cognitive-routing/references/executor-mode.md +48 -0
  10. package/capabilities/cognitive-routing/references/helper-mode.md +47 -0
  11. package/capabilities/compaction/SKILL.md +49 -0
  12. package/capabilities/compaction/references/summary-format.md +239 -0
  13. package/command-surface.json +4 -0
  14. package/gemini-extension.json +1 -1
  15. package/hooks/shared/runtime-context.mjs +28 -30
  16. package/licenses/Apache-2.0.txt +55 -0
  17. package/package.json +22 -9
  18. package/pi-extension/README.md +41 -0
  19. package/pi-extension/dist/cognitive-routing/assembler.js +393 -0
  20. package/pi-extension/dist/{cognitive-routing-v2 → cognitive-routing}/config.js +5 -2
  21. package/pi-extension/dist/cognitive-routing/economics.js +45 -0
  22. package/pi-extension/dist/cognitive-routing/event-schema.js +388 -0
  23. package/pi-extension/dist/{session-sources/events.js → cognitive-routing/event-store.js} +103 -40
  24. package/pi-extension/dist/cognitive-routing/gate.js +241 -0
  25. package/pi-extension/dist/cognitive-routing/handlers.js +899 -0
  26. package/pi-extension/dist/cognitive-routing/handoffs.js +350 -0
  27. package/pi-extension/dist/cognitive-routing/model-control.js +467 -0
  28. package/pi-extension/dist/{cognitive-routing-v2 → cognitive-routing}/projection.js +229 -56
  29. package/pi-extension/dist/cognitive-routing/provenance.js +81 -0
  30. package/pi-extension/dist/cognitive-routing/runtime.js +396 -0
  31. package/pi-extension/dist/{cognitive-routing-v2 → cognitive-routing}/schemas.js +43 -7
  32. package/pi-extension/dist/cognitive-routing/session.js +221 -0
  33. package/pi-extension/dist/{session-sources → cognitive-routing}/sources.js +99 -85
  34. package/pi-extension/dist/{cognitive-routing-v2 → cognitive-routing}/state.js +37 -378
  35. package/pi-extension/dist/cognitive-routing/status.js +80 -0
  36. package/pi-extension/dist/{cognitive-routing-v2 → cognitive-routing}/tools.js +12 -12
  37. package/pi-extension/dist/{cognitive-routing-v2 → cognitive-routing}/types.js +5 -11
  38. package/pi-extension/dist/compaction/carry.js +191 -0
  39. package/pi-extension/dist/compaction/config.js +22 -0
  40. package/pi-extension/dist/compaction/controller.js +471 -0
  41. package/pi-extension/dist/compaction/harness.js +77 -0
  42. package/pi-extension/dist/compaction/results.js +158 -0
  43. package/pi-extension/dist/compaction/thresholds.js +35 -0
  44. package/pi-extension/dist/compaction/tool.js +57 -0
  45. package/pi-extension/dist/host/branch.js +13 -0
  46. package/pi-extension/dist/host/canonical.js +12 -0
  47. package/pi-extension/dist/host/catalog.js +77 -0
  48. package/pi-extension/dist/host/config.js +374 -0
  49. package/pi-extension/dist/host/projection-tags.js +60 -0
  50. package/pi-extension/dist/host/prompts.js +82 -0
  51. package/pi-extension/dist/{session-sources → host}/read-only-session.js +112 -1
  52. package/pi-extension/dist/{runtime → host}/request-history.js +121 -39
  53. package/pi-extension/dist/host/runtime-state.js +189 -0
  54. package/pi-extension/dist/{settings/settings-ui.js → host/settings/freeflow-command.js} +620 -410
  55. package/pi-extension/dist/{settings/settings-tui.js → host/settings/settings-view.js} +186 -73
  56. package/pi-extension/dist/host/staging.js +28 -0
  57. package/pi-extension/dist/host/status.js +101 -0
  58. package/pi-extension/dist/index.js +423 -173
  59. package/pi-extension/dist/provider-support/cache/anchor.js +21 -0
  60. package/pi-extension/dist/provider-support/cache/anthropic.js +102 -0
  61. package/pi-extension/dist/provider-support/cache/health.js +73 -0
  62. package/pi-extension/dist/provider-support/cache/index.js +58 -0
  63. package/pi-extension/dist/provider-support/cache/keepalive.js +210 -0
  64. package/pi-extension/dist/provider-support/cache/monitor.js +17 -0
  65. package/pi-extension/dist/provider-support/cache/openai-responses.js +48 -0
  66. package/pi-extension/dist/provider-support/effort.js +13 -0
  67. package/pi-extension/dist/provider-support/index.js +44 -3
  68. package/pi-extension/dist/provider-support/{astra → openai}/adapter.js +46 -29
  69. package/pi-extension/dist/provider-support/{astra → openai}/history.js +10 -8
  70. package/pi-extension/dist/provider-support/{astra → openai}/index.js +8 -4
  71. package/pi-extension/dist/provider-support/{astra → openai}/session-state.js +29 -38
  72. package/pi-extension/dist/provider-support/routes.js +17 -0
  73. package/pi-extension/dist/tool-execution/apply-patch/grammar.js +23 -0
  74. package/pi-extension/dist/tool-execution/apply-patch/match.js +38 -0
  75. package/pi-extension/dist/tool-execution/apply-patch/parser.js +177 -0
  76. package/pi-extension/dist/tool-execution/apply-patch/plan.js +201 -0
  77. package/pi-extension/dist/tool-execution/apply-patch/tool.js +84 -0
  78. package/pi-extension/dist/tool-execution/background.js +238 -0
  79. package/pi-extension/dist/tool-execution/bash-guard.js +23 -0
  80. package/pi-extension/dist/tool-execution/config.js +22 -0
  81. package/pi-extension/dist/tool-execution/edit-messages.js +120 -0
  82. package/pi-extension/dist/tool-execution/file-state.js +138 -0
  83. package/pi-extension/dist/tool-execution/file-tracking.js +143 -0
  84. package/pi-extension/dist/tool-execution/prompt.js +54 -0
  85. package/pi-extension/dist/tool-execution/tools.js +16 -0
  86. package/pi-extension/freeflow/README.md +9 -0
  87. package/plugin.json +1 -1
  88. package/runtime/prompts/cognitive-routing.md +4 -4
  89. package/runtime/prompts/core.md +22 -75
  90. package/runtime/prompts/interaction-contract.md +15 -8
  91. package/runtime/prompts/tool-execution.md +38 -0
  92. package/runtime/prompts/working-method.md +108 -0
  93. package/skills/action-selection/SKILL.md +39 -47
  94. package/skills/action-selection/references/trajectory-stalls.md +2 -0
  95. package/skills/bypass/SKILL.md +2 -2
  96. package/skills/commit-work/SKILL.md +31 -83
  97. package/skills/commit-work/references/git-state-edges.md +1 -1
  98. package/skills/commit-work/references/simple-push.md +2 -0
  99. package/skills/commit-work/references/staging-decisions.md +1 -1
  100. package/skills/decision-gate/SKILL.md +31 -50
  101. package/skills/design-for-depth/SKILL.md +44 -53
  102. package/skills/design-for-depth/references/design-pressure-signals.md +46 -144
  103. package/skills/design-for-depth/references/implementation-decisions.md +1 -1
  104. package/skills/design-for-depth/references/interface-design-loop.md +5 -36
  105. package/skills/design-for-depth/references/module-and-dependency-design.md +2 -32
  106. package/skills/design-for-depth/references/state-and-failure-boundaries.md +2 -13
  107. package/skills/diagnose-failure/SKILL.md +39 -24
  108. package/skills/diagnose-failure/references/diagnostic-loop-catalog.md +2 -0
  109. package/skills/diagnose-failure/references/flaky-and-performance-diagnosis.md +2 -2
  110. package/skills/discuss/SKILL.md +35 -11
  111. package/skills/discuss/references/discussion-continuity.md +7 -27
  112. package/skills/evaluate-skill/SKILL.md +49 -80
  113. package/skills/evaluate-skill/references/evaluation-definition-schema.md +35 -5
  114. package/skills/evaluate-skill/references/evaluation-design.md +14 -14
  115. package/skills/evaluate-skill/references/execution-and-evidence.md +26 -5
  116. package/skills/evaluate-skill/references/review-and-revision.md +3 -1
  117. package/skills/evaluate-skill/scripts/lib/definitions.mjs +49 -8
  118. package/skills/evaluate-skill/scripts/lib/pi.mjs +14 -7
  119. package/skills/evaluate-skill/scripts/lib/review.mjs +196 -0
  120. package/skills/evaluate-skill/scripts/lib/runner.mjs +45 -8
  121. package/skills/evaluate-skill/scripts/lib/sandbox.mjs +2 -0
  122. package/skills/evaluate-skill/scripts/lib/trials.mjs +103 -0
  123. package/skills/evaluate-skill/scripts/lib/view.mjs +67 -1
  124. package/skills/evaluate-skill/scripts/pi-command-tool.mjs +144 -0
  125. package/skills/evaluate-skill/scripts/pi-guard.mjs +30 -13
  126. package/skills/evaluate-skill/scripts/skill-eval.mjs +114 -34
  127. package/skills/execute-work/SKILL.md +41 -9
  128. package/skills/execute-work/references/code-practices.md +2 -0
  129. package/skills/execute-work/references/execute-work-edges.md +11 -1
  130. package/skills/handoff/SKILL.md +1 -1
  131. package/skills/handoff/references/handoff-templates.md +1 -1
  132. package/skills/review-artifact/SKILL.md +42 -41
  133. package/skills/review-artifact/references/adjudicate-artifact-review.md +1 -1
  134. package/skills/review-artifact/references/independent-artifact-reviewer-contract.md +1 -1
  135. package/skills/review-work/SKILL.md +38 -32
  136. package/skills/review-work/references/adjudicate-work-review.md +1 -1
  137. package/skills/review-work/references/independent-work-reviewer-contract.md +1 -1
  138. package/skills/review-work/references/security-risk-lens.md +2 -4
  139. package/skills/setup-freeflow/SKILL.md +3 -1
  140. package/skills/setup-freeflow/references/activation-contract.md +7 -13
  141. package/skills/setup-freeflow/references/host-setup.md +3 -3
  142. package/skills/track-work/SKILL.md +113 -161
  143. package/skills/track-work/references/working-record-format.md +189 -350
  144. package/skills/track-work/scripts/lib/document.mjs +62 -6
  145. package/skills/track-work/scripts/lib/format.mjs +16 -1
  146. package/skills/track-work/scripts/lib/store.mjs +9 -8
  147. package/skills/track-work/scripts/lib/transitions.mjs +79 -4
  148. package/skills/track-work/scripts/working-record.mjs +34 -12
  149. package/skills/verify-work/SKILL.md +29 -20
  150. package/skills/verify-work/references/browser-runtime-evidence.md +1 -5
  151. package/skills/verify-work/references/integration-evidence.md +2 -0
  152. package/skills/verify-work/references/performance-evidence.md +3 -1
  153. package/skills/verify-work/references/test-design.md +3 -1
  154. package/skills/workflow/SKILL.md +65 -21
  155. package/skills/workflow/references/domain-skill-composition.md +5 -3
  156. package/skills/write-docs/SKILL.md +83 -0
  157. package/skills/write-docs/references/documentation-shapes.md +119 -0
  158. package/skills/write-docs/scripts/docs-check.mjs +139 -0
  159. package/skills/write-plan/SKILL.md +40 -40
  160. package/skills/write-plan/references/plan-shapes.md +1 -1
  161. package/skills/write-skill/SKILL.md +107 -147
  162. package/skills/write-skill/references/activation-boundaries.md +2 -0
  163. package/skills/write-skill/references/agent-first-instructions.md +22 -10
  164. package/skills/write-skill/references/progressive-disclosure.md +4 -2
  165. package/skills/write-skill/references/skill-dependency-graphs.md +2 -0
  166. package/skills/write-skill/scripts/lib/similarity.mjs +56 -0
  167. package/skills/write-skill/scripts/skill-author.mjs +18 -2
  168. package/skills/write-spec/SKILL.md +34 -41
  169. package/skills/write-spec/references/artifact-standards.md +1 -1
  170. package/skills/write-spec/references/decision-records.md +2 -0
  171. package/skills/write-spec/references/spec-shapes.md +1 -1
  172. package/capabilities/context-virtualization/SKILL.md +0 -103
  173. package/capabilities/conversation-history/SKILL.md +0 -53
  174. package/hooks/tests/check-host-adapters.test.mjs +0 -82
  175. package/hooks/tests/check-runtime-context-hook.sh +0 -212
  176. package/pi-extension/dist/cognitive-routing-v2/runtime.js +0 -2181
  177. package/pi-extension/dist/context-virtualization/commands.js +0 -89
  178. package/pi-extension/dist/context-virtualization/projector.js +0 -42
  179. package/pi-extension/dist/context-virtualization/runtime.js +0 -378
  180. package/pi-extension/dist/context-virtualization/state.js +0 -57
  181. package/pi-extension/dist/context-virtualization/types.js +0 -10
  182. package/pi-extension/dist/conversation-history/passages.js +0 -66
  183. package/pi-extension/dist/conversation-history/ranking.js +0 -105
  184. package/pi-extension/dist/conversation-history/runtime.js +0 -226
  185. package/pi-extension/dist/conversation-history/search.js +0 -56
  186. package/pi-extension/dist/conversation-history/types.js +0 -1
  187. package/pi-extension/dist/freeflow-context/resolver.js +0 -120
  188. package/pi-extension/dist/freeflow-context/runtime.js +0 -80
  189. package/pi-extension/dist/freeflow-context/source-projector.js +0 -95
  190. package/pi-extension/dist/freeflow-context/tool.js +0 -314
  191. package/pi-extension/dist/freeflow-context/types.js +0 -12
  192. package/pi-extension/dist/freeflow-context/visibility.js +0 -28
  193. package/pi-extension/dist/integrations/mcp-config.js +0 -29
  194. package/pi-extension/dist/runtime/runtime-context.js +0 -754
  195. package/pi-extension/dist/runtime/runtime-identity.js +0 -5
  196. package/pi-extension/dist/session-sources/provenance.js +0 -32
  197. package/pi-extension/dist/ui/text-component.js +0 -57
  198. package/pi-extension/src/cognitive-routing-v2/budget.ts +0 -42
  199. package/pi-extension/src/cognitive-routing-v2/config.ts +0 -173
  200. package/pi-extension/src/cognitive-routing-v2/projection.ts +0 -375
  201. package/pi-extension/src/cognitive-routing-v2/render.ts +0 -277
  202. package/pi-extension/src/cognitive-routing-v2/runtime.ts +0 -2241
  203. package/pi-extension/src/cognitive-routing-v2/schemas.ts +0 -180
  204. package/pi-extension/src/cognitive-routing-v2/state.ts +0 -839
  205. package/pi-extension/src/cognitive-routing-v2/tools.ts +0 -62
  206. package/pi-extension/src/cognitive-routing-v2/types.ts +0 -254
  207. package/pi-extension/src/context-virtualization/commands.ts +0 -111
  208. package/pi-extension/src/context-virtualization/projector.ts +0 -62
  209. package/pi-extension/src/context-virtualization/runtime.ts +0 -428
  210. package/pi-extension/src/context-virtualization/state.ts +0 -79
  211. package/pi-extension/src/context-virtualization/types.ts +0 -92
  212. package/pi-extension/src/conversation-history/passages.ts +0 -81
  213. package/pi-extension/src/conversation-history/ranking.ts +0 -155
  214. package/pi-extension/src/conversation-history/runtime.ts +0 -250
  215. package/pi-extension/src/conversation-history/search.ts +0 -79
  216. package/pi-extension/src/conversation-history/types.ts +0 -31
  217. package/pi-extension/src/freeflow-context/resolver.ts +0 -139
  218. package/pi-extension/src/freeflow-context/runtime.ts +0 -100
  219. package/pi-extension/src/freeflow-context/source-projector.ts +0 -106
  220. package/pi-extension/src/freeflow-context/tool.ts +0 -366
  221. package/pi-extension/src/freeflow-context/types.ts +0 -70
  222. package/pi-extension/src/freeflow-context/visibility.ts +0 -53
  223. package/pi-extension/src/index.ts +0 -390
  224. package/pi-extension/src/integrations/mcp-config.ts +0 -32
  225. package/pi-extension/src/provider-support/astra/adapter.ts +0 -108
  226. package/pi-extension/src/provider-support/astra/history.ts +0 -145
  227. package/pi-extension/src/provider-support/astra/index.ts +0 -12
  228. package/pi-extension/src/provider-support/astra/session-state.ts +0 -113
  229. package/pi-extension/src/provider-support/index.ts +0 -5
  230. package/pi-extension/src/runtime/request-history.ts +0 -243
  231. package/pi-extension/src/runtime/runtime-context.ts +0 -860
  232. package/pi-extension/src/runtime/runtime-identity.ts +0 -8
  233. package/pi-extension/src/session-sources/events.ts +0 -230
  234. package/pi-extension/src/session-sources/provenance.ts +0 -39
  235. package/pi-extension/src/session-sources/read-only-session.ts +0 -170
  236. package/pi-extension/src/session-sources/sources.ts +0 -334
  237. package/pi-extension/src/settings/settings-tui.ts +0 -501
  238. package/pi-extension/src/settings/settings-ui.ts +0 -1652
  239. package/pi-extension/src/ui/text-component.ts +0 -66
  240. package/pi-extension/tests/cache-reuse/history.test.js +0 -117
  241. package/pi-extension/tests/cache-reuse/native.test.js +0 -91
  242. package/pi-extension/tests/cache-reuse/settings.test.js +0 -211
  243. package/pi-extension/tests/cognitive-routing-v2/communication.test.js +0 -577
  244. package/pi-extension/tests/cognitive-routing-v2/compaction-input.test.js +0 -183
  245. package/pi-extension/tests/cognitive-routing-v2/delegation.test.js +0 -557
  246. package/pi-extension/tests/cognitive-routing-v2/native.test.js +0 -474
  247. package/pi-extension/tests/cognitive-routing-v2/performance.test.js +0 -87
  248. package/pi-extension/tests/cognitive-routing-v2/persistence.test.js +0 -300
  249. package/pi-extension/tests/cognitive-routing-v2/projection.test.js +0 -241
  250. package/pi-extension/tests/cognitive-routing-v2/recovery.test.js +0 -1034
  251. package/pi-extension/tests/cognitive-routing-v2/render.test.js +0 -244
  252. package/pi-extension/tests/cognitive-routing-v2/runtime.test.js +0 -423
  253. package/pi-extension/tests/cognitive-routing-v2/stabilization.test.js +0 -521
  254. package/pi-extension/tests/cognitive-routing-v2/state.test.js +0 -586
  255. package/pi-extension/tests/context-virtualization/extension.test.js +0 -775
  256. package/pi-extension/tests/context-virtualization/projector.test.js +0 -110
  257. package/pi-extension/tests/context-virtualization/runtime.test.js +0 -230
  258. package/pi-extension/tests/conversation-history/runtime.test.js +0 -98
  259. package/pi-extension/tests/conversation-history/search.test.js +0 -97
  260. package/pi-extension/tests/fixtures/pi-host.js +0 -4
  261. package/pi-extension/tests/fixtures/routing-native.js +0 -191
  262. package/pi-extension/tests/integration/mode-free-core.test.js +0 -220
  263. package/pi-extension/tests/integration/pi-extension.test.js +0 -1447
  264. package/pi-extension/tests/integration/prompt-architecture.test.js +0 -447
  265. package/pi-extension/tests/integration/unaffected-regressions.test.js +0 -478
  266. package/pi-extension/tests/provider-support/astra-boundaries.test.js +0 -75
  267. package/pi-extension/tests/provider-support/astra.test.js +0 -306
  268. package/pi-extension/tests/provider-support/cli.test.js +0 -92
  269. package/pi-extension/tests/provider-support/native.test.js +0 -183
  270. package/pi-extension/tests/settings/settings-tui.test.js +0 -243
  271. package/pi-extension/tsconfig.json +0 -16
  272. package/runtime/prompts/context-virtualization.md +0 -3
  273. package/runtime/prompts/conversation-history.md +0 -3
  274. package/skills/design-for-depth/references/software-design-philosophy.md +0 -202
  275. /package/pi-extension/dist/{cognitive-routing-v2 → cognitive-routing}/budget.js +0 -0
  276. /package/pi-extension/dist/{cognitive-routing-v2 → cognitive-routing}/render.js +0 -0
@@ -0,0 +1,467 @@
1
+ import { ROUTING_RECOVERY_HINT } from "./session.js";
2
+ export { ROUTING_RECOVERY_HINT };
3
+ import { sessionStage, stagedFor } from "../host/staging.js";
4
+ import { resolveCognitiveRoutingState } from "./config.js";
5
+ import { presetWarnings } from "./economics.js";
6
+ import { keepsCacheAcrossEffort } from "../provider-support/effort.js";
7
+ import { workersForDelegation, RoutingError, isWorkerProfile, requireCondition as check, samePair } from "./types.js";
8
+ /**
9
+ * The only part of routing that changes the host model and effort: manual holds and releases, session presets,
10
+ * delegation mode, switches staged until the next prompt, and preset advice. Idle switches are recorded and applied
11
+ * as the next prompt starts; switches during a run apply at once, and a recorded profile never names a model the
12
+ * host is not running.
13
+ */
14
+ export class ModelControl {
15
+ session;
16
+ pi;
17
+ constructor(session, pi) {
18
+ this.session = session;
19
+ this.pi = pi;
20
+ }
21
+ applying;
22
+ externalChange = false;
23
+ heldNotice;
24
+ /** A manual hold whose profile model is not what the host runs, e.g. after a change in Pi's model picker. */
25
+ heldMismatch(state = this.session.stateData()) {
26
+ if (!this.session.supported() || state.control !== "manual" || !state.profile || this.applying) return;
27
+ const observed = this.intended();
28
+ let expected;
29
+ try {
30
+ expected = this.session.profilePair(state.profile);
31
+ } catch {
32
+ return;
33
+ }
34
+ return observed && !samePair(observed, expected) ? { profile: state.profile, expected, observed } : undefined;
35
+ }
36
+ /** Set by a user prompt's before_agent_start; consumed by the run it starts. */
37
+ promptRun = false;
38
+ /** A run that no user prompt prepared has started and its first message has not been seen yet. */
39
+ unpreparedRun = false;
40
+ /** The next run is already prepared by routing itself (explicit resume sends the message that starts it). */
41
+ preparedRun() {
42
+ this.promptRun = true;
43
+ }
44
+ /** Pi starts a run: from a user prompt (prepared in beforeRun), a message an extension sent, or a retry. */
45
+ runStarted() {
46
+ this.unpreparedRun = !this.promptRun;
47
+ this.promptRun = false;
48
+ }
49
+ /**
50
+ * Whether this message starts a run an extension's message triggered. Pi skips before_agent_start for those runs,
51
+ * so routing prepares them here, before their first request. A retry's run starts with no message and is left as is.
52
+ */
53
+ startedByMessage(message) {
54
+ if (!this.unpreparedRun) return false;
55
+ this.unpreparedRun = false;
56
+ return message?.role === "custom";
57
+ }
58
+ async beforeRun(ctx, fromPrompt = true) {
59
+ if (fromPrompt) this.promptRun = true;
60
+ this.session.ctx = ctx;
61
+ if (this.session.store && this.session.supported() && !this.session.store.blocked && !this.session.error) {
62
+ if (this.session.stateData().control === "automatic" && isWorkerProfile(this.session.stateData().profile)) {
63
+ const result = await this.control("coordinator", false);
64
+ check(result.status !== "blocked", "reconciliation_required", result.reason);
65
+ }
66
+ // A switch made while idle reaches Pi's model now, as the prompt starts and before it is recorded.
67
+ const target = this.pending();
68
+ try {
69
+ await this.applyPending();
70
+ } catch (error) {
71
+ const current = this.session.observed();
72
+ ctx.ui?.notify?.(
73
+ `Could not switch to ${target?.modelId}/${target?.thinking} (${error instanceof Error ? error.message : String(error)}); this prompt runs on ${current?.modelId}/${current?.thinking}.`,
74
+ "warning",
75
+ );
76
+ }
77
+ }
78
+ const held = this.session.store ? this.heldMismatch() : undefined;
79
+ const notice = held && JSON.stringify(held);
80
+ // The hold is the user's; report the divergence once instead of overriding a deliberate model choice.
81
+ if (notice && notice !== this.heldNotice)
82
+ ctx.ui?.notify?.(
83
+ `Manual hold is ${held.profile} (${held.expected.modelId}/${held.expected.thinking}) but Pi is using ${held.observed.modelId}/${held.observed.thinking}. Run /freeflow profile ${held.profile} to reapply it, or /freeflow profile auto to release the hold.`,
84
+ "warning",
85
+ );
86
+ this.heldNotice = notice;
87
+ }
88
+ async validateProfilePairs(pairs) {
89
+ check(pairs.coordinator, "profile_missing", "Configure the coordinator profile.");
90
+ const helper = pairs.helper !== undefined;
91
+ const executor = pairs.executor !== undefined;
92
+ check(helper || executor, "profile_missing", "Configure an enabled worker profile.");
93
+ const delegation = helper && executor ? "both" : helper ? "helper" : "executor";
94
+ const profiles = Object.fromEntries(
95
+ Object.entries(pairs).map(([profile, pair]) => [
96
+ profile,
97
+ { provider: pair.provider, model: pair.modelId, thinking: pair.thinking },
98
+ ]),
99
+ );
100
+ const result = await resolveCognitiveRoutingState(
101
+ { cognitiveRouting: { enabled: true, delegation, profiles } },
102
+ {},
103
+ this.session.ctx,
104
+ );
105
+ check(result.effective, result.blockingReason.code, result.blockingReason.message);
106
+ }
107
+ model(profile) {
108
+ const pair = this.session.profilePair(profile);
109
+ const model = this.session.ctx?.modelRegistry?.find(pair.provider, pair.modelId);
110
+ check(model, "profile_unavailable");
111
+ return model;
112
+ }
113
+ /** Advisory preset checks for the effective Coordinator and enabled workers. */
114
+ presetWarnings(state = this.session.stateData()) {
115
+ if (!this.session.supported()) return [];
116
+ const pairs = {};
117
+ for (const profile of ["coordinator", ...this.session.enabledWorkers(state)])
118
+ try {
119
+ pairs[profile] = this.session.profilePair(profile);
120
+ } catch {}
121
+ return presetWarnings(
122
+ pairs,
123
+ this.session.enabledWorkers(state),
124
+ (provider, modelId) => this.session.ctx?.modelRegistry?.find?.(provider, modelId),
125
+ (model) => keepsCacheAcrossEffort(model, this.session.ctx),
126
+ // Pi's only retention control; unset means the short tier.
127
+ process.env.PI_CACHE_RETENTION === "long" ? "long" : "short",
128
+ );
129
+ }
130
+ presetNotice;
131
+ announcePresets(ctx = this.session.ctx) {
132
+ const warnings = this.presetWarnings();
133
+ const notice = warnings.length ? JSON.stringify(warnings) : undefined;
134
+ if (notice && notice !== this.presetNotice)
135
+ ctx?.ui?.notify?.(`Cognitive Routing preset: ${warnings.join(" ")}`, "warning");
136
+ this.presetNotice = notice;
137
+ }
138
+ /**
139
+ * The user picked a model or effort in Pi itself. Under automatic control that ends routing's ownership of the
140
+ * model: returns true when it did, so the caller turns routing off for the session where the user can see it.
141
+ */
142
+ async nativeChange(ctx) {
143
+ this.session.ctx = ctx;
144
+ if (!this.session.supported() || !this.session.store) return false;
145
+ if (this.applying) {
146
+ if (ctx.model?.provider !== this.applying.provider || ctx.model?.id !== this.applying.modelId) {
147
+ this.externalChange = true;
148
+ this.session.revision++;
149
+ }
150
+ return false; // Never await a queue already held by our setter.
151
+ }
152
+ // The user's own pick in Pi wins over a switch still waiting for the next prompt.
153
+ this.setPending(undefined);
154
+ const state = this.session.stateData();
155
+ if (
156
+ state.control !== "automatic" ||
157
+ !state.profile ||
158
+ samePair(this.session.observed(), this.session.profilePair(state.profile))
159
+ )
160
+ return false;
161
+ try {
162
+ this.session.revision++;
163
+ this.session.manualHold = undefined;
164
+ this.session.automaticControl = false;
165
+ this.session.append({ type: "control", control: "inactive", reason: "External native model/effort change" });
166
+ const now = this.session.observed();
167
+ ctx.ui?.notify?.(
168
+ `Cognitive Routing is off for this session: the model or effort was changed outside routing${now ? ` (now ${now.modelId}/${now.thinking})` : ""}. Turn it back on with /freeflow profile auto, Ctrl+Shift+A, or /freeflow settings.`,
169
+ "warning",
170
+ );
171
+ return true;
172
+ } catch (error) {
173
+ this.session.mark(error);
174
+ return false;
175
+ }
176
+ }
177
+ async applyPair(target) {
178
+ const subject = this.session.subject(),
179
+ token = this.session.token,
180
+ prior = this.session.observed();
181
+ if (samePair(prior, target)) return;
182
+ const model = this.session.ctx.modelRegistry.find(target.provider, target.modelId);
183
+ check(model, "profile_unavailable");
184
+ const auth = await this.session.ctx.modelRegistry.getApiKeyAndHeaders(model);
185
+ this.session.guard(subject);
186
+ check(auth?.ok, "profile_unauthenticated");
187
+ this.applying = target;
188
+ let ownedPair = target;
189
+ this.externalChange = false;
190
+ try {
191
+ check(await this.pi.setModel(model), "model_rejected");
192
+ this.session.guard(subject);
193
+ this.pi.setThinkingLevel(target.thinking);
194
+ check(!this.externalChange && samePair(this.session.observed(), target), "configuration_mismatch");
195
+ } catch (error) {
196
+ if (this.session.current(subject) && prior && !this.externalChange) {
197
+ const old = this.session.ctx.modelRegistry.find(prior.provider, prior.modelId);
198
+ try {
199
+ this.applying = prior;
200
+ ownedPair = prior;
201
+ check(old && (await this.pi.setModel(old)), "rollback_failed");
202
+ this.session.guard(subject);
203
+ this.pi.setThinkingLevel(prior.thinking);
204
+ check(samePair(this.session.observed(), prior), "rollback_failed");
205
+ } catch {
206
+ if (this.session.current(subject))
207
+ this.session.error = "configuration_unknown: rollback could not be observed";
208
+ }
209
+ }
210
+ throw error;
211
+ } finally {
212
+ if (token === this.session.token && this.applying === ownedPair) this.applying = undefined;
213
+ }
214
+ }
215
+ /** A profile switch made while idle, kept with the session's stage so a reload keeps it. */
216
+ pending() {
217
+ return stagedFor(this.session.ctx?.sessionManager)?.pair;
218
+ }
219
+ setPending(pair) {
220
+ const stage = pair ? sessionStage(this.session.ctx?.sessionManager) : stagedFor(this.session.ctx?.sessionManager);
221
+ if (!stage) return;
222
+ if (pair) stage.pair = pair;
223
+ else delete stage.pair;
224
+ }
225
+ /** The pair routing intends the host to run: a pending switch, or what the host runs now. */
226
+ intended() {
227
+ return this.pending() ?? this.session.observed();
228
+ }
229
+ async checkPair(target) {
230
+ const model = this.session.ctx.modelRegistry.find(target.provider, target.modelId);
231
+ check(model, "profile_unavailable");
232
+ check((await this.session.ctx.modelRegistry.getApiKeyAndHeaders(model))?.ok, "profile_unauthenticated");
233
+ }
234
+ /**
235
+ * Apply a switch made while idle, so the prompt runs on the profile the user chose. If the host cannot run it,
236
+ * the switch and the control changes staged with it are dropped, so recorded control still names what runs.
237
+ */
238
+ async applyPending() {
239
+ const target = this.pending();
240
+ if (!target) return;
241
+ try {
242
+ await this.applyPair(target);
243
+ } catch (error) {
244
+ const stage = stagedFor(this.session.ctx?.sessionManager);
245
+ if (stage) stage.events = [];
246
+ throw error;
247
+ } finally {
248
+ this.setPending(undefined);
249
+ }
250
+ }
251
+ /**
252
+ * Switch the host model, then record what the switch means. A switch that fails records nothing, and a
253
+ * record that cannot be written restores the prior model, so recorded control never names a profile
254
+ * whose model the host is not running.
255
+ */
256
+ async transition(target, record, subject) {
257
+ if (this.session.ctx?.isIdle?.() !== false) {
258
+ // An idle switch reaches Pi's model at the next prompt; checking the target now keeps errors immediate.
259
+ if (!samePair(this.intended(), target)) await this.checkPair(target);
260
+ this.session.guard(subject);
261
+ record();
262
+ this.setPending(samePair(this.session.observed(), target) ? undefined : target);
263
+ return;
264
+ }
265
+ this.setPending(undefined);
266
+ const prior = this.session.observed();
267
+ await this.applyPair(target);
268
+ try {
269
+ this.session.guard(subject);
270
+ record();
271
+ } catch (error) {
272
+ if (subject.token === this.session.token && prior && !samePair(this.session.observed(), prior))
273
+ await this.applyPair(prior).catch(() => {});
274
+ throw error;
275
+ }
276
+ }
277
+ /**
278
+ * Control requests run one at a time, each against the state left by the previous one. The host model
279
+ * is switched before the new control is recorded, so a superseded or failed request never leaves a
280
+ * recorded profile running on another profile's model.
281
+ */
282
+ async control(request, manual) {
283
+ return this.session.enqueue(async () => {
284
+ this.session.revision++;
285
+ const subject = this.session.subject();
286
+ try {
287
+ check(this.session.capability?.effective && this.session.store, "routing_unavailable");
288
+ await this.session.store.reconcile();
289
+ this.session.guard(subject);
290
+ const previous = this.session.stateData();
291
+ const profile = typeof request === "function" ? request(previous) : request;
292
+ if (manual && isWorkerProfile(profile))
293
+ check(
294
+ this.session.enabledWorkers().includes(profile),
295
+ "worker_disabled",
296
+ `${profile} is not enabled by delegation mode.`,
297
+ );
298
+ this.session.error = undefined;
299
+ this.session.suppressed = false;
300
+ if (
301
+ previous.control === (manual ? "manual" : "automatic") &&
302
+ previous.profile === profile &&
303
+ samePair(this.intended(), this.session.profilePair(profile))
304
+ ) {
305
+ this.session.manualHold = manual ? profile : undefined;
306
+ this.session.automaticControl = !manual;
307
+ return { status: manual ? "active" : "automatic" };
308
+ }
309
+ await this.transition(
310
+ this.session.profilePair(profile),
311
+ () =>
312
+ this.session.append({
313
+ type: "control",
314
+ control: manual ? "manual" : "automatic",
315
+ profile,
316
+ reason: manual ? "Explicit user manual hold" : "Explicit user automatic release/reconciliation",
317
+ }),
318
+ subject,
319
+ );
320
+ this.session.manualHold = manual ? profile : undefined;
321
+ this.session.automaticControl = !manual;
322
+ return { status: manual ? "active" : "automatic" };
323
+ } catch (error) {
324
+ if (this.session.current(subject) && !(error instanceof RoutingError && error.code === "worker_disabled"))
325
+ this.session.mark(error);
326
+ return { status: "blocked", reason: error instanceof Error ? error.message : String(error) };
327
+ }
328
+ });
329
+ }
330
+ /** Advance one manual-hold step from the state left by any earlier queued request. */
331
+ cycleManualProfile() {
332
+ return this.control((state) => {
333
+ const profiles = ["coordinator", ...workersForDelegation(this.session.delegation(state))];
334
+ return profiles[(profiles.indexOf(state.profile ?? "coordinator") + 1) % profiles.length];
335
+ }, true);
336
+ }
337
+ setManualProfile(profile, _mechanism) {
338
+ return this.control(profile, true);
339
+ }
340
+ sessionProfileOverrides() {
341
+ return Object.fromEntries(this.session.stateData().profileOverrides);
342
+ }
343
+ sessionDelegationOverride() {
344
+ return this.session.stateData().delegationOverride;
345
+ }
346
+ async setSessionDelegationOverride(override, mechanism = "Session delegation override") {
347
+ return this.session.enqueue(async () => {
348
+ this.session.revision++;
349
+ const subject = this.session.subject();
350
+ try {
351
+ check(this.session.capability?.effective && this.session.store, "routing_unavailable");
352
+ check(
353
+ this.session.ctx?.isIdle?.() !== false,
354
+ "host_busy",
355
+ "Wait for Pi to become idle before changing delegation mode.",
356
+ );
357
+ await this.session.store.reconcile();
358
+ this.session.guard(subject);
359
+ const state = this.session.stateData();
360
+ if ((state.delegationOverride ?? null) === override) return { status: "unchanged" };
361
+ const delegation = override ?? this.session.capability.delegation;
362
+ await this.validateProfilePairs(this.session.profilePairs(this.session.requiredProfiles(state, delegation)));
363
+ this.session.guard(subject);
364
+ this.session.append({ type: "delegation-override", delegation: override, reason: mechanism });
365
+ this.announcePresets();
366
+ return { status: "stored" };
367
+ } catch (error) {
368
+ return { status: "blocked", reason: error instanceof Error ? error.message : String(error) };
369
+ }
370
+ });
371
+ }
372
+ async setSessionProfileOverride(profile, override, mechanism = "Session profile override") {
373
+ return this.session.enqueue(async () => {
374
+ this.session.revision++;
375
+ const subject = this.session.subject();
376
+ try {
377
+ check(this.session.capability?.effective && this.session.store, "routing_unavailable");
378
+ check(
379
+ this.session.ctx?.isIdle?.() !== false,
380
+ "host_busy",
381
+ "Wait for Pi to become idle before changing session presets.",
382
+ );
383
+ await this.session.store.reconcile();
384
+ this.session.guard(subject);
385
+ const state = this.session.stateData();
386
+ const previous = state.profileOverrides.get(profile);
387
+ if ((override === null && !previous) || (override !== null && samePair(previous, override)))
388
+ return { status: "unchanged" };
389
+ const target = override ?? this.session.configuredProfilePair(profile);
390
+ const profiles = new Set(this.session.requiredProfiles(state));
391
+ profiles.add(profile);
392
+ const pairs = Object.fromEntries(
393
+ [...profiles].map((name) => [name, name === profile ? target : this.session.profilePair(name)]),
394
+ );
395
+ await this.validateProfilePairs(pairs);
396
+ const record = () =>
397
+ this.session.append({
398
+ type: "profile-overrides",
399
+ overrides: { [profile]: override },
400
+ reason: mechanism,
401
+ });
402
+ if (state.control !== "inactive" && state.profile === profile) {
403
+ await this.transition(target, record, subject);
404
+ this.session.error = undefined;
405
+ this.announcePresets();
406
+ return { status: "active" };
407
+ }
408
+ this.session.guard(subject);
409
+ record();
410
+ this.session.error = undefined;
411
+ this.announcePresets();
412
+ return { status: "stored" };
413
+ } catch (error) {
414
+ if (this.session.current(subject)) this.session.mark(error);
415
+ return { status: "blocked", reason: error instanceof Error ? error.message : String(error) };
416
+ }
417
+ });
418
+ }
419
+ async resetSessionProfileOverrides(mechanism = "Reset session profile overrides") {
420
+ return this.session.enqueue(async () => {
421
+ this.session.revision++;
422
+ const subject = this.session.subject();
423
+ try {
424
+ check(this.session.capability?.effective && this.session.store, "routing_unavailable");
425
+ check(
426
+ this.session.ctx?.isIdle?.() !== false,
427
+ "host_busy",
428
+ "Wait for Pi to become idle before resetting session presets.",
429
+ );
430
+ await this.session.store.reconcile();
431
+ this.session.guard(subject);
432
+ const state = this.session.stateData();
433
+ if (state.profileOverrides.size === 0) return { status: "unchanged" };
434
+ const previous = Object.fromEntries(state.profileOverrides);
435
+ const required = this.session.requiredProfiles(state);
436
+ const targetPairs = Object.fromEntries(
437
+ required.map((profile) => [profile, this.session.configuredProfilePair(profile)]),
438
+ );
439
+ await this.validateProfilePairs(targetPairs);
440
+ const record = () =>
441
+ this.session.append({
442
+ type: "profile-overrides",
443
+ overrides: { coordinator: null, helper: null, executor: null },
444
+ reason: mechanism,
445
+ });
446
+ const active = state.control !== "inactive" ? state.profile : undefined;
447
+ if (active && previous[active]) {
448
+ await this.transition(targetPairs[active], record, subject);
449
+ this.session.error = undefined;
450
+ this.announcePresets();
451
+ return { status: "active" };
452
+ }
453
+ this.session.guard(subject);
454
+ record();
455
+ this.session.error = undefined;
456
+ this.announcePresets();
457
+ return { status: "stored" };
458
+ } catch (error) {
459
+ if (this.session.current(subject)) this.session.mark(error);
460
+ return { status: "blocked", reason: error instanceof Error ? error.message : String(error) };
461
+ }
462
+ });
463
+ }
464
+ setAutomaticControl(_mechanism) {
465
+ return this.control("coordinator", false);
466
+ }
467
+ }