@hassangameryt/freeflow 0.8.0 → 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 (290) 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/README.md +96 -149
  7. package/capabilities/cognitive-routing/SKILL.md +21 -129
  8. package/capabilities/cognitive-routing/references/coordinator.md +163 -0
  9. package/capabilities/cognitive-routing/references/worker.md +38 -0
  10. package/capabilities/compaction/SKILL.md +3 -3
  11. package/capabilities/compaction/references/summary-format.md +26 -15
  12. package/capabilities/messaging/SKILL.md +49 -0
  13. package/capabilities/task-memory/SKILL.md +87 -0
  14. package/command-surface.json +6 -54
  15. package/gemini-extension.json +1 -1
  16. package/hooks/claude/hooks.json +13 -3
  17. package/hooks/codex/hooks.json +2 -1
  18. package/hooks/shared/runtime-context.mjs +69 -77
  19. package/package.json +25 -8
  20. package/pi-extension/dist/ask-user/ask.js +108 -0
  21. package/pi-extension/dist/ask-user/form.js +250 -0
  22. package/pi-extension/dist/ask-user/questions.js +66 -0
  23. package/pi-extension/dist/ask-user/tool.js +124 -0
  24. package/pi-extension/dist/cognitive-routing/assembler.js +35 -11
  25. package/pi-extension/dist/cognitive-routing/bootstrap.js +90 -0
  26. package/pi-extension/dist/cognitive-routing/config.js +3 -4
  27. package/pi-extension/dist/cognitive-routing/event-store.js +4 -1
  28. package/pi-extension/dist/cognitive-routing/gate.js +20 -5
  29. package/pi-extension/dist/cognitive-routing/handlers.js +15 -13
  30. package/pi-extension/dist/cognitive-routing/model-control.js +17 -8
  31. package/pi-extension/dist/cognitive-routing/projection.js +23 -6
  32. package/pi-extension/dist/cognitive-routing/provenance.js +3 -1
  33. package/pi-extension/dist/cognitive-routing/render.js +15 -15
  34. package/pi-extension/dist/cognitive-routing/runtime.js +4 -2
  35. package/pi-extension/dist/cognitive-routing/schemas.js +4 -4
  36. package/pi-extension/dist/cognitive-routing/session.js +3 -1
  37. package/pi-extension/dist/cognitive-routing/sources.js +29 -4
  38. package/pi-extension/dist/cognitive-routing/state.js +18 -8
  39. package/pi-extension/dist/cognitive-routing/status.js +4 -3
  40. package/pi-extension/dist/cognitive-routing/tools.js +29 -15
  41. package/pi-extension/dist/cognitive-routing/types.js +10 -1
  42. package/pi-extension/dist/compaction/carry.js +36 -22
  43. package/pi-extension/dist/compaction/config.js +4 -2
  44. package/pi-extension/dist/compaction/controller.js +52 -29
  45. package/pi-extension/dist/compaction/harness.js +19 -3
  46. package/pi-extension/dist/compaction/results.js +14 -13
  47. package/pi-extension/dist/compaction/tool.js +6 -6
  48. package/pi-extension/dist/host/catalog.js +20 -35
  49. package/pi-extension/dist/host/config-validation.js +233 -0
  50. package/pi-extension/dist/host/config.js +78 -91
  51. package/pi-extension/dist/host/inbound.js +142 -0
  52. package/pi-extension/dist/host/projection-tags.js +1 -1
  53. package/pi-extension/dist/host/prompts.js +24 -13
  54. package/pi-extension/dist/host/read-only-session.js +6 -1
  55. package/pi-extension/dist/host/request-history.js +7 -3
  56. package/pi-extension/dist/host/rows.js +25 -0
  57. package/pi-extension/dist/host/runtime-state.js +17 -2
  58. package/pi-extension/dist/host/section-keeper.js +85 -0
  59. package/pi-extension/dist/host/settings/freeflow-command.js +846 -93
  60. package/pi-extension/dist/host/settings/settings-view.js +27 -8
  61. package/pi-extension/dist/host/status.js +19 -5
  62. package/pi-extension/dist/index.js +638 -72
  63. package/pi-extension/dist/messaging/config.js +27 -0
  64. package/pi-extension/dist/messaging/messaging.js +398 -0
  65. package/pi-extension/dist/messaging/registry.js +57 -0
  66. package/pi-extension/dist/messaging/render.js +90 -0
  67. package/pi-extension/dist/messaging/tools.js +109 -0
  68. package/pi-extension/dist/messaging/transport.js +101 -0
  69. package/pi-extension/dist/provider-support/cache/keepalive.js +16 -4
  70. package/pi-extension/dist/task-memory/config.js +49 -0
  71. package/pi-extension/dist/task-memory/registry.js +119 -0
  72. package/pi-extension/dist/task-memory/render.js +107 -0
  73. package/pi-extension/dist/task-memory/runtime.js +994 -0
  74. package/pi-extension/dist/task-memory/tool.js +233 -0
  75. package/pi-extension/dist/tool-execution/apply-patch/tool.js +1 -1
  76. package/pi-extension/dist/tool-execution/background.js +220 -28
  77. package/pi-extension/dist/tool-execution/config.js +14 -3
  78. package/pi-extension/dist/tool-execution/file-tracking.js +24 -4
  79. package/pi-extension/dist/tool-execution/guidance-notices.js +124 -0
  80. package/pi-extension/dist/tool-execution/skill-reads.js +143 -0
  81. package/pi-extension/dist/tool-execution/tools.js +2 -2
  82. package/runtime/prompts/cognitive-routing.md +4 -4
  83. package/runtime/prompts/core.md +24 -14
  84. package/runtime/prompts/interaction-contract.md +7 -4
  85. package/runtime/prompts/tool-execution.md +3 -16
  86. package/runtime/prompts/working-method.md +16 -99
  87. package/skills/deliver-work/SKILL.md +76 -0
  88. package/skills/deliver-work/references/branch-closeout.md +134 -0
  89. package/skills/deliver-work/references/commit.md +83 -0
  90. package/skills/deliver-work/references/launch.md +94 -0
  91. package/skills/deliver-work/references/release.md +85 -0
  92. package/skills/deliver-work/scripts/commit-state.mjs +139 -0
  93. package/skills/deliver-work/scripts/delivery-state.mjs +107 -0
  94. package/skills/deliver-work/scripts/lib/git-state.mjs +178 -0
  95. package/skills/deliver-work/scripts/push-route.mjs +122 -0
  96. package/skills/deliver-work/scripts/release-preflight.mjs +177 -0
  97. package/skills/discuss-work/SKILL.md +148 -0
  98. package/skills/{discuss → discuss-work}/references/discussion-continuity.md +10 -27
  99. package/skills/discuss-work/references/learning-action.md +36 -0
  100. package/skills/discuss-work/scripts/choices.mjs +101 -0
  101. package/skills/discuss-work/scripts/lib/choices.mjs +159 -0
  102. package/skills/evaluate-behavior/SKILL.md +107 -0
  103. package/skills/evaluate-behavior/references/authoring.md +381 -0
  104. package/skills/evaluate-behavior/references/coding-tasks.md +145 -0
  105. package/skills/evaluate-behavior/references/conversations-pipelines-judges.md +177 -0
  106. package/skills/evaluate-behavior/references/design-and-measurement.md +273 -0
  107. package/skills/evaluate-behavior/references/running-and-evidence.md +245 -0
  108. package/skills/evaluate-behavior/scripts/claude-guard.mjs +76 -0
  109. package/skills/evaluate-behavior/scripts/guard-rules.mjs +309 -0
  110. package/skills/evaluate-behavior/scripts/lib/agreement.mjs +90 -0
  111. package/skills/evaluate-behavior/scripts/lib/arms.mjs +55 -0
  112. package/skills/evaluate-behavior/scripts/lib/budget.mjs +87 -0
  113. package/skills/evaluate-behavior/scripts/lib/build-source.mjs +287 -0
  114. package/skills/evaluate-behavior/scripts/lib/clear.mjs +74 -0
  115. package/skills/evaluate-behavior/scripts/lib/container.mjs +395 -0
  116. package/skills/evaluate-behavior/scripts/lib/decision.mjs +418 -0
  117. package/skills/evaluate-behavior/scripts/lib/definitions.mjs +1072 -0
  118. package/skills/evaluate-behavior/scripts/lib/freeflow.mjs +103 -0
  119. package/skills/evaluate-behavior/scripts/lib/grade-command.mjs +591 -0
  120. package/skills/evaluate-behavior/scripts/lib/grade-comparison.mjs +90 -0
  121. package/skills/{evaluate-skill → evaluate-behavior}/scripts/lib/grade-interaction.mjs +19 -18
  122. package/skills/{evaluate-skill → evaluate-behavior}/scripts/lib/grade-support.mjs +32 -5
  123. package/skills/{evaluate-skill → evaluate-behavior}/scripts/lib/grade-workspace.mjs +28 -12
  124. package/skills/evaluate-behavior/scripts/lib/grade.mjs +101 -0
  125. package/skills/evaluate-behavior/scripts/lib/hooks.mjs +98 -0
  126. package/skills/evaluate-behavior/scripts/lib/hosts/claude-code/confinement.mjs +196 -0
  127. package/skills/evaluate-behavior/scripts/lib/hosts/claude-code/constraints.mjs +111 -0
  128. package/skills/evaluate-behavior/scripts/lib/hosts/claude-code/index.mjs +48 -0
  129. package/skills/evaluate-behavior/scripts/lib/hosts/claude-code/isolation.mjs +74 -0
  130. package/skills/evaluate-behavior/scripts/lib/hosts/claude-code/launch.mjs +123 -0
  131. package/skills/evaluate-behavior/scripts/lib/hosts/claude-code/model-call.mjs +73 -0
  132. package/skills/evaluate-behavior/scripts/lib/hosts/claude-code/recorder.mjs +178 -0
  133. package/skills/evaluate-behavior/scripts/lib/hosts/claude-code/session.mjs +494 -0
  134. package/skills/evaluate-behavior/scripts/lib/hosts/claude-code/skills.mjs +81 -0
  135. package/skills/evaluate-behavior/scripts/lib/hosts/index.mjs +31 -0
  136. package/skills/evaluate-behavior/scripts/lib/hosts/names.mjs +3 -0
  137. package/skills/evaluate-behavior/scripts/lib/hosts/pi/confinement.mjs +173 -0
  138. package/skills/evaluate-behavior/scripts/lib/hosts/pi/host.mjs +148 -0
  139. package/skills/evaluate-behavior/scripts/lib/hosts/pi/index.mjs +70 -0
  140. package/skills/evaluate-behavior/scripts/lib/hosts/pi/isolation.mjs +81 -0
  141. package/skills/evaluate-behavior/scripts/lib/hosts/pi/model-call.mjs +79 -0
  142. package/skills/evaluate-behavior/scripts/lib/hosts/pi/process.mjs +241 -0
  143. package/skills/{evaluate-skill/scripts/lib → evaluate-behavior/scripts/lib/hosts/pi}/rpc.mjs +144 -26
  144. package/skills/evaluate-behavior/scripts/lib/identity.mjs +82 -0
  145. package/skills/evaluate-behavior/scripts/lib/judge.mjs +688 -0
  146. package/skills/evaluate-behavior/scripts/lib/metrics.mjs +332 -0
  147. package/skills/evaluate-behavior/scripts/lib/model-call.mjs +17 -0
  148. package/skills/evaluate-behavior/scripts/lib/participants.mjs +616 -0
  149. package/skills/{evaluate-skill → evaluate-behavior}/scripts/lib/process.mjs +22 -0
  150. package/skills/evaluate-behavior/scripts/lib/queue.mjs +54 -0
  151. package/skills/evaluate-behavior/scripts/lib/reference.mjs +343 -0
  152. package/skills/evaluate-behavior/scripts/lib/reuse.mjs +277 -0
  153. package/skills/evaluate-behavior/scripts/lib/review.mjs +933 -0
  154. package/skills/evaluate-behavior/scripts/lib/runner.mjs +732 -0
  155. package/skills/{evaluate-skill → evaluate-behavior}/scripts/lib/sandbox.mjs +119 -14
  156. package/skills/evaluate-behavior/scripts/lib/scan-shell.mjs +457 -0
  157. package/skills/evaluate-behavior/scripts/lib/scan.mjs +479 -0
  158. package/skills/evaluate-behavior/scripts/lib/seal.mjs +91 -0
  159. package/skills/evaluate-behavior/scripts/lib/shell.mjs +560 -0
  160. package/skills/evaluate-behavior/scripts/lib/source-override.mjs +64 -0
  161. package/skills/evaluate-behavior/scripts/lib/subject-file.mjs +88 -0
  162. package/skills/{evaluate-skill/scripts/lib/pi.mjs → evaluate-behavior/scripts/lib/subject.mjs} +235 -265
  163. package/skills/evaluate-behavior/scripts/lib/task-copy.mjs +124 -0
  164. package/skills/evaluate-behavior/scripts/lib/trials.mjs +524 -0
  165. package/skills/evaluate-behavior/scripts/lib/validity.mjs +68 -0
  166. package/skills/evaluate-behavior/scripts/lib/view-html.mjs +454 -0
  167. package/skills/{evaluate-skill → evaluate-behavior}/scripts/lib/view.mjs +159 -18
  168. package/skills/{evaluate-skill → evaluate-behavior}/scripts/pi-command-tool.mjs +21 -6
  169. package/skills/evaluate-behavior/scripts/pi-container-route.mjs +23 -0
  170. package/skills/evaluate-behavior/scripts/pi-guard.mjs +77 -0
  171. package/skills/evaluate-behavior/scripts/pi-login-refresh.mjs +31 -0
  172. package/skills/evaluate-behavior/scripts/skill-eval.mjs +586 -0
  173. package/skills/execute-work/SKILL.md +96 -104
  174. package/skills/execute-work/references/code-practices.md +27 -51
  175. package/skills/execute-work/references/design.md +271 -0
  176. package/skills/execute-work/references/diagnose.md +117 -0
  177. package/skills/execute-work/references/migration.md +108 -0
  178. package/skills/execute-work/references/simplify.md +65 -0
  179. package/skills/execute-work/scripts/self-review.mjs +422 -0
  180. package/skills/execute-work/scripts/touch-map.mjs +290 -0
  181. package/skills/review-work/SKILL.md +89 -65
  182. package/skills/review-work/references/adjudication.md +94 -0
  183. package/skills/review-work/references/artifact.md +198 -0
  184. package/skills/review-work/references/evals-and-judges.md +66 -0
  185. package/skills/review-work/references/work.md +177 -0
  186. package/skills/review-work/scripts/check-brief.mjs +128 -0
  187. package/skills/review-work/scripts/review-packet.mjs +233 -0
  188. package/skills/setup-freeflow/SKILL.md +68 -17
  189. package/skills/track-work/SKILL.md +52 -20
  190. package/skills/track-work/references/handoff.md +190 -0
  191. package/skills/track-work/references/projects.md +51 -0
  192. package/skills/track-work/references/working-record-format.md +36 -18
  193. package/skills/track-work/scripts/lib/document.mjs +17 -13
  194. package/skills/track-work/scripts/lib/format.mjs +2 -0
  195. package/skills/track-work/scripts/lib/recover.mjs +254 -0
  196. package/skills/track-work/scripts/lib/registry.mjs +656 -0
  197. package/skills/track-work/scripts/lib/search.mjs +40 -0
  198. package/skills/track-work/scripts/lib/store.mjs +46 -15
  199. package/skills/track-work/scripts/lib/views.mjs +50 -1
  200. package/skills/track-work/scripts/working-record.mjs +232 -13
  201. package/skills/verify-work/SKILL.md +45 -37
  202. package/skills/verify-work/references/feature-map.md +120 -0
  203. package/skills/verify-work/references/runtime-evidence.md +203 -0
  204. package/skills/verify-work/references/test-design.md +25 -6
  205. package/skills/verify-work/scripts/test-changes.mjs +246 -0
  206. package/skills/write-docs/SKILL.md +113 -25
  207. package/skills/write-docs/references/documentation-shapes.md +68 -3
  208. package/skills/write-docs/references/spec-and-plan-shapes.md +308 -0
  209. package/skills/write-docs/scripts/artifact-check.mjs +148 -0
  210. package/skills/write-docs/scripts/docs-check.mjs +96 -6
  211. package/skills/write-docs/scripts/plan-lint.mjs +123 -0
  212. package/skills/write-skill/SKILL.md +92 -52
  213. package/skills/write-skill/references/activation-and-wording.md +143 -0
  214. package/skills/write-skill/references/placement-and-dependencies.md +154 -0
  215. package/skills/write-skill/scripts/lib/compare.mjs +701 -0
  216. package/skills/write-skill/scripts/lib/skill-author.mjs +197 -3
  217. package/skills/write-skill/scripts/skill-author.mjs +51 -8
  218. package/capabilities/cognitive-routing/references/both-mode.md +0 -36
  219. package/capabilities/cognitive-routing/references/executor-mode.md +0 -48
  220. package/capabilities/cognitive-routing/references/helper-mode.md +0 -47
  221. package/plugin.json +0 -33
  222. package/skills/action-selection/SKILL.md +0 -92
  223. package/skills/action-selection/references/trajectory-stalls.md +0 -73
  224. package/skills/bypass/SKILL.md +0 -153
  225. package/skills/commit-work/SKILL.md +0 -93
  226. package/skills/commit-work/references/git-state-edges.md +0 -94
  227. package/skills/commit-work/references/simple-push.md +0 -94
  228. package/skills/commit-work/references/staging-decisions.md +0 -106
  229. package/skills/decision-gate/SKILL.md +0 -87
  230. package/skills/design-for-depth/SKILL.md +0 -108
  231. package/skills/design-for-depth/references/design-pressure-signals.md +0 -84
  232. package/skills/design-for-depth/references/implementation-decisions.md +0 -76
  233. package/skills/design-for-depth/references/interface-design-loop.md +0 -105
  234. package/skills/design-for-depth/references/module-and-dependency-design.md +0 -117
  235. package/skills/design-for-depth/references/state-and-failure-boundaries.md +0 -125
  236. package/skills/diagnose-failure/SKILL.md +0 -140
  237. package/skills/diagnose-failure/references/diagnostic-loop-catalog.md +0 -50
  238. package/skills/diagnose-failure/references/flaky-and-performance-diagnosis.md +0 -51
  239. package/skills/discuss/SKILL.md +0 -140
  240. package/skills/evaluate-skill/SKILL.md +0 -188
  241. package/skills/evaluate-skill/references/evaluation-definition-schema.md +0 -415
  242. package/skills/evaluate-skill/references/evaluation-design.md +0 -112
  243. package/skills/evaluate-skill/references/execution-and-evidence.md +0 -111
  244. package/skills/evaluate-skill/references/review-and-revision.md +0 -94
  245. package/skills/evaluate-skill/scripts/lib/definitions.mjs +0 -448
  246. package/skills/evaluate-skill/scripts/lib/grade-comparison.mjs +0 -60
  247. package/skills/evaluate-skill/scripts/lib/grade.mjs +0 -64
  248. package/skills/evaluate-skill/scripts/lib/review.mjs +0 -196
  249. package/skills/evaluate-skill/scripts/lib/runner.mjs +0 -405
  250. package/skills/evaluate-skill/scripts/lib/trials.mjs +0 -103
  251. package/skills/evaluate-skill/scripts/pi-guard.mjs +0 -181
  252. package/skills/evaluate-skill/scripts/skill-eval.mjs +0 -182
  253. package/skills/execute-work/references/execute-work-edges.md +0 -78
  254. package/skills/finish-branch/SKILL.md +0 -117
  255. package/skills/finish-branch/references/integration-options.md +0 -80
  256. package/skills/handoff/SKILL.md +0 -82
  257. package/skills/handoff/references/handoff-templates.md +0 -150
  258. package/skills/launch-work/SKILL.md +0 -116
  259. package/skills/launch-work/references/launch-readiness.md +0 -85
  260. package/skills/migration-work/SKILL.md +0 -115
  261. package/skills/migration-work/references/migration-lifecycle.md +0 -82
  262. package/skills/release-work/SKILL.md +0 -118
  263. package/skills/release-work/references/release-evidence.md +0 -78
  264. package/skills/review-artifact/SKILL.md +0 -143
  265. package/skills/review-artifact/references/adjudicate-artifact-review.md +0 -101
  266. package/skills/review-artifact/references/independent-artifact-reviewer-contract.md +0 -159
  267. package/skills/review-work/references/adjudicate-work-review.md +0 -97
  268. package/skills/review-work/references/independent-work-reviewer-contract.md +0 -139
  269. package/skills/review-work/references/security-risk-lens.md +0 -67
  270. package/skills/setup-freeflow/references/activation-contract.md +0 -103
  271. package/skills/setup-freeflow/references/host-setup.md +0 -67
  272. package/skills/simplify-code/SKILL.md +0 -85
  273. package/skills/simplify-code/references/simplification-patterns.md +0 -70
  274. package/skills/verify-work/references/browser-runtime-evidence.md +0 -51
  275. package/skills/verify-work/references/integration-evidence.md +0 -49
  276. package/skills/verify-work/references/performance-evidence.md +0 -59
  277. package/skills/workflow/SKILL.md +0 -171
  278. package/skills/workflow/references/domain-skill-composition.md +0 -45
  279. package/skills/write-plan/SKILL.md +0 -92
  280. package/skills/write-plan/references/plan-shapes.md +0 -156
  281. package/skills/write-skill/references/activation-boundaries.md +0 -106
  282. package/skills/write-skill/references/agent-first-instructions.md +0 -133
  283. package/skills/write-skill/references/progressive-disclosure.md +0 -129
  284. package/skills/write-skill/references/skill-dependency-graphs.md +0 -192
  285. package/skills/write-spec/SKILL.md +0 -85
  286. package/skills/write-spec/references/artifact-standards.md +0 -86
  287. package/skills/write-spec/references/decision-records.md +0 -80
  288. package/skills/write-spec/references/spec-shapes.md +0 -143
  289. /package/skills/{evaluate-skill → evaluate-behavior}/scripts/lib/evidence.mjs +0 -0
  290. /package/skills/{evaluate-skill → evaluate-behavior}/scripts/pi-observer.mjs +0 -0
@@ -0,0 +1,250 @@
1
+ import { Input, Key, matchesKey, visibleWidth, wrapTextWithAnsi } from "@earendil-works/pi-tui";
2
+ const OWN_ANSWER = "Type my own answer";
3
+ export class QuestionForm {
4
+ questions;
5
+ theme;
6
+ done;
7
+ requestRender;
8
+ options;
9
+ tab = 0;
10
+ cursor;
11
+ picked;
12
+ written = new Map();
13
+ notes = new Map();
14
+ answered = new Set();
15
+ mode = "choose";
16
+ input = new Input();
17
+ cache;
18
+ focusedValue = false;
19
+ constructor(questions, theme, done, requestRender = () => {}, options = {}) {
20
+ this.questions = questions;
21
+ this.theme = theme;
22
+ this.done = done;
23
+ this.requestRender = requestRender;
24
+ this.options = options;
25
+ this.cursor = questions.map(() => 0);
26
+ this.picked = questions.map(() => new Set());
27
+ this.input.onSubmit = (value) => this.submitInput(value);
28
+ this.input.onEscape = () => this.leaveInput();
29
+ }
30
+ get focused() {
31
+ return this.focusedValue;
32
+ }
33
+ set focused(value) {
34
+ this.focusedValue = value;
35
+ this.input.focused = value && this.mode !== "choose";
36
+ }
37
+ get many() {
38
+ return this.questions.length > 1;
39
+ }
40
+ get onSubmitTab() {
41
+ return this.many && this.tab === this.questions.length;
42
+ }
43
+ get ownAnswer() {
44
+ return this.options.ownAnswer !== false;
45
+ }
46
+ rows(index) {
47
+ return this.questions[index].options.length + (this.ownAnswer ? 1 : 0);
48
+ }
49
+ refresh() {
50
+ this.cache = undefined;
51
+ this.requestRender();
52
+ }
53
+ finish() {
54
+ const answers = this.questions.map((question, index) => {
55
+ const note = this.notes.get(index);
56
+ const text = this.written.get(index);
57
+ return {
58
+ header: question.header,
59
+ question: question.question,
60
+ selected:
61
+ text === undefined
62
+ ? [...this.picked[index]].sort((a, b) => a - b).map((at) => question.options[at].label)
63
+ : [],
64
+ ...(text === undefined ? {} : { text }),
65
+ ...(note ? { note } : {}),
66
+ };
67
+ });
68
+ this.done({ status: "answered", answers });
69
+ }
70
+ /** After a question is answered: the next unanswered question, the Submit tab, or done for a single question. */
71
+ advance() {
72
+ this.answered.add(this.tab);
73
+ if (!this.many) return this.finish();
74
+ const next = this.questions.findIndex((_, index) => !this.answered.has(index));
75
+ this.tab = next === -1 ? this.questions.length : next;
76
+ this.refresh();
77
+ }
78
+ submitInput(value) {
79
+ const text = value.trim();
80
+ if (this.mode === "own") {
81
+ if (!text) return;
82
+ this.written.set(this.tab, text);
83
+ this.picked[this.tab].clear();
84
+ this.leaveInput(false);
85
+ this.advance();
86
+ return;
87
+ }
88
+ if (text) this.notes.set(this.tab, text);
89
+ else this.notes.delete(this.tab);
90
+ this.leaveInput();
91
+ }
92
+ leaveInput(render = true) {
93
+ this.mode = "choose";
94
+ this.input.setValue("");
95
+ this.input.focused = false;
96
+ if (render) this.refresh();
97
+ }
98
+ enterInput(mode) {
99
+ this.mode = mode;
100
+ this.input.setValue(mode === "note" ? (this.notes.get(this.tab) ?? "") : (this.written.get(this.tab) ?? ""));
101
+ this.input.focused = this.focusedValue;
102
+ this.refresh();
103
+ }
104
+ handleInput(data) {
105
+ if (this.mode !== "choose") {
106
+ this.input.handleInput(data);
107
+ this.refresh();
108
+ return;
109
+ }
110
+ if (matchesKey(data, Key.escape)) return this.done({ status: "dismissed" });
111
+ if (this.many && (matchesKey(data, Key.tab) || matchesKey(data, Key.right))) {
112
+ this.tab = (this.tab + 1) % (this.questions.length + 1);
113
+ return this.refresh();
114
+ }
115
+ if (this.many && (matchesKey(data, Key.shift("tab")) || matchesKey(data, Key.left))) {
116
+ this.tab = (this.tab + this.questions.length) % (this.questions.length + 1);
117
+ return this.refresh();
118
+ }
119
+ if (this.onSubmitTab) {
120
+ if (matchesKey(data, Key.enter) && this.answered.size === this.questions.length) this.finish();
121
+ return;
122
+ }
123
+ const question = this.questions[this.tab];
124
+ const at = this.cursor[this.tab];
125
+ const own = at === question.options.length;
126
+ if (matchesKey(data, Key.up)) {
127
+ this.cursor[this.tab] = Math.max(0, at - 1);
128
+ return this.refresh();
129
+ }
130
+ if (matchesKey(data, Key.down)) {
131
+ this.cursor[this.tab] = Math.min(this.rows(this.tab) - 1, at + 1);
132
+ return this.refresh();
133
+ }
134
+ if (data === "n" && this.options.notes !== false) return this.enterInput("note");
135
+ if (question.multiSelect && data === " " && !own) {
136
+ const picked = this.picked[this.tab];
137
+ if (picked.has(at)) picked.delete(at);
138
+ else picked.add(at);
139
+ this.written.delete(this.tab);
140
+ this.answered.delete(this.tab);
141
+ return this.refresh();
142
+ }
143
+ if (matchesKey(data, Key.enter)) {
144
+ if (own) return this.enterInput("own");
145
+ const picked = this.picked[this.tab];
146
+ if (!question.multiSelect) {
147
+ picked.clear();
148
+ picked.add(at);
149
+ } else if (picked.size === 0) picked.add(at);
150
+ this.written.delete(this.tab);
151
+ this.advance();
152
+ }
153
+ }
154
+ invalidate() {
155
+ this.cache = undefined;
156
+ }
157
+ render(width) {
158
+ if (this.cache) return this.cache;
159
+ const t = this.theme;
160
+ const w = Math.max(1, width);
161
+ const lines = [];
162
+ const wrap = (prefix, text) => {
163
+ const indent = visibleWidth(prefix);
164
+ const body = wrapTextWithAnsi(text, Math.max(1, w - indent));
165
+ body.forEach((line, i) => lines.push(`${i === 0 ? prefix : " ".repeat(indent)}${line}`));
166
+ };
167
+ lines.push(t.fg("accent", "─".repeat(w)));
168
+ if (this.many) {
169
+ const tabs = this.questions.map((question, index) => {
170
+ const label = ` ${this.answered.has(index) ? "■" : "□"} ${question.header} `;
171
+ return index === this.tab
172
+ ? t.bold(t.fg("accent", label))
173
+ : t.fg(this.answered.has(index) ? "success" : "muted", label);
174
+ });
175
+ const ready = this.answered.size === this.questions.length;
176
+ const submit = " ✓ Submit ";
177
+ tabs.push(this.onSubmitTab ? t.bold(t.fg("accent", submit)) : t.fg(ready ? "success" : "dim", submit));
178
+ wrap(" ", tabs.join(" "));
179
+ lines.push("");
180
+ }
181
+ if (this.onSubmitTab) {
182
+ wrap(" ", t.bold("Your answers"));
183
+ lines.push("");
184
+ this.questions.forEach((question, index) => {
185
+ const text = this.written.get(index);
186
+ const chosen = [...this.picked[index]].map((at) => question.options[at].label).join(", ");
187
+ const answer =
188
+ text !== undefined ? `"${text}"` : this.answered.has(index) ? chosen : t.fg("warning", "not answered");
189
+ wrap(" ", `${t.fg("muted", `${question.header}:`)} ${answer}`);
190
+ const note = this.notes.get(index);
191
+ if (note) wrap(" ", t.fg("dim", `note: ${note}`));
192
+ });
193
+ lines.push("");
194
+ wrap(
195
+ " ",
196
+ this.answered.size === this.questions.length
197
+ ? t.fg("success", "Enter to submit")
198
+ : t.fg("warning", "Answer every question to submit"),
199
+ );
200
+ } else {
201
+ const index = this.tab;
202
+ const question = this.questions[index];
203
+ wrap(" ", `${t.fg("muted", `${question.header} ·`)} ${t.bold(question.question)}`);
204
+ lines.push("");
205
+ const rows = [...question.options, ...(this.ownAnswer ? [{ label: OWN_ANSWER }] : [])];
206
+ rows.forEach((option, at) => {
207
+ const own = at === question.options.length;
208
+ const here = at === this.cursor[index] && this.mode === "choose";
209
+ const mark = question.multiSelect && !own ? (this.picked[index].has(at) ? "[x] " : "[ ] ") : "";
210
+ const chosen = !question.multiSelect && !own && this.answered.has(index) && this.picked[index].has(at);
211
+ const label = `${at + 1}. ${mark}${option.label}${chosen ? " ✓" : ""}`;
212
+ wrap(here ? t.fg("accent", "› ") : " ", here ? t.fg("accent", label) : label);
213
+ const description = own ? this.written.get(index) : option.description;
214
+ if (description) wrap(" ", t.fg("muted", own ? `"${description}"` : description));
215
+ });
216
+ const note = this.notes.get(index);
217
+ if (note && this.mode !== "note") {
218
+ lines.push("");
219
+ wrap(" ", t.fg("dim", `Note: ${note}`));
220
+ }
221
+ if (this.mode !== "choose") {
222
+ lines.push("");
223
+ wrap(" ", t.fg("muted", this.mode === "own" ? "Your answer:" : "Note for this question:"));
224
+ for (const line of this.input.render(Math.max(1, w - 2))) lines.push(` ${line}`);
225
+ }
226
+ }
227
+ lines.push("");
228
+ if (this.options.deadline !== undefined) {
229
+ const left = Math.max(0, Math.ceil((this.options.deadline - Date.now()) / 1000));
230
+ wrap(
231
+ " ",
232
+ t.fg("dim", `Closes in ${Math.floor(left / 60)}:${String(left % 60).padStart(2, "0")} without an answer`),
233
+ );
234
+ }
235
+ const help =
236
+ this.mode !== "choose"
237
+ ? "Enter to save · Esc to go back"
238
+ : [
239
+ "↑↓ move",
240
+ this.questions[this.tab]?.multiSelect ? "Space toggle · Enter confirm" : "Enter choose",
241
+ ...(this.options.notes !== false ? ["n note"] : []),
242
+ ...(this.many ? ["Tab next"] : []),
243
+ "Esc close without answering",
244
+ ].join(" · ");
245
+ wrap(" ", t.fg("dim", help));
246
+ lines.push(t.fg("accent", "─".repeat(w)));
247
+ this.cache = lines;
248
+ return lines;
249
+ }
250
+ }
@@ -0,0 +1,66 @@
1
+ /*
2
+ * The questions ask_user puts to the user, and the answers it returns: Claude Code's AskUserQuestion shape (1-4
3
+ * questions, a short header each, 2-4 options with descriptions, multi-select), since models know it. Every question
4
+ * also accepts the user's own words and a note; the dialog adds those, the model never lists them.
5
+ */
6
+ export const MAX_QUESTIONS = 4;
7
+ export const MIN_OPTIONS = 2;
8
+ export const MAX_OPTIONS = 4;
9
+ export const MAX_HEADER = 12;
10
+ const text = (value) => (typeof value === "string" ? value.trim() : "");
11
+ /** The questions as given, or the first reason they cannot be asked. */
12
+ export function validateQuestions(value) {
13
+ if (!Array.isArray(value) || value.length < 1 || value.length > MAX_QUESTIONS)
14
+ return { error: `Ask 1 to ${MAX_QUESTIONS} questions.` };
15
+ const questions = [];
16
+ const headers = new Set();
17
+ for (const [index, raw] of value.entries()) {
18
+ const at = `Question ${index + 1}`;
19
+ const question = text(raw?.question);
20
+ const header = text(raw?.header);
21
+ if (!question) return { error: `${at} has no question text.` };
22
+ if (!header || header.length > MAX_HEADER)
23
+ return { error: `${at} needs a header of 1 to ${MAX_HEADER} characters.` };
24
+ if (headers.has(header)) return { error: `${at} repeats the header "${header}".` };
25
+ headers.add(header);
26
+ const options = Array.isArray(raw?.options) ? raw.options : [];
27
+ if (options.length < MIN_OPTIONS || options.length > MAX_OPTIONS)
28
+ return { error: `${at} needs ${MIN_OPTIONS} to ${MAX_OPTIONS} options.` };
29
+ const labels = new Set();
30
+ const normalized = [];
31
+ for (const option of options) {
32
+ const label = text(option?.label);
33
+ if (!label) return { error: `${at} has an option with no label.` };
34
+ if (/^other\b/i.test(label))
35
+ return { error: `${at} lists "${label}": the dialog adds the user's own answer itself; remove it.` };
36
+ if (labels.has(label)) return { error: `${at} repeats the option "${label}".` };
37
+ labels.add(label);
38
+ const description = text(option?.description);
39
+ normalized.push(description ? { label, description } : { label });
40
+ }
41
+ questions.push({
42
+ question,
43
+ header,
44
+ options: normalized,
45
+ ...(raw?.multiSelect === true ? { multiSelect: true } : {}),
46
+ });
47
+ }
48
+ return { questions };
49
+ }
50
+ /** What the model reads after the dialog. */
51
+ export function outcomeText(outcome) {
52
+ if (outcome.status === "unavailable") return `${outcome.reason} Ask in your reply and end your turn.`;
53
+ if (outcome.status === "dismissed")
54
+ return "The user closed the dialog without answering. Do not act on the choice: ask in your reply, or continue only with work that does not depend on it.";
55
+ const lines = ["The user answered in a dialog:"];
56
+ for (const answer of outcome.answers) {
57
+ lines.push(`- ${answer.header}: ${answer.question}`);
58
+ if (answer.text !== undefined) lines.push(` Answer, in their words: ${answer.text}`);
59
+ else lines.push(` Answer: ${answer.selected.join("; ")}`);
60
+ if (answer.note) lines.push(` Note: ${answer.note}`);
61
+ }
62
+ lines.push(
63
+ "Each answer decides only its question. A note or written answer that asks something, sets a condition or asks for more information is not a decision: respond to it before acting.",
64
+ );
65
+ return lines.join("\n");
66
+ }
@@ -0,0 +1,124 @@
1
+ import { Text } from "@earendil-works/pi-tui";
2
+ import { firstLine, foldRow, paint } from "../host/rows.js";
3
+ import { askUser } from "./ask.js";
4
+ import { MAX_HEADER, MAX_OPTIONS, MAX_QUESTIONS, MIN_OPTIONS, outcomeText, validateQuestions } from "./questions.js";
5
+ export const ASK_USER = "ask_user";
6
+ const DESCRIPTION = [
7
+ "Ask the user in a dialog when a choice is theirs to make: what to do, which direction to take, whether to go beyond what they asked. The call waits for their answer.",
8
+ `Ask 1 to ${MAX_QUESTIONS} questions. Each has a full-sentence question, a header of at most ${MAX_HEADER} characters, and ${MIN_OPTIONS} to ${MAX_OPTIONS} options: a label of a few words and a description of what choosing it means. Put your recommended option first and end its label with "(Recommended)". Use multiSelect when the options are not exclusive. The dialog adds a field for the user's own answer and a note, so never list an "Other" option.`,
9
+ "Several questions in one call only when each is the user's and none depends on another's answer. Say any context the choice needs in your reply before calling: the dialog shows only the questions and options.",
10
+ "If the user closes the dialog or no one can answer it, the result says so: then ask in your reply.",
11
+ ].join("\n\n");
12
+ // Pi adds a tool's guidelines to the system prompt's rules while the tool is active.
13
+ const GUIDELINE =
14
+ "Use ask_user for a choice that is the user's to make, with the options that matter and your recommendation first; never for facts you can look up or work out.";
15
+ const textOf = (result) =>
16
+ (result?.content ?? [])
17
+ .filter((part) => part?.type === "text")
18
+ .map((part) => part.text ?? "")
19
+ .join("\n");
20
+ /** Registered once, declared only while Freeflow is effective outside subagents. */
21
+ export function registerAskUserTool(pi, host) {
22
+ pi.registerTool({
23
+ name: ASK_USER,
24
+ label: "Question",
25
+ description: DESCRIPTION,
26
+ promptGuidelines: [GUIDELINE],
27
+ parameters: {
28
+ type: "object",
29
+ additionalProperties: false,
30
+ properties: {
31
+ questions: {
32
+ type: "array",
33
+ minItems: 1,
34
+ maxItems: MAX_QUESTIONS,
35
+ items: {
36
+ type: "object",
37
+ additionalProperties: false,
38
+ properties: {
39
+ question: { type: "string", description: "The full question, ending with a question mark." },
40
+ header: {
41
+ type: "string",
42
+ description: `A label of at most ${MAX_HEADER} characters, such as "Default".`,
43
+ },
44
+ options: {
45
+ type: "array",
46
+ minItems: MIN_OPTIONS,
47
+ maxItems: MAX_OPTIONS,
48
+ items: {
49
+ type: "object",
50
+ additionalProperties: false,
51
+ properties: {
52
+ label: { type: "string", description: "A few words; the recommended one first." },
53
+ description: { type: "string", description: "What choosing it means, and its tradeoff." },
54
+ },
55
+ required: ["label", "description"],
56
+ },
57
+ },
58
+ multiSelect: { type: "boolean", description: "True when the user may choose more than one." },
59
+ },
60
+ required: ["question", "header", "options"],
61
+ },
62
+ },
63
+ },
64
+ required: ["questions"],
65
+ },
66
+ // It asks the user, so scripts must not call it, and its transcript views apply (see task_memory).
67
+ exposure: "model-only",
68
+ defaultActive: false,
69
+ executionMode: "sequential",
70
+ // One line each for what was asked and what the user answered; options and notes when expanded or short.
71
+ renderCall: (args, theme, context) => {
72
+ const questions = (Array.isArray(args?.questions) ? args.questions : []).filter((q) => q?.question);
73
+ const title = paint(theme, "toolTitle", "Question");
74
+ if (!questions.length) return new Text(title, 0, 0);
75
+ const head =
76
+ questions.length === 1
77
+ ? `${title} · ${paint(theme, "accent", String(questions[0].header ?? ""))}: ${firstLine(questions[0].question, 120)}`
78
+ : `${title} · ${questions.map((q) => paint(theme, "accent", String(q.header ?? ""))).join(", ")}`;
79
+ const body = questions.flatMap((q) => [
80
+ ...(questions.length > 1 ? [`${q.header}: ${q.question}`] : q.question.length > 120 ? [q.question] : []),
81
+ ...(Array.isArray(q.options) ? q.options : []).map(
82
+ (o) => `${questions.length > 1 ? " " : ""}- ${o.label}${o.description ? ` — ${o.description}` : ""}`,
83
+ ),
84
+ ]);
85
+ return foldRow(head, context?.expanded === true ? body : [], context?.expanded === true, theme);
86
+ },
87
+ renderResult: (result, options, theme) => {
88
+ const outcome = result?.details?.outcome;
89
+ if (outcome?.status === "answered") {
90
+ const lines = outcome.answers.map(
91
+ (answer) =>
92
+ `${answer.header}: ${answer.text !== undefined ? `"${answer.text}"` : answer.selected.join(", ")}${answer.note ? ` (note: ${answer.note})` : ""}`,
93
+ );
94
+ const head = paint(theme, "success", lines.length === 1 ? `Answered · ${lines[0]}` : "Answered");
95
+ return foldRow(head, lines.length === 1 ? [] : lines, options?.expanded === true, theme);
96
+ }
97
+ if (outcome?.status === "dismissed")
98
+ return new Text(paint(theme, "warning", "Closed without answering (Esc)"), 0, 0);
99
+ return new Text(paint(theme, "warning", firstLine(textOf(result)) || "Not asked"), 0, 0);
100
+ },
101
+ async execute(_id, params, signal, _update, ctx) {
102
+ const refused = host.refusal(ctx);
103
+ const checked = validateQuestions(params?.questions);
104
+ let outcome;
105
+ if (refused) outcome = { status: "unavailable", reason: refused };
106
+ else if ("error" in checked)
107
+ return { content: [{ type: "text", text: checked.error }], details: {}, isError: true };
108
+ else outcome = await askUser(ctx, checked.questions, signal);
109
+ return {
110
+ content: [{ type: "text", text: refused ? refused : outcomeText(outcome) }],
111
+ details: { outcome },
112
+ ...(outcome.status === "answered" ? {} : { isError: true }),
113
+ };
114
+ },
115
+ });
116
+ }
117
+ /** Declare or withdraw ask_user, keeping other tools in place. */
118
+ export function applyAskUserTool(pi, effective) {
119
+ if (typeof pi.getActiveTools !== "function" || typeof pi.setActiveTools !== "function") return;
120
+ const current = pi.getActiveTools();
121
+ const present = current.includes(ASK_USER);
122
+ if (effective && !present) pi.setActiveTools([...current, ASK_USER]);
123
+ if (!effective && present) pi.setActiveTools(current.filter((name) => name !== ASK_USER));
124
+ }
@@ -1,4 +1,5 @@
1
1
  import { randomUUID } from "node:crypto";
2
+ import { viewHoldsSkill } from "./bootstrap.js";
2
3
  import { estimateRequest } from "./budget.js";
3
4
  import { prepareView } from "./projection.js";
4
5
  import { annotateSources } from "./provenance.js";
@@ -92,12 +93,28 @@ export class ContextAssembler {
92
93
  this.session.projectionError ? `Evidence limitation: ${this.session.projectionError}` : "",
93
94
  attention
94
95
  ? recovery
95
- ? `Assessment paused for attention; ordinary admitted history remains. ${selection.selected.length} sources remain selected. Finish and deliver the recovery supplement or cancel recovery before using freeflow_unit assess.`
96
- : `Assessment paused for attention; ordinary admitted history remains. ${selection.selected.length} sources remain selected. Use freeflow_unit assess to restore the assessment.`
96
+ ? `Assessment paused for attention; ordinary admitted history remains. ${selection.selected.length} sources remain selected. Finish and deliver the recovery supplement or cancel recovery before using routing_unit assess.`
97
+ : `Assessment paused for attention; ordinary admitted history remains. ${selection.selected.length} sources remain selected. Use routing_unit assess to restore the assessment.`
97
98
  : "",
98
99
  a?.state === "returned" && recovery?.state !== "reading"
99
100
  ? "Worker ordinary task work has ended. Only supported handoff correction is permitted."
100
101
  : "",
102
+ // After a compaction the routing skill may be out of view, and Haiku 5.5 workers then answered as Coordinator
103
+ // and ended without returning; Coordinator saw only "outstanding" and waited (Task 008 S-005).
104
+ state.control === "automatic" && a?.state === "outstanding" && worker && state.profile === worker
105
+ ? `You are ${label(worker)}: carry out A${assignmentNumber} from its delegate_work contract, then hand it back with return_work. Coordinator receives nothing until you return.`
106
+ : "",
107
+ state.control === "automatic" && a?.state === "outstanding" && worker && state.profile === "coordinator"
108
+ ? `${label(worker)}'s run on A${assignmentNumber} ended without return_work, so nothing was handed back. Send it again with delegate_work replace, close the unit, or ask the user.`
109
+ : "",
110
+ // Worker calls reach the Coordinator as assistant turns; Haiku 5.5 took them for its own (Task 008 S-005).
111
+ state.profile === "coordinator" && state.assessment
112
+ ? "The worker's run appears in your view as assistant turns: those calls are the worker's, as the source provenance after the run says, not yours."
113
+ : "",
114
+ // Selected bodies are not re-sent with the return; they stay where the worker produced them (Task 004 S-005).
115
+ state.profile === "coordinator" && state.assessment && selection.selected.length
116
+ ? "Selected evidence is already in your view, in place in the worker's run: the source provenance after the run names each selected ref's call (its path or command) and marks it `task evidence`. Recover only evidence the assessment needs that is not there."
117
+ : "",
101
118
  isWorkerProfile(this.session.turn?.profile) &&
102
119
  a &&
103
120
  this.session.turn.basisUserEntryId !== this.session.workerBasis(state, a.id)
@@ -166,19 +183,19 @@ export class ContextAssembler {
166
183
  const data = entry.data?.data;
167
184
  if (data?.type === "assessment-suspended" && data.handoffId === state.assessment?.handoffId)
168
185
  suspension = data.reason;
186
+ // A recovery during user attention leaves that attention outstanding, as the reducer does.
169
187
  if (
170
188
  data?.type === "recovery-request-accepted" &&
171
- data.recovery?.assessmentHandoffId === state.assessment?.handoffId
189
+ data.recovery?.assessmentHandoffId === state.assessment?.handoffId &&
190
+ suspension !== "user-attention"
172
191
  )
173
192
  suspension = "recovery";
174
- // After a recovery the Coordinator resumes the view it last saw, since it made no requests while suspended:
175
- // only a suspension it kept working through (user attention) leaves evidence new to it at the resume.
176
- if (
177
- data?.type === "assessment-resumed" &&
178
- data.handoffId === state.assessment?.handoffId &&
179
- suspension !== "recovery"
180
- )
181
- resumedAt = rank;
193
+ // After a recovery alone the Coordinator resumes the view it last saw, since it made no requests while
194
+ // suspended: only a suspension it kept working through (user attention) leaves evidence new to it at the resume.
195
+ if (data?.type === "assessment-resumed" && data.handoffId === state.assessment?.handoffId) {
196
+ if (suspension !== "recovery") resumedAt = rank;
197
+ suspension = undefined;
198
+ }
182
199
  if (data?.type === "return-accepted" || data?.type === "recovery-supplement-accepted")
183
200
  assessedSinceCompaction.add(data.handoff.assignmentId);
184
201
  if (data?.type !== "selection-changed") continue;
@@ -333,6 +350,12 @@ export class ContextAssembler {
333
350
  prepared = this.prepared("coordinator", input);
334
351
  }
335
352
  check(prepared.ready, "context_unavailable", prepared.problems.map((p) => p.detail).join("; "));
353
+ if (this.session.routingSkill)
354
+ this.session.turn.skillInView = viewHoldsSkill(
355
+ prepared.messages,
356
+ this.session.routingSkill,
357
+ this.session.turn.profile,
358
+ );
336
359
  // Only worker views record exposure; hashing every source for the Coordinator's view would be wasted.
337
360
  if (isWorkerProfile(this.session.turn.profile)) {
338
361
  const known = this.session.stateData().exposure;
@@ -384,6 +407,7 @@ function compactedInAssignment(branch, assignmentId) {
384
407
  const freeflow = [...branch].reverse().find((entry) => entry?.type === "compaction")?.details?.freeflow;
385
408
  return freeflow && !freeflow.fallback && freeflow.assignment === assignmentId ? freeflow.cycle : undefined;
386
409
  }
410
+ const label = (profile) => profile.charAt(0).toUpperCase() + profile.slice(1);
387
411
  function workerCompaction(branch) {
388
412
  const latest = [...branch].reverse().find((entry) => entry?.type === "compaction");
389
413
  const freeflow = latest?.details?.freeflow;
@@ -0,0 +1,90 @@
1
+ import { existsSync, readFileSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ const ROLE_REFERENCES = { coordinator: "coordinator.md", worker: "worker.md" };
4
+ function lines(path) {
5
+ return readFileSync(path, "utf8")
6
+ .split("\n")
7
+ .map((line) => line.trim())
8
+ .filter(Boolean);
9
+ }
10
+ /** A reference and its markers: its heading and its closing line. */
11
+ function reference(path) {
12
+ if (!existsSync(path)) return undefined;
13
+ try {
14
+ const text = lines(path);
15
+ const first = text[0],
16
+ last = text.at(-1);
17
+ return first && last && first !== last ? { path, first, last } : undefined;
18
+ } catch {
19
+ return undefined;
20
+ }
21
+ }
22
+ /** The packaged skill, its role references and their markers, or undefined when the skill cannot be read: then nothing is gated on it. */
23
+ export function findRoutingSkill(packageRoots) {
24
+ for (const root of packageRoots) {
25
+ const dir = join(root, "capabilities", "cognitive-routing");
26
+ const path = join(dir, "SKILL.md");
27
+ if (!existsSync(path)) continue;
28
+ try {
29
+ const text = lines(path);
30
+ // The frontmatter's name line, and the closing rule: together they show the whole body was read.
31
+ const first = text.find((line) => line.startsWith("name:"));
32
+ const last = text.at(-1);
33
+ if (!first || !last || first === last) continue;
34
+ const roles = {};
35
+ for (const role of Object.keys(ROLE_REFERENCES)) {
36
+ const found = reference(join(dir, "references", ROLE_REFERENCES[role]));
37
+ if (found) roles[role] = found;
38
+ }
39
+ return { path, first, last, roles };
40
+ } catch {}
41
+ }
42
+ return undefined;
43
+ }
44
+ /** The role reference a view's profile needs; `solo` needs none. */
45
+ export function routingRole(view) {
46
+ if (view === "coordinator") return "coordinator";
47
+ if (view === "helper" || view === "executor") return "worker";
48
+ return undefined;
49
+ }
50
+ const SKILL = 1,
51
+ COORDINATOR = 2,
52
+ WORKER = 4;
53
+ const ROLE_BIT = { coordinator: COORDINATOR, worker: WORKER };
54
+ /** Which files each tool result holds, per skill. Renderings are shared across requests, so each is checked once. */
55
+ const holds = new WeakMap();
56
+ function holdsFile(file, text) {
57
+ return !!file && text.includes(file.first) && text.includes(file.last);
58
+ }
59
+ /** Whether a prepared view holds a complete read of the skill and of the reference for the view's profile. */
60
+ export function viewHoldsSkill(messages, skill, view) {
61
+ const role = routingRole(view);
62
+ const need = SKILL | (role && skill.roles[role] ? ROLE_BIT[role] : 0);
63
+ let cache = holds.get(skill);
64
+ if (!cache) holds.set(skill, (cache = new WeakMap()));
65
+ let found = 0;
66
+ for (let i = messages.length - 1; i >= 0; i--) {
67
+ const message = messages[i];
68
+ if (message?.role !== "toolResult" || typeof message !== "object") continue;
69
+ let bits = cache.get(message);
70
+ if (bits === undefined) {
71
+ const text = Array.isArray(message.content)
72
+ ? message.content.map((block) => (block?.type === "text" ? block.text : "")).join("\n")
73
+ : String(message.content ?? "");
74
+ bits =
75
+ (holdsFile(skill, text) ? SKILL : 0) |
76
+ (holdsFile(skill.roles.coordinator, text) ? COORDINATOR : 0) |
77
+ (holdsFile(skill.roles.worker, text) ? WORKER : 0);
78
+ cache.set(message, bits);
79
+ }
80
+ found |= bits;
81
+ if ((found & need) === need) return true;
82
+ }
83
+ return false;
84
+ }
85
+ /** The files a profile must have in view before its commands or edits: the skill, then its role's reference. */
86
+ export function requiredReads(skill, view) {
87
+ const role = routingRole(view);
88
+ const ref = role ? skill.roles[role] : undefined;
89
+ return ref ? [skill.path, ref.path] : [skill.path];
90
+ }
@@ -6,12 +6,11 @@ export const pairFromProfile = (profile) => ({
6
6
  modelId: profile.model,
7
7
  thinking: profile.thinking,
8
8
  });
9
+ /** The keys `cognitiveRouting` accepts; the settings test checks each has a /freeflow settings row. */
10
+ export const COGNITIVE_ROUTING_KEYS = ["enabled", "delegation", "projection", "profiles"];
9
11
  function parseLayer(raw) {
10
12
  if (raw === undefined) return {};
11
- if (
12
- !isObject(raw) ||
13
- Object.keys(raw).some((key) => !["enabled", "delegation", "projection", "profiles"].includes(key))
14
- )
13
+ if (!isObject(raw) || Object.keys(raw).some((key) => !COGNITIVE_ROUTING_KEYS.includes(key)))
15
14
  throw new Error(
16
15
  "Routing requires the enabled/delegation/projection/profiles schema; experimental legacy configuration is not migrated.",
17
16
  );
@@ -87,7 +87,10 @@ export class EventStore {
87
87
  const event = parseRoutingEvent(entry.data);
88
88
  const receipt = this.observed.get(eventKey(event));
89
89
  return (
90
- receipt?.entryId === entry.id && receipt.eventId === event.eventId && receipt.value === eventValue(event)
90
+ receipt !== undefined &&
91
+ receipt.entryId === entry.id &&
92
+ receipt.eventId === event.eventId &&
93
+ receipt.value === eventValue(event)
91
94
  );
92
95
  })
93
96
  ) {