@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
@@ -1,50 +1,152 @@
1
+ import { calculateContextTokens, compact as piCompact, estimateTokens } from "@earendil-works/pi-coding-agent";
1
2
  import { dirname, resolve } from "node:path";
2
3
  import { fileURLToPath } from "node:url";
3
- import { RequestHistory } from "./runtime/request-history.js";
4
+ import { RequestHistory } from "./host/request-history.js";
4
5
  import { registerProviderSupport } from "./provider-support/index.js";
5
- import { RoutingRuntime } from "./cognitive-routing-v2/runtime.js";
6
- import { workersForDelegation } from "./cognitive-routing-v2/types.js";
7
- import { applyRoutingToolVisibility, registerRoutingTools } from "./cognitive-routing-v2/tools.js";
8
- import { ConversationHistoryRuntime } from "./conversation-history/runtime.js";
9
- import { FreeflowContextRuntime } from "./freeflow-context/runtime.js";
10
- import { CONTEXT_VIRTUALIZATION_TOOL_NAME, registerFreeflowContextTool } from "./freeflow-context/tool.js";
11
- import { handleContextCommand } from "./context-virtualization/commands.js";
12
- import { ContextVirtualizationRuntime } from "./context-virtualization/runtime.js";
13
- import { handleFreeflowCommand } from "./settings/settings-ui.js";
14
- import { isPiFlowHost } from "./runtime/runtime-identity.js";
6
+ import { CacheHealth } from "./provider-support/cache/health.js";
7
+ import { CacheMonitor } from "./provider-support/cache/monitor.js";
8
+ import { RoutingRuntime } from "./cognitive-routing/runtime.js";
9
+ import { applyRoutingToolVisibility, registerRoutingTools } from "./cognitive-routing/tools.js";
10
+ import { handleFreeflowCommand } from "./host/settings/freeflow-command.js";
11
+ import { toolExecutionSections, toolExecutionTail } from "./tool-execution/prompt.js";
12
+ import { FileTracking, withLines } from "./tool-execution/file-tracking.js";
13
+ import { GuidanceNotices } from "./tool-execution/guidance-notices.js";
14
+ import { registerApplyPatch } from "./tool-execution/apply-patch/tool.js";
15
+ import { applyToolExecutionTools } from "./tool-execution/tools.js";
16
+ import {
17
+ BackgroundJobs,
18
+ MONITOR_PER_MINUTE,
19
+ MONITOR_TYPE,
20
+ freeflowNotice,
21
+ NOTICE_TYPE,
22
+ registerBackgroundTools,
23
+ } from "./tool-execution/background.js";
24
+ import { Inbound, renderNotice } from "./host/inbound.js";
25
+ import { currentSections, headSections, keepSections } from "./host/section-keeper.js";
26
+ import { backgroundRefusal } from "./tool-execution/bash-guard.js";
27
+ import { CompactionController, RECOVERY_TYPE, USER_REQUEST } from "./compaction/controller.js";
28
+ import { applyCompactionTools, registerCompactionTool } from "./compaction/tool.js";
29
+ import { strictest } from "./compaction/thresholds.js";
30
+ import { SNAPSHOT_TYPE, TASK_ENTRY, TaskMemory } from "./task-memory/runtime.js";
31
+ import { applyTaskMemoryTools, registerTaskMemoryTool } from "./task-memory/tool.js";
32
+ import { freeflowHome } from "./task-memory/registry.js";
33
+ import { DEFAULT_INBOUND_POLICY } from "./messaging/config.js";
34
+ import { MESSAGE_TYPE, Messaging } from "./messaging/messaging.js";
35
+ import { applyMessagingTools, registerMessagingTools } from "./messaging/tools.js";
36
+ import { renderMessage } from "./messaging/render.js";
37
+ import { renderSnapshotMessage } from "./task-memory/render.js";
38
+ import { applyAskUserTool, registerAskUserTool } from "./ask-user/tool.js";
39
+ import { confirmWithUser } from "./ask-user/ask.js";
40
+ import { tagProjectedMessages } from "./host/projection-tags.js";
41
+ import { trustStartedSession } from "./host/read-only-session.js";
42
+ import { takeStage } from "./host/staging.js";
15
43
  import {
16
44
  CONTRIBUTOR_COMMANDS,
17
45
  WORKFLOW_COMMANDS,
18
46
  freeflowModelSkillPaths,
47
+ freeflowSkillPath,
48
+ STABLE_FREEFLOW_SURFACE,
49
+ skillPrompt,
50
+ } from "./host/catalog.js";
51
+ import {
52
+ readCapabilityState,
53
+ restoreSessionOverrides,
54
+ setSessionCoreOverride,
55
+ writeStagedSessionOverrides,
56
+ } from "./host/config.js";
57
+ import {
19
58
  getRuntimeContext,
20
59
  hasUsableMandatoryPrompts,
21
60
  isPromptAvailable,
22
- readCapabilityState,
23
61
  refreshRuntimeContext,
24
- restoreSessionOverrides,
25
62
  stableRuntimeContext,
26
- STABLE_FREEFLOW_SURFACE,
27
- filterBootstrapMessage,
28
- setFreeflowStatus,
29
- skillPrompt,
30
- withFreeflowRuntimeState,
31
- } from "./runtime/runtime-context.js";
63
+ } from "./host/prompts.js";
64
+ import { filterBootstrapMessage, withFreeflowRuntimeState } from "./host/runtime-state.js";
65
+ import { setFreeflowStatus } from "./host/status.js";
66
+ import { cachedBranch } from "./host/branch.js";
67
+ const COMPACTION_NOTICE_TYPE = "freeflow-compaction-notice";
68
+ // The user's /freeflow compact, delivered as Freeflow's message.
69
+ const COMPACT_REQUEST_TYPE = "freeflow-compaction-request";
70
+ /** Estimated size of the full native history plus the system prompt: what a request without projection carries. */
71
+ /**
72
+ * The context a turn ended with, as Pi estimates it: the turn's own usage plus the tool results after it. Pi's
73
+ * getContextUsage rebuilds the session projection to find the same numbers. Undefined when the turn has no usable usage
74
+ * (aborted, failed, or none reported), where Pi's own estimate applies.
75
+ */
76
+ function turnContextTokens(turn) {
77
+ const message = turn?.message;
78
+ if (message?.role !== "assistant" || message.stopReason === "aborted" || message.stopReason === "error") return;
79
+ if (!message.usage || calculateContextTokens(message.usage) <= 0) return;
80
+ return (turn.toolResults ?? []).reduce(
81
+ (sum, result) => sum + estimateTokens(result),
82
+ calculateContextTokens(message.usage),
83
+ );
84
+ }
85
+ /** Entry types that add no message to Pi's session projection. */
86
+ /** The sections Freeflow sets: its own, and Pi's rules and docs, which Tool Execution changes. */
87
+ const OWN_SECTION_NAMES = ["freeflow_guidance", "environment", "tool_execution", "rules", "docs"];
88
+ const NO_MESSAGE = new Set(["custom", "label", "session_info", "model_change", "thinking_level_change"]);
89
+ const historyEstimate = new WeakMap();
90
+ /**
91
+ * The full history's size, as every message's estimate plus the system prompt. Kept per session and extended by the
92
+ * messages appended since, because the branch only grows between compactions; a compaction, summary or custom
93
+ * message (which Pi's projection may place or replace) recomputes it from Pi's projection.
94
+ */
95
+ function fullHistoryTokens(ctx) {
96
+ const manager = ctx.sessionManager;
97
+ const system = Math.ceil((ctx.getSystemPrompt?.() ?? "").length / 4);
98
+ if (!manager?.buildSessionProjection) return system;
99
+ const branch = cachedBranch(manager);
100
+ const previous = historyEstimate.get(manager);
101
+ let tokens;
102
+ if (previous && branch === previous.branch) tokens = previous.tokens;
103
+ else if (
104
+ previous &&
105
+ previous.branch.length > 0 &&
106
+ branch.length >= previous.branch.length &&
107
+ branch[previous.branch.length - 1] === previous.branch.at(-1)
108
+ ) {
109
+ const added = branch.slice(previous.branch.length);
110
+ if (added.every((entry) => entry.type === "message" || NO_MESSAGE.has(entry.type)))
111
+ tokens = added.reduce(
112
+ (sum, entry) => sum + (entry.type === "message" ? estimateTokens(entry.message) : 0),
113
+ previous.tokens,
114
+ );
115
+ }
116
+ const counted =
117
+ tokens ??
118
+ (manager.buildSessionProjection()?.messages ?? []).reduce((sum, message) => sum + estimateTokens(message), 0);
119
+ historyEstimate.set(manager, { branch, tokens: counted });
120
+ return counted + system;
121
+ }
122
+ /**
123
+ * Send a skill command's prompt as the user's message. A command can run while Pi is busy, which from Pi 1.2.0 includes
124
+ * the start of a run (before_agent_start); Pi then refuses a user message without `deliverAs`, so it is queued to
125
+ * follow the current work instead.
126
+ */
127
+ function sendSkillPrompt(pi, skill, args) {
128
+ pi.sendUserMessage(skillPrompt(skill, args), { expandPromptTemplates: true, deliverAs: "followUp" });
129
+ }
32
130
  async function sendSkillCommand(pi, ctx, skill, args) {
33
- const state = await readCapabilityState(ctx.cwd, ctx, pi.host);
131
+ const state = await readCapabilityState(ctx.cwd, ctx);
34
132
  if (skill === "setup-freeflow" && !state.configured) {
35
- await pi.sendUserMessage(skillPrompt(skill, args), { expandPromptTemplates: true });
133
+ sendSkillPrompt(pi, skill, args);
36
134
  return;
37
135
  }
38
136
  if (!state.configured || !state.enabled) {
39
137
  ctx.ui.notify("Run /setup-freeflow or enable Freeflow before dispatching this skill.", "warning");
40
138
  return;
41
139
  }
140
+ if (state.skills?.off?.includes(skill)) {
141
+ ctx.ui.notify(`${skill} is turned off in /freeflow settings (Skills).`, "warning");
142
+ return;
143
+ }
42
144
  const prompts = await getRuntimeContext(state);
43
145
  if (!hasUsableMandatoryPrompts(prompts)) {
44
146
  ctx.ui.notify("Freeflow core prompts are unavailable; the requested skill cannot be dispatched.", "warning");
45
147
  return;
46
148
  }
47
- await pi.sendUserMessage(skillPrompt(skill, args), { expandPromptTemplates: true });
149
+ sendSkillPrompt(pi, skill, args);
48
150
  }
49
151
  function freeflowCompletions(prefix, routingAvailable) {
50
152
  const query = prefix ?? "";
@@ -54,51 +156,117 @@ function freeflowCompletions(prefix, routingAvailable) {
54
156
  ["settings local", "local", "Edit personal overrides for this repository"],
55
157
  ["settings repo", "repo", "Edit shared repository settings"],
56
158
  ]
57
- : query.startsWith("profile ") && routingAvailable
159
+ : query.startsWith("task ")
58
160
  ? [
59
- ["profile coordinator", "coordinator", "Hold Coordinator manually"],
60
- ["profile helper", "helper", "Hold Helper manually when enabled"],
61
- ["profile executor", "executor", "Hold Executor manually when enabled"],
62
- ["profile auto", "auto", "Return to automatic Coordinator reconciliation"],
63
- ["profile history", "history", "Read routing observations"],
161
+ ["task list", "list", "This repository's records, or a project's tasks"],
162
+ ["task new", "new", "Create a task in this repository's project and attach it"],
163
+ ["task attach", "attach", "Attach a record by task ID or path"],
164
+ ["task focus", "focus", "Move the focus to an attached record"],
165
+ ["task detach", "detach", "Detach a record"],
166
+ ["task confirm", "confirm", "Make held records writable here"],
167
+ ["task fanout", "fanout", "Create a child of the focused task with its own worktree"],
168
+ ["task child", "child", "Create a child of a task and attach it here"],
169
+ ["task import", "import", "Move a repository record into a project"],
170
+ ["task remove", "remove", "Move a task and its children to the trash"],
64
171
  ]
65
- : query.startsWith("context ")
172
+ : query.startsWith("project ")
66
173
  ? [
67
- ["context status", "status", "Show Freeflow Context state"],
68
- ["context list", "list", "List archived context projections"],
69
- ["context restore", "restore", "Restore one or more context references"],
70
- ["context reset all", "reset all", "Reset projection decisions on the active branch"],
174
+ ["project list", "list", "Every project"],
175
+ ["project new", "new", "Create a project and link this repository"],
176
+ ["project link", "link", "Link this repository to a project"],
177
+ ["project remove", "remove", "Move a project and its tasks to the trash"],
71
178
  ]
72
- : [
73
- ["settings", "settings", "Open personal override settings"],
74
- ["status", "status", "Show effective Freeflow state"],
75
- ["context", "context", "Inspect Freeflow Context"],
76
- ...(routingAvailable
77
- ? [
78
- ["profile", "profile", "Hold or release Cognitive Routing profile control"],
79
- ["resume", "resume", "Resume the current saved routing responsibility"],
80
- ]
81
- : []),
82
- ["enable", "enable", "Enable Freeflow for this repository"],
83
- ["disable", "disable", "Disable Freeflow for this repository"],
84
- ];
179
+ : query.startsWith("profile ") && routingAvailable
180
+ ? [
181
+ ["profile coordinator", "coordinator", "Hold Coordinator manually"],
182
+ ["profile helper", "helper", "Hold Helper manually when enabled"],
183
+ ["profile executor", "executor", "Hold Executor manually when enabled"],
184
+ ["profile auto", "auto", "Return to automatic Coordinator reconciliation"],
185
+ ["profile history", "history", "Read routing observations"],
186
+ ]
187
+ : [
188
+ ["settings", "settings", "Open settings; Tab switches Session, Personal and Repository"],
189
+ ["status", "status", "Show effective Freeflow state"],
190
+ ["compact", "compact", "Prepare, compact and recover now"],
191
+ ["task", "task", "Attach, focus, detach or confirm Working Records (task memory)"],
192
+ ["project", "project", "Create or link a task-memory project"],
193
+ ["trash", "trash", "Removed tasks and projects; trash empty deletes them for good"],
194
+ ...(routingAvailable
195
+ ? [
196
+ ["profile", "profile", "Hold or release Cognitive Routing profile control"],
197
+ ["resume", "resume", "Resume the current saved routing responsibility"],
198
+ ]
199
+ : []),
200
+ ["enable", "enable", "Enable Freeflow for this repository"],
201
+ ["disable", "disable", "Disable Freeflow for this repository"],
202
+ ];
85
203
  return choices
86
204
  .filter(([value]) => value.startsWith(query))
87
205
  .map(([value, label, description]) => ({ value, label, description }));
88
206
  }
89
207
  export default function freeflow(pi) {
90
- registerProviderSupport(pi);
208
+ // Cache diagnostics are reported to /freeflow status; the footer only shows current settings.
209
+ const cacheMonitor = new CacheMonitor();
210
+ // Only an explicit master-switch disable turns provider support off; unconfigured repositories keep it.
211
+ const providerSupport = registerProviderSupport(
212
+ pi,
213
+ () => !(capability?.configured === true && capability.enabled === false),
214
+ cacheMonitor,
215
+ );
91
216
  const api = pi;
92
217
  const packageRoot = resolve(dirname(fileURLToPath(import.meta.url)), "../..");
93
218
  const routing = new RoutingRuntime(api, [packageRoot]);
94
- const requestHistory = new RequestHistory(api);
219
+ // A Coordinator waiting on its worker will resume, so its prompt cache is worth keeping warm.
220
+ providerSupport.keepAlive.setHoldSource(() => routing.suspendedCoordinator());
221
+ // A worker may share the Coordinator's model; each profile keeps its own warmed request.
222
+ providerSupport.keepAlive.setRequesterSource(() => routing.observationScope().profile);
223
+ const requestHistory = new RequestHistory(api, cacheMonitor);
224
+ // Where the Freeflow Runtime State was last placed on paths that bypass request history.
225
+ const runtimeStateAnchor = {};
226
+ const resetHistory = () => {
227
+ requestHistory.reset();
228
+ delete runtimeStateAnchor.content;
229
+ delete runtimeStateAnchor.index;
230
+ };
95
231
  let capability;
232
+ // The system sections of the last prompt's run, while Freeflow sets sections; see keepSections.
233
+ let ownSections;
234
+ let ownSectionsVersion = 0;
235
+ let ownHead;
236
+ // The sections Freeflow sets, and whether the current run is one a prompt started (so its requests carry them), or
237
+ // one an extension's message started that Freeflow still set its sections for (Pi 1.2.0 and later).
238
+ let ownNames = [];
239
+ let promptRun = false;
240
+ let messageRun = false;
241
+ // Whether the session has had a prompt. Pi before 1.2.0 builds the system prompt only for prompts, so until then a
242
+ // turn Freeflow started for a notice would reach the model with none: notices wait for the user's first prompt.
243
+ let prompted = false;
244
+ const files = new FileTracking();
245
+ const toolExecutionEffective = () => capability?.toolExecution?.effective === true;
246
+ const guidance = new GuidanceNotices(packageRoot);
247
+ const guidanceNoticesEffective = () =>
248
+ toolExecutionEffective() && capability?.toolExecution?.guidanceNotices !== false;
249
+ const compactionEffective = () => capability?.compaction?.effective === true;
250
+ const taskMemoryEffective = () => capability?.taskMemory?.effective === true;
251
+ const askUserEffective = () => capability?.askUser?.effective === true;
252
+ const messagingEffective = () => capability?.messaging?.effective === true;
253
+ const cacheHealth = new CacheHealth();
96
254
  let prompts;
97
- let context;
98
- let virtualization;
99
- let history;
100
255
  let refreshState = true;
101
- let sessionContext;
256
+ // Skills already read, as the Runtime State lists them: taken only where the state refreshes anyway (run
257
+ // boundaries, settings changes, compaction, navigation), so a skill read mid-run adds no state message.
258
+ let stateSkills = [];
259
+ // Pi's event contexts read the host model and effort live. Its interactive shortcut contexts copy them at the
260
+ // key press, so after a switch they still name the previous model; routing must never judge a switch by them.
261
+ let liveHost;
262
+ const withLiveModel = (ctx) =>
263
+ Object.defineProperties(
264
+ { ...ctx },
265
+ {
266
+ model: { enumerable: true, get: () => liveHost?.model ?? ctx.model },
267
+ thinkingLevel: { enumerable: true, get: () => api.getThinkingLevel?.() ?? ctx.thinkingLevel },
268
+ },
269
+ );
102
270
  let surfaceGeneration = 0;
103
271
  const unavailable = (state, message) => ({
104
272
  ...state,
@@ -107,171 +275,786 @@ export default function freeflow(pi) {
107
275
  });
108
276
  async function loadSurface(ctx) {
109
277
  const generation = surfaceGeneration;
110
- const next = await readCapabilityState(ctx.cwd, ctx, pi.host);
278
+ const next = await readCapabilityState(ctx.cwd, ctx);
111
279
  const loaded = await getRuntimeContext(STABLE_FREEFLOW_SURFACE);
112
280
  if (generation !== surfaceGeneration) throw new Error("Discarded surface preparation for a replaced session.");
113
281
  if (!hasUsableMandatoryPrompts(loaded)) {
114
- for (const key of ["cognitiveRouting", "contextVirtualization", "conversationHistory"])
282
+ for (const key of ["cognitiveRouting", "toolExecution"])
115
283
  next[key] = unavailable(next[key], "Mandatory Freeflow prompts are unavailable.");
116
284
  }
117
285
  if (next.cognitiveRouting.effective && !isPromptAvailable(loaded.cognitiveRoutingPrompt))
118
286
  next.cognitiveRouting = unavailable(next.cognitiveRouting, "Routing bootstrap cue is unavailable.");
119
- if (next.contextVirtualization.effective && !isPromptAvailable(loaded.contextVirtualizationPrompt))
120
- next.contextVirtualization = unavailable(
121
- next.contextVirtualization,
122
- "Context Virtualization guidance is unavailable.",
123
- );
124
- if (next.conversationHistory.effective && !isPromptAvailable(loaded.conversationHistoryPrompt))
125
- next.conversationHistory = unavailable(next.conversationHistory, "Conversation History guidance is unavailable.");
126
- // The existing reduction engine is not the future Context Control port. Do not pretend
127
- // its independent transform ordering has been qualified with the new projection.
128
- if (
129
- next.cognitiveRouting.effective &&
130
- next.cognitiveRouting.projection &&
131
- (next.contextVirtualization.effective || next.conversationHistory.effective)
132
- )
133
- next.cognitiveRouting = unavailable(
134
- next.cognitiveRouting,
135
- "New routing projection with legacy context transforms is not qualified. Disable that combination explicitly.",
136
- );
137
- if (next.cognitiveRouting.enabled && isPiFlowHost(pi.host))
138
- next.cognitiveRouting = unavailable(
139
- next.cognitiveRouting,
140
- "The redesigned PiFlow adapter requires separate qualification.",
141
- );
142
287
  capability = next;
143
288
  prompts = loaded;
144
- sessionContext = ctx;
145
289
  return next;
146
290
  }
147
- function contextTools() {
148
- registerFreeflowContextTool(
149
- api,
150
- () => virtualization,
151
- () => history,
152
- {
153
- contextVirtualization: capability?.contextVirtualization?.effective === true,
154
- conversationHistory: capability?.conversationHistory?.effective === true,
155
- },
156
- );
157
- if (api.getActiveTools && api.setActiveTools) {
158
- const current = new Set(api.getActiveTools());
159
- current.add(CONTEXT_VIRTUALIZATION_TOOL_NAME);
160
- const next = [...current];
161
- if (JSON.stringify(next) !== JSON.stringify(api.getActiveTools())) api.setActiveTools(next);
162
- }
163
- }
164
291
  function status(ctx) {
165
292
  applyRoutingToolVisibility(api, routing, capability?.cognitiveRouting?.effective === true);
293
+ applyToolExecutionTools(api, toolExecutionEffective());
294
+ applyCompactionTools(api, compactionEffective());
295
+ applyTaskMemoryTools(api, taskMemoryEffective());
296
+ applyAskUserTool(api, askUserEffective());
297
+ applyMessagingTools(api, messagingEffective());
298
+ syncMessaging(ctx);
166
299
  setFreeflowStatus(ctx, capability, routing.state(), prompts);
167
300
  }
301
+ // Control and setting changes made between prompts are staged; their net effect is written as a prompt starts.
302
+ function writeStagedControl(ctx) {
303
+ const staged = takeStage(ctx.sessionManager);
304
+ routing.writeStaged(staged.events);
305
+ writeStagedSessionOverrides(staged.overrides, api, ctx.sessionManager);
306
+ }
168
307
  async function update(ctx) {
169
308
  const next = await loadSurface(ctx);
170
309
  await routing.refresh(ctx, next.cognitiveRouting);
171
- contextTools();
172
310
  status(ctx);
173
311
  refreshState = true;
312
+ // The Coordinator's projected view may lack reads the full history holds, and every profile shares the state.
313
+ stateSkills = toolExecutionEffective() && !routing.projectionEnabled ? files.loadedSkills(ctx) : [];
314
+ }
315
+ /**
316
+ * Turn Cognitive Routing on for this session when it is off: clear a session "off" (such as the one an outside model
317
+ * change sets), and switch it on for the session if configuration leaves it off. Re-enabling hands control to the
318
+ * automatic Coordinator. Configuration that still blocks routing (no profiles, invalid presets) is reported by the
319
+ * control that follows.
320
+ */
321
+ async function turnRoutingOn(ctx) {
322
+ if (capability?.cognitiveRouting?.effective === true || ctx.isIdle?.() === false) return;
323
+ if (capability?.cognitiveRouting?.enabledSource === "session")
324
+ await setSessionCoreOverride("cognitiveRouting.enabled", null, ctx, api);
325
+ if ((await readCapabilityState(ctx.cwd, ctx)).cognitiveRouting?.enabled !== true)
326
+ await setSessionCoreOverride("cognitiveRouting.enabled", true, ctx, api);
327
+ await update(ctx);
174
328
  }
175
329
  registerRoutingTools(api, routing);
176
- registerFreeflowContextTool(
177
- api,
178
- () => virtualization,
179
- () => history,
180
- { contextVirtualization: false, conversationHistory: false },
330
+ registerAskUserTool(api, {
331
+ // Under automatic routing only the Coordinator talks to the user; a worker returns the choice in its report.
332
+ refusal: () => {
333
+ const scope = routing.observationScope();
334
+ return scope.control === "automatic" && (scope.profile === "helper" || scope.profile === "executor")
335
+ ? "A Cognitive Routing worker does not ask the user. Put the choice, its options and your recommendation in your report (return_work); the Coordinator asks."
336
+ : undefined;
337
+ },
338
+ });
339
+ registerApplyPatch(api, {
340
+ effective: toolExecutionEffective,
341
+ files: (ctx) => files.forPatch(ctx),
342
+ written: (paths, ctx) => files.written(paths, ctx),
343
+ });
344
+ // Content arriving from outside the turn (host/inbound.ts): background exits now, later monitors and messages.
345
+ const inbound = new Inbound({
346
+ sendMessage: (message, options) => api.sendMessage(message, options),
347
+ sendUserMessage: (content) => api.sendUserMessage(content),
348
+ isIdle: () => liveHost?.isIdle?.(),
349
+ canWake: () => prompted,
350
+ });
351
+ api.registerMessageRenderer?.(NOTICE_TYPE, renderNotice("Background"));
352
+ api.registerMessageRenderer?.(MONITOR_TYPE, renderNotice("Monitor"));
353
+ api.registerMessageRenderer?.(COMPACT_REQUEST_TYPE, renderNotice("Compaction"));
354
+ // Compaction's warnings and its recovery message, and the changed-file notice: one line each, the rest on expand.
355
+ api.registerMessageRenderer?.(COMPACTION_NOTICE_TYPE, renderNotice("Compaction"));
356
+ api.registerMessageRenderer?.(RECOVERY_TYPE, renderNotice("Compaction"));
357
+ api.registerMessageRenderer?.("freeflow-files", renderNotice("Files"));
358
+ const backgroundHost = {
359
+ effective: toolExecutionEffective,
360
+ shellPath: () => api.getSettings?.()?.shellPath,
361
+ // An exit notice starts a turn when idle; mid-run it is steered in after the current turn's tool results.
362
+ send: (content, details, triggerTurn) =>
363
+ inbound.deliver({ customType: NOTICE_TYPE, content, details, wake: triggerTurn }),
364
+ // A monitor's lines wake an idle agent (spec O-6); the rate limit bounds what that costs.
365
+ event: (id, line, join, onLimit) =>
366
+ inbound.deliver(
367
+ { customType: MONITOR_TYPE, content: line, details: { id }, wake: true },
368
+ {
369
+ key: `monitor:${id}`,
370
+ perMinute: MONITOR_PER_MINUTE,
371
+ onLimit,
372
+ join: (notices) => join(notices.map((notice) => notice.content)),
373
+ },
374
+ ),
375
+ endEvents: (id) => {
376
+ inbound.flush(`monitor:${id}`);
377
+ inbound.close(`monitor:${id}`);
378
+ },
379
+ discardEvents: (id) => inbound.close(`monitor:${id}`),
380
+ };
381
+ const background = new BackgroundJobs(backgroundHost);
382
+ registerBackgroundTools(api, background, backgroundHost);
383
+ const compaction = new CompactionController({
384
+ effective: compactionEffective,
385
+ routingProfile: () => {
386
+ const state = routing.state();
387
+ return state.effective ? state.activeProfile : undefined;
388
+ },
389
+ routingAssignment: () => routing.currentAssignment(),
390
+ assignmentResults: () => routing.assignmentResults(),
391
+ background: () => background.running().map(({ id, label, outputPath }) => ({ id, label, outputPath })),
392
+ notice: (text) => freeflowNotice("compaction", text),
393
+ carryEnabled: () => capability?.compaction?.carry !== false,
394
+ nativeRefs: () => capability?.cognitiveRouting?.effective === true,
395
+ resolveRef: (ref) => routing.evidenceSource(ref),
396
+ coordinatorUnderProjection: () =>
397
+ capability?.cognitiveRouting?.effective === true &&
398
+ capability.cognitiveRouting.projection === true &&
399
+ routing.state().effective === true &&
400
+ routing.state().activeProfile === "coordinator",
401
+ summarize: async (preparation, instructions, signal, ctx) => {
402
+ const auth = await ctx.modelRegistry?.getApiKeyAndHeaders?.(ctx.model);
403
+ if (!ctx.model || !auth?.ok) throw new Error("No request auth for the summarizer");
404
+ const model = auth.baseUrl ? { ...ctx.model, baseUrl: auth.baseUrl } : ctx.model;
405
+ return piCompact(
406
+ preparation,
407
+ model,
408
+ auth.apiKey,
409
+ auth.headers,
410
+ instructions,
411
+ signal,
412
+ api.getThinkingLevel?.(),
413
+ undefined,
414
+ auth.env,
415
+ );
416
+ },
417
+ measure: (ctx, turn) => {
418
+ // Every model that may receive the full history: the active one and, under routing, each profile's.
419
+ const routingCapability = capability?.cognitiveRouting;
420
+ const models = [ctx.model];
421
+ if (routingCapability?.effective)
422
+ for (const profile of Object.values(routingCapability.profiles ?? {}))
423
+ models.push(ctx.modelRegistry?.find?.(profile.provider, profile.model));
424
+ const limits = strictest(models, api.getSettings?.());
425
+ if (!limits) return undefined;
426
+ let tokens = turnContextTokens(turn) ?? ctx.getContextUsage?.()?.tokens ?? 0;
427
+ // A Coordinator under projection sends a reduced view; the next worker request carries the whole history.
428
+ if (
429
+ routingCapability?.effective &&
430
+ routingCapability.projection &&
431
+ routing.state().activeProfile === "coordinator"
432
+ )
433
+ tokens = Math.max(tokens, fullHistoryTokens(ctx));
434
+ return { tokens, thresholds: limits };
435
+ },
436
+ });
437
+ registerCompactionTool(api, compaction);
438
+ const sendSnapshot = (content, record) =>
439
+ void Promise.resolve(
440
+ api.sendMessage({ customType: SNAPSHOT_TYPE, content, display: true, details: { record } }),
441
+ ).catch(() => {});
442
+ const taskMemory = new TaskMemory({
443
+ effective: taskMemoryEffective,
444
+ append: (data) => api.appendEntry(TASK_ENTRY, data),
445
+ send: sendSnapshot,
446
+ owner: () => ({
447
+ session: liveHost?.sessionManager?.getSessionId?.() ?? "unknown",
448
+ ...(liveHost?.sessionManager?.getSessionFile?.() ? { file: liveHost.sessionManager.getSessionFile() } : {}),
449
+ since: new Date().toISOString(),
450
+ }),
451
+ confirm: async (title, message) => (liveHost?.hasUI ? confirmWithUser(liveHost, title, message) : false),
452
+ cwd: () => liveHost?.cwd ?? process.cwd(),
453
+ packageRoot,
454
+ });
455
+ // Messaging between live Pi sessions (Task 002 D-005). Its tools are deferred: tool_search loads them.
456
+ // Read from the event context that last synced messaging, while it was current: a captured context goes stale when
457
+ // Pi replaces the session, so later asynchronous work uses these values, not the context.
458
+ let messagingSeen;
459
+ let agentRunning = false;
460
+ const live = (read, fallback) => {
461
+ try {
462
+ return read();
463
+ } catch {
464
+ return fallback;
465
+ }
466
+ };
467
+ const messaging = new Messaging(
468
+ {
469
+ identity: async () => {
470
+ const seen = messagingSeen ?? { sessionId: `pid-${process.pid}`, cwd: process.cwd() };
471
+ const project = await taskMemory.registry.projectFor(seen.cwd).catch(() => undefined);
472
+ const focus = taskMemory.state().focus;
473
+ const task = focus ? await taskMemory.registry.byRecord(focus).catch(() => undefined) : undefined;
474
+ // The tasks this session owns, so a child finds its parent's session in list_sessions (Task 002 D-009).
475
+ const tasks = await taskMemory.registry
476
+ .read()
477
+ .then((data) => data.tasks)
478
+ .catch(() => ({}));
479
+ const owns = Object.entries(tasks)
480
+ .filter(([, each]) => each?.owner?.session === seen.sessionId)
481
+ .map(([id]) => id);
482
+ return {
483
+ ...seen,
484
+ ...(project ? { project: project.slug } : {}),
485
+ ...(task ? { task: task.id } : {}),
486
+ ...(owns.length ? { owns } : {}),
487
+ };
488
+ },
489
+ policy: () => capability?.messaging?.inbound ?? DEFAULT_INBOUND_POLICY,
490
+ busy: () => agentRunning,
491
+ deliver: (content, details) => {
492
+ inbound.deliver({ customType: MESSAGE_TYPE, content, details, wake: true });
493
+ if (!prompted && details.kind === "message")
494
+ live(
495
+ () =>
496
+ liveHost?.ui?.notify?.(
497
+ `A message from ${String(details.name ?? "another session")} is waiting: the agent reads it with your next prompt.`,
498
+ "info",
499
+ ),
500
+ undefined,
501
+ );
502
+ },
503
+ canWake: () => prompted,
504
+ canAsk: () => live(() => liveHost?.hasUI === true, false),
505
+ // The same dialog as ask_user, with a countdown (Task 002 D-008).
506
+ confirm: async (title, message, timeout) =>
507
+ (await Promise.resolve(
508
+ live(
509
+ () => confirmWithUser(liveHost, title, message, { timeoutMs: timeout, yes: "Deliver", no: "Refuse" }),
510
+ false,
511
+ ),
512
+ ).catch(() => false)) === true,
513
+ },
514
+ freeflowHome(),
181
515
  );
516
+ registerMessagingTools(api, messaging, messagingEffective);
517
+ api.registerMessageRenderer?.(MESSAGE_TYPE, renderMessage);
518
+ // Start or stop listening as messaging's effective state changes; a running session refreshes its registry entry.
519
+ let messagingSync = Promise.resolve();
520
+ function syncMessaging(ctx) {
521
+ const effective = messagingEffective();
522
+ if (effective)
523
+ messagingSeen = live(() => {
524
+ const manager = ctx.sessionManager;
525
+ const name = manager?.getSessionName?.();
526
+ return {
527
+ sessionId: manager?.getSessionId?.() ?? `pid-${process.pid}`,
528
+ ...(name ? { name } : {}),
529
+ cwd: ctx.cwd ?? process.cwd(),
530
+ };
531
+ }, messagingSeen);
532
+ messagingSync = messagingSync
533
+ .then(() => (effective ? messaging.start() : messaging.running() ? messaging.stop() : undefined))
534
+ .catch(() => {});
535
+ }
536
+ // A delivered resume view shows as one line naming the task; expanding shows the view the model received.
537
+ api.registerMessageRenderer?.(SNAPSHOT_TYPE, (message, options, theme) =>
538
+ renderSnapshotMessage(message, options, theme),
539
+ );
540
+ registerTaskMemoryTool(api, {
541
+ memory: taskMemory,
542
+ // Under automatic routing only the Coordinator proposes task memory actions; workers do not talk to the user.
543
+ refusal: () => {
544
+ const scope = routing.observationScope();
545
+ return scope.control === "automatic" && (scope.profile === "helper" || scope.profile === "executor")
546
+ ? "A Cognitive Routing worker may only read task memory (projects, list, view, search, env). Put the action you would propose in your report (return_work); the Coordinator proposes it."
547
+ : undefined;
548
+ },
549
+ // Proposals in groups the user allowed run without the dialog (Task 004 D-002).
550
+ autoApproved: (group) => capability?.taskMemory?.autoApprove?.[group] === true,
551
+ changed: (ctx) => {
552
+ refreshState = true;
553
+ status(ctx);
554
+ },
555
+ });
556
+ // /freeflow project: the user creates projects and links repositories to them; the model never does (D-015).
557
+ // Where the user can browse the project's records, when the project link was made (Task 004 D-003).
558
+ async function browseNote(cwd) {
559
+ const link = await taskMemory.registry.linkHere(cwd).catch(() => undefined);
560
+ return link && !link.error ? ` Browse its records in ${link.link}.` : "";
561
+ }
562
+ // Removal asks even from the user's own command: it moves records out of every session that holds them (Task 006).
563
+ async function removeWithConfirmation(ctx, target, title) {
564
+ const { question, error } = await taskMemory.removalQuestion(target);
565
+ if (!question) return { ok: false, text: error ?? "Nothing to remove." };
566
+ if (!ctx.hasUI || !(await confirmWithUser(ctx, title, question))) return { ok: false, text: "Not removed." };
567
+ return taskMemory.remove(target);
568
+ }
569
+ // The trash holds removed tasks and projects. Only this command empties it, so only the user can (Task 006).
570
+ async function trashCommand(rest, ctx) {
571
+ const where = await taskMemory.registry.trashPath();
572
+ const entries = await taskMemory.registry.trashEntries();
573
+ if (rest.trim().toLowerCase() !== "empty") {
574
+ ctx.ui.notify(
575
+ entries.length
576
+ ? `Trash (${where}):\n${entries.map((entry) => `- ${entry}`).join("\n")}\n/freeflow trash empty deletes them for good.`
577
+ : "The trash is empty.",
578
+ "info",
579
+ );
580
+ return;
581
+ }
582
+ if (!entries.length) {
583
+ ctx.ui.notify("The trash is empty.", "info");
584
+ return;
585
+ }
586
+ const yes =
587
+ ctx.hasUI &&
588
+ (await confirmWithUser(
589
+ ctx,
590
+ "Empty the trash?",
591
+ `Delete ${entries.length} removed item(s) in ${where} for good? This cannot be undone.`,
592
+ ));
593
+ if (!yes) {
594
+ ctx.ui.notify("The trash was not emptied.", "info");
595
+ return;
596
+ }
597
+ const deleted = await taskMemory.registry.emptyTrash();
598
+ ctx.ui.notify(`Deleted ${deleted.length} item(s) from the trash.`, "info");
599
+ }
600
+ async function projectCommand(rest, ctx) {
601
+ if (!taskMemoryEffective()) {
602
+ ctx.ui.notify("Task memory is off. Turn it on in /freeflow settings (Task Memory).", "warning");
603
+ return;
604
+ }
605
+ const [sub, ...words] = rest.trim().split(/\s+/).filter(Boolean);
606
+ // --worktrees <dir>: where fan-out puts this project's child worktrees.
607
+ const at = words.indexOf("--worktrees");
608
+ const worktrees = at >= 0 ? words.splice(at, 2)[1] : undefined;
609
+ const name = words.join(" ");
610
+ try {
611
+ if (sub === "new" && name) {
612
+ const slug = await taskMemory.registry.createProject(name, ctx.cwd, worktrees);
613
+ ctx.ui.notify(`Project ${slug} created and linked to this repository.${await browseNote(ctx.cwd)}`, "info");
614
+ } else if (sub === "link" && name) {
615
+ const slug = await taskMemory.registry.linkProject(name, ctx.cwd, worktrees);
616
+ ctx.ui.notify(`This repository is linked to project ${slug}.${await browseNote(ctx.cwd)}`, "info");
617
+ } else if (sub === "remove" && name) {
618
+ const result = await removeWithConfirmation(ctx, { project: name }, "Remove project?");
619
+ ctx.ui.notify(result.text, result.ok ? "info" : "warning");
620
+ refreshState = true;
621
+ status(ctx);
622
+ } else if (sub === "list") {
623
+ const result = await taskMemory.projects(ctx.cwd);
624
+ ctx.ui.notify(result.text, result.ok ? "info" : "warning");
625
+ } else {
626
+ const found = await taskMemory.registry.projectFor(ctx.cwd);
627
+ ctx.ui.notify(
628
+ found
629
+ ? `Project ${found.project.name} (${found.slug}), store ${found.project.store}.${await browseNote(ctx.cwd)}\n${await taskMemory.list(ctx.cwd)}`
630
+ : "This repository is not linked to a project. Use /freeflow project new <name> or link <name>.",
631
+ "info",
632
+ );
633
+ }
634
+ } catch (error) {
635
+ ctx.ui.notify(error?.message ?? String(error), "warning");
636
+ }
637
+ }
638
+ // /freeflow task: the user's own attach, focus, detach and confirm, and a picker over this repository's records.
639
+ async function taskCommand(rest, ctx) {
640
+ if (!taskMemoryEffective()) {
641
+ ctx.ui.notify("Task memory is off. Turn it on in /freeflow settings (Task Memory).", "warning");
642
+ return;
643
+ }
644
+ const [sub, ...words] = rest.trim().split(/\s+/).filter(Boolean);
645
+ let action = sub?.toLowerCase();
646
+ let target = words.join(" ");
647
+ if (!action || action === "pick") {
648
+ const records = await taskMemory.records(ctx.cwd);
649
+ if (!records.length || !ctx.hasUI) {
650
+ ctx.ui.notify(await taskMemory.list(ctx.cwd), "info");
651
+ return;
652
+ }
653
+ const labels = records.map((record) => `${taskMemory.name(record)} — ${record}`);
654
+ const chosen = await ctx.ui.select("Task records", labels);
655
+ if (!chosen) return;
656
+ target = records[labels.indexOf(chosen)];
657
+ action = taskMemory.state().attached.includes(target) ? "focus" : "attach";
658
+ }
659
+ let result;
660
+ if (action === "list") result = { ok: true, text: await taskMemory.list(ctx.cwd, target) };
661
+ else if (action === "new") {
662
+ // The user's own command: create the task in this repository's project and attach it, with no dialog.
663
+ const created = await taskMemory.create(target, target, ctx.cwd);
664
+ result = created.ok && created.id ? await taskMemory.attach(created.id, ctx.cwd) : created;
665
+ } else if (action === "attach") result = await taskMemory.attach(target, ctx.cwd);
666
+ else if (action === "focus") result = await taskMemory.focus(target, ctx.cwd);
667
+ else if (action === "detach") result = await taskMemory.detach(target, ctx.cwd);
668
+ else if (action === "confirm") result = await taskMemory.confirm(target, ctx.cwd);
669
+ else if (action === "child") {
670
+ // From the new session: create a child of the named parent and attach it here.
671
+ const [parent, ...title] = words;
672
+ const created = await taskMemory.child(title.join(" "), title.join(" "), parent);
673
+ const id = /Created child (\S+)/.exec(created.text)?.[1];
674
+ result = created.ok && id ? await taskMemory.attach(id, ctx.cwd) : created;
675
+ } else if (action === "fanout") {
676
+ const title = words.join(" ");
677
+ result = await taskMemory.fanout(title, title);
678
+ } else if (action === "import") {
679
+ const [path, ...project] = words;
680
+ const yes = ctx.hasUI
681
+ ? await confirmWithUser(ctx, "Import record?", `Move ${path} into the ${project.join(" ")} project's store?`)
682
+ : false;
683
+ result = yes
684
+ ? await taskMemory.importRecord(path, project.join(" "), ctx.cwd)
685
+ : { ok: false, text: "Not imported." };
686
+ } else if (action === "remove") {
687
+ result = await removeWithConfirmation(ctx, { task: target }, "Remove task?");
688
+ } else
689
+ result = {
690
+ ok: false,
691
+ text: "Use /freeflow task [list [project]|new <title>|attach <id or record>|focus <id or record>|detach <id or record>|confirm [record]|child <parent-id> <title>|fanout <title>|import <record> <project>|remove <id>].",
692
+ };
693
+ if (result.snapshot) {
694
+ sendSnapshot(result.text, result.snapshot);
695
+ ctx.ui.notify(
696
+ `Task memory: ${(await taskMemory.brief(result.snapshot)).label} is the focus. ${taskMemory.attachedText()}`,
697
+ "info",
698
+ );
699
+ } else if (result.command)
700
+ ctx.ui.notify(`${result.headline ?? result.text}\nRun in a new terminal:\n ${result.command}`, "info");
701
+ else ctx.ui.notify(result.text, result.ok ? "info" : "warning");
702
+ refreshState = true;
703
+ status(ctx);
704
+ }
705
+ // The user's /freeflow compact makes compaction due now: the agent prepares, compacts and recovers.
706
+ async function compactNow(ctx) {
707
+ if (!compactionEffective()) {
708
+ ctx.ui.notify("Freeflow compaction is off; Pi's /compact still works.", "warning");
709
+ return;
710
+ }
711
+ compaction.requestByUser(ctx);
712
+ // Freeflow's message saying the user asked, not text in the user's name (Task 002 D-008); the run it starts keeps
713
+ // Freeflow's system sections (host/section-keeper.ts).
714
+ const index = compaction.indexText(ctx);
715
+ const text = freeflowNotice(
716
+ "compaction",
717
+ `The user ran /freeflow compact. ${USER_REQUEST}${index ? `\n\n${index}` : ""}`,
718
+ );
719
+ await Promise.resolve(
720
+ api.sendMessage(
721
+ { customType: COMPACT_REQUEST_TYPE, content: text, display: true, details: {} },
722
+ { triggerTurn: true, deliverAs: "steer" },
723
+ ),
724
+ ).catch(() => {});
725
+ }
182
726
  pi.on("resources_discover", async (event, ctx) => {
183
727
  const state = capability ?? (await loadSurface(ctx ?? { cwd: event?.cwd ?? process.cwd() }));
184
- return { skillPaths: freeflowModelSkillPaths(STABLE_FREEFLOW_SURFACE) };
728
+ return {
729
+ // Only what this configuration can use (Task 003 D-002): capability skills only while their capability is
730
+ // effective, and while Freeflow is inactive (disabled, unconfigured, or its prompts missing) only Setup Freeflow,
731
+ // so /setup-freeflow still works.
732
+ skillPaths:
733
+ state?.enabled === true && hasUsableMandatoryPrompts(prompts)
734
+ ? freeflowModelSkillPaths(state)
735
+ : [freeflowSkillPath("setup-freeflow")],
736
+ };
185
737
  });
738
+ /** Sections recorded on another session or branch are not this one's; whether this branch had a prompt is its own. */
739
+ const forgetSections = (ctx) => {
740
+ ownSections = undefined;
741
+ ownHead = undefined;
742
+ ownSectionsVersion++;
743
+ prompted = (cachedBranch(ctx.sessionManager) ?? []).some(
744
+ (entry) =>
745
+ entry?.type === "message" &&
746
+ (entry.message?.role === "user" || (entry.message?.role === "system" && entry.message.sections)),
747
+ );
748
+ };
186
749
  pi.on("session_start", async (event, ctx) => {
750
+ liveHost = ctx;
751
+ // Sections recorded for another session (after /new or /resume) are not this one's.
752
+ forgetSections(ctx);
753
+ promptRun = false;
754
+ messageRun = false;
187
755
  const generation = ++surfaceGeneration;
188
- requestHistory.reset();
756
+ resetHistory();
757
+ // Where Pi has just parsed this session file, later checks read only the bytes appended since.
758
+ trustStartedSession(event?.reason, ctx.sessionManager);
189
759
  routing.unbind();
190
760
  capability = undefined;
191
761
  prompts = undefined;
192
762
  refreshState = true;
193
763
  restoreSessionOverrides(ctx);
194
- const initial = await readCapabilityState(ctx.cwd, ctx, pi.host);
195
764
  await refreshRuntimeContext(STABLE_FREEFLOW_SURFACE);
196
765
  if (generation !== surfaceGeneration) return;
197
766
  await loadSurface(ctx);
198
- context = new FreeflowContextRuntime(ctx);
199
- virtualization = new ContextVirtualizationRuntime(pi, ctx, context);
200
- history = new ConversationHistoryRuntime(
201
- ctx,
202
- (entryId) => virtualization?.isSourceFullyProjected(entryId) ?? true,
203
- context,
204
- );
205
- await virtualization.recover(ctx);
767
+ cacheHealth.reset();
768
+ files.reset();
769
+ stateSkills = [];
770
+ compaction.reset();
206
771
  await routing.bind(ctx, capability.cognitiveRouting, event?.reason !== "reload");
207
- contextTools();
772
+ taskMemory.rebuild(cachedBranch(ctx.sessionManager));
773
+ if (taskMemoryEffective()) {
774
+ await taskMemory.load();
775
+ // The project link for the user to browse records in the editor, made again if missing (Task 004 D-003). Not
776
+ // awaited: it runs Git, and nothing at startup reads it.
777
+ void taskMemory.registry.linkHere(ctx.cwd).catch(() => undefined);
778
+ // A fork or clone copies the branch's attachments; they stay held until the user confirms them here.
779
+ if (event?.reason === "fork") {
780
+ const held = taskMemory.holdInherited();
781
+ if (held.length)
782
+ ctx.ui?.notify?.(
783
+ `Task memory: ${held.length} inherited record(s) are held, not writable. Release with /freeflow task confirm.`,
784
+ "info",
785
+ );
786
+ }
787
+ await taskMemory.deliver(cachedBranch(ctx.sessionManager));
788
+ // A session started in a task's directory with nothing attached offers that task (D-017). Not awaited, so
789
+ // startup finishes first and the user answers when the dialog appears; a decline is kept on the branch.
790
+ const suggested = ctx.hasUI ? await taskMemory.suggestion(cachedBranch(ctx.sessionManager)) : undefined;
791
+ if (suggested)
792
+ void (async () => {
793
+ const yes = await confirmWithUser(
794
+ ctx,
795
+ `Attach ${suggested.id}?`,
796
+ `This directory belongs to ${suggested.title} (${suggested.id}). Attach it to this session?`,
797
+ );
798
+ if (generation !== surfaceGeneration) return;
799
+ if (!yes) {
800
+ taskMemory.decline(suggested.record);
801
+ return;
802
+ }
803
+ const result = await taskMemory.attach(suggested.id, ctx.cwd);
804
+ if (result.snapshot) sendSnapshot(result.text, result.snapshot);
805
+ ctx.ui.notify(
806
+ result.ok ? `Task memory: ${suggested.id} attached.` : result.text,
807
+ result.ok ? "info" : "warning",
808
+ );
809
+ refreshState = true;
810
+ status(ctx);
811
+ })().catch(() => {});
812
+ }
208
813
  status(ctx);
209
814
  });
210
815
  pi.on("session_shutdown", async () => {
211
816
  surfaceGeneration++;
212
- requestHistory.reset();
817
+ // Leave the live registry and close the socket, after any start still queued; sessions waiting for this one learn
818
+ // that it ended.
819
+ messagingSync = messagingSync.then(() => messaging.stop()).catch(() => {});
820
+ await messagingSync;
821
+ // Background processes belong to this extension instance; none may report into the next session.
822
+ await background.stopAll();
823
+ inbound.reset();
824
+ files.reset();
825
+ stateSkills = [];
826
+ compaction.reset();
827
+ resetHistory();
213
828
  routing.unbind();
214
- context = undefined;
215
- virtualization = undefined;
216
- history = undefined;
217
829
  capability = undefined;
218
830
  prompts = undefined;
219
- sessionContext = undefined;
220
831
  });
221
832
  pi.on("before_agent_start", async (event, ctx) => {
833
+ // Pi 1.2.0 and later fire this for a run an extension's message starts too (messageType "custom").
834
+ const fromMessage = event.messageType === "custom";
222
835
  await update(ctx);
223
- await routing.beforeRun(ctx);
224
- status(ctx);
225
- const text = stableRuntimeContext(prompts);
226
- return { systemPrompt: text ? `${event.systemPrompt}\n\n${text}` : event.systemPrompt };
836
+ try {
837
+ await routing.beforeRun(ctx, fromMessage ? "message" : "prompt");
838
+ } finally {
839
+ // Pi reports a handler error and still sends the prompt, so the fixed guidance section and the staged
840
+ // control must never be skipped: a missing section would change the cached prompt head.
841
+ // After the run's own reconciliation, so everything staged lands ahead of the prompt in one pass.
842
+ writeStagedControl(ctx);
843
+ status(ctx);
844
+ // Pi 0.87 records changed sections at their native transcript position.
845
+ // Returning systemPrompt would force one replacement head for every request.
846
+ const sections = event.systemPromptOptions.sections;
847
+ const toolExecution = capability?.toolExecution?.effective === true;
848
+ // Pi's own sections are replaced in place. Freeflow's are re-inserted at the end in a fixed order: its guidance,
849
+ // then Tool Execution's environment facts and guidance.
850
+ if (toolExecution)
851
+ Object.assign(sections, toolExecutionSections(event.systemPrompt, api.getSettings?.()?.shellPath));
852
+ for (const name of ["freeflow_guidance", "environment", "tool_execution"]) delete sections[name];
853
+ // Nothing of Freeflow while it is disabled or unconfigured (Task 003 D-002).
854
+ if (capability?.enabled === true && hasUsableMandatoryPrompts(prompts))
855
+ sections.freeflow_guidance = stableRuntimeContext(prompts, {
856
+ routing: capability?.cognitiveRouting?.effective === true,
857
+ prompts: capability?.prompts,
858
+ });
859
+ if (toolExecution)
860
+ Object.assign(sections, toolExecutionTail(api.getSettings?.()?.shellPath, prompts?.toolExecutionPrompt));
861
+ // What Freeflow set, so requests of runs an extension's message starts keep it (host/section-keeper.ts).
862
+ ownNames = OWN_SECTION_NAMES.filter(
863
+ (name) => (toolExecution || (name !== "rules" && name !== "docs")) && typeof sections[name] === "string",
864
+ );
865
+ promptRun = !fromMessage;
866
+ messageRun = fromMessage;
867
+ prompted = true;
868
+ }
869
+ // Like Claude Code's next-turn reminder: files the model read that changed while it was not running a tool.
870
+ const changed = toolExecutionEffective() ? await files.changedNotice(ctx, false) : undefined;
871
+ if (changed)
872
+ return { message: { customType: "freeflow-files", content: freeflowNotice("files", changed), display: true } };
873
+ });
874
+ pi.on("agent_start", () => {
875
+ routing.runStarted();
876
+ guidance.runStarted();
877
+ agentRunning = true;
878
+ if (messaging.running()) void messaging.setBusy(true).catch(() => {});
227
879
  });
228
- pi.on("message_end", (event) => routing.messageEnd(event.message));
229
- pi.on("tool_call", (event, ctx) => {
880
+ pi.on("message_end", async (event, ctx) => {
881
+ const message = event.message;
230
882
  if (
231
- event.toolName === CONTEXT_VIRTUALIZATION_TOOL_NAME &&
232
- !capability?.contextVirtualization?.effective &&
233
- !capability?.conversationHistory?.effective
234
- )
235
- return { block: true, reason: "Freeflow Context is disabled." };
236
- return routing.preflight(event, ctx);
883
+ capability?.enabled === true &&
884
+ message?.role === "assistant" &&
885
+ message.usage &&
886
+ message.stopReason !== "error"
887
+ ) {
888
+ const scope = routing.observationScope();
889
+ const lifetime = ctx.model?.promptCache?.[process.env.PI_CACHE_RETENTION === "long" ? "long" : "short"];
890
+ const warning = cacheHealth.observe({
891
+ provider: message.provider,
892
+ model: message.model,
893
+ thinking: ctx.thinkingLevel,
894
+ at: Date.now(),
895
+ input: message.usage.input ?? 0,
896
+ cacheRead: message.usage.cacheRead ?? 0,
897
+ cacheWrite: message.usage.cacheWrite ?? 0,
898
+ ...(scope.control === "automatic" && scope.profile !== "solo" ? { lane: scope.profile } : {}),
899
+ ...(typeof lifetime === "number" ? { ttlMs: lifetime * 1000 } : {}),
900
+ });
901
+ if (warning) ctx.ui?.notify?.(warning, "warning");
902
+ }
903
+ routing.messageEnd(event.message);
904
+ // A run started by an extension's message (a background notice, for one) gets the same preparation as a prompt.
905
+ // Pi 1.2.0 and later prepare it through before_agent_start; this covers earlier Pi, which skips that event there.
906
+ if (routing.startedByMessage(event.message)) {
907
+ await update(ctx);
908
+ try {
909
+ await routing.beforeRun(ctx, "late");
910
+ } finally {
911
+ writeStagedControl(ctx);
912
+ status(ctx);
913
+ }
914
+ }
915
+ });
916
+ pi.on("tool_call", async (event, ctx) => {
917
+ const recordGate = await taskMemory.gate(event.toolName, event.input, ctx.cwd);
918
+ if (recordGate) return recordGate;
919
+ const gate = routing.preflight(event, ctx);
920
+ if (gate?.block || !toolExecutionEffective()) return gate;
921
+ const refusal = event.toolName === "bash" ? backgroundRefusal(event.input?.command) : undefined;
922
+ if (refusal) return { block: true, reason: refusal };
923
+ return (await files.toolCall(event, ctx)) ?? gate;
924
+ });
925
+ pi.on("tool_result", async (event, ctx) => {
926
+ if (!toolExecutionEffective()) return undefined;
927
+ const shaped = await files.toolResult(event, ctx);
928
+ // The push retriever's notice joins the result before it is delivered, after anything Tool Execution added.
929
+ const executeWorkUnread = () =>
930
+ !(capability?.skills?.off ?? []).includes("execute-work") && !files.loadedSkills(ctx).includes("execute-work");
931
+ const lines = guidanceNoticesEffective() ? await guidance.toolResult(event, ctx.cwd, executeWorkUnread) : [];
932
+ return lines.length ? { content: withLines(shaped?.content ?? event.content ?? [], lines) } : shaped;
237
933
  });
238
934
  pi.on("turn_end", async (event, ctx) => {
239
935
  await routing.turnEnd(event, ctx);
936
+ files.clearPending();
240
937
  status(ctx);
938
+ const compacting = await compaction.turnEnd(event, ctx);
939
+ if (compacting) {
940
+ // The focused record arrives after the summary, so recovery starts from it rather than a path to look up.
941
+ const snapshot = await taskMemory.snapshotEntry();
942
+ if (snapshot) compacting.entries.push(snapshot);
943
+ return compacting;
944
+ }
945
+ const notice = compaction.observe(ctx, event);
946
+ if (notice) {
947
+ // Mid-run the notice joins the next request; when the run is ending it waits for the next prompt rather than
948
+ // starting work nobody asked for.
949
+ const midRun = (event.message?.content ?? []).some((block) => block?.type === "toolCall");
950
+ await Promise.resolve(
951
+ api.sendMessage(
952
+ { customType: COMPACTION_NOTICE_TYPE, content: notice.text, display: true, details: { level: notice.level } },
953
+ { deliverAs: midRun ? "steer" : "nextTurn" },
954
+ ),
955
+ ).catch(() => {});
956
+ }
957
+ return undefined;
958
+ });
959
+ // A Freeflow compaction fires no session_compact, so its resets run here, before the new cycle's first request.
960
+ pi.on("turn_start", async (_event, ctx) => {
961
+ const compacted = compaction.takeCompacted();
962
+ if (!compacted) return;
963
+ // File state first, so the state restore refreshes lists no skill from before the compaction.
964
+ await files.compacted(compacted.carriedFiles, ctx);
965
+ await restore(ctx, false);
241
966
  });
242
967
  pi.on("agent_settled", async (_event, ctx) => {
968
+ promptRun = false;
969
+ messageRun = false;
970
+ agentRunning = false;
971
+ if (messaging.running()) await messaging.setBusy(false).catch(() => {});
243
972
  await routing.settled(ctx);
244
973
  await update(ctx);
245
974
  });
246
- pi.on("model_select", async (_event, ctx) => {
247
- await routing.nativeChange(ctx);
248
- status(ctx);
249
- });
250
- pi.on("thinking_level_select", async (_event, ctx) => {
251
- await routing.nativeChange(ctx);
975
+ // A model or effort picked in Pi itself (/model, Shift+Tab) ends automatic routing. Routing then turns off for this
976
+ // session through its Session switch, so settings and the footer show it off, and any routing control turns it on.
977
+ const externalChange = async (ctx) => {
978
+ cacheHealth.noteSwitch();
979
+ if (await routing.nativeChange(ctx)) {
980
+ await setSessionCoreOverride("cognitiveRouting.enabled", false, ctx, api);
981
+ await update(ctx);
982
+ }
252
983
  status(ctx);
253
- });
254
- pi.on("context", async (event, ctx) => {
984
+ };
985
+ pi.on("model_select", async (_event, ctx) => externalChange(ctx));
986
+ pi.on("thinking_level_select", async (_event, ctx) => externalChange(ctx));
987
+ pi.on("context_with_system", async (event, ctx) => {
988
+ writeStagedControl(ctx);
255
989
  if (!capability) await loadSurface(ctx);
256
- let messages = event.messages.map(filterBootstrapMessage).filter(Boolean);
257
- if (virtualization) {
258
- virtualization.setContext(ctx);
259
- const projected = await virtualization.project(messages, capability.contextVirtualization.effective);
260
- messages = projected.messages;
261
- }
262
- if (history && capability.conversationHistory.effective) {
263
- history.setContext(ctx);
264
- history.capture(capability.contextVirtualization.effective);
990
+ // Entry identity lets attribution and request history skip hashing unchanged native history.
991
+ tagProjectedMessages(
992
+ event.messages,
993
+ ctx.sessionManager?.buildSessionProjection?.(),
994
+ cachedBranch(ctx.sessionManager) ?? [],
995
+ );
996
+ // A run an extension's message starts keeps the prompt of the last prompt's run: every section, as Pi rendered it,
997
+ // so the sections other extensions set only for prompts (Pi's own MCP servers, for one) are not removed either; on
998
+ // Pi 1.2.0 and later Freeflow's own follow what it set for that run. Recorded only while Freeflow sets sections;
999
+ // with Freeflow off, Pi's prompt passes through unchanged.
1000
+ const strings = (current) =>
1001
+ Object.fromEntries(Object.entries(current).filter((entry) => typeof entry[1] === "string"));
1002
+ if (promptRun) {
1003
+ const own = ownNames.length ? strings(currentSections(event.messages)) : {};
1004
+ if (
1005
+ !ownSections ||
1006
+ Object.keys(own).length !== Object.keys(ownSections).length ||
1007
+ Object.entries(own).some(([name, value]) => ownSections[name] !== value)
1008
+ ) {
1009
+ ownSections = own;
1010
+ ownSectionsVersion++;
1011
+ }
1012
+ ownHead = headSections(event.messages);
1013
+ } else if (messageRun && ownSections) {
1014
+ // Pi 1.2.0 and later give a run an extension's message starts the sections Freeflow set for it, so Freeflow's
1015
+ // follow this run, while every other section keeps the last prompt's value (its extension may set it only for
1016
+ // prompts). The head stays the one recorded, so a head Pi rebuilds here still gets the recorded sections.
1017
+ const current = currentSections(event.messages);
1018
+ const next = { ...ownSections };
1019
+ for (const name of OWN_SECTION_NAMES) {
1020
+ const value = current[name];
1021
+ if (typeof value === "string") next[name] = value;
1022
+ else delete next[name];
1023
+ }
1024
+ if (OWN_SECTION_NAMES.some((name) => next[name] !== ownSections[name])) {
1025
+ ownSections = next;
1026
+ ownSectionsVersion++;
1027
+ }
1028
+ } else if (!ownSections) {
1029
+ // A run Pi started before any prompt of this process (a resumed session messaged while idle): the transcript
1030
+ // still carries the last prompt's sections, when Freeflow's guidance is among them.
1031
+ const current = currentSections(event.messages);
1032
+ if (typeof current.freeflow_guidance === "string") {
1033
+ ownSections = strings(current);
1034
+ ownHead = headSections(event.messages);
1035
+ ownSectionsVersion++;
1036
+ }
265
1037
  }
1038
+ let messages = keepSections(event.messages, ownSections, ownSectionsVersion, ownHead)
1039
+ .map(filterBootstrapMessage)
1040
+ .filter(Boolean);
266
1041
  messages = withFreeflowRuntimeState(messages, capability, routing.state(), prompts, {
267
1042
  force: refreshState,
1043
+ anchor: runtimeStateAnchor,
268
1044
  projectionFailure: routing.state().projectionFailure,
1045
+ taskMemory: taskMemory.runtimeLines(),
1046
+ skills: stateSkills,
269
1047
  });
270
1048
  refreshState = false;
271
- const routed = await routing.context(ctx, messages);
272
- const projected = virtualization?.decorate(routed) ?? routed;
1049
+ // Disabled Freeflow pays no per-request provenance work; re-enabling rebuilds it from session history.
1050
+ const projected = capability?.enabled === true ? await routing.context(ctx, messages) : messages;
273
1051
  const state = routing.state();
274
- if (projected.some((message) => message.details?.routingRequestBlocked === true)) return { messages: projected };
1052
+ if (projected.some((message) => message.details?.routingRequestBlocked === true)) {
1053
+ // Routing must not expose hidden worker history on failure, but Pi's full-
1054
+ // transcript hook still requires the leading system prompt/tool state.
1055
+ const head = event.messages[0]?.role === "system" ? [event.messages[0]] : [];
1056
+ return { messages: [...head, ...projected] };
1057
+ }
275
1058
  const view = routing.projectionEnabled && state.activeProfile === "coordinator" ? "coordinator" : "ordinary";
276
1059
  return {
277
1060
  messages: await requestHistory.assemble(projected, view, ctx, (assembled) =>
@@ -280,19 +1063,41 @@ export default function freeflow(pi) {
280
1063
  };
281
1064
  });
282
1065
  const restore = async (ctx, navigation = true) => {
283
- requestHistory.reset();
1066
+ cacheHealth.noteBreak();
1067
+ resetHistory();
1068
+ // Navigation selects another branch, whose reads differ; compaction keeps the branch and its file state.
1069
+ if (navigation) files.invalidate();
284
1070
  restoreSessionOverrides(ctx);
285
1071
  refreshState = true;
286
- if (virtualization) {
287
- virtualization.setContext(ctx);
288
- await virtualization.recover(ctx);
289
- }
290
- history?.setContext(ctx);
291
1072
  await routing.ancestryChanged(ctx, navigation);
292
1073
  await update(ctx);
293
1074
  };
294
- pi.on("session_tree", async (_event, ctx) => restore(ctx));
295
- pi.on("session_compact", async (_event, ctx) => restore(ctx, false));
1075
+ pi.on("session_tree", async (_event, ctx) => {
1076
+ // A run Pi starts on this branch keeps this branch's last prompt's sections, not the branch left behind.
1077
+ forgetSections(ctx);
1078
+ // Task memory follows the branch: the records attached at this point of the conversation.
1079
+ const before = taskMemory.state();
1080
+ const changed = taskMemory.rebuild(cachedBranch(ctx.sessionManager));
1081
+ if (taskMemoryEffective()) await taskMemory.load();
1082
+ await restore(ctx);
1083
+ if (!taskMemoryEffective()) return;
1084
+ if (changed) {
1085
+ const focus = taskMemory.state().focus;
1086
+ ctx.ui?.notify?.(
1087
+ `Task memory follows this branch: focus ${before.focus ? taskMemory.name(before.focus) : "none"} -> ${focus ? taskMemory.name(focus) : "none"}. ${taskMemory.attachedText()} Change it with /freeflow task.`,
1088
+ "info",
1089
+ );
1090
+ }
1091
+ await taskMemory.deliver(cachedBranch(ctx.sessionManager));
1092
+ });
1093
+ // Pi compacting on its own (threshold, overflow, /compact) keeps its summarizer and kept tail, with Freeflow's additions.
1094
+ pi.on("session_before_compact", async (event, ctx) => compaction.fallback(event, ctx));
1095
+ pi.on("session_compact", async (_event, ctx) => {
1096
+ files.piCompacted();
1097
+ await restore(ctx, false);
1098
+ background.restate();
1099
+ await taskMemory.deliver(cachedBranch(ctx.sessionManager));
1100
+ });
296
1101
  pi.on("session_compact_failed", async (_event, ctx) => {
297
1102
  refreshState = true;
298
1103
  status(ctx);
@@ -305,26 +1110,26 @@ export default function freeflow(pi) {
305
1110
  ) {
306
1111
  pi.registerShortcut("ctrl+shift+r", {
307
1112
  description: "Cycle enabled Cognitive Routing manual holds",
308
- handler: async (ctx) => {
1113
+ handler: async (shortcutCtx) => {
1114
+ const ctx = withLiveModel(shortcutCtx);
309
1115
  if (!ctx.isIdle()) {
310
1116
  ctx.ui.notify("Wait for Pi to become idle before changing control.", "warning");
311
1117
  return;
312
1118
  }
313
- const state = routing.state();
314
- const profiles = ["coordinator", ...workersForDelegation(state.delegation)];
315
- const current = profiles.indexOf(state.activeProfile ?? "coordinator");
316
- const target = profiles[(current + 1) % profiles.length];
317
- ctx.ui.notify(JSON.stringify(await routing.setManualProfile(target)), "info");
1119
+ await turnRoutingOn(ctx);
1120
+ ctx.ui.notify(JSON.stringify(await routing.cycleManualProfile()), "info");
318
1121
  await update(ctx);
319
1122
  },
320
1123
  });
321
1124
  pi.registerShortcut("ctrl+shift+a", {
322
- description: "Release manual hold to Automatic Coordinator",
323
- handler: async (ctx) => {
1125
+ description: "Turn Cognitive Routing on, or release a manual hold, to the automatic Coordinator",
1126
+ handler: async (shortcutCtx) => {
1127
+ const ctx = withLiveModel(shortcutCtx);
324
1128
  if (!ctx.isIdle()) {
325
1129
  ctx.ui.notify("Wait for Pi to become idle before changing control.", "warning");
326
1130
  return;
327
1131
  }
1132
+ await turnRoutingOn(ctx);
328
1133
  ctx.ui.notify(JSON.stringify(await routing.setAutomaticControl()), "info");
329
1134
  await update(ctx);
330
1135
  },
@@ -333,15 +1138,6 @@ export default function freeflow(pi) {
333
1138
  for (const { command, skill } of WORKFLOW_COMMANDS)
334
1139
  pi.registerCommand(command, {
335
1140
  description: command === skill ? `Run Freeflow ${skill}` : `Run Freeflow ${skill} via ${command}`,
336
- ...(command === "bypass"
337
- ? {
338
- getArgumentCompletions: (prefix) =>
339
- [
340
- { value: "next", label: "next", description: "Skip one optional step" },
341
- { value: "task", label: "task", description: "Reduce optional pressure for the current task" },
342
- ].filter((item) => item.value.startsWith(prefix ?? "")),
343
- }
344
- : {}),
345
1141
  handler: (args, ctx) => sendSkillCommand(pi, ctx, skill, args),
346
1142
  });
347
1143
  for (const skill of CONTRIBUTOR_COMMANDS)
@@ -350,7 +1146,7 @@ export default function freeflow(pi) {
350
1146
  handler: (args, ctx) => sendSkillCommand(pi, ctx, skill, args),
351
1147
  });
352
1148
  pi.registerCommand("freeflow", {
353
- description: "Freeflow settings, status, profile control, saved-operation resume, and context tools",
1149
+ description: "Freeflow settings, status, profile control, and saved-operation resume",
354
1150
  getArgumentCompletions: (prefix) =>
355
1151
  freeflowCompletions(
356
1152
  prefix,
@@ -359,22 +1155,42 @@ export default function freeflow(pi) {
359
1155
  typeof api.setThinkingLevel === "function",
360
1156
  ),
361
1157
  handler: async (args, ctx) => {
362
- if (await routing.command(args ?? "", ctx)) {
363
- await update(ctx);
1158
+ const task = /^task(?:\s+(.*))?$/i.exec((args ?? "").trim());
1159
+ if (task) {
1160
+ await taskCommand(task[1] ?? "", ctx);
364
1161
  return;
365
1162
  }
366
- const input = (args ?? "").trim();
367
- if (input === "context" || input.startsWith("context ")) {
368
- await handleContextCommand(
369
- input.slice(7).trim(),
370
- ctx,
371
- virtualization,
372
- capability?.contextVirtualization.effective,
373
- capability?.conversationHistory.effective,
374
- );
1163
+ const trash = /^trash(?:\s+(.*))?$/i.exec((args ?? "").trim());
1164
+ if (trash) {
1165
+ await trashCommand(trash[1] ?? "", ctx);
1166
+ return;
1167
+ }
1168
+ const project = /^project(?:\s+(.*))?$/i.exec((args ?? "").trim());
1169
+ if (project) {
1170
+ await projectCommand(project[1] ?? "", ctx);
1171
+ return;
1172
+ }
1173
+ if ((args ?? "").trim().toLowerCase() === "compact") {
1174
+ await compactNow(ctx);
1175
+ return;
1176
+ }
1177
+ // A routing control (a hold or `auto`) turns routing on first when it is off.
1178
+ if (/^profile\s+(?!history\b)\S/i.test((args ?? "").trim())) await turnRoutingOn(ctx);
1179
+ if (await routing.command(args ?? "", ctx)) {
1180
+ await update(ctx);
375
1181
  return;
376
1182
  }
377
- await handleFreeflowCommand(args, ctx, async () => update(ctx), pi, routing);
1183
+ await handleFreeflowCommand(args, ctx, async () => update(ctx), pi, routing, {
1184
+ status: () => ({
1185
+ cacheHealth: [...cacheHealth.status(), ...cacheMonitor.lines()],
1186
+ backgroundRunning: background.running().length,
1187
+ compaction: compaction.statusText(ctx),
1188
+ taskMemory: taskMemory.attachedText(),
1189
+ messaging: messaging.self()
1190
+ ? `this session is ${messaging.self().name}${messaging.heldCount() ? `, ${messaging.heldCount()} held` : ""}`
1191
+ : undefined,
1192
+ }),
1193
+ });
378
1194
  },
379
1195
  });
380
1196
  }