@hassangameryt/freeflow 0.7.1 → 0.7.3

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 (147) 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 +153 -128
  6. package/capabilities/cognitive-routing/SKILL.md +126 -274
  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 +136 -0
  12. package/pi-extension/dist/cognitive-routing-v2/projection.js +330 -0
  13. package/pi-extension/dist/cognitive-routing-v2/render.js +252 -0
  14. package/pi-extension/dist/cognitive-routing-v2/runtime.js +2181 -0
  15. package/pi-extension/dist/cognitive-routing-v2/schemas.js +173 -0
  16. package/pi-extension/dist/cognitive-routing-v2/state.js +828 -0
  17. package/pi-extension/dist/cognitive-routing-v2/tools.js +60 -0
  18. package/pi-extension/dist/cognitive-routing-v2/types.js +48 -0
  19. package/pi-extension/dist/context-virtualization/runtime.js +29 -7
  20. package/pi-extension/dist/freeflow-context/tool.js +12 -6
  21. package/pi-extension/dist/index.js +302 -626
  22. package/pi-extension/dist/provider-support/astra/adapter.js +110 -0
  23. package/pi-extension/dist/provider-support/astra/history.js +125 -0
  24. package/pi-extension/dist/provider-support/astra/index.js +11 -0
  25. package/pi-extension/dist/provider-support/astra/session-state.js +114 -0
  26. package/pi-extension/dist/provider-support/index.js +4 -0
  27. package/pi-extension/dist/runtime/request-history.js +216 -0
  28. package/pi-extension/dist/runtime/runtime-context.js +85 -16
  29. package/pi-extension/dist/session-sources/events.js +217 -0
  30. package/pi-extension/dist/session-sources/provenance.js +32 -0
  31. package/pi-extension/dist/session-sources/read-only-session.js +148 -0
  32. package/pi-extension/dist/session-sources/sources.js +277 -0
  33. package/pi-extension/dist/settings/settings-ui.js +274 -147
  34. package/pi-extension/src/cognitive-routing-v2/budget.ts +42 -0
  35. package/pi-extension/src/cognitive-routing-v2/config.ts +173 -0
  36. package/pi-extension/src/cognitive-routing-v2/projection.ts +375 -0
  37. package/pi-extension/src/cognitive-routing-v2/render.ts +277 -0
  38. package/pi-extension/src/cognitive-routing-v2/runtime.ts +2241 -0
  39. package/pi-extension/src/cognitive-routing-v2/schemas.ts +180 -0
  40. package/pi-extension/src/cognitive-routing-v2/state.ts +839 -0
  41. package/pi-extension/src/cognitive-routing-v2/tools.ts +62 -0
  42. package/pi-extension/src/cognitive-routing-v2/types.ts +254 -0
  43. package/pi-extension/src/context-virtualization/runtime.ts +30 -8
  44. package/pi-extension/src/freeflow-context/tool.ts +12 -6
  45. package/pi-extension/src/index.ts +308 -675
  46. package/pi-extension/src/provider-support/astra/adapter.ts +108 -0
  47. package/pi-extension/src/provider-support/astra/history.ts +145 -0
  48. package/pi-extension/src/provider-support/astra/index.ts +12 -0
  49. package/pi-extension/src/provider-support/astra/session-state.ts +113 -0
  50. package/pi-extension/src/provider-support/index.ts +5 -0
  51. package/pi-extension/src/runtime/request-history.ts +243 -0
  52. package/pi-extension/src/runtime/runtime-context.ts +100 -17
  53. package/pi-extension/src/session-sources/events.ts +230 -0
  54. package/pi-extension/src/session-sources/provenance.ts +39 -0
  55. package/pi-extension/src/session-sources/read-only-session.ts +170 -0
  56. package/pi-extension/src/session-sources/sources.ts +334 -0
  57. package/pi-extension/src/settings/settings-ui.ts +311 -158
  58. package/pi-extension/tests/cache-reuse/history.test.js +117 -0
  59. package/pi-extension/tests/cache-reuse/native.test.js +91 -0
  60. package/pi-extension/tests/cache-reuse/settings.test.js +211 -0
  61. package/pi-extension/tests/cognitive-routing-v2/communication.test.js +577 -0
  62. package/pi-extension/tests/cognitive-routing-v2/compaction-input.test.js +183 -0
  63. package/pi-extension/tests/cognitive-routing-v2/delegation.test.js +557 -0
  64. package/pi-extension/tests/cognitive-routing-v2/native.test.js +474 -0
  65. package/pi-extension/tests/cognitive-routing-v2/performance.test.js +87 -0
  66. package/pi-extension/tests/cognitive-routing-v2/persistence.test.js +300 -0
  67. package/pi-extension/tests/cognitive-routing-v2/projection.test.js +241 -0
  68. package/pi-extension/tests/cognitive-routing-v2/recovery.test.js +1034 -0
  69. package/pi-extension/tests/cognitive-routing-v2/render.test.js +244 -0
  70. package/pi-extension/tests/cognitive-routing-v2/runtime.test.js +423 -0
  71. package/pi-extension/tests/cognitive-routing-v2/stabilization.test.js +521 -0
  72. package/pi-extension/tests/cognitive-routing-v2/state.test.js +586 -0
  73. package/pi-extension/tests/context-virtualization/extension.test.js +33 -7
  74. package/pi-extension/tests/context-virtualization/runtime.test.js +17 -3
  75. package/pi-extension/tests/fixtures/routing-native.js +191 -0
  76. package/pi-extension/tests/integration/mode-free-core.test.js +6 -4
  77. package/pi-extension/tests/integration/pi-extension.test.js +655 -59
  78. package/pi-extension/tests/integration/prompt-architecture.test.js +127 -30
  79. package/pi-extension/tests/integration/unaffected-regressions.test.js +78 -11
  80. package/pi-extension/tests/provider-support/astra-boundaries.test.js +75 -0
  81. package/pi-extension/tests/provider-support/astra.test.js +306 -0
  82. package/pi-extension/tests/provider-support/cli.test.js +92 -0
  83. package/pi-extension/tests/provider-support/native.test.js +183 -0
  84. package/plugin.json +1 -1
  85. package/runtime/prompts/cognitive-routing.md +4 -5
  86. package/runtime/prompts/core.md +51 -45
  87. package/runtime/prompts/interaction-contract.md +5 -5
  88. package/skills/action-selection/SKILL.md +57 -97
  89. package/skills/design-for-depth/SKILL.md +58 -193
  90. package/skills/design-for-depth/references/implementation-decisions.md +76 -0
  91. package/skills/diagnose-failure/SKILL.md +64 -196
  92. package/skills/discuss/SKILL.md +64 -123
  93. package/skills/discuss/references/discussion-continuity.md +7 -3
  94. package/skills/execute-work/SKILL.md +69 -88
  95. package/skills/execute-work/references/execute-work-edges.md +9 -3
  96. package/skills/handoff/SKILL.md +44 -54
  97. package/skills/handoff/references/handoff-templates.md +15 -6
  98. package/skills/review-artifact/SKILL.md +80 -127
  99. package/skills/review-artifact/references/independent-artifact-reviewer-contract.md +1 -1
  100. package/skills/review-work/SKILL.md +65 -115
  101. package/skills/review-work/references/independent-work-reviewer-contract.md +1 -1
  102. package/skills/simplify-code/SKILL.md +47 -100
  103. package/skills/track-work/SKILL.md +128 -119
  104. package/skills/track-work/references/working-record-format.md +14 -2
  105. package/skills/verify-work/SKILL.md +58 -62
  106. package/skills/verify-work/references/integration-evidence.md +4 -2
  107. package/skills/verify-work/references/test-design.md +89 -0
  108. package/skills/workflow/SKILL.md +73 -119
  109. package/skills/write-plan/SKILL.md +46 -77
  110. package/skills/write-plan/references/plan-shapes.md +5 -5
  111. package/skills/write-spec/SKILL.md +51 -63
  112. package/skills/write-spec/references/artifact-standards.md +2 -2
  113. package/skills/write-spec/references/decision-records.md +3 -2
  114. package/skills/write-spec/references/spec-shapes.md +15 -0
  115. package/pi-extension/dist/cognitive-routing/commands.js +0 -96
  116. package/pi-extension/dist/cognitive-routing/config.js +0 -232
  117. package/pi-extension/dist/cognitive-routing/controller.js +0 -1292
  118. package/pi-extension/dist/cognitive-routing/history.js +0 -451
  119. package/pi-extension/dist/cognitive-routing/host.js +0 -188
  120. package/pi-extension/dist/cognitive-routing/pi-routing-session.js +0 -414
  121. package/pi-extension/dist/cognitive-routing/pi-session-control.js +0 -320
  122. package/pi-extension/dist/cognitive-routing/runtime.js +0 -43
  123. package/pi-extension/dist/cognitive-routing/tool.js +0 -212
  124. package/pi-extension/dist/cognitive-routing/types.js +0 -6
  125. package/pi-extension/src/cognitive-routing/commands.ts +0 -129
  126. package/pi-extension/src/cognitive-routing/config.ts +0 -303
  127. package/pi-extension/src/cognitive-routing/controller.ts +0 -1628
  128. package/pi-extension/src/cognitive-routing/history.ts +0 -650
  129. package/pi-extension/src/cognitive-routing/host.ts +0 -266
  130. package/pi-extension/src/cognitive-routing/pi-routing-session.ts +0 -538
  131. package/pi-extension/src/cognitive-routing/pi-session-control.ts +0 -444
  132. package/pi-extension/src/cognitive-routing/runtime.ts +0 -57
  133. package/pi-extension/src/cognitive-routing/tool.ts +0 -273
  134. package/pi-extension/src/cognitive-routing/types.ts +0 -89
  135. package/pi-extension/tests/cognitive-routing/commands.test.js +0 -134
  136. package/pi-extension/tests/cognitive-routing/controller.test.js +0 -1128
  137. package/pi-extension/tests/cognitive-routing/extension-lifecycle.test.js +0 -1142
  138. package/pi-extension/tests/cognitive-routing/history.test.js +0 -274
  139. package/pi-extension/tests/cognitive-routing/host-identity.test.js +0 -63
  140. package/pi-extension/tests/cognitive-routing/pi-normal-lifecycle.test.js +0 -434
  141. package/pi-extension/tests/cognitive-routing/pi-session-control.test.js +0 -174
  142. package/pi-extension/tests/cognitive-routing/pi-session-history.test.js +0 -91
  143. package/pi-extension/tests/cognitive-routing/profile-resolution.test.js +0 -279
  144. package/pi-extension/tests/cognitive-routing/tool.test.js +0 -230
  145. package/skills/tdd/SKILL.md +0 -143
  146. package/skills/tdd/references/test-design.md +0 -96
  147. /package/pi-extension/tests/{cognitive-routing/host-fixture.js → fixtures/pi-host.js} +0 -0
@@ -5,8 +5,10 @@ import { join } from "node:path";
5
5
  import { tmpdir } from "node:os";
6
6
  import test from "node:test";
7
7
  import { promisify } from "node:util";
8
+ import { SessionManager } from "@earendil-works/pi-coding-agent";
8
9
 
9
10
  import freeflowExtension from "../../dist/index.js";
11
+ import { handleFreeflowCommand } from "../../dist/settings/settings-ui.js";
10
12
  import {
11
13
  readCapabilityState,
12
14
  readFreeflowConfigLayers,
@@ -15,7 +17,8 @@ import {
15
17
  setFreeflowStatus,
16
18
  setSessionCoreOverride,
17
19
  } from "../../dist/runtime/runtime-context.js";
18
- import { PIFLOW_HOST } from "../cognitive-routing/host-fixture.js";
20
+ import { PIFLOW_HOST } from "../fixtures/pi-host.js";
21
+ import { matches } from "../../dist/cognitive-routing-v2/schemas.js";
19
22
 
20
23
  const execFileAsync = promisify(execFile);
21
24
 
@@ -153,7 +156,7 @@ test("keeps Runtime State before the latest user message during context refreshe
153
156
  const conversation = [
154
157
  { role: "user", content: "release 0.6.1" },
155
158
  { role: "assistant", content: "delegating" },
156
- { role: "toolResult", toolName: "freeflow_switch_profile", content: [] },
159
+ { role: "toolResult", toolName: "freeflow_delegate", content: [] },
157
160
  ];
158
161
  const first = await handlers.get("context")({ messages: conversation }, ctx);
159
162
  const firstUserIndex = first.messages.findIndex((message) => message.role === "user");
@@ -187,6 +190,136 @@ test("keeps Runtime State before the latest user message during context refreshe
187
190
  }
188
191
  });
189
192
 
193
+ test("new routing tools and projection state follow the configured contract", async () => {
194
+ const cases = [
195
+ { projection: false, expectedMode: "disabled", exposesProjection: false },
196
+ // Projection can be effective while the Coordinator correctly lacks Executor-only tools.
197
+ { projection: true, expectedMode: "enabled", exposesProjection: false },
198
+ ];
199
+
200
+ for (const scenario of cases) {
201
+ const cwd = await configuredRepo({
202
+ cognitiveRouting: {
203
+ enabled: true,
204
+ projection: scenario.projection,
205
+ profiles: {
206
+ coordinator: { provider: "test", model: "model-a", thinking: "low" },
207
+ executor: { provider: "test", model: "model-b", thinking: "high" },
208
+ },
209
+ },
210
+ });
211
+ try {
212
+ let liveContext;
213
+ const loaded = loadExtension(freeflowExtension, null, {
214
+ appendEntry(customType, data) {
215
+ liveContext.sessionManager.appendCustomEntry(customType, data);
216
+ },
217
+ async setModel(model) {
218
+ liveContext.model = model;
219
+ return true;
220
+ },
221
+ setThinkingLevel(level) {
222
+ liveContext.thinkingLevel = level;
223
+ },
224
+ });
225
+ const ctx = context(cwd);
226
+ liveContext = ctx;
227
+ ctx.model = { provider: "test", id: "return" };
228
+ ctx.thinkingLevel = "medium";
229
+ ctx.modelRegistry = cognitiveRoutingModelRegistry();
230
+ ctx.isIdle = () => true;
231
+ ctx.sessionManager = SessionManager.create(cwd, join(cwd, "sessions"));
232
+ // Reconciliation needs genuine persisted ancestry, not a fake mutable branch array.
233
+ ctx.sessionManager.appendMessage({
234
+ role: "assistant",
235
+ content: [{ type: "text", text: "fixture seed" }],
236
+ timestamp: 1,
237
+ stopReason: "stop",
238
+ });
239
+ await loaded.handlers.get("session_start")({ type: "session_start", reason: "startup" }, ctx);
240
+ await loaded.handlers.get("before_agent_start")({ systemPrompt: "base prompt" }, ctx);
241
+
242
+ const toolNames = loaded.tools.map((tool) => tool.name);
243
+ assert.deepEqual(
244
+ toolNames.filter((name) =>
245
+ ["freeflow_delegate", "freeflow_return", "freeflow_unit", "freeflow_project"].includes(name),
246
+ ),
247
+ ["freeflow_delegate", "freeflow_return", "freeflow_unit", "freeflow_project"],
248
+ );
249
+ const delegate = loaded.tools.find((tool) => tool.name === "freeflow_delegate");
250
+ const returning = loaded.tools.find((tool) => tool.name === "freeflow_return");
251
+ assert.deepEqual(
252
+ delegate.parameters.oneOf.map((branch) => branch.properties.operation.enum[0]),
253
+ ["assign", "replace"],
254
+ );
255
+ assert.deepEqual(delegate.parameters.properties.worker.enum, ["helper", "executor"]);
256
+ assert.equal(
257
+ delegate.parameters.oneOf.every((branch) => !branch.required.includes("worker")),
258
+ true,
259
+ );
260
+ assert.deepEqual(
261
+ returning.parameters.oneOf.map((branch) => branch.properties.operation.enum[0]),
262
+ ["submit", "supplement", "retry"],
263
+ );
264
+ assert.equal(loaded.activeToolNames().includes("freeflow_project"), true);
265
+
266
+ const providerContext = await loaded.handlers.get("context")({ messages: [] }, ctx);
267
+ assert.match(providerContext.messages[0].content, /Delegation: `executor`/);
268
+ assert.match(providerContext.messages[0].content, new RegExp("Projection: `" + scenario.expectedMode + "`"));
269
+
270
+ await loaded.commands.find((command) => command.name === "freeflow").definition.handler("profile executor", ctx);
271
+ const manualContext = await loaded.handlers.get("context")({ messages: [] }, ctx);
272
+ assert.match(
273
+ manualContext.messages.findLast((message) => message.customType === "freeflow-runtime-state").content,
274
+ new RegExp("Projection: `" + (scenario.projection ? "manual-bypass" : "disabled") + "`"),
275
+ );
276
+ assert.equal(loaded.activeToolNames().includes("freeflow_project"), true);
277
+ } finally {
278
+ await rm(cwd, { recursive: true, force: true });
279
+ }
280
+ }
281
+ });
282
+
283
+ test("Pi describes strict evidence-selection operation shapes", () => {
284
+ const { tools } = loadExtension();
285
+ const project = tools.find((tool) => tool.name === "freeflow_project");
286
+ assert.ok(project);
287
+ assert.deepEqual(
288
+ project.parameters.oneOf.map((branch) => branch.properties.operation.enum[0]),
289
+ ["inspect", "add", "remove"],
290
+ );
291
+ assert.deepEqual(Object.keys(project.parameters.oneOf[1].properties), ["operation", "refs"]);
292
+ assert.deepEqual(Object.keys(project.parameters.oneOf[2].properties), ["operation", "refs", "reason"]);
293
+ assert.match(project.description, /reason is remove-only/i);
294
+ assert.match(project.promptGuidelines.join(" "), /reason is required only for remove and invalid for add\/inspect/i);
295
+ assert.match(
296
+ project.promptGuidelines.join(" "),
297
+ /for add.*never add refs marked not offered.*for remove.*currently selected or unresolved/i,
298
+ );
299
+ assert.match(project.promptGuidelines.join(" "), /add exact eligible visible refs directly/i);
300
+ assert.doesNotMatch(project.promptGuidelines.join(" "), /inspect first/i);
301
+ assert.match(project.parameters.properties.reason.description, /only for remove/i);
302
+ assert.match(project.parameters.oneOf[1].properties.refs.description, /never add a ref marked not offered/i);
303
+ assert.match(
304
+ project.parameters.oneOf[2].properties.refs.description,
305
+ /currently selected or unresolved.*non-selectable ref may be removed/i,
306
+ );
307
+ assert.match(
308
+ project.parameters.properties.refs.description,
309
+ /For add.*eligible.*For remove.*currently selected or unresolved/i,
310
+ );
311
+
312
+ assert.equal(
313
+ matches({ operation: "add", refs: ["ctx:example"], reason: "selection explanation" }, project.parameters),
314
+ false,
315
+ );
316
+ assert.equal(matches({ operation: "add", refs: ["ctx:example"] }, project.parameters), true);
317
+ assert.equal(
318
+ matches({ operation: "remove", refs: ["ctx:example"], reason: "selection explanation" }, project.parameters),
319
+ true,
320
+ );
321
+ });
322
+
190
323
  test("Pi registers the remaining Freeflow commands without mode controls or retired router tools", () => {
191
324
  const { commands, shortcuts, tools } = loadExtension();
192
325
  const commandNames = commands.map((command) => command.name);
@@ -199,12 +332,14 @@ test("Pi registers the remaining Freeflow commands without mode controls or reti
199
332
  assert.ok(!commandNames.includes("discover"));
200
333
  assert.ok(!commandNames.includes("execute-plan"));
201
334
  assert.ok(!commandNames.includes("workflow"));
335
+ assert.deepEqual(shortcuts, []);
336
+ assert.ok(tools.some((tool) => tool.name === "freeflow_context"));
202
337
  assert.deepEqual(
203
- shortcuts.map(({ shortcut }) => shortcut),
204
- ["ctrl+shift+r", "ctrl+shift+a"],
338
+ toolNames.filter((name) => name.startsWith("freeflow_")),
339
+ ["freeflow_delegate", "freeflow_return", "freeflow_unit", "freeflow_project", "freeflow_context"],
205
340
  );
206
- assert.ok(tools.some((tool) => tool.name === "freeflow_context"));
207
- assert.ok(tools.some((tool) => tool.name === "freeflow_switch_profile"));
341
+ assert.ok(!toolNames.includes("freeflow_switch_profile"));
342
+ assert.ok(!toolNames.includes("freeflow_cognitive_routing_history"));
208
343
  assert.ok(freeflowCommand);
209
344
  assert.ok(!freeflowCommand.definition.getArgumentCompletions("").some((item) => item.value === "mode"));
210
345
  assert.deepEqual(freeflowCommand.definition.getArgumentCompletions("mode "), []);
@@ -213,22 +348,27 @@ test("Pi registers the remaining Freeflow commands without mode controls or reti
213
348
  );
214
349
  });
215
350
 
216
- test("unsupported Pi hosts keep Cognitive Routing unavailable while exposing its configuration", async () => {
351
+ test("PiFlow keeps Cognitive Routing unavailable while exposing its configuration", async () => {
217
352
  const cwd = await configuredRepo({
218
353
  cognitiveRouting: {
219
354
  enabled: true,
220
355
  profiles: {
221
- standard: { provider: "test", model: "standard", thinkingLevel: "high" },
222
- reasoning: { provider: "test", model: "reasoning", thinkingLevel: "max" },
356
+ coordinator: { provider: "test", model: "coordinator", thinking: "high" },
357
+ executor: { provider: "test", model: "executor", thinking: "max" },
223
358
  },
224
359
  },
225
360
  });
226
361
  try {
227
- const { commands, handlers, shortcuts, tools } = loadExtension(freeflowExtension, null);
362
+ const { commands, handlers, shortcuts, tools, activeToolNames } = loadExtension(freeflowExtension, PIFLOW_HOST);
228
363
  const freeflowCommand = commands.find((command) => command.name === "freeflow");
229
364
  assert.ok(freeflowCommand);
230
365
  assert.deepEqual(shortcuts, []);
366
+ assert.deepEqual(
367
+ tools.filter((tool) => tool.name.startsWith("freeflow_")).map((tool) => tool.name),
368
+ ["freeflow_delegate", "freeflow_return", "freeflow_unit", "freeflow_project", "freeflow_context"],
369
+ );
231
370
  assert.ok(!tools.some((tool) => tool.name === "freeflow_switch_profile"));
371
+ assert.ok(!tools.some((tool) => tool.name === "freeflow_cognitive_routing_history"));
232
372
  assert.ok(!freeflowCommand.definition.getArgumentCompletions("").some((item) => item.value === "profile"));
233
373
  const capabilityState = await readCapabilityState(cwd, undefined, undefined);
234
374
  assert.equal(capabilityState.cognitiveRouting.enabled, true);
@@ -237,9 +377,10 @@ test("unsupported Pi hosts keep Cognitive Routing unavailable while exposing its
237
377
 
238
378
  const ctx = context(cwd);
239
379
  await handlers.get("session_start")({ type: "session_start" }, ctx);
240
- assert.match(ctx.statuses.at(-1).value, /cognitive blocked · host_unsupported/);
380
+ assert.match(ctx.statuses.at(-1).value, /cognitive blocked ·/);
381
+ assert.ok(activeToolNames().includes("freeflow_delegate"));
241
382
  const before = await handlers.get("before_agent_start")({ systemPrompt: "base" }, ctx);
242
- assert.doesNotMatch(before.systemPrompt, /Cognitive Routing/);
383
+ assert.match(before.systemPrompt, /guidance is dormant/);
243
384
  } finally {
244
385
  await rm(cwd, { recursive: true, force: true });
245
386
  }
@@ -250,8 +391,8 @@ test("normal Pi settings expose active Cognitive Routing configuration", async (
250
391
  cognitiveRouting: {
251
392
  enabled: true,
252
393
  profiles: {
253
- standard: { provider: "test", model: "model-a", thinkingLevel: "low" },
254
- reasoning: { provider: "test", model: "model-b", thinkingLevel: "high" },
394
+ coordinator: { provider: "test", model: "model-a", thinking: "low" },
395
+ executor: { provider: "test", model: "model-b", thinking: "high" },
255
396
  },
256
397
  },
257
398
  });
@@ -273,7 +414,7 @@ test("normal Pi settings expose active Cognitive Routing configuration", async (
273
414
  settingsCtx.ui.custom = async (factory) => {
274
415
  const component = factory({ requestRender() {} }, testTheme, {}, () => {});
275
416
  const rootText = renderText(component);
276
- assert.match(rootText, /Cognitive Routing\s+enabled \(5\) active/);
417
+ assert.match(rootText, /Cognitive Routing\s+enabled \(6\) active/);
277
418
  assert.doesNotMatch(rootText, /PiFlow only/);
278
419
  return undefined;
279
420
  };
@@ -284,6 +425,180 @@ test("normal Pi settings expose active Cognitive Routing configuration", async (
284
425
  }
285
426
  });
286
427
 
428
+ test("Pi settings persist delegation mode without rewriting complete presets", async () => {
429
+ const initial = {
430
+ cognitiveRouting: {
431
+ enabled: true,
432
+ profiles: {
433
+ coordinator: { provider: "test", model: "model-a", thinking: "low" },
434
+ helper: { provider: "test", model: "model-b", thinking: "high" },
435
+ executor: { provider: "test", model: "model-b", thinking: "max" },
436
+ },
437
+ },
438
+ };
439
+ const cwd = await configuredRepo(initial);
440
+ try {
441
+ const { commands } = loadExtension(freeflowExtension, null, {
442
+ appendEntry() {},
443
+ async setModel() {
444
+ return true;
445
+ },
446
+ setThinkingLevel() {},
447
+ });
448
+ const command = commands.find((candidate) => candidate.name === "freeflow");
449
+ const settingsCtx = context(cwd);
450
+ settingsCtx.isIdle = () => true;
451
+ settingsCtx.modelRegistry = cognitiveRoutingModelRegistry();
452
+ settingsCtx.ui.custom = async (factory) => {
453
+ let result;
454
+ const component = factory({ requestRender() {} }, testTheme, {}, (value) => {
455
+ result = value;
456
+ });
457
+ component.handleInput("\u001b[B");
458
+ component.handleInput("\r");
459
+ component.handleInput("\u001b[B");
460
+ component.handleInput("\r");
461
+ component.handleInput("\u001b[B");
462
+ component.handleInput("\u001b[B");
463
+ component.handleInput("\r");
464
+ await component.waitForWrites();
465
+ component.handleInput("\u001b");
466
+ component.handleInput("\u001b");
467
+ return result;
468
+ };
469
+ await command.definition.handler("settings repo", settingsCtx);
470
+ const saved = JSON.parse(await readFile(join(cwd, ".freeflow/config.json"), "utf8"));
471
+ assert.equal(saved.cognitiveRouting.delegation, "both");
472
+ assert.deepEqual(saved.cognitiveRouting.profiles, initial.cognitiveRouting.profiles);
473
+ } finally {
474
+ await rm(cwd, { recursive: true, force: true });
475
+ }
476
+ });
477
+
478
+ test("personal delegation override and inherit preserve repository mode and other local presets", async () => {
479
+ const repository = {
480
+ cognitiveRouting: {
481
+ enabled: true,
482
+ delegation: "helper",
483
+ profiles: {
484
+ coordinator: { provider: "test", model: "model-a", thinking: "low" },
485
+ helper: { provider: "test", model: "model-b", thinking: "high" },
486
+ executor: { provider: "test", model: "model-b", thinking: "max" },
487
+ },
488
+ },
489
+ };
490
+ const local = {
491
+ contextVirtualization: true,
492
+ cognitiveRouting: {
493
+ profiles: {
494
+ helper: { provider: "test", model: "model-b", thinking: "medium" },
495
+ },
496
+ },
497
+ };
498
+ const cwd = await configuredRepo(repository);
499
+ const repositoryPath = join(cwd, ".freeflow/config.json");
500
+ const localPath = join(cwd, ".freeflow/local.json");
501
+ await writeFile(localPath, JSON.stringify(local, null, 2), "utf8");
502
+ try {
503
+ const { commands, pi } = loadExtension(freeflowExtension, null, {
504
+ appendEntry() {},
505
+ async setModel() {
506
+ return true;
507
+ },
508
+ setThinkingLevel() {},
509
+ });
510
+ const command = commands.find((candidate) => candidate.name === "freeflow");
511
+ const settingsCtx = context(cwd);
512
+ settingsCtx.isIdle = () => true;
513
+ settingsCtx.modelRegistry = cognitiveRoutingModelRegistry();
514
+
515
+ const chooseDelegation = async (direction, commit = true) => {
516
+ settingsCtx.ui.custom = async (factory) => {
517
+ let result;
518
+ const component = factory({ requestRender() {} }, testTheme, {}, (value) => {
519
+ result = value;
520
+ });
521
+ component.handleInput("\u001b[B");
522
+ component.handleInput("\r");
523
+ component.handleInput("\u001b[B");
524
+ component.handleInput("\r");
525
+ component.handleInput(direction);
526
+ component.handleInput(commit ? "\r" : "\u001b");
527
+ if (commit) await component.waitForWrites();
528
+ component.handleInput("\u001b");
529
+ component.handleInput("\u001b");
530
+ return result;
531
+ };
532
+ await command.definition.handler("settings", settingsCtx);
533
+ };
534
+
535
+ await chooseDelegation("\u001b[B");
536
+ let saved = JSON.parse(await readFile(localPath, "utf8"));
537
+ assert.equal(saved.cognitiveRouting.delegation, "executor");
538
+ assert.deepEqual(saved.cognitiveRouting.profiles, local.cognitiveRouting.profiles);
539
+ assert.deepEqual(JSON.parse(await readFile(repositoryPath, "utf8")), repository);
540
+ assert.equal((await readCapabilityState(cwd, settingsCtx, pi.host)).cognitiveRouting.delegation, "executor");
541
+
542
+ await chooseDelegation("\u001b[A");
543
+ saved = JSON.parse(await readFile(localPath, "utf8"));
544
+ assert.equal(saved.cognitiveRouting.delegation, undefined);
545
+ assert.deepEqual(saved.cognitiveRouting.profiles, local.cognitiveRouting.profiles);
546
+ assert.equal((await readCapabilityState(cwd, settingsCtx, pi.host)).cognitiveRouting.delegation, "helper");
547
+
548
+ const beforeCancel = await readFile(localPath, "utf8");
549
+ await chooseDelegation("\u001b[B", false);
550
+ assert.equal(await readFile(localPath, "utf8"), beforeCancel);
551
+ assert.deepEqual(JSON.parse(await readFile(repositoryPath, "utf8")), repository);
552
+ } finally {
553
+ await rm(cwd, { recursive: true, force: true });
554
+ }
555
+ });
556
+
557
+ test("both mode session settings expose Helper and both existing profile presets", async () => {
558
+ const cwd = await configuredRepo({
559
+ cognitiveRouting: {
560
+ enabled: true,
561
+ delegation: "both",
562
+ profiles: {
563
+ coordinator: { provider: "test", model: "model-a", thinking: "low" },
564
+ helper: { provider: "test", model: "model-b", thinking: "high" },
565
+ executor: { provider: "test", model: "model-b", thinking: "max" },
566
+ },
567
+ },
568
+ });
569
+ try {
570
+ const { commands } = loadExtension(freeflowExtension, null, {
571
+ appendEntry() {},
572
+ async setModel() {
573
+ return true;
574
+ },
575
+ setThinkingLevel() {},
576
+ });
577
+ const command = commands.find((candidate) => candidate.name === "freeflow");
578
+ const settingsCtx = context(cwd);
579
+ settingsCtx.isIdle = () => true;
580
+ settingsCtx.modelRegistry = cognitiveRoutingModelRegistry();
581
+ settingsCtx.ui.custom = async (factory) => {
582
+ let result;
583
+ const component = factory({ requestRender() {} }, testTheme, {}, (value) => {
584
+ result = value;
585
+ });
586
+ component.handleInput("\u001b[B");
587
+ component.handleInput("\u001b[B");
588
+ component.handleInput("\r");
589
+ const presets = component.render(180).join("\n");
590
+ assert.match(presets, /Coordinator preset/);
591
+ assert.match(presets, /Helper preset/);
592
+ assert.match(presets, /Executor preset/);
593
+ result = { changed: false };
594
+ return result;
595
+ };
596
+ await command.definition.handler("settings session", settingsCtx);
597
+ } finally {
598
+ await rm(cwd, { recursive: true, force: true });
599
+ }
600
+ });
601
+
287
602
  test("Pi statusline reports only dynamic Cognitive Routing and context state", () => {
288
603
  const ctx = context();
289
604
  setFreeflowStatus(
@@ -296,14 +611,14 @@ test("Pi statusline reports only dynamic Cognitive Routing and context state", (
296
611
  conversationHistory: { effective: false },
297
612
  cognitiveRouting: { enabled: true, effective: true, blockingReason: null },
298
613
  },
299
- { effective: true, activeProfile: "reasoning", controlMode: "automatic" },
614
+ { effective: true, activeProfile: "executor", controlMode: "automatic" },
300
615
  readyFreeflowContext,
301
616
  );
302
- assert.equal(ctx.statuses.at(-1).value, "freeflow: reasoning · automatic · context");
617
+ assert.equal(ctx.statuses.at(-1).value, "freeflow: executor · automatic · context");
303
618
  assert.doesNotMatch(ctx.statuses.at(-1).value, /interaction|workflow|mode|skills/i);
304
619
  });
305
620
 
306
- test("Pi statusline shows the configured Cognitive Routing profile while activation is pending", () => {
621
+ test("Pi statusline defaults pending activation to the Coordinator profile", () => {
307
622
  const ctx = context();
308
623
  setFreeflowStatus(
309
624
  ctx,
@@ -316,7 +631,6 @@ test("Pi statusline shows the configured Cognitive Routing profile while activat
316
631
  cognitiveRouting: {
317
632
  enabled: true,
318
633
  effective: true,
319
- sessionStart: { profile: "reasoning" },
320
634
  blockingReason: null,
321
635
  },
322
636
  },
@@ -324,7 +638,7 @@ test("Pi statusline shows the configured Cognitive Routing profile while activat
324
638
  readyFreeflowContext,
325
639
  { cognitiveRoutingStartupPending: true },
326
640
  );
327
- assert.equal(ctx.statuses.at(-1).value, "freeflow: reasoning · pending");
641
+ assert.equal(ctx.statuses.at(-1).value, "freeflow: coordinator · pending");
328
642
  });
329
643
 
330
644
  test("Pi statusline keeps an inactive Cognitive Routing runtime blocked", () => {
@@ -339,20 +653,20 @@ test("Pi statusline keeps an inactive Cognitive Routing runtime blocked", () =>
339
653
  conversationHistory: { effective: false },
340
654
  cognitiveRouting: { enabled: true, effective: true, blockingReason: null },
341
655
  },
342
- { effective: false, activeProfile: "standard", controlMode: "automatic" },
656
+ { effective: false, activeProfile: "coordinator", controlMode: "automatic" },
343
657
  readyFreeflowContext,
344
658
  { cognitiveRoutingStartupPending: true },
345
659
  );
346
660
  assert.equal(ctx.statuses.at(-1).value, "freeflow: cognitive blocked · runtime_inactive");
347
661
  });
348
662
 
349
- test("PiFlow empty configured sessions show a pending profile before first prompt", async () => {
663
+ test("PiFlow keeps configured routing unavailable before the first prompt", async () => {
350
664
  const cwd = await configuredRepo({
351
665
  cognitiveRouting: {
352
666
  enabled: true,
353
667
  profiles: {
354
- standard: { provider: "test", model: "model-a", thinkingLevel: "low" },
355
- reasoning: { provider: "test", model: "model-b", thinkingLevel: "high" },
668
+ coordinator: { provider: "test", model: "model-a", thinking: "low" },
669
+ executor: { provider: "test", model: "model-b", thinking: "high" },
356
670
  },
357
671
  },
358
672
  });
@@ -361,7 +675,7 @@ test("PiFlow empty configured sessions show a pending profile before first promp
361
675
  const ctx = context(cwd);
362
676
  ctx.modelRegistry = cognitiveRoutingModelRegistry();
363
677
  await handlers.get("session_start")({ type: "session_start", reason: "startup" }, ctx);
364
- assert.equal(ctx.statuses.at(-1).value, "freeflow: reasoning · automatic");
678
+ assert.match(ctx.statuses.at(-1).value, /cognitive blocked ·/);
365
679
  } finally {
366
680
  await rm(cwd, { recursive: true, force: true });
367
681
  }
@@ -377,8 +691,16 @@ test("Pi exposes bypass scope argument completions", () => {
377
691
  ]);
378
692
  });
379
693
 
380
- test("Pi describes the mode-free Freeflow argument surface", () => {
381
- const { commands } = loadExtension();
694
+ test("Pi describes the mode-free Freeflow argument surface and manual profile controls", () => {
695
+ const { commands } = loadExtension(
696
+ freeflowExtension,
697
+ {},
698
+ {
699
+ appendEntry() {},
700
+ setModel() {},
701
+ setThinkingLevel() {},
702
+ },
703
+ );
382
704
  const freeflowCommand = commands.find((command) => command.name === "freeflow");
383
705
  assert.ok(freeflowCommand);
384
706
  assert.deepEqual(freeflowCommand.definition.getArgumentCompletions(""), [
@@ -386,9 +708,17 @@ test("Pi describes the mode-free Freeflow argument surface", () => {
386
708
  { value: "status", label: "status", description: "Show effective Freeflow state" },
387
709
  { value: "context", label: "context", description: "Inspect Freeflow Context" },
388
710
  { value: "profile", label: "profile", description: "Hold or release Cognitive Routing profile control" },
711
+ { value: "resume", label: "resume", description: "Resume the current saved routing responsibility" },
389
712
  { value: "enable", label: "enable", description: "Enable Freeflow for this repository" },
390
713
  { value: "disable", label: "disable", description: "Disable Freeflow for this repository" },
391
714
  ]);
715
+ assert.deepEqual(freeflowCommand.definition.getArgumentCompletions("profile "), [
716
+ { value: "profile coordinator", label: "coordinator", description: "Hold Coordinator manually" },
717
+ { value: "profile helper", label: "helper", description: "Hold Helper manually when enabled" },
718
+ { value: "profile executor", label: "executor", description: "Hold Executor manually when enabled" },
719
+ { value: "profile auto", label: "auto", description: "Return to automatic Coordinator reconciliation" },
720
+ { value: "profile history", label: "history", description: "Read routing observations" },
721
+ ]);
392
722
  assert.deepEqual(freeflowCommand.definition.getArgumentCompletions("mode "), []);
393
723
  assert.deepEqual(freeflowCommand.definition.getArgumentCompletions("context "), [
394
724
  { value: "context status", label: "status", description: "Show Freeflow Context state" },
@@ -398,18 +728,21 @@ test("Pi describes the mode-free Freeflow argument surface", () => {
398
728
  ]);
399
729
  });
400
730
 
401
- test("Pi exposes 25 base skills without a mode skill or compatibility aliases", async () => {
731
+ test("Pi exposes 24 base skills without TDD, a mode skill, or compatibility aliases", async () => {
402
732
  const cwd = await configuredRepo();
403
733
  try {
404
734
  const { handlers, commands, sentMessages, sentMessageOptions } = loadExtension();
405
735
  const resources = await handlers.get("resources_discover")({ cwd }, context(cwd));
406
- const skillNames = resources.skillPaths.map((path) => {
407
- const match = path.match(/[\\/]skills[\\/]([^\\/]+)\/SKILL\.md$/);
408
- assert.ok(match, `unexpected skill path: ${path}`);
409
- return match[1];
410
- });
411
- assert.equal(skillNames.length, 25);
736
+ const skillNames = resources.skillPaths
737
+ .filter((path) => path.includes("/skills/"))
738
+ .map((path) => {
739
+ const match = path.match(/[\\/]skills[\\/]([^\\/]+)\/SKILL\.md$/);
740
+ assert.ok(match, `unexpected skill path: ${path}`);
741
+ return match[1];
742
+ });
743
+ assert.equal(skillNames.length, 24);
412
744
  assert.ok(skillNames.includes("workflow"));
745
+ assert.ok(!skillNames.includes("tdd"));
413
746
  assert.ok(!skillNames.includes("mode-contract"));
414
747
  assert.ok(!skillNames.includes("discover"));
415
748
  assert.ok(!skillNames.includes("execute-plan"));
@@ -452,10 +785,11 @@ test("Pi keeps Freeflow inactive until repository activation exists", async () =
452
785
  try {
453
786
  const { handlers } = loadExtension();
454
787
  const resources = await handlers.get("resources_discover")({ cwd }, context(cwd));
455
- assert.equal(resources.skillPaths.length, 1);
456
- assert.match(resources.skillPaths[0], /setup-freeflow\/SKILL\.md$/);
788
+ assert.equal(resources.skillPaths.length, 27);
789
+ assert.ok(resources.skillPaths.some((path) => path.endsWith("/skills/setup-freeflow/SKILL.md")));
457
790
  const result = await handlers.get("before_agent_start")({ systemPrompt: "base prompt" }, context(cwd));
458
- assert.equal(result.systemPrompt, "base prompt");
791
+ assert.ok(result.systemPrompt.startsWith("base prompt\n\n"));
792
+ assert.match(result.systemPrompt, /guidance is dormant/);
459
793
  } finally {
460
794
  await rm(cwd, { recursive: true, force: true });
461
795
  }
@@ -480,10 +814,11 @@ test("Pi treats invalid configuration as inactive", async () => {
480
814
  try {
481
815
  const { handlers, commands } = loadExtension();
482
816
  const resources = await handlers.get("resources_discover")({ cwd }, context(cwd));
483
- assert.equal(resources.skillPaths.length, 1);
484
- assert.match(resources.skillPaths[0], /setup-freeflow\/SKILL\.md$/);
817
+ assert.equal(resources.skillPaths.length, 27);
818
+ assert.ok(resources.skillPaths.some((path) => path.endsWith("/skills/setup-freeflow/SKILL.md")));
485
819
  const result = await handlers.get("before_agent_start")({ systemPrompt: "base prompt" }, context(cwd));
486
- assert.equal(result.systemPrompt, "base prompt");
820
+ assert.ok(result.systemPrompt.startsWith("base prompt\n\n"));
821
+ assert.match(result.systemPrompt, /guidance is dormant/);
487
822
  const freeflowCommand = commands.find((command) => command.name === "freeflow");
488
823
  const statusCtx = context(cwd);
489
824
  await freeflowCommand.definition.handler("status", statusCtx);
@@ -578,8 +913,8 @@ test("Pi fails closed when an existing local override is invalid", async () => {
578
913
  assert.equal(state.enabled, false);
579
914
  const { handlers } = loadExtension();
580
915
  const resources = await handlers.get("resources_discover")({ cwd }, context(cwd));
581
- assert.equal(resources.skillPaths.length, 1);
582
- assert.match(resources.skillPaths[0], /setup-freeflow\/SKILL\.md$/);
916
+ assert.equal(resources.skillPaths.length, 27);
917
+ assert.ok(resources.skillPaths.some((path) => path.endsWith("/skills/setup-freeflow/SKILL.md")));
583
918
  } finally {
584
919
  await rm(cwd, { recursive: true, force: true });
585
920
  }
@@ -598,22 +933,23 @@ test("Pi local enablement overrides the repository master switch", async () => {
598
933
  }
599
934
  });
600
935
 
601
- test("Pi master Freeflow toggle suppresses core prompts, skills, and capabilities", async () => {
936
+ test("Pi master Freeflow toggle makes features inactive while preserving their reference surface", async () => {
602
937
  const cwd = await configuredRepo({ enabled: false });
603
938
  try {
604
939
  const { handlers, activeToolNames } = loadExtension();
605
940
  const ctx = context(cwd);
606
941
  const resources = await handlers.get("resources_discover")({ cwd }, ctx);
607
- assert.deepEqual(resources.skillPaths, []);
942
+ assert.equal(resources.skillPaths.length, 27);
608
943
  const result = await handlers.get("before_agent_start")({ systemPrompt: "base prompt" }, ctx);
609
- assert.equal(result.systemPrompt, "base prompt");
944
+ assert.ok(result.systemPrompt.startsWith("base prompt\n\n"));
945
+ assert.match(result.systemPrompt, /guidance is dormant/);
610
946
  const providerContext = await handlers.get("context")({ messages: [] }, ctx);
611
947
  assert.match(lastRuntimeState(providerContext.messages).content, /Freeflow: inactive/);
612
948
  assert.doesNotMatch(
613
949
  lastRuntimeState(providerContext.messages).content,
614
950
  /Default mode|Active mode|Interaction Contract|Skills/,
615
951
  );
616
- assert.ok(!activeToolNames().includes("freeflow_context"));
952
+ assert.ok(activeToolNames().includes("freeflow_context"));
617
953
  } finally {
618
954
  await rm(cwd, { recursive: true, force: true });
619
955
  }
@@ -738,13 +1074,23 @@ test("Pi Cognitive Routing settings preserve complete presets", async () => {
738
1074
  cognitiveRouting: {
739
1075
  enabled: true,
740
1076
  profiles: {
741
- standard: { provider: "test", model: "model-a", thinkingLevel: "low" },
742
- reasoning: { provider: "test", model: "model-b", thinkingLevel: "high" },
1077
+ coordinator: { provider: "test", model: "model-a", thinking: "low" },
1078
+ executor: { provider: "test", model: "model-b", thinking: "high" },
743
1079
  },
744
1080
  },
745
1081
  });
746
1082
  try {
747
- const { commands } = loadExtension();
1083
+ const { commands } = loadExtension(
1084
+ freeflowExtension,
1085
+ {},
1086
+ {
1087
+ appendEntry() {},
1088
+ async setModel() {
1089
+ return true;
1090
+ },
1091
+ setThinkingLevel() {},
1092
+ },
1093
+ );
748
1094
  const freeflowCommand = commands.find((command) => command.name === "freeflow");
749
1095
  assert.ok(freeflowCommand);
750
1096
  const settingsCtx = context(cwd);
@@ -757,11 +1103,10 @@ test("Pi Cognitive Routing settings preserve complete presets", async () => {
757
1103
  });
758
1104
  component.handleInput("\u001b[B");
759
1105
  component.handleInput("\r");
760
- component.handleInput("\u001b[B");
1106
+ for (let index = 0; index < 4; index++) component.handleInput("\u001b[B");
761
1107
  component.handleInput("\r");
762
- component.handleInput("\u001b[B");
763
1108
  component.handleInput("\r");
764
- for (let index = 0; index < 4; index++) component.handleInput("\u001b[B");
1109
+ for (let index = 0; index < 1; index++) component.handleInput("\u001b[B");
765
1110
  component.handleInput("\r");
766
1111
  component.handleInput("\r");
767
1112
  await component.waitForWrites();
@@ -769,10 +1114,10 @@ test("Pi Cognitive Routing settings preserve complete presets", async () => {
769
1114
  };
770
1115
  await freeflowCommand.definition.handler("settings repo", settingsCtx);
771
1116
  const saved = JSON.parse(await readFile(join(cwd, ".freeflow/config.json"), "utf8"));
772
- assert.deepEqual(saved.cognitiveRouting.profiles.standard, {
1117
+ assert.deepEqual(saved.cognitiveRouting.profiles.executor, {
773
1118
  provider: "test",
774
1119
  model: "model-b",
775
- thinkingLevel: "max",
1120
+ thinking: "max",
776
1121
  });
777
1122
  assert.equal(settingsCtx.reloads.length, 1);
778
1123
  } finally {
@@ -780,6 +1125,256 @@ test("Pi Cognitive Routing settings preserve complete presets", async () => {
780
1125
  }
781
1126
  });
782
1127
 
1128
+ test("Pi session settings expose both routing preset wizards without changing config files", async () => {
1129
+ const cwd = await configuredRepo({
1130
+ cognitiveRouting: {
1131
+ enabled: true,
1132
+ profiles: {
1133
+ coordinator: { provider: "test", model: "model-a", thinking: "low" },
1134
+ executor: { provider: "test", model: "model-b", thinking: "high" },
1135
+ },
1136
+ },
1137
+ });
1138
+ try {
1139
+ const configPath = join(cwd, ".freeflow/config.json");
1140
+ const localPath = join(cwd, ".freeflow/local.json");
1141
+ const originalConfig = await readFile(configPath, "utf8");
1142
+ const originalLocal = await readFile(localPath, "utf8").catch(() => undefined);
1143
+ const { commands } = loadExtension(
1144
+ freeflowExtension,
1145
+ {},
1146
+ {
1147
+ async setModel() {
1148
+ return true;
1149
+ },
1150
+ setThinkingLevel() {},
1151
+ },
1152
+ );
1153
+ const freeflowCommand = commands.find((command) => command.name === "freeflow");
1154
+ assert.ok(freeflowCommand);
1155
+ const settingsCtx = context(cwd);
1156
+ settingsCtx.isIdle = () => true;
1157
+ settingsCtx.modelRegistry = cognitiveRoutingModelRegistry();
1158
+ settingsCtx.ui.custom = async (factory) => {
1159
+ let result;
1160
+ const component = factory({ requestRender() {} }, testTheme, {}, (value) => {
1161
+ result = value;
1162
+ });
1163
+ const rendered = component.render(180).join("\\n");
1164
+ assert.match(rendered, /Cognitive Routing presets/);
1165
+ component.handleInput("\u001b[B");
1166
+ component.handleInput("\u001b[B");
1167
+ component.handleInput("\r");
1168
+ const presetRendered = component.render(180).join("\\n");
1169
+ assert.match(presetRendered, /Coordinator preset/);
1170
+ assert.match(presetRendered, /Executor preset/);
1171
+ result = { changed: false };
1172
+ await component.waitForWrites();
1173
+ return result;
1174
+ };
1175
+ await freeflowCommand.definition.handler("settings session", settingsCtx);
1176
+ assert.equal(await readFile(configPath, "utf8"), originalConfig);
1177
+ assert.equal(await readFile(localPath, "utf8").catch(() => undefined), originalLocal);
1178
+ } finally {
1179
+ await rm(cwd, { recursive: true, force: true });
1180
+ }
1181
+ });
1182
+
1183
+ test("Pi session preset wizard applies a complete pair without writing config", async () => {
1184
+ const cwd = await configuredRepo({
1185
+ cognitiveRouting: {
1186
+ enabled: true,
1187
+ profiles: {
1188
+ coordinator: { provider: "test", model: "model-a", thinking: "low" },
1189
+ executor: { provider: "test", model: "model-b", thinking: "high" },
1190
+ },
1191
+ },
1192
+ });
1193
+ try {
1194
+ const configPath = join(cwd, ".freeflow/config.json");
1195
+ const originalConfig = await readFile(configPath, "utf8");
1196
+ const calls = [];
1197
+ const controller = {
1198
+ state: () => ({ effective: true, controlMode: "automatic", activeProfile: "coordinator" }),
1199
+ sessionProfileOverrides: () => ({}),
1200
+ setManualProfile: async () => ({ status: "active" }),
1201
+ setAutomaticControl: async () => ({ status: "automatic" }),
1202
+ setSessionProfileOverride: async (profile, override) => {
1203
+ calls.push({ profile, override });
1204
+ return { status: "active" };
1205
+ },
1206
+ resetSessionProfileOverrides: async () => ({ status: "unchanged" }),
1207
+ };
1208
+ const settingsCtx = context(cwd);
1209
+ settingsCtx.isIdle = () => true;
1210
+ settingsCtx.modelRegistry = cognitiveRoutingModelRegistry();
1211
+ const pi = {
1212
+ host: {},
1213
+ appendEntry() {},
1214
+ async setModel() {
1215
+ return true;
1216
+ },
1217
+ setThinkingLevel() {},
1218
+ };
1219
+ settingsCtx.ui.custom = async (factory) => {
1220
+ let finish;
1221
+ const component = factory({ requestRender() {} }, testTheme, {}, (value) => {
1222
+ finish = value;
1223
+ });
1224
+ component.handleInput("\u001b[B");
1225
+ component.handleInput("\u001b[B");
1226
+ component.handleInput("\r");
1227
+ component.handleInput("\r");
1228
+ component.handleInput("\u001b[B");
1229
+ component.handleInput("\r");
1230
+ component.handleInput("\u001b[B");
1231
+ component.handleInput("\r");
1232
+ component.handleInput("\r");
1233
+ await component.waitForWrites();
1234
+ finish?.({ changed: false });
1235
+ return { changed: false };
1236
+ };
1237
+ await handleFreeflowCommand("settings session", settingsCtx, async () => {}, pi, controller);
1238
+ assert.deepEqual(calls, [
1239
+ {
1240
+ profile: "coordinator",
1241
+ override: { provider: "test", modelId: "model-a", thinking: "low" },
1242
+ },
1243
+ ]);
1244
+ assert.equal(await readFile(configPath, "utf8"), originalConfig);
1245
+ } finally {
1246
+ await rm(cwd, { recursive: true, force: true });
1247
+ }
1248
+ });
1249
+
1250
+ test("both mode session wizard applies a Helper pair without writing config", async () => {
1251
+ const cwd = await configuredRepo({
1252
+ cognitiveRouting: {
1253
+ enabled: true,
1254
+ delegation: "both",
1255
+ profiles: {
1256
+ coordinator: { provider: "test", model: "model-a", thinking: "low" },
1257
+ helper: { provider: "test", model: "model-b", thinking: "high" },
1258
+ executor: { provider: "test", model: "model-b", thinking: "max" },
1259
+ },
1260
+ },
1261
+ });
1262
+ try {
1263
+ const configPath = join(cwd, ".freeflow/config.json");
1264
+ const originalConfig = await readFile(configPath, "utf8");
1265
+ const calls = [];
1266
+ const controller = {
1267
+ state: () => ({ effective: true, controlMode: "automatic", activeProfile: "coordinator" }),
1268
+ sessionProfileOverrides: () => ({}),
1269
+ setManualProfile: async () => ({ status: "active" }),
1270
+ setAutomaticControl: async () => ({ status: "automatic" }),
1271
+ setSessionProfileOverride: async (profile, override) => {
1272
+ calls.push({ profile, override });
1273
+ return { status: "stored" };
1274
+ },
1275
+ resetSessionProfileOverrides: async () => ({ status: "unchanged" }),
1276
+ };
1277
+ const settingsCtx = context(cwd);
1278
+ settingsCtx.isIdle = () => true;
1279
+ settingsCtx.modelRegistry = cognitiveRoutingModelRegistry();
1280
+ const pi = {
1281
+ host: {},
1282
+ appendEntry() {},
1283
+ async setModel() {
1284
+ return true;
1285
+ },
1286
+ setThinkingLevel() {},
1287
+ };
1288
+ settingsCtx.ui.custom = async (factory) => {
1289
+ let finish;
1290
+ const component = factory({ requestRender() {} }, testTheme, {}, (value) => {
1291
+ finish = value;
1292
+ });
1293
+ component.handleInput("\u001b[B");
1294
+ component.handleInput("\u001b[B");
1295
+ component.handleInput("\r");
1296
+ component.handleInput("\u001b[B");
1297
+ component.handleInput("\r");
1298
+ component.handleInput("\u001b[B");
1299
+ component.handleInput("\r");
1300
+ component.handleInput("\u001b[B");
1301
+ component.handleInput("\r");
1302
+ component.handleInput("\r");
1303
+ await component.waitForWrites();
1304
+ finish?.({ changed: false });
1305
+ return { changed: false };
1306
+ };
1307
+ await handleFreeflowCommand("settings session", settingsCtx, async () => {}, pi, controller);
1308
+ assert.deepEqual(calls, [
1309
+ {
1310
+ profile: "helper",
1311
+ override: { provider: "test", modelId: "model-a", thinking: "low" },
1312
+ },
1313
+ ]);
1314
+ assert.equal(await readFile(configPath, "utf8"), originalConfig);
1315
+ } finally {
1316
+ await rm(cwd, { recursive: true, force: true });
1317
+ }
1318
+ });
1319
+
1320
+ test("session reset preserves core overrides when routing reset fails", async () => {
1321
+ const cwd = await configuredRepo({
1322
+ cognitiveRouting: {
1323
+ enabled: true,
1324
+ profiles: {
1325
+ coordinator: { provider: "test", model: "model-a", thinking: "low" },
1326
+ executor: { provider: "test", model: "model-b", thinking: "high" },
1327
+ },
1328
+ },
1329
+ });
1330
+ try {
1331
+ const configPath = join(cwd, ".freeflow/config.json");
1332
+ const originalConfig = await readFile(configPath, "utf8");
1333
+ const settingsCtx = context(cwd);
1334
+ settingsCtx.isIdle = () => true;
1335
+ settingsCtx.modelRegistry = cognitiveRoutingModelRegistry();
1336
+ const pi = {
1337
+ host: {},
1338
+ appendEntry() {},
1339
+ async setModel() {
1340
+ return true;
1341
+ },
1342
+ setThinkingLevel() {},
1343
+ };
1344
+ await setSessionCoreOverride("contextVirtualization", true, settingsCtx, pi);
1345
+ const controller = {
1346
+ state: () => ({ effective: true, controlMode: "automatic", activeProfile: "coordinator" }),
1347
+ sessionProfileOverrides: () => ({}),
1348
+ setManualProfile: async () => ({ status: "active" }),
1349
+ setAutomaticControl: async () => ({ status: "automatic" }),
1350
+ setSessionProfileOverride: async () => ({ status: "stored" }),
1351
+ resetSessionProfileOverrides: async () => ({ status: "blocked", reason: "fixture failure" }),
1352
+ };
1353
+ settingsCtx.ui.custom = async (factory) => {
1354
+ let finish;
1355
+ const component = factory({ requestRender() {} }, testTheme, {}, (value) => {
1356
+ finish = value;
1357
+ });
1358
+ component.handleInput("\u001b[B");
1359
+ component.handleInput("\u001b[B");
1360
+ component.handleInput("\u001b[B");
1361
+ component.handleInput("\r");
1362
+ component.handleInput("\r");
1363
+ await component.waitForWrites();
1364
+ finish?.({ changed: false });
1365
+ return { changed: false };
1366
+ };
1367
+ await handleFreeflowCommand("settings session", settingsCtx, async () => {}, pi, controller);
1368
+ const state = await readCapabilityState(cwd, settingsCtx, pi.host);
1369
+ assert.equal(state.contextVirtualization.effective, true);
1370
+ assert.equal(state.sessionOverrides.contextVirtualization, true);
1371
+ assert.equal(await readFile(configPath, "utf8"), originalConfig);
1372
+ assert.match(settingsCtx.notifications.at(-1)?.message ?? "", /fixture failure/);
1373
+ } finally {
1374
+ await rm(cwd, { recursive: true, force: true });
1375
+ }
1376
+ });
1377
+
783
1378
  test("Pi filters persisted Workflow and Cognitive Routing bootstrap entries without creating them", async () => {
784
1379
  const cwd = await configuredRepo();
785
1380
  try {
@@ -802,7 +1397,7 @@ test("Pi filters persisted Workflow and Cognitive Routing bootstrap entries with
802
1397
  }
803
1398
  });
804
1399
 
805
- test("Pi preserves the host prompt when a mandatory prompt file is missing", async () => {
1400
+ test("Pi preserves the host prompt prefix and dormant contract when a mandatory file is missing", async () => {
806
1401
  const root = await mkdtemp(join(tmpdir(), "freeflow-pi-missing-prompt-"));
807
1402
  const cwd = await configuredRepo();
808
1403
  try {
@@ -816,9 +1411,10 @@ test("Pi preserves the host prompt when a mandatory prompt file is missing", asy
816
1411
  ).default;
817
1412
  const { handlers } = loadExtension(extension);
818
1413
  const before = await handlers.get("before_agent_start")({ systemPrompt: "base prompt" }, context(cwd));
819
- assert.equal(before.systemPrompt, "base prompt");
1414
+ assert.ok(before.systemPrompt.startsWith("base prompt\n\n"));
1415
+ assert.match(before.systemPrompt, /guidance is dormant/);
820
1416
  const resources = await handlers.get("resources_discover")({ cwd }, context(cwd));
821
- assert.deepEqual(resources.skillPaths, []);
1417
+ assert.equal(resources.skillPaths.length, 27);
822
1418
  } finally {
823
1419
  await rm(cwd, { recursive: true, force: true });
824
1420
  await rm(root, { recursive: true, force: true });
@@ -827,8 +1423,8 @@ test("Pi preserves the host prompt when a mandatory prompt file is missing", asy
827
1423
 
828
1424
  function cognitiveRoutingModelRegistry() {
829
1425
  const models = [
830
- { provider: "test", id: "model-a", name: "Model A", reasoning: true },
831
- { provider: "test", id: "model-b", name: "Model B", reasoning: true },
1426
+ { provider: "test", id: "model-a", name: "Model A", reasoning: true, contextWindow: 128000 },
1427
+ { provider: "test", id: "model-b", name: "Model B", reasoning: true, contextWindow: 128000 },
832
1428
  ];
833
1429
  const supported = new Map([
834
1430
  ["model-a", new Set(["off", "low", "medium", "high"])],