@hassangameryt/freeflow 0.7.2 → 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 (79) 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 +147 -127
  6. package/capabilities/cognitive-routing/SKILL.md +162 -93
  7. package/gemini-extension.json +1 -1
  8. package/package.json +1 -1
  9. package/pi-extension/dist/cognitive-routing-v2/config.js +35 -19
  10. package/pi-extension/dist/cognitive-routing-v2/projection.js +53 -44
  11. package/pi-extension/dist/cognitive-routing-v2/render.js +31 -10
  12. package/pi-extension/dist/cognitive-routing-v2/runtime.js +701 -105
  13. package/pi-extension/dist/cognitive-routing-v2/schemas.js +90 -24
  14. package/pi-extension/dist/cognitive-routing-v2/state.js +243 -27
  15. package/pi-extension/dist/cognitive-routing-v2/tools.js +19 -50
  16. package/pi-extension/dist/cognitive-routing-v2/types.js +5 -1
  17. package/pi-extension/dist/context-virtualization/runtime.js +29 -7
  18. package/pi-extension/dist/freeflow-context/tool.js +7 -6
  19. package/pi-extension/dist/index.js +36 -15
  20. package/pi-extension/dist/provider-support/astra/adapter.js +110 -0
  21. package/pi-extension/dist/provider-support/astra/history.js +125 -0
  22. package/pi-extension/dist/provider-support/astra/index.js +11 -0
  23. package/pi-extension/dist/provider-support/astra/session-state.js +114 -0
  24. package/pi-extension/dist/provider-support/index.js +4 -0
  25. package/pi-extension/dist/runtime/request-history.js +216 -0
  26. package/pi-extension/dist/runtime/runtime-context.js +29 -10
  27. package/pi-extension/dist/session-sources/events.js +22 -0
  28. package/pi-extension/dist/session-sources/provenance.js +32 -0
  29. package/pi-extension/dist/session-sources/sources.js +51 -7
  30. package/pi-extension/dist/settings/settings-ui.js +247 -47
  31. package/pi-extension/src/cognitive-routing-v2/config.ts +49 -19
  32. package/pi-extension/src/cognitive-routing-v2/projection.ts +53 -43
  33. package/pi-extension/src/cognitive-routing-v2/render.ts +31 -10
  34. package/pi-extension/src/cognitive-routing-v2/runtime.ts +712 -105
  35. package/pi-extension/src/cognitive-routing-v2/schemas.ts +95 -24
  36. package/pi-extension/src/cognitive-routing-v2/state.ts +244 -27
  37. package/pi-extension/src/cognitive-routing-v2/tools.ts +19 -50
  38. package/pi-extension/src/cognitive-routing-v2/types.ts +35 -3
  39. package/pi-extension/src/context-virtualization/runtime.ts +30 -8
  40. package/pi-extension/src/freeflow-context/tool.ts +7 -6
  41. package/pi-extension/src/index.ts +36 -14
  42. package/pi-extension/src/provider-support/astra/adapter.ts +108 -0
  43. package/pi-extension/src/provider-support/astra/history.ts +145 -0
  44. package/pi-extension/src/provider-support/astra/index.ts +12 -0
  45. package/pi-extension/src/provider-support/astra/session-state.ts +113 -0
  46. package/pi-extension/src/provider-support/index.ts +5 -0
  47. package/pi-extension/src/runtime/request-history.ts +243 -0
  48. package/pi-extension/src/runtime/runtime-context.ts +32 -10
  49. package/pi-extension/src/session-sources/events.ts +22 -0
  50. package/pi-extension/src/session-sources/provenance.ts +39 -0
  51. package/pi-extension/src/session-sources/sources.ts +77 -6
  52. package/pi-extension/src/settings/settings-ui.ts +276 -46
  53. package/pi-extension/tests/cache-reuse/history.test.js +117 -0
  54. package/pi-extension/tests/cache-reuse/native.test.js +91 -0
  55. package/pi-extension/tests/cache-reuse/settings.test.js +211 -0
  56. package/pi-extension/tests/cognitive-routing-v2/communication.test.js +94 -1
  57. package/pi-extension/tests/cognitive-routing-v2/delegation.test.js +557 -0
  58. package/pi-extension/tests/cognitive-routing-v2/native.test.js +34 -1
  59. package/pi-extension/tests/cognitive-routing-v2/persistence.test.js +55 -0
  60. package/pi-extension/tests/cognitive-routing-v2/recovery.test.js +800 -2
  61. package/pi-extension/tests/cognitive-routing-v2/render.test.js +15 -1
  62. package/pi-extension/tests/cognitive-routing-v2/runtime.test.js +273 -3
  63. package/pi-extension/tests/cognitive-routing-v2/state.test.js +312 -1
  64. package/pi-extension/tests/context-virtualization/extension.test.js +20 -10
  65. package/pi-extension/tests/context-virtualization/runtime.test.js +17 -3
  66. package/pi-extension/tests/fixtures/routing-native.js +4 -1
  67. package/pi-extension/tests/integration/mode-free-core.test.js +4 -3
  68. package/pi-extension/tests/integration/pi-extension.test.js +508 -29
  69. package/pi-extension/tests/integration/prompt-architecture.test.js +56 -40
  70. package/pi-extension/tests/integration/unaffected-regressions.test.js +3 -3
  71. package/pi-extension/tests/provider-support/astra-boundaries.test.js +75 -0
  72. package/pi-extension/tests/provider-support/astra.test.js +306 -0
  73. package/pi-extension/tests/provider-support/cli.test.js +92 -0
  74. package/pi-extension/tests/provider-support/native.test.js +183 -0
  75. package/plugin.json +1 -1
  76. package/runtime/prompts/cognitive-routing.md +4 -4
  77. package/skills/design-for-depth/SKILL.md +1 -1
  78. package/skills/design-for-depth/references/implementation-decisions.md +3 -3
  79. package/skills/discuss/SKILL.md +2 -2
@@ -8,6 +8,7 @@ import { promisify } from "node:util";
8
8
  import { SessionManager } from "@earendil-works/pi-coding-agent";
9
9
 
10
10
  import freeflowExtension from "../../dist/index.js";
11
+ import { handleFreeflowCommand } from "../../dist/settings/settings-ui.js";
11
12
  import {
12
13
  readCapabilityState,
13
14
  readFreeflowConfigLayers,
@@ -17,6 +18,7 @@ import {
17
18
  setSessionCoreOverride,
18
19
  } from "../../dist/runtime/runtime-context.js";
19
20
  import { PIFLOW_HOST } from "../fixtures/pi-host.js";
21
+ import { matches } from "../../dist/cognitive-routing-v2/schemas.js";
20
22
 
21
23
  const execFileAsync = promisify(execFile);
22
24
 
@@ -250,28 +252,74 @@ test("new routing tools and projection state follow the configured contract", as
250
252
  delegate.parameters.oneOf.map((branch) => branch.properties.operation.enum[0]),
251
253
  ["assign", "replace"],
252
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
+ );
253
260
  assert.deepEqual(
254
261
  returning.parameters.oneOf.map((branch) => branch.properties.operation.enum[0]),
255
- ["submit", "retry"],
262
+ ["submit", "supplement", "retry"],
256
263
  );
257
- assert.equal(loaded.activeToolNames().includes("freeflow_project"), scenario.exposesProjection);
264
+ assert.equal(loaded.activeToolNames().includes("freeflow_project"), true);
258
265
 
259
266
  const providerContext = await loaded.handlers.get("context")({ messages: [] }, ctx);
267
+ assert.match(providerContext.messages[0].content, /Delegation: `executor`/);
260
268
  assert.match(providerContext.messages[0].content, new RegExp("Projection: `" + scenario.expectedMode + "`"));
261
269
 
262
270
  await loaded.commands.find((command) => command.name === "freeflow").definition.handler("profile executor", ctx);
263
271
  const manualContext = await loaded.handlers.get("context")({ messages: [] }, ctx);
264
272
  assert.match(
265
- manualContext.messages[0].content,
273
+ manualContext.messages.findLast((message) => message.customType === "freeflow-runtime-state").content,
266
274
  new RegExp("Projection: `" + (scenario.projection ? "manual-bypass" : "disabled") + "`"),
267
275
  );
268
- assert.equal(loaded.activeToolNames().includes("freeflow_project"), false);
276
+ assert.equal(loaded.activeToolNames().includes("freeflow_project"), true);
269
277
  } finally {
270
278
  await rm(cwd, { recursive: true, force: true });
271
279
  }
272
280
  }
273
281
  });
274
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
+
275
323
  test("Pi registers the remaining Freeflow commands without mode controls or retired router tools", () => {
276
324
  const { commands, shortcuts, tools } = loadExtension();
277
325
  const commandNames = commands.map((command) => command.name);
@@ -330,9 +378,9 @@ test("PiFlow keeps Cognitive Routing unavailable while exposing its configuratio
330
378
  const ctx = context(cwd);
331
379
  await handlers.get("session_start")({ type: "session_start" }, ctx);
332
380
  assert.match(ctx.statuses.at(-1).value, /cognitive blocked ·/);
333
- assert.ok(!activeToolNames().some((name) => name.startsWith("freeflow_delegate") || name === "freeflow_return"));
381
+ assert.ok(activeToolNames().includes("freeflow_delegate"));
334
382
  const before = await handlers.get("before_agent_start")({ systemPrompt: "base" }, ctx);
335
- assert.doesNotMatch(before.systemPrompt, /Cognitive Routing/);
383
+ assert.match(before.systemPrompt, /guidance is dormant/);
336
384
  } finally {
337
385
  await rm(cwd, { recursive: true, force: true });
338
386
  }
@@ -366,7 +414,7 @@ test("normal Pi settings expose active Cognitive Routing configuration", async (
366
414
  settingsCtx.ui.custom = async (factory) => {
367
415
  const component = factory({ requestRender() {} }, testTheme, {}, () => {});
368
416
  const rootText = renderText(component);
369
- assert.match(rootText, /Cognitive Routing\s+enabled \(4\) active/);
417
+ assert.match(rootText, /Cognitive Routing\s+enabled \(6\) active/);
370
418
  assert.doesNotMatch(rootText, /PiFlow only/);
371
419
  return undefined;
372
420
  };
@@ -377,6 +425,180 @@ test("normal Pi settings expose active Cognitive Routing configuration", async (
377
425
  }
378
426
  });
379
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
+
380
602
  test("Pi statusline reports only dynamic Cognitive Routing and context state", () => {
381
603
  const ctx = context();
382
604
  setFreeflowStatus(
@@ -492,7 +714,8 @@ test("Pi describes the mode-free Freeflow argument surface and manual profile co
492
714
  ]);
493
715
  assert.deepEqual(freeflowCommand.definition.getArgumentCompletions("profile "), [
494
716
  { value: "profile coordinator", label: "coordinator", description: "Hold Coordinator manually" },
495
- { value: "profile executor", label: "executor", description: "Hold Executor 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" },
496
719
  { value: "profile auto", label: "auto", description: "Return to automatic Coordinator reconciliation" },
497
720
  { value: "profile history", label: "history", description: "Read routing observations" },
498
721
  ]);
@@ -510,11 +733,13 @@ test("Pi exposes 24 base skills without TDD, a mode skill, or compatibility alia
510
733
  try {
511
734
  const { handlers, commands, sentMessages, sentMessageOptions } = loadExtension();
512
735
  const resources = await handlers.get("resources_discover")({ cwd }, context(cwd));
513
- const skillNames = resources.skillPaths.map((path) => {
514
- const match = path.match(/[\\/]skills[\\/]([^\\/]+)\/SKILL\.md$/);
515
- assert.ok(match, `unexpected skill path: ${path}`);
516
- return match[1];
517
- });
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
+ });
518
743
  assert.equal(skillNames.length, 24);
519
744
  assert.ok(skillNames.includes("workflow"));
520
745
  assert.ok(!skillNames.includes("tdd"));
@@ -560,10 +785,11 @@ test("Pi keeps Freeflow inactive until repository activation exists", async () =
560
785
  try {
561
786
  const { handlers } = loadExtension();
562
787
  const resources = await handlers.get("resources_discover")({ cwd }, context(cwd));
563
- assert.equal(resources.skillPaths.length, 1);
564
- 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")));
565
790
  const result = await handlers.get("before_agent_start")({ systemPrompt: "base prompt" }, context(cwd));
566
- assert.equal(result.systemPrompt, "base prompt");
791
+ assert.ok(result.systemPrompt.startsWith("base prompt\n\n"));
792
+ assert.match(result.systemPrompt, /guidance is dormant/);
567
793
  } finally {
568
794
  await rm(cwd, { recursive: true, force: true });
569
795
  }
@@ -588,10 +814,11 @@ test("Pi treats invalid configuration as inactive", async () => {
588
814
  try {
589
815
  const { handlers, commands } = loadExtension();
590
816
  const resources = await handlers.get("resources_discover")({ cwd }, context(cwd));
591
- assert.equal(resources.skillPaths.length, 1);
592
- 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")));
593
819
  const result = await handlers.get("before_agent_start")({ systemPrompt: "base prompt" }, context(cwd));
594
- assert.equal(result.systemPrompt, "base prompt");
820
+ assert.ok(result.systemPrompt.startsWith("base prompt\n\n"));
821
+ assert.match(result.systemPrompt, /guidance is dormant/);
595
822
  const freeflowCommand = commands.find((command) => command.name === "freeflow");
596
823
  const statusCtx = context(cwd);
597
824
  await freeflowCommand.definition.handler("status", statusCtx);
@@ -686,8 +913,8 @@ test("Pi fails closed when an existing local override is invalid", async () => {
686
913
  assert.equal(state.enabled, false);
687
914
  const { handlers } = loadExtension();
688
915
  const resources = await handlers.get("resources_discover")({ cwd }, context(cwd));
689
- assert.equal(resources.skillPaths.length, 1);
690
- 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")));
691
918
  } finally {
692
919
  await rm(cwd, { recursive: true, force: true });
693
920
  }
@@ -706,22 +933,23 @@ test("Pi local enablement overrides the repository master switch", async () => {
706
933
  }
707
934
  });
708
935
 
709
- 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 () => {
710
937
  const cwd = await configuredRepo({ enabled: false });
711
938
  try {
712
939
  const { handlers, activeToolNames } = loadExtension();
713
940
  const ctx = context(cwd);
714
941
  const resources = await handlers.get("resources_discover")({ cwd }, ctx);
715
- assert.deepEqual(resources.skillPaths, []);
942
+ assert.equal(resources.skillPaths.length, 27);
716
943
  const result = await handlers.get("before_agent_start")({ systemPrompt: "base prompt" }, ctx);
717
- assert.equal(result.systemPrompt, "base prompt");
944
+ assert.ok(result.systemPrompt.startsWith("base prompt\n\n"));
945
+ assert.match(result.systemPrompt, /guidance is dormant/);
718
946
  const providerContext = await handlers.get("context")({ messages: [] }, ctx);
719
947
  assert.match(lastRuntimeState(providerContext.messages).content, /Freeflow: inactive/);
720
948
  assert.doesNotMatch(
721
949
  lastRuntimeState(providerContext.messages).content,
722
950
  /Default mode|Active mode|Interaction Contract|Skills/,
723
951
  );
724
- assert.ok(!activeToolNames().includes("freeflow_context"));
952
+ assert.ok(activeToolNames().includes("freeflow_context"));
725
953
  } finally {
726
954
  await rm(cwd, { recursive: true, force: true });
727
955
  }
@@ -875,7 +1103,7 @@ test("Pi Cognitive Routing settings preserve complete presets", async () => {
875
1103
  });
876
1104
  component.handleInput("\u001b[B");
877
1105
  component.handleInput("\r");
878
- component.handleInput("\u001b[B");
1106
+ for (let index = 0; index < 4; index++) component.handleInput("\u001b[B");
879
1107
  component.handleInput("\r");
880
1108
  component.handleInput("\r");
881
1109
  for (let index = 0; index < 1; index++) component.handleInput("\u001b[B");
@@ -897,6 +1125,256 @@ test("Pi Cognitive Routing settings preserve complete presets", async () => {
897
1125
  }
898
1126
  });
899
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
+
900
1378
  test("Pi filters persisted Workflow and Cognitive Routing bootstrap entries without creating them", async () => {
901
1379
  const cwd = await configuredRepo();
902
1380
  try {
@@ -919,7 +1397,7 @@ test("Pi filters persisted Workflow and Cognitive Routing bootstrap entries with
919
1397
  }
920
1398
  });
921
1399
 
922
- 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 () => {
923
1401
  const root = await mkdtemp(join(tmpdir(), "freeflow-pi-missing-prompt-"));
924
1402
  const cwd = await configuredRepo();
925
1403
  try {
@@ -933,9 +1411,10 @@ test("Pi preserves the host prompt when a mandatory prompt file is missing", asy
933
1411
  ).default;
934
1412
  const { handlers } = loadExtension(extension);
935
1413
  const before = await handlers.get("before_agent_start")({ systemPrompt: "base prompt" }, context(cwd));
936
- assert.equal(before.systemPrompt, "base prompt");
1414
+ assert.ok(before.systemPrompt.startsWith("base prompt\n\n"));
1415
+ assert.match(before.systemPrompt, /guidance is dormant/);
937
1416
  const resources = await handlers.get("resources_discover")({ cwd }, context(cwd));
938
- assert.deepEqual(resources.skillPaths, []);
1417
+ assert.equal(resources.skillPaths.length, 27);
939
1418
  } finally {
940
1419
  await rm(cwd, { recursive: true, force: true });
941
1420
  await rm(root, { recursive: true, force: true });