@hassangameryt/freeflow 0.7.1 → 0.7.2

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 (122) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/.codex-plugin/plugin.json +1 -1
  4. package/.cursor-plugin/plugin.json +1 -1
  5. package/README.md +28 -23
  6. package/capabilities/cognitive-routing/SKILL.md +94 -311
  7. package/command-surface.json +7 -1
  8. package/gemini-extension.json +1 -1
  9. package/package.json +4 -4
  10. package/pi-extension/dist/cognitive-routing-v2/budget.js +42 -0
  11. package/pi-extension/dist/cognitive-routing-v2/config.js +120 -0
  12. package/pi-extension/dist/cognitive-routing-v2/projection.js +321 -0
  13. package/pi-extension/dist/cognitive-routing-v2/render.js +231 -0
  14. package/pi-extension/dist/cognitive-routing-v2/runtime.js +1585 -0
  15. package/pi-extension/dist/cognitive-routing-v2/schemas.js +107 -0
  16. package/pi-extension/dist/cognitive-routing-v2/state.js +612 -0
  17. package/pi-extension/dist/cognitive-routing-v2/tools.js +91 -0
  18. package/pi-extension/dist/cognitive-routing-v2/types.js +44 -0
  19. package/pi-extension/dist/freeflow-context/tool.js +5 -0
  20. package/pi-extension/dist/index.js +279 -624
  21. package/pi-extension/dist/runtime/runtime-context.js +63 -13
  22. package/pi-extension/dist/session-sources/events.js +195 -0
  23. package/pi-extension/dist/session-sources/read-only-session.js +148 -0
  24. package/pi-extension/dist/session-sources/sources.js +233 -0
  25. package/pi-extension/dist/settings/settings-ui.js +44 -117
  26. package/pi-extension/src/cognitive-routing-v2/budget.ts +42 -0
  27. package/pi-extension/src/cognitive-routing-v2/config.ts +143 -0
  28. package/pi-extension/src/cognitive-routing-v2/projection.ts +365 -0
  29. package/pi-extension/src/cognitive-routing-v2/render.ts +256 -0
  30. package/pi-extension/src/cognitive-routing-v2/runtime.ts +1634 -0
  31. package/pi-extension/src/cognitive-routing-v2/schemas.ts +109 -0
  32. package/pi-extension/src/cognitive-routing-v2/state.ts +622 -0
  33. package/pi-extension/src/cognitive-routing-v2/tools.ts +93 -0
  34. package/pi-extension/src/cognitive-routing-v2/types.ts +222 -0
  35. package/pi-extension/src/freeflow-context/tool.ts +5 -0
  36. package/pi-extension/src/index.ts +285 -674
  37. package/pi-extension/src/runtime/runtime-context.ts +75 -14
  38. package/pi-extension/src/session-sources/events.ts +208 -0
  39. package/pi-extension/src/session-sources/read-only-session.ts +170 -0
  40. package/pi-extension/src/session-sources/sources.ts +263 -0
  41. package/pi-extension/src/settings/settings-ui.ts +54 -131
  42. package/pi-extension/tests/cognitive-routing-v2/communication.test.js +484 -0
  43. package/pi-extension/tests/cognitive-routing-v2/compaction-input.test.js +183 -0
  44. package/pi-extension/tests/cognitive-routing-v2/native.test.js +441 -0
  45. package/pi-extension/tests/cognitive-routing-v2/performance.test.js +87 -0
  46. package/pi-extension/tests/cognitive-routing-v2/persistence.test.js +245 -0
  47. package/pi-extension/tests/cognitive-routing-v2/projection.test.js +241 -0
  48. package/pi-extension/tests/cognitive-routing-v2/recovery.test.js +236 -0
  49. package/pi-extension/tests/cognitive-routing-v2/render.test.js +230 -0
  50. package/pi-extension/tests/cognitive-routing-v2/runtime.test.js +153 -0
  51. package/pi-extension/tests/cognitive-routing-v2/stabilization.test.js +521 -0
  52. package/pi-extension/tests/cognitive-routing-v2/state.test.js +275 -0
  53. package/pi-extension/tests/context-virtualization/extension.test.js +16 -0
  54. package/pi-extension/tests/fixtures/routing-native.js +188 -0
  55. package/pi-extension/tests/integration/mode-free-core.test.js +3 -2
  56. package/pi-extension/tests/integration/pi-extension.test.js +154 -37
  57. package/pi-extension/tests/integration/prompt-architecture.test.js +87 -6
  58. package/pi-extension/tests/integration/unaffected-regressions.test.js +77 -10
  59. package/plugin.json +1 -1
  60. package/runtime/prompts/cognitive-routing.md +4 -5
  61. package/runtime/prompts/core.md +51 -45
  62. package/runtime/prompts/interaction-contract.md +5 -5
  63. package/skills/action-selection/SKILL.md +57 -97
  64. package/skills/design-for-depth/SKILL.md +58 -193
  65. package/skills/design-for-depth/references/implementation-decisions.md +76 -0
  66. package/skills/diagnose-failure/SKILL.md +64 -196
  67. package/skills/discuss/SKILL.md +64 -123
  68. package/skills/discuss/references/discussion-continuity.md +7 -3
  69. package/skills/execute-work/SKILL.md +69 -88
  70. package/skills/execute-work/references/execute-work-edges.md +9 -3
  71. package/skills/handoff/SKILL.md +44 -54
  72. package/skills/handoff/references/handoff-templates.md +15 -6
  73. package/skills/review-artifact/SKILL.md +80 -127
  74. package/skills/review-artifact/references/independent-artifact-reviewer-contract.md +1 -1
  75. package/skills/review-work/SKILL.md +65 -115
  76. package/skills/review-work/references/independent-work-reviewer-contract.md +1 -1
  77. package/skills/simplify-code/SKILL.md +47 -100
  78. package/skills/track-work/SKILL.md +128 -119
  79. package/skills/track-work/references/working-record-format.md +14 -2
  80. package/skills/verify-work/SKILL.md +58 -62
  81. package/skills/verify-work/references/integration-evidence.md +4 -2
  82. package/skills/verify-work/references/test-design.md +89 -0
  83. package/skills/workflow/SKILL.md +73 -119
  84. package/skills/write-plan/SKILL.md +46 -77
  85. package/skills/write-plan/references/plan-shapes.md +5 -5
  86. package/skills/write-spec/SKILL.md +51 -63
  87. package/skills/write-spec/references/artifact-standards.md +2 -2
  88. package/skills/write-spec/references/decision-records.md +3 -2
  89. package/skills/write-spec/references/spec-shapes.md +15 -0
  90. package/pi-extension/dist/cognitive-routing/commands.js +0 -96
  91. package/pi-extension/dist/cognitive-routing/config.js +0 -232
  92. package/pi-extension/dist/cognitive-routing/controller.js +0 -1292
  93. package/pi-extension/dist/cognitive-routing/history.js +0 -451
  94. package/pi-extension/dist/cognitive-routing/host.js +0 -188
  95. package/pi-extension/dist/cognitive-routing/pi-routing-session.js +0 -414
  96. package/pi-extension/dist/cognitive-routing/pi-session-control.js +0 -320
  97. package/pi-extension/dist/cognitive-routing/runtime.js +0 -43
  98. package/pi-extension/dist/cognitive-routing/tool.js +0 -212
  99. package/pi-extension/dist/cognitive-routing/types.js +0 -6
  100. package/pi-extension/src/cognitive-routing/commands.ts +0 -129
  101. package/pi-extension/src/cognitive-routing/config.ts +0 -303
  102. package/pi-extension/src/cognitive-routing/controller.ts +0 -1628
  103. package/pi-extension/src/cognitive-routing/history.ts +0 -650
  104. package/pi-extension/src/cognitive-routing/host.ts +0 -266
  105. package/pi-extension/src/cognitive-routing/pi-routing-session.ts +0 -538
  106. package/pi-extension/src/cognitive-routing/pi-session-control.ts +0 -444
  107. package/pi-extension/src/cognitive-routing/runtime.ts +0 -57
  108. package/pi-extension/src/cognitive-routing/tool.ts +0 -273
  109. package/pi-extension/src/cognitive-routing/types.ts +0 -89
  110. package/pi-extension/tests/cognitive-routing/commands.test.js +0 -134
  111. package/pi-extension/tests/cognitive-routing/controller.test.js +0 -1128
  112. package/pi-extension/tests/cognitive-routing/extension-lifecycle.test.js +0 -1142
  113. package/pi-extension/tests/cognitive-routing/history.test.js +0 -274
  114. package/pi-extension/tests/cognitive-routing/host-identity.test.js +0 -63
  115. package/pi-extension/tests/cognitive-routing/pi-normal-lifecycle.test.js +0 -434
  116. package/pi-extension/tests/cognitive-routing/pi-session-control.test.js +0 -174
  117. package/pi-extension/tests/cognitive-routing/pi-session-history.test.js +0 -91
  118. package/pi-extension/tests/cognitive-routing/profile-resolution.test.js +0 -279
  119. package/pi-extension/tests/cognitive-routing/tool.test.js +0 -230
  120. package/skills/tdd/SKILL.md +0 -143
  121. package/skills/tdd/references/test-design.md +0 -96
  122. /package/pi-extension/tests/{cognitive-routing/host-fixture.js → fixtures/pi-host.js} +0 -0
@@ -0,0 +1,233 @@
1
+ import { createHash } from "node:crypto";
2
+ import { sessionEntryToContextMessages } from "@earendil-works/pi-coding-agent";
3
+ import { canonical, idFor, refFor } from "../cognitive-routing-v2/types.js";
4
+ export const bodyHash = (value) => createHash("sha256").update(canonical(value)).digest("hex");
5
+ export const textRef = (ref) => `${ref}#text`;
6
+ const routingTools = new Set(["freeflow_delegate", "freeflow_return", "freeflow_project", "freeflow_unit"]);
7
+ // Discovery and new selections share this policy. Existing saved selections keep
8
+ // their original delivery semantics; mixed assistant messages offer visible text.
9
+ export function isTaskEvidence(source) {
10
+ if (source.original) return true;
11
+ if (source.message.role === "toolResult") return !routingTools.has(source.message.toolName);
12
+ return (
13
+ source.message.role === "assistant" &&
14
+ !source.message.content?.some((b) => b.type === "toolCall" && routingTools.has(b.name)) &&
15
+ source.message.content?.some((b) => b.type === "text" && b.text?.trim())
16
+ );
17
+ }
18
+ const callKey = (id, name) => JSON.stringify([id, name]);
19
+ export class Sources {
20
+ byRef = new Map();
21
+ ambiguous = new Set();
22
+ byBody = new Map();
23
+ exchanges = new Map();
24
+ owners = new Map();
25
+ tail;
26
+ associated;
27
+ entries = [];
28
+ activeIds;
29
+ constructor(entries, state, activeIds) {
30
+ this.refresh(entries, state, activeIds);
31
+ }
32
+ refresh(entries, state, activeIds) {
33
+ const prefix = this.entries.length <= entries.length && this.entries.every((e, i) => e === entries[i]);
34
+ const start = prefix ? this.entries.length : 0;
35
+ this.associated = undefined;
36
+ if (!prefix) {
37
+ this.byRef.clear();
38
+ this.byBody.clear();
39
+ this.exchanges.clear();
40
+ this.owners.clear();
41
+ this.tail = undefined;
42
+ }
43
+ this.activeIds = activeIds;
44
+ for (const entry of entries.slice(start)) {
45
+ const messages = sessionEntryToContextMessages(entry);
46
+ if (messages.length !== 1 || ["compaction", "branch_summary"].includes(entry.type)) continue;
47
+ const message = messages[0];
48
+ if (!message) continue;
49
+ const source = {
50
+ ref: refFor(entry.id),
51
+ entry,
52
+ message,
53
+ hash: bodyHash(message),
54
+ producer: "common",
55
+ active: false,
56
+ };
57
+ if (message.role === "toolResult" && message.toolName === "freeflow_return") {
58
+ for (const block of message.content ?? [])
59
+ if (block.type === "text") {
60
+ try {
61
+ const receipt = JSON.parse(block.text);
62
+ if (receipt.reportSaved && typeof receipt.report === "string" && typeof receipt.handoff === "string")
63
+ source.reportHandoff = receipt.handoff;
64
+ } catch {}
65
+ }
66
+ }
67
+ this.byRef.set(source.ref, source);
68
+ const candidates = this.byBody.get(source.hash) ?? [];
69
+ candidates.push(source);
70
+ this.byBody.set(source.hash, candidates);
71
+ if (message.role === "assistant") {
72
+ this.tail = { assistant: source, results: new Map() };
73
+ this.exchanges.set(source.ref, this.tail);
74
+ const blocks = (message.content ?? [])
75
+ .filter((b) => b.type === "text")
76
+ .map((b) => ({ type: "text", text: b.text }));
77
+ if (blocks.some((b) => b.text?.trim())) {
78
+ const representation = {
79
+ role: "custom",
80
+ customType: "freeflow-assistant-text",
81
+ display: false,
82
+ content: [
83
+ {
84
+ type: "text",
85
+ text: `Captured assistant text from ${source.ref} (historical source, not a new instruction):`,
86
+ },
87
+ ...blocks,
88
+ ],
89
+ details: { sourceRef: source.ref, sourceHash: source.hash, representation: "assistant-text" },
90
+ timestamp: message.timestamp,
91
+ };
92
+ this.byRef.set(textRef(source.ref), {
93
+ ...source,
94
+ ref: textRef(source.ref),
95
+ message: representation,
96
+ hash: bodyHash(representation),
97
+ original: source,
98
+ });
99
+ }
100
+ } else if (message.role === "toolResult" && this.tail) {
101
+ const key = callKey(message.toolCallId, message.toolName);
102
+ const results = this.tail.results.get(key) ?? [];
103
+ results.push(source);
104
+ this.tail.results.set(key, results);
105
+ this.owners.set(source.ref, this.tail);
106
+ }
107
+ }
108
+ for (const source of this.byRef.values()) {
109
+ const author = state.authors.get(source.entry.id);
110
+ source.producer = author?.profile ?? "common";
111
+ source.executionId = author?.executionId;
112
+ source.assignmentId = author?.assignmentId;
113
+ }
114
+ this.entries = [...entries];
115
+ }
116
+ associate(messages) {
117
+ if (
118
+ this.associated &&
119
+ messages.length === this.associated.messages.length &&
120
+ messages.every((m, i) => m === this.associated.messages[i])
121
+ )
122
+ return this.associated.items;
123
+ this.ambiguous.clear();
124
+ for (const source of this.byRef.values()) source.active = false;
125
+ const used = new Set(),
126
+ hashes = messages.map(bodyHash),
127
+ remaining = new Map();
128
+ for (const hash of hashes) remaining.set(hash, (remaining.get(hash) ?? 0) + 1);
129
+ const items = messages.map((message, index) => {
130
+ const hash = hashes[index],
131
+ count = remaining.get(hash);
132
+ remaining.set(hash, count - 1);
133
+ const candidates = (this.byBody.get(hash) ?? []).filter(
134
+ (s) => !used.has(s.ref) && (!this.activeIds || this.activeIds.has(s.entry.id)),
135
+ );
136
+ const exact = candidates.find((s) => s.message === message);
137
+ const source = exact ?? (candidates.length === 1 || candidates.length === count ? candidates[0] : undefined);
138
+ if (!source) {
139
+ for (const candidate of candidates) {
140
+ this.ambiguous.add(candidate.ref);
141
+ this.ambiguous.add(textRef(candidate.ref));
142
+ }
143
+ return { message };
144
+ }
145
+ used.add(source.ref);
146
+ source.active = true;
147
+ const text = this.byRef.get(textRef(source.ref));
148
+ if (text) text.active = true;
149
+ return { message, source };
150
+ });
151
+ this.associated = { messages: [...messages], items };
152
+ return items;
153
+ }
154
+ eligible(ref, state) {
155
+ const source = this.byRef.get(ref);
156
+ if (!idFor(ref) || !source)
157
+ return { ref, code: "source_unavailable", detail: "Exact source is unavailable on current native ancestry." };
158
+ const original = source.original ?? source;
159
+ if (bodyHash(original.message) !== original.hash || bodyHash(source.message) !== source.hash)
160
+ return {
161
+ ref,
162
+ code: "source_changed",
163
+ detail: "The captured source changed; reconcile evidence before delivery.",
164
+ };
165
+ if (source.producer !== "executor")
166
+ return { ref, code: "source_origin", detail: "Selection requires observed Executor attribution." };
167
+ if (state.exposure.get(original.ref) !== original.hash && state.exposure.get(source.ref) !== source.hash)
168
+ return {
169
+ ref,
170
+ code: "source_unexposed",
171
+ detail: "The captured body has not been fully exposed to Executor on this ancestry.",
172
+ };
173
+ return undefined;
174
+ }
175
+ selectionProblem(ref, state) {
176
+ const problem = this.eligible(ref, state);
177
+ if (problem) return problem;
178
+ const source = this.byRef.get(ref);
179
+ if (!isTaskEvidence(source))
180
+ return {
181
+ ref,
182
+ code: "routing_source",
183
+ detail: this.byRef.has(textRef(ref))
184
+ ? `Routing control messages are not task evidence. Use ${textRef(ref)} if its visible assistant text is the intended evidence.`
185
+ : "Routing control receipts are not task evidence; saved communication is delivered separately.",
186
+ };
187
+ return undefined;
188
+ }
189
+ exchange(source) {
190
+ if (source.original) return { sources: [source], problems: [] };
191
+ if (!["assistant", "toolResult"].includes(source.message.role)) return { sources: [source], problems: [] };
192
+ const group = source.message.role === "assistant" ? this.exchanges.get(source.ref) : this.owners.get(source.ref);
193
+ const calls = group?.assistant.message.content?.filter((b) => b.type === "toolCall") ?? [];
194
+ if (
195
+ !group ||
196
+ (source.message.role === "toolResult" &&
197
+ !calls.some((c) => c.id === source.message.toolCallId && c.name === source.message.toolName))
198
+ )
199
+ return {
200
+ sources: [],
201
+ problems: [
202
+ { ref: source.ref, code: "ambiguous_exchange", detail: "Result has no unique native assistant call." },
203
+ ],
204
+ };
205
+ const sources = [group.assistant],
206
+ problems = [];
207
+ if (new Set(calls.map((c) => c.id)).size !== calls.length)
208
+ problems.push({
209
+ ref: group.assistant.ref,
210
+ code: "duplicate_call_id",
211
+ detail: "One native assistant exchange repeats a tool-call identity.",
212
+ });
213
+ for (const call of calls) {
214
+ const results = group.results.get(callKey(call.id, call.name)) ?? [];
215
+ if (results.length !== 1)
216
+ problems.push({
217
+ ref: group.assistant.ref,
218
+ code: "incomplete_exchange",
219
+ detail: `Native call ${call.id} does not have exactly one captured result.`,
220
+ });
221
+ else sources.push(results[0]);
222
+ }
223
+ return { sources, problems };
224
+ }
225
+ ordered(sources) {
226
+ const ids = new Set([...sources].map((s) => s.ref));
227
+ return this.entries.flatMap((entry) =>
228
+ [this.byRef.get(refFor(entry.id)), this.byRef.get(textRef(refFor(entry.id)))].filter(
229
+ (s) => !!s && ids.has(s.ref),
230
+ ),
231
+ );
232
+ }
233
+ }
@@ -11,7 +11,6 @@ import {
11
11
  setSessionCoreOverride,
12
12
  } from "../runtime/runtime-context.js";
13
13
  import { PiSettingsComponent } from "./settings-tui.js";
14
- import { supportsPiSessionModelStateApi } from "../cognitive-routing/pi-session-control.js";
15
14
  import { isPiFlowHost } from "../runtime/runtime-identity.js";
16
15
  const DEFAULT_FREEFLOW_ENABLED = true;
17
16
  const DEFAULT_CONTEXT_VIRTUALIZATION_ENABLED = false;
@@ -102,7 +101,7 @@ function effectiveItemValue(item) {
102
101
  function formatCoreValue(value) {
103
102
  if (typeof value === "boolean") return booleanValue(value);
104
103
  if (isRecord(value) && typeof value.provider === "string" && typeof value.model === "string") {
105
- const effort = typeof value.thinkingLevel === "string" ? ` · ${value.thinkingLevel}` : "";
104
+ const effort = typeof value.thinking === "string" ? ` · ${value.thinking}` : "";
106
105
  return `${value.provider}/${value.model}${effort}`;
107
106
  }
108
107
  return String(value ?? "");
@@ -291,10 +290,10 @@ function sessionFreeflowItems(state, cognitiveRoutingController) {
291
290
  conversationHistoryItem.inactive = freeflowInactive;
292
291
  const cognitiveRoutingState = cognitiveRoutingController?.state();
293
292
  const cognitiveRoutingProfile =
294
- cognitiveRoutingState?.controlMode === "manual-standard"
295
- ? "standard"
296
- : cognitiveRoutingState?.controlMode === "manual-reasoning"
297
- ? "reasoning"
293
+ cognitiveRoutingState?.controlMode === "manual-executor"
294
+ ? "executor"
295
+ : cognitiveRoutingState?.controlMode === "manual-coordinator"
296
+ ? "coordinator"
298
297
  : "auto";
299
298
  const cognitiveRoutingItem = state.cognitiveRouting
300
299
  ? {
@@ -303,16 +302,16 @@ function sessionFreeflowItems(state, cognitiveRoutingController) {
303
302
  description: "Hold a profile manually or release the hold for automatic model control.",
304
303
  kind: "enum",
305
304
  value: cognitiveRoutingProfile,
306
- values: ["auto", "standard", "reasoning"],
305
+ values: ["auto", "executor", "coordinator"],
307
306
  valueLabels: {
308
307
  auto: "automatic",
309
- standard: "manual · standard",
310
- reasoning: "manual · reasoning",
308
+ executor: "manual · executor",
309
+ coordinator: "manual · coordinator",
311
310
  },
312
311
  valueDescriptions: {
313
- auto: "Release the manual hold without forcing a model transition.",
314
- standard: "Hold the standard profile until /freeflow profile auto.",
315
- reasoning: "Hold the reasoning profile until /freeflow profile auto.",
312
+ auto: "Release the manual hold and return to Coordinator reconciliation.",
313
+ executor: "Hold the executor profile until /freeflow profile auto.",
314
+ coordinator: "Hold the coordinator profile until /freeflow profile auto.",
316
315
  },
317
316
  inactive: freeflowInactive || !state.cognitiveRouting.effective || cognitiveRoutingController === undefined,
318
317
  runtimeInactive: state.cognitiveRouting.blockingReason?.code === "runtime_disabled",
@@ -363,8 +362,8 @@ function isCognitiveRoutingProfile(value) {
363
362
  isRecord(value) &&
364
363
  typeof value.provider === "string" &&
365
364
  typeof value.model === "string" &&
366
- typeof value.thinkingLevel === "string" &&
367
- COGNITIVE_ROUTING_THINKING_LEVELS.includes(value.thinkingLevel)
365
+ typeof value.thinking === "string" &&
366
+ COGNITIVE_ROUTING_THINKING_LEVELS.includes(value.thinking)
368
367
  );
369
368
  }
370
369
  function cognitiveRoutingThinkingLevels(model, registry) {
@@ -487,8 +486,7 @@ function createCognitiveRoutingProfileWizard(currentValue, models, allowInherit)
487
486
  }
488
487
  const model = selected;
489
488
  if (!model?.key || model.thinkingLevels.length === 0) return undefined;
490
- const currentEffort =
491
- currentProfile && currentModelKey === model.key ? currentProfile.thinkingLevel : undefined;
489
+ const currentEffort = currentProfile && currentModelKey === model.key ? currentProfile.thinking : undefined;
492
490
  return {
493
491
  title: `Choose effort · ${model.label}`,
494
492
  choices: model.thinkingLevels.map((level) => ({
@@ -514,7 +512,7 @@ function createCognitiveRoutingProfileWizard(currentValue, models, allowInherit)
514
512
  return {
515
513
  provider: model.provider,
516
514
  model: model.model,
517
- thinkingLevel: selectedValues[1],
515
+ thinking: selectedValues[1],
518
516
  };
519
517
  },
520
518
  };
@@ -550,90 +548,13 @@ function cognitiveRoutingProfileItem(options) {
550
548
  wizard: () => createCognitiveRoutingProfileWizard(value, models, options.scope === "local"),
551
549
  };
552
550
  }
553
- function cognitiveRoutingSessionStartItem(options) {
554
- const path = ["cognitiveRouting", "sessionStart", options.setting];
555
- const defaultValue = options.setting === "control" ? "automatic" : "reasoning";
556
- const values = options.setting === "control" ? ["automatic", "manual"] : ["standard", "reasoning"];
557
- const labels = Object.fromEntries(values.map((value) => [value, value]));
558
- const descriptions =
559
- options.setting === "control"
560
- ? {
561
- automatic: "Start the new session in Reasoning; automatic delegation may use Standard during execution.",
562
- manual: "Hold the selected profile for the new session until the user changes it.",
563
- }
564
- : {
565
- standard: "Start a new manual session with the configured Standard preset.",
566
- reasoning: "Start a new manual session with the configured Reasoning preset.",
567
- };
568
- const repositoryValue = getPath(options.rawConfig, path);
569
- const localValue = getPath(options.localConfig, path);
570
- const isValidValue = (value) => values.includes(value);
571
- const repositorySetting = isValidValue(repositoryValue) ? repositoryValue : undefined;
572
- const localSetting = isValidValue(localValue) ? localValue : undefined;
573
- const inheritedValue = repositorySetting ?? defaultValue;
574
- const inheritedSource = repositorySetting ? "repository" : "builtin";
575
- const configuredSource =
576
- options.capabilityState?.sessionStartSources?.[options.setting] ?? (repositorySetting ? "repository" : undefined);
577
- const effectiveSessionStartControl =
578
- options.capabilityState?.sessionStart?.control ??
579
- getPath(options.localConfig, ["cognitiveRouting", "sessionStart", "control"]) ??
580
- getPath(options.rawConfig, ["cognitiveRouting", "sessionStart", "control"]) ??
581
- "automatic";
582
- const effectiveValue =
583
- options.setting === "profile" && effectiveSessionStartControl === "automatic"
584
- ? "reasoning"
585
- : (options.capabilityState?.sessionStart?.[options.setting] ?? inheritedValue);
586
- const effectiveSource = cognitiveRoutingSettingsSource(configuredSource);
587
- const label = options.setting === "control" ? "Session start control" : "Manual session-start profile";
588
- const description =
589
- options.setting === "control"
590
- ? "Choose automatic or manual control for new sessions only; changing this does not alter the active session."
591
- : "Choose the profile for manual session starts; automatic sessions always start in Reasoning.";
592
- const item =
593
- options.scope === "local"
594
- ? {
595
- id: `freeflow.cognitiveRouting.sessionStart.${options.setting}`,
596
- label,
597
- description: `${description} Choose inherit to use the repository value; use /freeflow settings repo to edit shared defaults.`,
598
- path,
599
- kind: "enum",
600
- value: localSetting ?? LOCAL_INHERIT,
601
- values: [LOCAL_INHERIT, ...values],
602
- valueLabels: { inherit: "Inherit repository", ...labels },
603
- valueDescriptions: {
604
- inherit: `Use ${inheritedValue} from ${inheritedSource}.`,
605
- ...descriptions,
606
- },
607
- format: (value) => String(value),
608
- configScope: "local",
609
- configValues: { inherit: undefined },
610
- effectiveValue,
611
- effectiveSource,
612
- inheritedValue,
613
- inheritedSource,
614
- }
615
- : {
616
- id: `freeflow.cognitiveRouting.sessionStart.${options.setting}`,
617
- label,
618
- description: `${description} This edits shared .freeflow/config.json.`,
619
- path,
620
- kind: "enum",
621
- value: inheritedValue,
622
- values,
623
- valueLabels: labels,
624
- valueDescriptions: descriptions,
625
- format: (value) => String(value),
626
- defaultValue,
627
- configScope: "repository",
628
- effectiveValue,
629
- effectiveSource,
630
- localOverrideValue: localSetting,
631
- };
632
- item.displaySuffix = coreDisplaySuffix(item);
633
- return item;
634
- }
635
551
  function isCognitiveRoutingRuntimeAvailable(pi) {
636
- return isPiFlowHost(pi?.host) || supportsPiSessionModelStateApi(pi);
552
+ return (
553
+ !isPiFlowHost(pi?.host) &&
554
+ typeof pi?.appendEntry === "function" &&
555
+ typeof pi?.setModel === "function" &&
556
+ typeof pi?.setThinkingLevel === "function"
557
+ );
637
558
  }
638
559
  function freeflowItems(rawConfig, options = {}) {
639
560
  const scope = options.scope ?? "repository";
@@ -689,7 +610,7 @@ function freeflowItems(rawConfig, options = {}) {
689
610
  label: "Enabled",
690
611
  description: cognitiveRoutingRuntimeDisabled
691
612
  ? "Requires a host model-state control API; configuration is read-only on this host."
692
- : "Allow Cognitive Routing to own the session model lease and switch the configured profiles.",
613
+ : "Allow Cognitive Routing to manage explicit assignments and request native profile transitions.",
693
614
  path: ["cognitiveRouting", "enabled"],
694
615
  effectiveValue: cognitiveRoutingState?.enabled ?? false,
695
616
  effectiveSource: cognitiveRoutingSettingsSource(cognitiveRoutingState?.enabledSource),
@@ -697,7 +618,22 @@ function freeflowItems(rawConfig, options = {}) {
697
618
  });
698
619
  cognitiveRoutingEnabledItem.inactive = freeflowInactive || cognitiveRoutingRuntimeDisabled;
699
620
  cognitiveRoutingEnabledItem.runtimeInactive = cognitiveRoutingRuntimeDisabled;
700
- const cognitiveRoutingProfiles = ["standard", "reasoning"].map((name) =>
621
+ const cognitiveRoutingProjectionItem = createScopedBooleanItem({
622
+ scope,
623
+ rawConfig,
624
+ localConfig,
625
+ id: "freeflow.cognitiveRouting.projection",
626
+ label: "Context projection",
627
+ description:
628
+ "Select Executor evidence for Coordinator; disable projection to keep ordinary Pi context in both profiles.",
629
+ path: ["cognitiveRouting", "projection"],
630
+ effectiveValue: cognitiveRoutingState?.projection ?? false,
631
+ effectiveSource: cognitiveRoutingSettingsSource(cognitiveRoutingState?.projectionSource),
632
+ defaultValue: false,
633
+ });
634
+ cognitiveRoutingProjectionItem.inactive = freeflowInactive || cognitiveRoutingRuntimeDisabled;
635
+ cognitiveRoutingProjectionItem.runtimeInactive = cognitiveRoutingRuntimeDisabled;
636
+ const cognitiveRoutingProfiles = ["executor", "coordinator"].map((name) =>
701
637
  cognitiveRoutingProfileItem({
702
638
  name: name,
703
639
  scope,
@@ -707,16 +643,7 @@ function freeflowItems(rawConfig, options = {}) {
707
643
  ctx: options.ctx,
708
644
  }),
709
645
  );
710
- const cognitiveRoutingSessionStart = ["control", "profile"].map((setting) =>
711
- cognitiveRoutingSessionStartItem({
712
- setting,
713
- scope,
714
- rawConfig,
715
- localConfig,
716
- capabilityState: cognitiveRoutingState,
717
- }),
718
- );
719
- for (const item of [...cognitiveRoutingProfiles, ...cognitiveRoutingSessionStart]) {
646
+ for (const item of cognitiveRoutingProfiles) {
720
647
  item.inactive ||= freeflowInactive || cognitiveRoutingRuntimeDisabled;
721
648
  item.runtimeInactive = cognitiveRoutingRuntimeDisabled;
722
649
  }
@@ -734,12 +661,12 @@ function freeflowItems(rawConfig, options = {}) {
734
661
  label: "Cognitive Routing",
735
662
  description: cognitiveRoutingRuntimeDisabled
736
663
  ? "Cognitive Routing configuration is visible for inspection but requires a host model-state control API."
737
- : "Configure the Standard and Reasoning profiles and choose whether Freeflow may manage model state for this repository.",
664
+ : "Configure the Executor and Coordinator profiles and choose whether Freeflow may manage model state for this repository.",
738
665
  kind: "group",
739
666
  value: cognitiveRoutingRuntimeDisabled ? false : (cognitiveRoutingState?.enabled ?? false),
740
667
  inactive: freeflowInactive,
741
668
  displaySuffix: cognitiveRoutingStatus,
742
- children: [cognitiveRoutingEnabledItem, ...cognitiveRoutingProfiles, ...cognitiveRoutingSessionStart],
669
+ children: [cognitiveRoutingEnabledItem, ...cognitiveRoutingProfiles, cognitiveRoutingProjectionItem],
743
670
  };
744
671
  })();
745
672
  return [
@@ -834,7 +761,7 @@ async function updateConfig(cwd, item, value, scope = item.configScope ?? "repos
834
761
  await writeFile(join(cwd, ".freeflow/config.json"), `${JSON.stringify(next, null, 2)}\n`, "utf8");
835
762
  }
836
763
  function isCognitiveRoutingProfileItem(item) {
837
- return item.id === "freeflow.cognitiveRouting.standard" || item.id === "freeflow.cognitiveRouting.reasoning";
764
+ return item.id === "freeflow.cognitiveRouting.executor" || item.id === "freeflow.cognitiveRouting.coordinator";
838
765
  }
839
766
  function valueForDisplay(item) {
840
767
  let value;
@@ -875,8 +802,8 @@ function refreshSettingsDerivedState(items) {
875
802
  const cognitiveRoutingGroup = findSettingsItem(items, "freeflow.cognitiveRouting");
876
803
  const cognitiveRoutingEnabledItem = findSettingsItem(items, "freeflow.cognitiveRouting.enabled");
877
804
  const cognitiveRoutingProfiles = [
878
- findSettingsItem(items, "freeflow.cognitiveRouting.standard"),
879
- findSettingsItem(items, "freeflow.cognitiveRouting.reasoning"),
805
+ findSettingsItem(items, "freeflow.cognitiveRouting.executor"),
806
+ findSettingsItem(items, "freeflow.cognitiveRouting.coordinator"),
880
807
  ];
881
808
  if (cognitiveRoutingGroup && cognitiveRoutingEnabledItem) {
882
809
  const enabled = effectiveItemValue(cognitiveRoutingEnabledItem) === true;
@@ -0,0 +1,42 @@
1
+ // A local planning estimate, not a provider token count or an output allocation.
2
+ // In particular, base64 and native usage/metadata are not model text.
3
+ export function estimateRequest(systemPrompt: string, tools: readonly any[], messages: readonly any[], model: any) {
4
+ const text = (value: string) => Math.ceil(value.length / 3);
5
+ const content = (value: any): number =>
6
+ typeof value === "string"
7
+ ? text(value)
8
+ : Array.isArray(value)
9
+ ? value.reduce(
10
+ (sum, b) =>
11
+ sum +
12
+ (b.type === "image"
13
+ ? 1200
14
+ : b.type === "toolCall"
15
+ ? text(b.name + JSON.stringify(b.arguments ?? {}))
16
+ : b.type === "thinking"
17
+ ? text(b.thinking ?? "")
18
+ : text(b.text ?? "")),
19
+ 0,
20
+ )
21
+ : 0;
22
+ const estimatedTokens =
23
+ text(systemPrompt) +
24
+ text(JSON.stringify(tools.map((t) => ({ name: t.name, description: t.description, parameters: t.parameters })))) +
25
+ messages.reduce((sum, m) => sum + 8 + content(m.content), 0);
26
+ const window = model?.contextWindow ?? 0;
27
+ const outputReserve = Math.min(4096, Math.max(1, Math.floor(window / 10)));
28
+ const maximumInputTokens = Math.max(0, window - outputReserve);
29
+ const estimateMethod =
30
+ "Planning estimate: text characters / 3, 1200 tokens/image, schemas and message framing; output headroom is not the provider allocation";
31
+ const warnings =
32
+ estimatedTokens > maximumInputTokens
33
+ ? [
34
+ {
35
+ ref: "",
36
+ code: "budget_estimate",
37
+ detail: `Estimated input ${estimatedTokens} exceeds planning allowance ${maximumInputTokens}. Actual output allocation and image tokenization are provider-dependent; fit is unconfirmed. Use native compaction or revise explicitly if the provider rejects the request.`,
38
+ },
39
+ ]
40
+ : [];
41
+ return { estimatedTokens, maximumInputTokens, outputReserve, estimateMethod, warnings };
42
+ }