@hassangameryt/freeflow 0.6.0 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (122) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +2 -2
  3. package/.codex-plugin/plugin.json +4 -5
  4. package/README.md +39 -55
  5. package/capabilities/cognitive-routing/SKILL.md +288 -130
  6. package/command-surface.json +0 -24
  7. package/hooks/freeflow-runtime-context.mjs +42 -477
  8. package/hooks/hooks.json +1 -26
  9. package/hooks/tests/check-runtime-context-hook.sh +108 -319
  10. package/package.json +4 -2
  11. package/pi-extension/dist/cognitive-routing/commands.js +3 -3
  12. package/pi-extension/dist/cognitive-routing/config.js +16 -20
  13. package/pi-extension/dist/cognitive-routing/controller.js +478 -48
  14. package/pi-extension/dist/cognitive-routing/history.js +12 -0
  15. package/pi-extension/dist/cognitive-routing/host.js +74 -12
  16. package/pi-extension/dist/cognitive-routing/pi-routing-session.js +414 -0
  17. package/pi-extension/dist/cognitive-routing/pi-session-control.js +320 -0
  18. package/pi-extension/dist/cognitive-routing/tool.js +1 -0
  19. package/pi-extension/dist/cognitive-routing/types.js +1 -1
  20. package/pi-extension/dist/index.js +236 -219
  21. package/pi-extension/dist/runtime/runtime-context.js +126 -286
  22. package/pi-extension/dist/settings/settings-ui.js +52 -336
  23. package/pi-extension/src/cognitive-routing/commands.ts +15 -5
  24. package/pi-extension/src/cognitive-routing/config.ts +16 -20
  25. package/pi-extension/src/cognitive-routing/controller.ts +561 -51
  26. package/pi-extension/src/cognitive-routing/history.ts +12 -0
  27. package/pi-extension/src/cognitive-routing/host.ts +110 -16
  28. package/pi-extension/src/cognitive-routing/pi-routing-session.ts +538 -0
  29. package/pi-extension/src/cognitive-routing/pi-session-control.ts +444 -0
  30. package/pi-extension/src/cognitive-routing/tool.ts +1 -0
  31. package/pi-extension/src/cognitive-routing/types.ts +1 -1
  32. package/pi-extension/src/index.ts +249 -239
  33. package/pi-extension/src/runtime/runtime-context.ts +129 -305
  34. package/pi-extension/src/settings/settings-ui.ts +53 -371
  35. package/pi-extension/tests/cognitive-routing/commands.test.js +1 -1
  36. package/pi-extension/tests/cognitive-routing/controller.test.js +69 -40
  37. package/pi-extension/tests/cognitive-routing/extension-lifecycle.test.js +128 -57
  38. package/pi-extension/tests/cognitive-routing/pi-normal-lifecycle.test.js +434 -0
  39. package/pi-extension/tests/cognitive-routing/pi-session-control.test.js +174 -0
  40. package/pi-extension/tests/cognitive-routing/pi-session-history.test.js +91 -0
  41. package/pi-extension/tests/cognitive-routing/profile-resolution.test.js +47 -4
  42. package/pi-extension/tests/integration/mode-free-core.test.js +218 -0
  43. package/pi-extension/tests/integration/pi-extension.test.js +430 -2478
  44. package/pi-extension/tests/integration/prompt-architecture.test.js +124 -79
  45. package/pi-extension/tests/integration/unaffected-regressions.test.js +411 -0
  46. package/runtime/prompts/cognitive-routing.md +5 -4
  47. package/runtime/prompts/conversation-history.md +1 -1
  48. package/runtime/prompts/core.md +77 -6
  49. package/runtime/prompts/interaction-contract.md +6 -2
  50. package/skills/action-selection/SKILL.md +93 -44
  51. package/skills/action-selection/references/trajectory-stalls.md +71 -0
  52. package/skills/bypass/SKILL.md +109 -39
  53. package/skills/commit-work/SKILL.md +94 -55
  54. package/skills/commit-work/references/git-state-edges.md +94 -0
  55. package/skills/commit-work/references/simple-push.md +92 -0
  56. package/skills/commit-work/references/staging-decisions.md +79 -46
  57. package/skills/decision-gate/SKILL.md +80 -22
  58. package/skills/design-for-depth/SKILL.md +208 -43
  59. package/skills/design-for-depth/references/design-pressure-signals.md +108 -240
  60. package/skills/design-for-depth/references/interface-design-loop.md +77 -97
  61. package/skills/design-for-depth/references/module-and-dependency-design.md +147 -0
  62. package/skills/design-for-depth/references/software-design-philosophy.md +127 -173
  63. package/skills/design-for-depth/references/state-and-failure-boundaries.md +136 -0
  64. package/skills/diagnose-failure/SKILL.md +198 -52
  65. package/skills/diagnose-failure/references/diagnostic-loop-catalog.md +19 -5
  66. package/skills/diagnose-failure/references/flaky-and-performance-diagnosis.md +13 -5
  67. package/skills/discuss/SKILL.md +118 -76
  68. package/skills/discuss/references/discussion-continuity.md +84 -32
  69. package/skills/evaluate-skill/SKILL.md +166 -52
  70. package/skills/evaluate-skill/references/evaluation-definition-schema.md +2 -2
  71. package/skills/evaluate-skill/references/evaluation-design.md +56 -30
  72. package/skills/evaluate-skill/references/execution-and-evidence.md +1 -1
  73. package/skills/evaluate-skill/references/review-and-revision.md +62 -21
  74. package/skills/execute-work/SKILL.md +98 -59
  75. package/skills/execute-work/references/code-practices.md +4 -4
  76. package/skills/execute-work/references/execute-work-edges.md +42 -22
  77. package/skills/review-artifact/SKILL.md +132 -71
  78. package/skills/review-artifact/references/adjudicate-artifact-review.md +101 -0
  79. package/skills/review-artifact/references/independent-artifact-reviewer-contract.md +52 -41
  80. package/skills/review-work/SKILL.md +115 -72
  81. package/skills/review-work/references/adjudicate-work-review.md +97 -0
  82. package/skills/review-work/references/independent-work-reviewer-contract.md +40 -35
  83. package/skills/review-work/references/security-risk-lens.md +17 -15
  84. package/skills/setup-freeflow/SKILL.md +14 -17
  85. package/skills/setup-freeflow/references/activation-contract.md +24 -30
  86. package/skills/setup-freeflow/references/host-setup.md +6 -7
  87. package/skills/simplify-code/SKILL.md +97 -39
  88. package/skills/simplify-code/references/simplification-patterns.md +1 -1
  89. package/skills/tdd/SKILL.md +108 -41
  90. package/skills/tdd/references/test-design.md +1 -1
  91. package/skills/track-work/SKILL.md +133 -300
  92. package/skills/track-work/references/working-record-format.md +515 -0
  93. package/skills/track-work/scripts/lib/document.mjs +713 -0
  94. package/skills/track-work/scripts/lib/format.mjs +166 -0
  95. package/skills/track-work/scripts/lib/store.mjs +233 -0
  96. package/skills/track-work/scripts/lib/transitions.mjs +790 -0
  97. package/skills/track-work/scripts/lib/views.mjs +27 -430
  98. package/skills/track-work/scripts/working-record.mjs +292 -98
  99. package/skills/verify-work/SKILL.md +46 -20
  100. package/skills/verify-work/references/browser-runtime-evidence.md +1 -1
  101. package/skills/verify-work/references/integration-evidence.md +1 -7
  102. package/skills/verify-work/references/performance-evidence.md +1 -1
  103. package/skills/workflow/SKILL.md +110 -80
  104. package/skills/workflow/references/domain-skill-composition.md +24 -18
  105. package/skills/write-skill/SKILL.md +218 -62
  106. package/skills/write-skill/references/activation-boundaries.md +63 -21
  107. package/skills/write-skill/references/agent-first-instructions.md +61 -27
  108. package/skills/write-skill/references/progressive-disclosure.md +91 -41
  109. package/skills/write-skill/references/skill-dependency-graphs.md +190 -0
  110. package/runtime/prompts/skills.md +0 -65
  111. package/skills/mode-contract/SKILL.md +0 -104
  112. package/skills/track-work/scripts/lib/codec.mjs +0 -570
  113. package/skills/track-work/scripts/lib/commands.mjs +0 -568
  114. package/skills/track-work/scripts/lib/contract.mjs +0 -303
  115. package/skills/track-work/scripts/lib/model.mjs +0 -506
  116. package/skills/track-work/scripts/lib/operations.mjs +0 -965
  117. package/skills/track-work/scripts/lib/record-store.mjs +0 -264
  118. package/skills/track-work/scripts/lib/result.mjs +0 -37
  119. package/skills/track-work/scripts/lib/rewrites.mjs +0 -740
  120. package/skills/track-work/scripts/lib/schema.mjs +0 -277
  121. package/skills/track-work/scripts/lib/source-inventory.mjs +0 -162
  122. package/skills/track-work/scripts/lib/workspace.mjs +0 -134
@@ -10,24 +10,23 @@ import freeflowExtension from "../../dist/index.js";
10
10
  import {
11
11
  readCapabilityState,
12
12
  readFreeflowConfigLayers,
13
- readModeState,
14
13
  resetSessionOverrides,
15
- setModeStatus,
16
- restoreModeOverride,
14
+ restoreSessionOverrides,
15
+ setFreeflowStatus,
17
16
  setSessionCoreOverride,
18
- setSessionMode,
19
17
  } from "../../dist/runtime/runtime-context.js";
20
18
  import { PIFLOW_HOST } from "../cognitive-routing/host-fixture.js";
21
19
 
22
20
  const execFileAsync = promisify(execFile);
23
21
 
24
- function loadExtension(host = PIFLOW_HOST) {
22
+ function loadExtension(extension = freeflowExtension, host = PIFLOW_HOST, runtimeApi = {}) {
25
23
  const handlers = new Map();
26
24
  const tools = [];
27
25
  const commands = [];
28
26
  const shortcuts = [];
29
27
  const entries = [];
30
28
  const sentMessages = [];
29
+ const sentMessageOptions = [];
31
30
  let activeToolNames;
32
31
  const pi = {
33
32
  host,
@@ -48,14 +47,12 @@ function loadExtension(host = PIFLOW_HOST) {
48
47
  appendEntry(customType, data) {
49
48
  entries.push({ customType, data });
50
49
  },
51
- sendUserMessage(message) {
50
+ sendUserMessage(message, options) {
52
51
  sentMessages.push(message);
52
+ sentMessageOptions.push(options);
53
53
  },
54
54
  getAllTools() {
55
- return tools.map((tool) => ({
56
- name: tool.name,
57
- sourceInfo: { source: "extension" },
58
- }));
55
+ return tools.map((tool) => ({ name: tool.name, sourceInfo: { source: "extension" } }));
59
56
  },
60
57
  getActiveTools() {
61
58
  return activeToolNames ?? tools.map((tool) => tool.name);
@@ -64,8 +61,9 @@ function loadExtension(host = PIFLOW_HOST) {
64
61
  activeToolNames = [...names];
65
62
  },
66
63
  };
64
+ Object.assign(pi, runtimeApi);
67
65
 
68
- freeflowExtension(pi);
66
+ extension(pi);
69
67
  return {
70
68
  pi,
71
69
  handlers,
@@ -74,6 +72,7 @@ function loadExtension(host = PIFLOW_HOST) {
74
72
  shortcuts,
75
73
  entries,
76
74
  sentMessages,
75
+ sentMessageOptions,
77
76
  activeToolNames: () => activeToolNames ?? tools.map((tool) => tool.name),
78
77
  };
79
78
  }
@@ -124,20 +123,79 @@ const testTheme = {
124
123
  },
125
124
  };
126
125
 
126
+ const readyFreeflowContext = {
127
+ corePrompt: "core",
128
+ interactionContractPrompt: "interaction contract",
129
+ };
130
+
127
131
  function renderText(component, width = 120) {
128
132
  return component.render(width).join("\n");
129
133
  }
130
134
 
131
- test("Pi registers Freeflow commands without retired router tools", () => {
135
+ async function configuredRepo(config = {}) {
136
+ const cwd = await mkdtemp(join(tmpdir(), "freeflow-pi-integration-"));
137
+ await mkdir(join(cwd, ".freeflow"));
138
+ await writeFile(join(cwd, ".freeflow/config.json"), JSON.stringify(config, null, 2), "utf8");
139
+ return cwd;
140
+ }
141
+
142
+ function lastRuntimeState(messages) {
143
+ return messages.findLast((message) => message.customType === "freeflow-runtime-state");
144
+ }
145
+
146
+ test("keeps Runtime State before the latest user message during context refreshes", async () => {
147
+ const cwd = await configuredRepo();
148
+ try {
149
+ const { handlers } = loadExtension();
150
+ const ctx = context(cwd);
151
+ await handlers.get("session_start")({ type: "session_start", reason: "startup" }, ctx);
152
+
153
+ const conversation = [
154
+ { role: "user", content: "release 0.6.1" },
155
+ { role: "assistant", content: "delegating" },
156
+ { role: "toolResult", toolName: "freeflow_switch_profile", content: [] },
157
+ ];
158
+ const first = await handlers.get("context")({ messages: conversation }, ctx);
159
+ const firstUserIndex = first.messages.findIndex((message) => message.role === "user");
160
+ const firstRuntimeIndex = first.messages.findIndex((message) => message.customType === "freeflow-runtime-state");
161
+ assert.equal(firstRuntimeIndex, firstUserIndex - 1);
162
+ assert.equal(first.messages.at(-1).role, "toolResult");
163
+
164
+ await handlers.get("session_compact")({ type: "session_compact", reason: "threshold" }, ctx);
165
+ const refreshed = await handlers.get("context")({ messages: first.messages }, ctx);
166
+ const refreshedUserIndex = refreshed.messages.findIndex((message) => message.role === "user");
167
+ const refreshedRuntimeIndex = refreshed.messages.findIndex(
168
+ (message) => message.customType === "freeflow-runtime-state",
169
+ );
170
+ assert.equal(refreshedRuntimeIndex, refreshedUserIndex - 1);
171
+ assert.equal(refreshed.messages.at(-1).role, "toolResult");
172
+
173
+ const interrupted = await handlers.get("context")(
174
+ {
175
+ messages: [...refreshed.messages, { role: "user", content: "stop" }],
176
+ },
177
+ ctx,
178
+ );
179
+ const latestUserIndex = interrupted.messages.findLastIndex((message) => message.role === "user");
180
+ const latestRuntimeIndex = interrupted.messages.findIndex(
181
+ (message) => message.customType === "freeflow-runtime-state",
182
+ );
183
+ assert.equal(latestRuntimeIndex, latestUserIndex - 1);
184
+ assert.equal(interrupted.messages.at(-1).content, "stop");
185
+ } finally {
186
+ await rm(cwd, { recursive: true, force: true });
187
+ }
188
+ });
189
+
190
+ test("Pi registers the remaining Freeflow commands without mode controls or retired router tools", () => {
132
191
  const { commands, shortcuts, tools } = loadExtension();
133
192
  const commandNames = commands.map((command) => command.name);
134
193
  const toolNames = tools.map((tool) => tool.name);
194
+ const freeflowCommand = commands.find((command) => command.name === "freeflow");
135
195
 
136
196
  assert.ok(commandNames.includes("freeflow"));
137
197
  assert.ok(!commandNames.includes("output-router"));
138
- for (const command of ["discuss", "track-work", "execute-work"]) {
139
- assert.ok(commandNames.includes(command));
140
- }
198
+ for (const command of ["discuss", "track-work", "execute-work"]) assert.ok(commandNames.includes(command));
141
199
  assert.ok(!commandNames.includes("discover"));
142
200
  assert.ok(!commandNames.includes("execute-plan"));
143
201
  assert.ok(!commandNames.includes("workflow"));
@@ -145,217 +203,165 @@ test("Pi registers Freeflow commands without retired router tools", () => {
145
203
  shortcuts.map(({ shortcut }) => shortcut),
146
204
  ["ctrl+shift+r", "ctrl+shift+a"],
147
205
  );
148
- assert.ok(toolNames.includes("freeflow_context"));
149
- assert.deepEqual(tools.find((tool) => tool.name === "freeflow_context").parameters.oneOf, []);
150
- assert.ok(toolNames.includes("freeflow_switch_profile"));
206
+ assert.ok(tools.some((tool) => tool.name === "freeflow_context"));
207
+ assert.ok(tools.some((tool) => tool.name === "freeflow_switch_profile"));
208
+ assert.ok(freeflowCommand);
209
+ assert.ok(!freeflowCommand.definition.getArgumentCompletions("").some((item) => item.value === "mode"));
210
+ assert.deepEqual(freeflowCommand.definition.getArgumentCompletions("mode "), []);
151
211
  assert.ok(
152
212
  !toolNames.some((name) => ["freeflow_status", "freeflow_search", "freeflow_run", "freeflow_batch"].includes(name)),
153
213
  );
154
- assert.ok(!toolNames.includes("freeflow_retrieve"));
155
- assert.ok(!toolNames.includes("freeflow_capture"));
156
214
  });
157
215
 
158
- test("normal Pi keeps Cognitive Routing disabled while showing its configuration", async () => {
159
- const cwd = await mkdtemp(join(tmpdir(), "freeflow-pi-normal-runtime-"));
216
+ test("unsupported Pi hosts keep Cognitive Routing unavailable while exposing its configuration", async () => {
217
+ const cwd = await configuredRepo({
218
+ cognitiveRouting: {
219
+ enabled: true,
220
+ profiles: {
221
+ standard: { provider: "test", model: "standard", thinkingLevel: "high" },
222
+ reasoning: { provider: "test", model: "reasoning", thinkingLevel: "max" },
223
+ },
224
+ },
225
+ });
160
226
  try {
161
- await mkdir(join(cwd, ".freeflow"));
162
- await writeFile(join(cwd, ".freeflow/config.json"), JSON.stringify({ defaultMode: "workflow" }), "utf8");
163
- await writeFile(
164
- join(cwd, ".freeflow/local.json"),
165
- JSON.stringify({
166
- cognitiveRouting: {
167
- enabled: true,
168
- profiles: {
169
- standard: { provider: "test", model: "standard", thinkingLevel: "high" },
170
- reasoning: { provider: "test", model: "reasoning", thinkingLevel: "max" },
171
- },
172
- },
173
- }),
174
- "utf8",
175
- );
176
- const { commands, handlers, shortcuts, tools } = loadExtension(null);
227
+ const { commands, handlers, shortcuts, tools } = loadExtension(freeflowExtension, null);
177
228
  const freeflowCommand = commands.find((command) => command.name === "freeflow");
178
229
  assert.ok(freeflowCommand);
179
230
  assert.deepEqual(shortcuts, []);
180
231
  assert.ok(!tools.some((tool) => tool.name === "freeflow_switch_profile"));
181
232
  assert.ok(!freeflowCommand.definition.getArgumentCompletions("").some((item) => item.value === "profile"));
182
- const capabilityState = await readCapabilityState(
183
- cwd,
184
- { modelRegistry: cognitiveRoutingModelRegistry() },
185
- undefined,
186
- );
233
+ const capabilityState = await readCapabilityState(cwd, undefined, undefined);
187
234
  assert.equal(capabilityState.cognitiveRouting.enabled, true);
188
235
  assert.equal(capabilityState.cognitiveRouting.effective, false);
189
- assert.equal(capabilityState.cognitiveRouting.blockingReason.code, "runtime_disabled");
236
+ assert.equal(capabilityState.cognitiveRouting.blockingReason.code, "host_unsupported");
190
237
 
191
238
  const ctx = context(cwd);
192
- await freeflowCommand.definition.handler("status", ctx);
193
- assert.match(ctx.notifications.at(-1).message, /cognitive routing: disabled \(PiFlow only\)/i);
194
239
  await handlers.get("session_start")({ type: "session_start" }, ctx);
195
- assert.match(ctx.statuses.at(-1).value, /cognitive disabled · PiFlow only/);
196
- const beforeAgentStart = await handlers.get("before_agent_start")({ systemPrompt: "base" }, ctx);
197
- assert.doesNotMatch(
198
- `${beforeAgentStart?.systemPrompt ?? ""}\n${beforeAgentStart?.message ?? ""}`,
199
- /Cognitive Routing/i,
200
- );
201
- ctx.ui.custom = async (factory) => {
202
- let result;
203
- const component = factory({ requestRender() {} }, testTheme, {}, (value) => {
204
- result = value;
205
- });
206
- const rootText = renderText(component);
207
- assert.match(rootText, /Cognitive Routing/);
208
- assert.match(rootText, /PiFlow only/);
209
- component.handleInput("\u001b[B");
210
- component.handleInput("\r");
211
- component.handleInput("\u001b[B");
212
- component.handleInput("\u001b[B");
213
- component.handleInput("\r");
214
- await component.waitForWrites();
215
- assert.doesNotMatch(renderText(component), /Cognitive Routing\s+enabled \(5\) configured · inactive/);
216
- for (let index = 0; index < 5; index += 1) component.handleInput("\u001b[B");
217
- component.handleInput("\r");
218
- const cognitiveText = renderText(component);
219
- assert.match(cognitiveText, /Freeflow Settings · Personal overrides › Cognitive Routing/);
220
- assert.match(cognitiveText, /Enabled\s+enabled \(local · inactive\)/);
221
- assert.match(cognitiveText, /Standard preset\s+test\/standard · high \(local · inactive\)/);
222
- assert.match(cognitiveText, /Reasoning preset\s+test\/reasoning · max \(local · inactive\)/);
223
- component.handleInput("\r");
224
- assert.match(renderText(component), /Freeflow Settings · Personal overrides › Cognitive Routing/);
225
- assert.doesNotMatch(renderText(component), /Inherit repository/);
226
- component.handleInput("\u001b");
227
- return result;
228
- };
229
- await freeflowCommand.definition.handler("settings", ctx);
240
+ assert.match(ctx.statuses.at(-1).value, /cognitive blocked · host_unsupported/);
241
+ const before = await handlers.get("before_agent_start")({ systemPrompt: "base" }, ctx);
242
+ assert.doesNotMatch(before.systemPrompt, /Cognitive Routing/);
230
243
  } finally {
231
244
  await rm(cwd, { recursive: true, force: true });
232
245
  }
233
246
  });
234
247
 
235
- test("PiFlow statusline reports the current cognitive profile and control mode", () => {
236
- const ctx = context();
237
- setModeStatus(
238
- ctx,
239
- { effectiveMode: "workflow", currentMode: null },
240
- {
241
- configured: true,
248
+ test("normal Pi settings expose active Cognitive Routing configuration", async () => {
249
+ const cwd = await configuredRepo({
250
+ cognitiveRouting: {
242
251
  enabled: true,
243
- configSources: { interactionContract: "builtin", skillsEnabled: "builtin", enabled: "builtin" },
244
- interactionContract: { effective: true },
245
- skills: { effective: true },
246
- cognitiveRouting: { enabled: true, effective: true, blockingReason: { code: "disabled" } },
252
+ profiles: {
253
+ standard: { provider: "test", model: "model-a", thinkingLevel: "low" },
254
+ reasoning: { provider: "test", model: "model-b", thinkingLevel: "high" },
255
+ },
247
256
  },
248
- { effective: true, activeProfile: "reasoning", controlMode: "automatic" },
249
- );
250
- assert.equal(ctx.statuses.at(-1).value, "freeflow: interaction · workflow · reasoning · automatic");
257
+ });
258
+ try {
259
+ const { commands } = loadExtension(freeflowExtension, null, {
260
+ appendEntry() {},
261
+ async setModel() {
262
+ return true;
263
+ },
264
+ setThinkingLevel() {},
265
+ });
266
+ const freeflowCommand = commands.find((command) => command.name === "freeflow");
267
+ assert.ok(freeflowCommand);
268
+ const settingsCtx = context(cwd);
269
+ settingsCtx.mode = "tui";
270
+ settingsCtx.hasUI = true;
271
+ settingsCtx.isIdle = () => true;
272
+ settingsCtx.modelRegistry = cognitiveRoutingModelRegistry();
273
+ settingsCtx.ui.custom = async (factory) => {
274
+ const component = factory({ requestRender() {} }, testTheme, {}, () => {});
275
+ const rootText = renderText(component);
276
+ assert.match(rootText, /Cognitive Routing\s+enabled \(5\) active/);
277
+ assert.doesNotMatch(rootText, /PiFlow only/);
278
+ return undefined;
279
+ };
280
+
281
+ await freeflowCommand.definition.handler("settings", settingsCtx);
282
+ } finally {
283
+ await rm(cwd, { recursive: true, force: true });
284
+ }
251
285
  });
252
286
 
253
- test("PiFlow statusline shows the configured standard profile pending first prompt activation", () => {
287
+ test("Pi statusline reports only dynamic Cognitive Routing and context state", () => {
254
288
  const ctx = context();
255
- setModeStatus(
289
+ setFreeflowStatus(
256
290
  ctx,
257
- { effectiveMode: "workflow", currentMode: null },
258
291
  {
259
292
  configured: true,
260
293
  enabled: true,
261
- configSources: { interactionContract: "builtin", skillsEnabled: "builtin", enabled: "builtin" },
262
- interactionContract: { effective: true },
263
- skills: { effective: true },
294
+ configSources: { enabled: "builtin" },
295
+ contextVirtualization: { effective: true },
296
+ conversationHistory: { effective: false },
264
297
  cognitiveRouting: { enabled: true, effective: true, blockingReason: null },
265
298
  },
266
- undefined,
267
- { cognitiveRoutingStartupPending: true },
299
+ { effective: true, activeProfile: "reasoning", controlMode: "automatic" },
300
+ readyFreeflowContext,
268
301
  );
269
- assert.equal(ctx.statuses.at(-1).value, "freeflow: interaction · workflow · standard · pending");
302
+ assert.equal(ctx.statuses.at(-1).value, "freeflow: reasoning · automatic · context");
303
+ assert.doesNotMatch(ctx.statuses.at(-1).value, /interaction|workflow|mode|skills/i);
270
304
  });
271
305
 
272
- test("PiFlow statusline shows the configured startup profile while pending", () => {
306
+ test("Pi statusline shows the configured Cognitive Routing profile while activation is pending", () => {
273
307
  const ctx = context();
274
- setModeStatus(
308
+ setFreeflowStatus(
275
309
  ctx,
276
- { effectiveMode: "workflow", currentMode: null },
277
310
  {
278
311
  configured: true,
279
312
  enabled: true,
280
- configSources: { interactionContract: "builtin", skillsEnabled: "builtin", enabled: "builtin" },
281
- interactionContract: { effective: true },
282
- skills: { effective: true },
313
+ configSources: { enabled: "builtin" },
314
+ contextVirtualization: { effective: false },
315
+ conversationHistory: { effective: false },
283
316
  cognitiveRouting: {
284
317
  enabled: true,
285
318
  effective: true,
286
- sessionStart: { control: "manual", profile: "reasoning" },
319
+ sessionStart: { profile: "reasoning" },
287
320
  blockingReason: null,
288
321
  },
289
322
  },
290
323
  undefined,
324
+ readyFreeflowContext,
291
325
  { cognitiveRoutingStartupPending: true },
292
326
  );
293
- assert.equal(ctx.statuses.at(-1).value, "freeflow: interaction · workflow · reasoning · pending");
327
+ assert.equal(ctx.statuses.at(-1).value, "freeflow: reasoning · pending");
294
328
  });
295
329
 
296
- test("PiFlow statusline keeps an inactive runtime blocked instead of showing startup pending", () => {
330
+ test("Pi statusline keeps an inactive Cognitive Routing runtime blocked", () => {
297
331
  const ctx = context();
298
- setModeStatus(
332
+ setFreeflowStatus(
299
333
  ctx,
300
- { effectiveMode: "workflow", currentMode: null },
301
334
  {
302
335
  configured: true,
303
336
  enabled: true,
304
- configSources: { interactionContract: "builtin", skillsEnabled: "builtin", enabled: "builtin" },
305
- interactionContract: { effective: true },
306
- skills: { effective: true },
337
+ configSources: { enabled: "builtin" },
338
+ contextVirtualization: { effective: false },
339
+ conversationHistory: { effective: false },
307
340
  cognitiveRouting: { enabled: true, effective: true, blockingReason: null },
308
341
  },
309
342
  { effective: false, activeProfile: "standard", controlMode: "automatic" },
343
+ readyFreeflowContext,
310
344
  { cognitiveRoutingStartupPending: true },
311
345
  );
312
- assert.equal(ctx.statuses.at(-1).value, "freeflow: interaction · workflow · cognitive blocked · runtime_inactive");
346
+ assert.equal(ctx.statuses.at(-1).value, "freeflow: cognitive blocked · runtime_inactive");
313
347
  });
314
348
 
315
- test("PiFlow statusline preserves startup suppression when the host selected a model", () => {
316
- const ctx = context();
317
- ctx.modelStateProvenance = { explicitModel: true };
318
- setModeStatus(
319
- ctx,
320
- { effectiveMode: "workflow", currentMode: null },
321
- {
322
- configured: true,
349
+ test("PiFlow empty configured sessions show a pending profile before first prompt", async () => {
350
+ const cwd = await configuredRepo({
351
+ cognitiveRouting: {
323
352
  enabled: true,
324
- configSources: { interactionContract: "builtin", skillsEnabled: "builtin", enabled: "builtin" },
325
- interactionContract: { effective: true },
326
- skills: { effective: true },
327
- cognitiveRouting: { enabled: true, effective: true, blockingReason: null },
353
+ profiles: {
354
+ standard: { provider: "test", model: "model-a", thinkingLevel: "low" },
355
+ reasoning: { provider: "test", model: "model-b", thinkingLevel: "high" },
356
+ },
328
357
  },
329
- undefined,
330
- { cognitiveRoutingStartupPending: true },
331
- );
332
- assert.equal(ctx.statuses.at(-1).value, "freeflow: interaction · workflow · cognitive blocked · runtime_inactive");
333
- });
334
-
335
- test("PiFlow empty configured sessions show standard pending before first prompt", async () => {
336
- const cwd = await mkdtemp(join(tmpdir(), "freeflow-pi-pending-cognitive-status-"));
358
+ });
337
359
  try {
338
- await mkdir(join(cwd, ".freeflow"));
339
- await writeFile(
340
- join(cwd, ".freeflow/config.json"),
341
- JSON.stringify({
342
- defaultMode: "workflow",
343
- cognitiveRouting: {
344
- enabled: true,
345
- profiles: {
346
- standard: { provider: "test", model: "model-a", thinkingLevel: "low" },
347
- reasoning: { provider: "test", model: "model-b", thinkingLevel: "high" },
348
- },
349
- },
350
- }),
351
- "utf8",
352
- );
353
360
  const { handlers } = loadExtension();
354
361
  const ctx = context(cwd);
355
362
  ctx.modelRegistry = cognitiveRoutingModelRegistry();
356
-
357
363
  await handlers.get("session_start")({ type: "session_start", reason: "startup" }, ctx);
358
- assert.equal(ctx.statuses.at(-1).value, "freeflow: interaction · workflow · standard · pending");
364
+ assert.equal(ctx.statuses.at(-1).value, "freeflow: reasoning · automatic");
359
365
  } finally {
360
366
  await rm(cwd, { recursive: true, force: true });
361
367
  }
@@ -365,82 +371,25 @@ test("Pi exposes bypass scope argument completions", () => {
365
371
  const { commands } = loadExtension();
366
372
  const bypassCommand = commands.find((command) => command.name === "bypass");
367
373
  assert.ok(bypassCommand);
368
-
369
374
  assert.deepEqual(bypassCommand.definition.getArgumentCompletions(""), [
370
375
  { value: "next", label: "next", description: "Skip one optional step" },
371
376
  { value: "task", label: "task", description: "Reduce optional pressure for the current task" },
372
377
  ]);
373
- assert.deepEqual(bypassCommand.definition.getArgumentCompletions("t"), [
374
- { value: "task", label: "task", description: "Reduce optional pressure for the current task" },
375
- ]);
376
- assert.deepEqual(bypassCommand.definition.getArgumentCompletions("unknown"), []);
377
378
  });
378
379
 
379
- test("Pi describes Freeflow argument completions", () => {
380
+ test("Pi describes the mode-free Freeflow argument surface", () => {
380
381
  const { commands } = loadExtension();
381
382
  const freeflowCommand = commands.find((command) => command.name === "freeflow");
382
383
  assert.ok(freeflowCommand);
383
-
384
384
  assert.deepEqual(freeflowCommand.definition.getArgumentCompletions(""), [
385
385
  { value: "settings", label: "settings", description: "Open personal override settings" },
386
386
  { value: "status", label: "status", description: "Show effective Freeflow state" },
387
387
  { value: "context", label: "context", description: "Inspect Freeflow Context" },
388
- { value: "mode", label: "mode", description: "Select a temporary session mode" },
389
388
  { value: "profile", label: "profile", description: "Hold or release Cognitive Routing profile control" },
390
389
  { value: "enable", label: "enable", description: "Enable Freeflow for this repository" },
391
390
  { value: "disable", label: "disable", description: "Disable Freeflow for this repository" },
392
391
  ]);
393
- assert.deepEqual(freeflowCommand.definition.getArgumentCompletions("profile "), [
394
- {
395
- value: "profile standard",
396
- label: "standard",
397
- description: "Hold standard profile manually",
398
- },
399
- {
400
- value: "profile reasoning",
401
- label: "reasoning",
402
- description: "Hold reasoning profile manually",
403
- },
404
- {
405
- value: "profile auto",
406
- label: "auto",
407
- description: "Release the manual hold without changing the model",
408
- },
409
- {
410
- value: "profile history",
411
- label: "history",
412
- description: "Show Cognitive Routing transition history",
413
- },
414
- {
415
- value: "profile history active",
416
- label: "history active",
417
- description: "Show current-branch Cognitive Routing history",
418
- },
419
- {
420
- value: "profile history anomalies",
421
- label: "history anomalies",
422
- description: "Show Cognitive Routing history anomalies",
423
- },
424
- ]);
425
- assert.deepEqual(freeflowCommand.definition.getArgumentCompletions("settings "), [
426
- { value: "settings session", label: "session", description: "Override Freeflow for this Pi session" },
427
- { value: "settings local", label: "local", description: "Edit personal overrides for this repository" },
428
- { value: "settings repo", label: "repo", description: "Edit shared repository settings" },
429
- ]);
430
- assert.deepEqual(freeflowCommand.definition.getArgumentCompletions("mode "), [
431
- { value: "mode conversation", label: "conversation", description: "Read-only discussion and inspection" },
432
- { value: "mode workflow", label: "workflow", description: "Adaptive workflow for consequential work" },
433
- {
434
- value: "mode strict-workflow",
435
- label: "strict-workflow",
436
- description: "Stronger pressure at high-risk boundaries",
437
- },
438
- {
439
- value: "mode reset",
440
- label: "reset",
441
- description: "Clear the session override and use the configured default",
442
- },
443
- ]);
392
+ assert.deepEqual(freeflowCommand.definition.getArgumentCompletions("mode "), []);
444
393
  assert.deepEqual(freeflowCommand.definition.getArgumentCompletions("context "), [
445
394
  { value: "context status", label: "status", description: "Show Freeflow Context state" },
446
395
  { value: "context list", label: "list", description: "List archived context projections" },
@@ -449,27 +398,21 @@ test("Pi describes Freeflow argument completions", () => {
449
398
  ]);
450
399
  });
451
400
 
452
- test("Pi exposes canonical model skills without compatibility aliases", async () => {
453
- const cwd = await mkdtemp(join(tmpdir(), "freeflow-pi-canonical-skills-"));
401
+ test("Pi exposes 25 base skills without a mode skill or compatibility aliases", async () => {
402
+ const cwd = await configuredRepo();
454
403
  try {
455
- await mkdir(join(cwd, ".freeflow"));
456
- await writeFile(join(cwd, ".freeflow/config.json"), JSON.stringify({ defaultMode: "workflow" }, null, 2), "utf8");
457
-
458
- const { handlers, commands, sentMessages } = loadExtension();
404
+ const { handlers, commands, sentMessages, sentMessageOptions } = loadExtension();
459
405
  const resources = await handlers.get("resources_discover")({ cwd }, context(cwd));
460
406
  const skillNames = resources.skillPaths.map((path) => {
461
- const match = path.match(/[\\/]skills[\\/]([^\\/]+)[\\/]SKILL\.md$/);
407
+ const match = path.match(/[\\/]skills[\\/]([^\\/]+)\/SKILL\.md$/);
462
408
  assert.ok(match, `unexpected skill path: ${path}`);
463
409
  return match[1];
464
410
  });
465
- assert.equal(skillNames.length, 26);
466
- for (const skill of ["action-selection", "discuss", "execute-work", "track-work"]) {
467
- assert.ok(skillNames.includes(skill));
468
- }
469
- assert.ok(!skillNames.includes("output-router"));
411
+ assert.equal(skillNames.length, 25);
412
+ assert.ok(skillNames.includes("workflow"));
413
+ assert.ok(!skillNames.includes("mode-contract"));
470
414
  assert.ok(!skillNames.includes("discover"));
471
415
  assert.ok(!skillNames.includes("execute-plan"));
472
- await Promise.all(resources.skillPaths.map((path) => readFile(path, "utf8")));
473
416
 
474
417
  for (const [commandName, expectedSkill] of [
475
418
  ["action-selection", "action-selection"],
@@ -481,2419 +424,428 @@ test("Pi exposes canonical model skills without compatibility aliases", async ()
481
424
  assert.ok(command);
482
425
  await command.definition.handler(undefined, context(cwd));
483
426
  assert.equal(sentMessages.at(-1), `/skill:${expectedSkill}`);
484
- }
485
- for (const alias of ["discover", "execute-plan"]) {
486
- assert.ok(!commands.some((candidate) => candidate.name === alias));
427
+ assert.deepEqual(sentMessageOptions.at(-1), { expandPromptTemplates: true });
487
428
  }
488
429
  } finally {
489
430
  await rm(cwd, { recursive: true, force: true });
490
431
  }
491
432
  });
492
433
 
493
- test("Pi keeps Freeflow inactive until setup config exists", async () => {
494
- const cwd = await mkdtemp(join(tmpdir(), "freeflow-pi-missing-setup-"));
434
+ test("Pi expands setup-freeflow when dispatching its unconfigured skill command", async () => {
435
+ const cwd = await mkdtemp(join(tmpdir(), "freeflow-pi-setup-dispatch-"));
495
436
  try {
496
- const { handlers } = loadExtension();
497
- const resourcesDiscover = handlers.get("resources_discover");
498
- const beforeAgentStart = handlers.get("before_agent_start");
499
- assert.ok(resourcesDiscover);
500
- assert.ok(beforeAgentStart);
437
+ const { commands, sentMessages, sentMessageOptions } = loadExtension();
438
+ const setup = commands.find((command) => command.name === "setup-freeflow");
439
+ assert.ok(setup);
501
440
 
502
- const resources = await resourcesDiscover({ cwd }, context(cwd));
503
- assert.equal(resources.skillPaths.length, 1);
504
- assert.match(resources.skillPaths[0], /setup-freeflow\/SKILL\.md$/);
441
+ await setup.definition.handler(undefined, context(cwd));
505
442
 
506
- const result = await beforeAgentStart({ systemPrompt: "base prompt" }, context(cwd));
507
- assert.equal(result.systemPrompt, "base prompt");
443
+ assert.equal(sentMessages.at(-1), "/skill:setup-freeflow");
444
+ assert.deepEqual(sentMessageOptions.at(-1), { expandPromptTemplates: true });
508
445
  } finally {
509
446
  await rm(cwd, { recursive: true, force: true });
510
447
  }
511
448
  });
512
449
 
513
- test("Pi treats invalid Freeflow setup config as inactive", async () => {
514
- const cwd = await mkdtemp(join(tmpdir(), "freeflow-pi-invalid-setup-"));
450
+ test("Pi keeps Freeflow inactive until repository activation exists", async () => {
451
+ const cwd = await mkdtemp(join(tmpdir(), "freeflow-pi-missing-setup-"));
515
452
  try {
516
- await mkdir(join(cwd, ".freeflow"));
517
- await writeFile(
518
- join(cwd, ".freeflow/config.json"),
519
- JSON.stringify(
520
- {
521
- defaultMode: "workflow",
522
- enabled: "false",
523
- interactionContract: "on",
524
- skills: { enabled: "no" },
525
- },
526
- null,
527
- 2,
528
- ),
529
- "utf8",
530
- );
531
-
532
- const { handlers, commands } = loadExtension();
453
+ const { handlers } = loadExtension();
533
454
  const resources = await handlers.get("resources_discover")({ cwd }, context(cwd));
534
455
  assert.equal(resources.skillPaths.length, 1);
535
456
  assert.match(resources.skillPaths[0], /setup-freeflow\/SKILL\.md$/);
536
-
537
457
  const result = await handlers.get("before_agent_start")({ systemPrompt: "base prompt" }, context(cwd));
538
458
  assert.equal(result.systemPrompt, "base prompt");
539
-
540
- const freeflowCommand = commands.find((command) => command.name === "freeflow");
541
- const statusCtx = context(cwd);
542
- await freeflowCommand.definition.handler("status", statusCtx);
543
- assert.match(statusCtx.notifications.at(-1).message, /invalid config/);
544
459
  } finally {
545
460
  await rm(cwd, { recursive: true, force: true });
546
461
  }
547
462
  });
548
463
 
549
- test("Pi rejects unsupported nested skills keys in repository and local config", async () => {
550
- for (const configFile of ["config.json", "local.json"]) {
551
- const cwd = await mkdtemp(join(tmpdir(), `freeflow-pi-unsupported-skills-${configFile}-`));
464
+ test("Pi rejects obsolete modes and removed core toggles in configuration", async () => {
465
+ for (const config of [{ defaultMode: "workflow" }, { interactionContract: false }, { skills: { enabled: false } }]) {
466
+ const cwd = await configuredRepo(config);
552
467
  try {
553
- await mkdir(join(cwd, ".freeflow"));
554
- if (configFile === "local.json") {
555
- await writeFile(join(cwd, ".freeflow/config.json"), JSON.stringify({ defaultMode: "workflow" }), "utf8");
556
- }
557
- await writeFile(join(cwd, `.freeflow/${configFile}`), JSON.stringify({ skills: { enable: false } }), "utf8");
558
-
559
468
  const layers = await readFreeflowConfigLayers(cwd);
560
- const invalidLayer = configFile === "config.json" ? layers.repository : layers.local;
561
- assert.equal(invalidLayer.valid, false, `${configFile} should reject unknown nested skills keys`);
562
469
  assert.equal(layers.configured, false);
563
- assert.equal(layers.parseError, "unsupported skills config key: enable");
470
+ assert.equal(layers.repository.valid, false);
471
+ assert.match(layers.parseError, /unsupported top-level config key/);
564
472
  } finally {
565
473
  await rm(cwd, { recursive: true, force: true });
566
474
  }
567
475
  }
568
476
  });
569
477
 
570
- test("Pi layers local core overrides over repository defaults with source evidence", async () => {
571
- const cwd = await mkdtemp(join(tmpdir(), "freeflow-pi-layered-core-"));
478
+ test("Pi treats invalid configuration as inactive", async () => {
479
+ const cwd = await configuredRepo({ enabled: "false" });
480
+ try {
481
+ const { handlers, commands } = loadExtension();
482
+ 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$/);
485
+ const result = await handlers.get("before_agent_start")({ systemPrompt: "base prompt" }, context(cwd));
486
+ assert.equal(result.systemPrompt, "base prompt");
487
+ const freeflowCommand = commands.find((command) => command.name === "freeflow");
488
+ const statusCtx = context(cwd);
489
+ await freeflowCommand.definition.handler("status", statusCtx);
490
+ assert.match(statusCtx.notifications.at(-1).message, /invalid config/);
491
+ } finally {
492
+ await rm(cwd, { recursive: true, force: true });
493
+ }
494
+ });
495
+
496
+ test("Pi resolves only the remaining layered core values", async () => {
497
+ const cwd = await configuredRepo();
572
498
  try {
573
- await mkdir(join(cwd, ".freeflow"));
574
- await writeFile(
575
- join(cwd, ".freeflow/config.json"),
576
- JSON.stringify(
577
- {
578
- enabled: false,
579
- interactionContract: false,
580
- skills: { enabled: true },
581
- defaultMode: "workflow",
582
- },
583
- null,
584
- 2,
585
- ),
586
- "utf8",
587
- );
588
499
  await writeFile(
589
500
  join(cwd, ".freeflow/local.json"),
590
- JSON.stringify(
591
- {
592
- enabled: true,
593
- skills: { enabled: false },
594
- defaultMode: "strict-workflow",
595
- processing: { unsafeUnsandboxed: { enabled: true } },
596
- },
597
- null,
598
- 2,
599
- ),
501
+ JSON.stringify({ enabled: false, contextVirtualization: true }, null, 2),
600
502
  "utf8",
601
503
  );
602
-
603
504
  const layers = await readFreeflowConfigLayers(cwd);
604
- assert.equal(layers.configured, true);
605
- assert.equal(layers.repositoryConfigured, true);
606
505
  assert.deepEqual(layers.coreConfig, {
607
- enabled: true,
608
- interactionContract: false,
609
- contextVirtualization: false,
506
+ enabled: false,
507
+ contextVirtualization: true,
610
508
  conversationHistory: false,
611
- skills: { enabled: false },
612
- defaultMode: "strict-workflow",
613
509
  });
614
510
  assert.deepEqual(layers.sources, {
615
511
  enabled: "local",
616
- interactionContract: "repository",
617
- contextVirtualization: "builtin",
512
+ contextVirtualization: "local",
618
513
  conversationHistory: "builtin",
619
- skillsEnabled: "local",
620
- defaultMode: "local",
621
- });
622
- assert.equal(layers.local.parsed.processing.unsafeUnsandboxed.enabled, true);
623
-
624
- const capabilityState = await readCapabilityState(cwd, undefined, PIFLOW_HOST);
625
- assert.equal(capabilityState.enabled, true);
626
- assert.equal(capabilityState.interactionContract.effective, false);
627
- assert.equal(capabilityState.skills.effective, false);
628
- assert.deepEqual(capabilityState.configSources, layers.sources);
629
-
630
- const modeState = await readModeState(cwd);
631
- assert.equal(modeState.repositoryDefaultMode, "workflow");
632
- assert.equal(modeState.repositoryDefaultModeSource, "repository");
633
- assert.equal(modeState.personalDefaultMode, "strict-workflow");
634
- assert.equal(modeState.defaultMode, "strict-workflow");
635
- assert.equal(modeState.defaultModeSource, "local");
636
- assert.equal(modeState.sessionMode, null);
637
- assert.equal(modeState.resolvedMode, "strict-workflow");
638
- assert.equal(modeState.active, false);
639
- assert.equal(modeState.effectiveMode, null);
640
- } finally {
641
- await rm(cwd, { recursive: true, force: true });
642
- }
643
- });
644
-
645
- test("Pi layers branch-aware session core overrides above configured values", async () => {
646
- const cwd = await mkdtemp(join(tmpdir(), "freeflow-pi-session-core-"));
647
- try {
648
- await mkdir(join(cwd, ".freeflow"));
649
- const configPath = join(cwd, ".freeflow/config.json");
650
- const configText = JSON.stringify({ defaultMode: "workflow" }, null, 2);
651
- await writeFile(configPath, configText, "utf8");
652
-
653
- const { pi, entries } = loadExtension();
654
- const ctx = context(cwd);
655
-
656
- let result = await setSessionCoreOverride("enabled", false, ctx, pi);
657
- assert.equal(result.changed, true);
658
- let capabilityState = await readCapabilityState(cwd, undefined, PIFLOW_HOST);
659
- assert.equal(capabilityState.enabled, false);
660
- assert.equal(capabilityState.configSources.enabled, "session");
661
- assert.deepEqual(capabilityState.sessionOverrides, { enabled: false });
662
- assert.deepEqual(entries.at(-1), {
663
- customType: "freeflow-session-overrides",
664
- data: { overrides: { enabled: false } },
665
514
  });
666
-
667
- result = await setSessionCoreOverride("enabled", false, ctx, pi);
668
- assert.equal(result.changed, false);
669
- assert.equal(entries.length, 1);
670
-
671
- await setSessionCoreOverride("enabled", null, ctx, pi);
672
- await setSessionCoreOverride("interactionContract", false, ctx, pi);
673
- await setSessionCoreOverride("skillsEnabled", false, ctx, pi);
674
- capabilityState = await readCapabilityState(cwd, undefined, PIFLOW_HOST);
675
- assert.equal(capabilityState.enabled, true);
676
- assert.equal(capabilityState.interactionContract.effective, false);
677
- assert.equal(capabilityState.skills.effective, false);
678
- assert.equal((await readModeState(cwd)).effectiveMode, null);
679
-
680
- await setSessionMode("conversation", ctx, pi);
681
- const reset = await resetSessionOverrides(ctx, pi);
682
- assert.equal(reset.changed, true);
683
- assert.equal(reset.reloadRequired, true);
684
- capabilityState = await readCapabilityState(cwd, undefined, PIFLOW_HOST);
685
- assert.deepEqual(capabilityState.sessionOverrides, {});
686
- assert.equal(capabilityState.interactionContract.effective, true);
687
- assert.equal(capabilityState.skills.effective, true);
688
- assert.equal((await readModeState(cwd)).sessionMode, null);
689
- assert.equal(await readFile(configPath, "utf8"), configText);
515
+ const state = await readCapabilityState(cwd, undefined, PIFLOW_HOST);
516
+ assert.equal(state.enabled, false);
517
+ assert.equal("skills" in state, false);
518
+ assert.equal("interactionContract" in state, false);
690
519
  } finally {
691
- restoreModeOverride(context(cwd));
692
520
  await rm(cwd, { recursive: true, force: true });
693
521
  }
694
522
  });
695
523
 
696
- test("Pi session enablement can override configured off but cannot bypass activation", async () => {
524
+ test("Pi session enablement cannot bypass repository activation", async () => {
697
525
  const cwd = await mkdtemp(join(tmpdir(), "freeflow-pi-session-activation-"));
526
+ await mkdir(join(cwd, ".freeflow"));
527
+ const { pi } = loadExtension();
528
+ const ctx = context(cwd);
698
529
  try {
699
- await mkdir(join(cwd, ".freeflow"));
700
- const { pi } = loadExtension();
701
- const ctx = context(cwd);
702
530
  await setSessionCoreOverride("enabled", true, ctx, pi);
531
+ let state = await readCapabilityState(cwd, undefined, PIFLOW_HOST);
532
+ assert.equal(state.configured, false);
533
+ assert.equal(state.enabled, false);
703
534
 
704
- let capabilityState = await readCapabilityState(cwd, undefined, PIFLOW_HOST);
705
- assert.equal(capabilityState.configured, false);
706
- assert.equal(capabilityState.enabled, false);
707
-
708
- await writeFile(join(cwd, ".freeflow/config.json"), "{ invalid", "utf8");
709
- capabilityState = await readCapabilityState(cwd, undefined, PIFLOW_HOST);
710
- assert.equal(capabilityState.configured, false);
711
- assert.equal(capabilityState.enabled, false);
712
-
713
- await writeFile(
714
- join(cwd, ".freeflow/config.json"),
715
- JSON.stringify({ enabled: false, defaultMode: "workflow" }),
716
- "utf8",
717
- );
718
- capabilityState = await readCapabilityState(cwd, undefined, PIFLOW_HOST);
719
- assert.equal(capabilityState.configured, true);
720
- assert.equal(capabilityState.enabled, true);
721
- assert.equal(capabilityState.configSources.enabled, "session");
535
+ await writeFile(join(cwd, ".freeflow/config.json"), JSON.stringify({ enabled: false }), "utf8");
536
+ state = await readCapabilityState(cwd, undefined, PIFLOW_HOST);
537
+ assert.equal(state.configured, true);
538
+ assert.equal(state.enabled, true);
539
+ assert.equal(state.configSources.enabled, "session");
722
540
  } finally {
723
- restoreModeOverride(context(cwd));
541
+ await resetSessionOverrides(ctx, pi);
724
542
  await rm(cwd, { recursive: true, force: true });
725
543
  }
726
544
  });
727
545
 
728
- test("Pi restores session core overrides from the active branch", async () => {
729
- const cwd = await mkdtemp(join(tmpdir(), "freeflow-pi-session-core-branch-"));
546
+ test("Pi restores remaining session overrides from the active branch and ignores old mode entries", async () => {
547
+ const cwd = await configuredRepo();
730
548
  try {
731
- await mkdir(join(cwd, ".freeflow"));
732
- await writeFile(join(cwd, ".freeflow/config.json"), JSON.stringify({ defaultMode: "workflow" }), "utf8");
733
549
  const activeBranchEntries = [
550
+ { type: "custom", customType: "freeflow-mode", data: { currentMode: "conversation" } },
734
551
  {
735
552
  type: "custom",
736
553
  customType: "freeflow-session-overrides",
737
- data: { overrides: { interactionContract: false } },
738
- },
739
- ];
740
- const allEntries = [
741
- ...activeBranchEntries,
742
- {
743
- type: "custom",
744
- customType: "freeflow-session-overrides",
745
- data: { overrides: { enabled: false } },
746
- },
747
- ];
748
- const { handlers } = loadExtension();
749
- await handlers.get("session_start")({ reason: "resume" }, context(cwd, allEntries, activeBranchEntries));
750
-
751
- let capabilityState = await readCapabilityState(cwd, undefined, PIFLOW_HOST);
752
- assert.equal(capabilityState.enabled, true);
753
- assert.equal(capabilityState.interactionContract.effective, false);
754
- assert.deepEqual(capabilityState.sessionOverrides, { interactionContract: false });
755
-
756
- const switchedBranchEntries = [
757
- {
758
- type: "custom",
759
- customType: "freeflow-session-overrides",
760
- data: { overrides: { enabled: false } },
554
+ data: { overrides: { enabled: false, contextVirtualization: true } },
761
555
  },
762
556
  ];
763
- await handlers.get("session_tree")({}, context(cwd, allEntries, switchedBranchEntries));
764
- capabilityState = await readCapabilityState(cwd, undefined, PIFLOW_HOST);
765
- assert.equal(capabilityState.enabled, false);
766
- assert.deepEqual(capabilityState.sessionOverrides, { enabled: false });
557
+ restoreSessionOverrides(context(cwd, activeBranchEntries, activeBranchEntries));
558
+ const state = await readCapabilityState(cwd, undefined, PIFLOW_HOST);
559
+ assert.equal(state.enabled, false);
560
+ assert.deepEqual(state.sessionOverrides, { enabled: false, contextVirtualization: true });
561
+ assert.equal("currentMode" in state, false);
767
562
  } finally {
768
- restoreModeOverride(context(cwd));
563
+ await resetSessionOverrides(context(cwd), loadExtension().pi);
769
564
  await rm(cwd, { recursive: true, force: true });
770
565
  }
771
566
  });
772
567
 
773
- test("Pi compact mode context distinguishes layered defaults, session state, and effective mode", async () => {
774
- const cwd = await mkdtemp(join(tmpdir(), "freeflow-pi-layered-mode-context-"));
568
+ test("Pi fails closed when an existing local override is invalid", async () => {
569
+ const cwd = await configuredRepo();
775
570
  try {
776
- await mkdir(join(cwd, ".freeflow"));
777
- await writeFile(join(cwd, ".freeflow/config.json"), JSON.stringify({ defaultMode: "workflow" }, null, 2), "utf8");
778
- await writeFile(
779
- join(cwd, ".freeflow/local.json"),
780
- JSON.stringify({ defaultMode: "strict-workflow" }, null, 2),
781
- "utf8",
782
- );
783
-
784
- const { handlers, commands } = loadExtension();
785
- const beforeAgentStart = handlers.get("before_agent_start");
786
- const freeflowCommand = commands.find((command) => command.name === "freeflow");
787
- assert.ok(beforeAgentStart);
788
- assert.ok(freeflowCommand);
789
-
790
- let result = await beforeAgentStart({ systemPrompt: "base prompt" }, context(cwd));
791
- assert.match(result.systemPrompt, /Use the latest extension-generated Freeflow Runtime State/);
792
- assert.doesNotMatch(result.systemPrompt, /Repository default mode:/);
793
- assert.doesNotMatch(result.systemPrompt, /Configured default mode:/);
794
- assert.doesNotMatch(result.systemPrompt, /Effective Freeflow mode:/);
795
- assert.match(
796
- result.systemPrompt,
797
- /Task type, skill selection, direct skill calls, usefulness, or new evidence does not change it/,
798
- );
799
- assert.doesNotMatch(result.systemPrompt, /## Strict Workflow Overlay/);
800
- assert.doesNotMatch(result.systemPrompt, /security, privacy, billing, data loss, migrations, public interfaces/);
801
- assert.doesNotMatch(result.systemPrompt, /## Conversation Mode Boundary/);
802
- const providerContext = await handlers.get("context")({ messages: [] }, context(cwd));
803
- assert.match(providerContext.messages.at(-1).content, /Default mode: `strict-workflow`/);
804
- assert.match(providerContext.messages.at(-1).content, /Active mode: `strict-workflow`/);
805
-
806
- await freeflowCommand.definition.handler("mode conversation", context(cwd));
807
- result = await beforeAgentStart({ systemPrompt: "base prompt" }, context(cwd));
808
- assert.doesNotMatch(result.systemPrompt, /Session mode override:/);
809
- assert.doesNotMatch(result.systemPrompt, /Resolved mode:/);
810
- assert.doesNotMatch(result.systemPrompt, /Effective Freeflow mode:/);
811
- assert.doesNotMatch(result.systemPrompt, /## Conversation Mode Boundary/);
812
- assert.doesNotMatch(result.systemPrompt, /Do not call write, edit, or mutating tools/);
813
- assert.doesNotMatch(result.systemPrompt, /an execution skill does not override this boundary/);
814
- assert.doesNotMatch(result.systemPrompt, /## Strict Workflow Overlay/);
815
-
816
- await freeflowCommand.definition.handler("mode reset", context(cwd));
571
+ await writeFile(join(cwd, ".freeflow/local.json"), "{ invalid\n", "utf8");
572
+ const layers = await readFreeflowConfigLayers(cwd);
573
+ assert.equal(layers.repositoryConfigured, true);
574
+ assert.equal(layers.configured, false);
575
+ assert.equal(layers.local.valid, false);
576
+ const state = await readCapabilityState(cwd, undefined, PIFLOW_HOST);
577
+ assert.equal(state.configured, false);
578
+ assert.equal(state.enabled, false);
579
+ const { handlers } = loadExtension();
580
+ 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$/);
817
583
  } finally {
818
584
  await rm(cwd, { recursive: true, force: true });
819
585
  }
820
586
  });
821
587
 
822
- test("Pi restores layered session mode on resume and preserves it through compaction", async () => {
823
- const cwd = await mkdtemp(join(tmpdir(), "freeflow-pi-mode-lifecycle-"));
588
+ test("Pi local enablement overrides the repository master switch", async () => {
589
+ const cwd = await configuredRepo({ enabled: true });
824
590
  try {
825
- await mkdir(join(cwd, ".freeflow"));
826
- await writeFile(join(cwd, ".freeflow/config.json"), JSON.stringify({ defaultMode: "workflow" }, null, 2), "utf8");
827
- const localPath = join(cwd, ".freeflow/local.json");
828
- await writeFile(localPath, JSON.stringify({ defaultMode: "strict-workflow" }, null, 2), "utf8");
829
-
830
- const sessionEntries = [
831
- {
832
- type: "custom",
833
- customType: "freeflow-mode",
834
- data: { currentMode: "conversation" },
835
- },
836
- ];
837
- const { handlers } = loadExtension();
838
- const sessionStart = handlers.get("session_start");
839
- const sessionCompact = handlers.get("session_compact");
840
- const beforeAgentStart = handlers.get("before_agent_start");
841
- assert.ok(sessionStart);
842
- assert.ok(sessionCompact);
843
- assert.ok(beforeAgentStart);
844
-
845
- const ctx = context(cwd, sessionEntries);
846
- await sessionStart({ reason: "resume" }, ctx);
847
- let modeState = await readModeState(cwd);
848
- assert.equal(modeState.repositoryDefaultMode, "workflow");
849
- assert.equal(modeState.personalDefaultMode, "strict-workflow");
850
- assert.equal(modeState.sessionMode, "conversation");
851
- assert.equal(modeState.resolvedMode, "conversation");
852
- assert.equal(modeState.effectiveMode, "conversation");
853
-
854
- await sessionCompact({ reason: "manual" }, ctx);
855
- let result = await beforeAgentStart({ prompt: "continue", systemPrompt: "base prompt" }, ctx);
856
- assert.doesNotMatch(result.systemPrompt, /## Conversation Mode Boundary/);
857
- assert.doesNotMatch(result.systemPrompt, /Session mode override:/);
858
- assert.doesNotMatch(result.systemPrompt, /Resolved mode:/);
859
- assert.doesNotMatch(result.systemPrompt, /Effective Freeflow mode:/);
860
- let providerContext = await handlers.get("context")({ messages: [] }, ctx);
861
- assert.match(providerContext.messages.at(-1).content, /Default mode: `strict-workflow`/);
862
- assert.match(providerContext.messages.at(-1).content, /Active mode: `conversation`/);
863
-
864
- await writeFile(
865
- localPath,
866
- JSON.stringify(
867
- {
868
- defaultMode: "strict-workflow",
869
- skills: { enabled: false },
870
- },
871
- null,
872
- 2,
873
- ),
874
- "utf8",
875
- );
876
- await sessionCompact({ reason: "manual" }, ctx);
877
- modeState = await readModeState(cwd);
878
- assert.equal(modeState.sessionMode, "conversation");
879
- assert.equal(modeState.resolvedMode, "conversation");
880
- assert.equal(modeState.active, false);
881
- assert.equal(modeState.effectiveMode, null);
882
- result = await beforeAgentStart({ prompt: "continue", systemPrompt: "base prompt" }, ctx);
883
- assert.doesNotMatch(result.systemPrompt, /Session mode override:/);
884
- assert.doesNotMatch(result.systemPrompt, /Resolved mode:/);
885
- assert.doesNotMatch(result.systemPrompt, /Effective Freeflow mode:/);
886
- providerContext = await handlers.get("context")({ messages: [] }, ctx);
887
- assert.match(providerContext.messages.at(-1).content, /Default mode: `strict-workflow`/);
888
- assert.match(providerContext.messages.at(-1).content, /Active mode: `inactive`/);
889
- assert.match(providerContext.messages.at(-1).content, /Skills: inactive/);
890
-
891
- sessionEntries.push({
892
- type: "custom",
893
- customType: "freeflow-mode",
894
- data: { currentMode: null },
895
- });
896
- await sessionStart({ reason: "resume" }, ctx);
897
- modeState = await readModeState(cwd);
898
- assert.equal(modeState.sessionMode, null);
899
- assert.equal(modeState.resolvedMode, "strict-workflow");
900
- assert.equal(modeState.active, false);
901
- assert.equal(modeState.effectiveMode, null);
591
+ await writeFile(join(cwd, ".freeflow/local.json"), JSON.stringify({ enabled: false }, null, 2), "utf8");
592
+ const state = await readCapabilityState(cwd, undefined, PIFLOW_HOST);
593
+ assert.equal(state.configured, true);
594
+ assert.equal(state.enabled, false);
595
+ assert.equal(state.configSources.enabled, "local");
902
596
  } finally {
903
597
  await rm(cwd, { recursive: true, force: true });
904
598
  }
905
599
  });
906
600
 
907
- test("Pi restores session mode from the active branch on resume and tree navigation", async () => {
908
- const cwd = await mkdtemp(join(tmpdir(), "freeflow-pi-mode-active-branch-"));
601
+ test("Pi master Freeflow toggle suppresses core prompts, skills, and capabilities", async () => {
602
+ const cwd = await configuredRepo({ enabled: false });
909
603
  try {
910
- await mkdir(join(cwd, ".freeflow"));
911
- await writeFile(join(cwd, ".freeflow/config.json"), JSON.stringify({ defaultMode: "workflow" }, null, 2), "utf8");
912
-
913
- const activeBranchEntries = [
914
- {
915
- type: "custom",
916
- customType: "freeflow-mode",
917
- data: { currentMode: "conversation" },
918
- },
919
- ];
920
- const allSessionEntries = [
921
- ...activeBranchEntries,
922
- {
923
- type: "custom",
924
- customType: "freeflow-mode",
925
- data: { currentMode: "strict-workflow" },
926
- },
927
- ];
928
- const { handlers } = loadExtension();
929
- const sessionStart = handlers.get("session_start");
930
- const sessionTree = handlers.get("session_tree");
931
- assert.ok(sessionStart);
932
- assert.ok(sessionTree);
933
-
934
- const ctx = context(cwd, allSessionEntries, activeBranchEntries);
935
- await sessionStart({ reason: "resume" }, ctx);
936
-
937
- let modeState = await readModeState(cwd);
938
- assert.equal(modeState.sessionMode, "conversation");
939
- assert.equal(modeState.effectiveMode, "conversation");
940
-
941
- activeBranchEntries.splice(0, activeBranchEntries.length, allSessionEntries.at(-1));
942
- await sessionTree({ newLeafId: "strict-branch" }, ctx);
943
-
944
- modeState = await readModeState(cwd);
945
- assert.equal(modeState.sessionMode, "strict-workflow");
946
- assert.equal(modeState.effectiveMode, "strict-workflow");
947
- } finally {
948
- restoreModeOverride(context(cwd));
949
- await rm(cwd, { recursive: true, force: true });
950
- }
951
- });
952
-
953
- test("Pi ordinary prompts and direct skill calls do not change mode state", async () => {
954
- const cwd = await mkdtemp(join(tmpdir(), "freeflow-pi-mode-boundaries-"));
955
- try {
956
- await mkdir(join(cwd, ".freeflow"));
957
- await writeFile(join(cwd, ".freeflow/config.json"), JSON.stringify({ defaultMode: "workflow" }, null, 2), "utf8");
958
- await writeFile(
959
- join(cwd, ".freeflow/local.json"),
960
- JSON.stringify({ defaultMode: "strict-workflow" }, null, 2),
961
- "utf8",
962
- );
963
-
964
- const { handlers, commands, entries, sentMessages } = loadExtension();
965
- const sessionStart = handlers.get("session_start");
966
- const beforeAgentStart = handlers.get("before_agent_start");
967
- const freeflowCommand = commands.find((command) => command.name === "freeflow");
968
- const commitWorkCommand = commands.find((command) => command.name === "commit-work");
969
- assert.ok(sessionStart);
970
- assert.ok(beforeAgentStart);
971
- assert.ok(freeflowCommand);
972
- assert.ok(commitWorkCommand);
973
-
974
- await sessionStart({ reason: "startup" }, context(cwd));
975
- let result = await beforeAgentStart(
976
- {
977
- prompt: "Switch to conversation mode and implement this change.",
978
- systemPrompt: "base prompt",
979
- },
980
- context(cwd),
981
- );
982
- let modeState = await readModeState(cwd);
983
- assert.equal(modeState.sessionMode, null);
984
- assert.equal(modeState.resolvedMode, "strict-workflow");
985
- assert.equal(modeState.effectiveMode, "strict-workflow");
986
- assert.match(
987
- result.systemPrompt,
988
- /Task type, skill selection, direct skill calls, usefulness, or new evidence does not change it/,
989
- );
990
- assert.deepEqual(entries, []);
991
-
992
- await freeflowCommand.definition.handler("mode conversation", context(cwd));
993
- assert.equal(entries.length, 1);
994
- await commitWorkCommand.definition.handler("create a checkpoint", context(cwd));
995
- assert.deepEqual(sentMessages, ["/skill:commit-work\n\ncreate a checkpoint"]);
996
- assert.equal(entries.length, 1);
997
-
998
- modeState = await readModeState(cwd);
999
- assert.equal(modeState.sessionMode, "conversation");
1000
- assert.equal(modeState.resolvedMode, "conversation");
1001
- assert.equal(modeState.effectiveMode, "conversation");
1002
- result = await beforeAgentStart({ prompt: "/commit-work", systemPrompt: "base prompt" }, context(cwd));
1003
- assert.doesNotMatch(result.systemPrompt, /Session mode override:/);
1004
- assert.doesNotMatch(result.systemPrompt, /Effective Freeflow mode:/);
1005
- const providerContext = await handlers.get("context")({ messages: [] }, context(cwd));
1006
- assert.match(providerContext.messages.at(-1).content, /Default mode: `strict-workflow`/);
1007
- assert.match(providerContext.messages.at(-1).content, /Active mode: `conversation`/);
1008
- } finally {
1009
- restoreModeOverride(context(cwd));
1010
- await rm(cwd, { recursive: true, force: true });
1011
- }
1012
- });
1013
-
1014
- test("Pi layered core config inherits omitted values and built-in defaults", async () => {
1015
- const cwd = await mkdtemp(join(tmpdir(), "freeflow-pi-layered-inherit-"));
1016
- try {
1017
- await mkdir(join(cwd, ".freeflow"));
1018
- await writeFile(join(cwd, ".freeflow/config.json"), JSON.stringify({}, null, 2), "utf8");
1019
- await writeFile(join(cwd, ".freeflow/local.json"), JSON.stringify({ interactionContract: false }, null, 2), "utf8");
1020
-
1021
- const layers = await readFreeflowConfigLayers(cwd);
1022
- assert.deepEqual(layers.coreConfig, {
1023
- enabled: true,
1024
- interactionContract: false,
1025
- contextVirtualization: false,
1026
- conversationHistory: false,
1027
- skills: { enabled: true },
1028
- defaultMode: "workflow",
1029
- });
1030
- assert.deepEqual(layers.sources, {
1031
- enabled: "builtin",
1032
- interactionContract: "local",
1033
- contextVirtualization: "builtin",
1034
- conversationHistory: "builtin",
1035
- skillsEnabled: "builtin",
1036
- defaultMode: "builtin",
1037
- });
1038
- } finally {
1039
- await rm(cwd, { recursive: true, force: true });
1040
- }
1041
- });
1042
-
1043
- test("Pi requires repository activation even when local overrides exist", async () => {
1044
- const cwd = await mkdtemp(join(tmpdir(), "freeflow-pi-local-only-"));
1045
- try {
1046
- await mkdir(join(cwd, ".freeflow"));
1047
- await writeFile(
1048
- join(cwd, ".freeflow/local.json"),
1049
- JSON.stringify({ enabled: true, defaultMode: "strict-workflow" }, null, 2),
1050
- "utf8",
1051
- );
1052
-
1053
- const layers = await readFreeflowConfigLayers(cwd);
1054
- assert.equal(layers.repositoryConfigured, false);
1055
- assert.equal(layers.configured, false);
1056
- const capabilityState = await readCapabilityState(cwd, undefined, PIFLOW_HOST);
1057
- assert.equal(capabilityState.enabled, false);
1058
- assert.equal(capabilityState.skills.effective, false);
1059
-
1060
- const { handlers } = loadExtension();
1061
- const resources = await handlers.get("resources_discover")({ cwd }, context(cwd));
1062
- assert.equal(resources.skillPaths.length, 1);
1063
- assert.match(resources.skillPaths[0], /setup-freeflow\/SKILL\.md$/);
1064
- } finally {
1065
- await rm(cwd, { recursive: true, force: true });
1066
- }
1067
- });
1068
-
1069
- test("Pi fails closed when an existing local override is invalid", async () => {
1070
- const cwd = await mkdtemp(join(tmpdir(), "freeflow-pi-invalid-local-"));
1071
- try {
1072
- await mkdir(join(cwd, ".freeflow"));
1073
- await writeFile(join(cwd, ".freeflow/config.json"), JSON.stringify({ defaultMode: "workflow" }, null, 2), "utf8");
1074
- await writeFile(join(cwd, ".freeflow/local.json"), "{ invalid\n", "utf8");
1075
-
1076
- const layers = await readFreeflowConfigLayers(cwd);
1077
- assert.equal(layers.repositoryConfigured, true);
1078
- assert.equal(layers.configured, false);
1079
- assert.equal(layers.local.exists, true);
1080
- assert.equal(layers.local.valid, false);
1081
- assert.equal(layers.blockingConfigPath, layers.local.path);
1082
- assert.match(layers.parseError, /JSON/);
1083
-
1084
- const capabilityState = await readCapabilityState(cwd, undefined, PIFLOW_HOST);
1085
- assert.equal(capabilityState.configured, false);
1086
- assert.equal(capabilityState.enabled, false);
1087
- assert.equal(capabilityState.interactionContract.effective, false);
1088
- assert.equal(capabilityState.skills.effective, false);
1089
- assert.equal(capabilityState.localConfigValid, false);
1090
- assert.match(capabilityState.localConfigParseError, /JSON/);
1091
-
1092
- const { handlers } = loadExtension();
1093
- const resources = await handlers.get("resources_discover")({ cwd }, context(cwd));
1094
- assert.equal(resources.skillPaths.length, 1);
1095
- assert.match(resources.skillPaths[0], /setup-freeflow\/SKILL\.md$/);
1096
- const result = await handlers.get("before_agent_start")({ systemPrompt: "base prompt" }, context(cwd));
1097
- assert.equal(result.systemPrompt, "base prompt");
1098
- } finally {
1099
- await rm(cwd, { recursive: true, force: true });
1100
- }
1101
- });
1102
-
1103
- test("Pi fails closed when a local core override has an invalid type", async () => {
1104
- const cwd = await mkdtemp(join(tmpdir(), "freeflow-pi-invalid-local-shape-"));
1105
- try {
1106
- await mkdir(join(cwd, ".freeflow"));
1107
- await writeFile(join(cwd, ".freeflow/config.json"), JSON.stringify({ defaultMode: "workflow" }, null, 2), "utf8");
1108
- await writeFile(join(cwd, ".freeflow/local.json"), JSON.stringify({ skills: { enabled: "no" } }, null, 2), "utf8");
1109
-
1110
- const layers = await readFreeflowConfigLayers(cwd);
1111
- assert.equal(layers.configured, false);
1112
- assert.equal(layers.local.valid, false);
1113
- assert.equal(layers.parseError, "skills.enabled must be a boolean");
1114
- const state = await readCapabilityState(cwd, undefined, PIFLOW_HOST);
1115
- assert.equal(state.enabled, false);
1116
- assert.equal(state.skills.effective, false);
1117
- } finally {
1118
- await rm(cwd, { recursive: true, force: true });
1119
- }
1120
- });
1121
-
1122
- test("Pi local enabled false overrides an enabled repository master switch", async () => {
1123
- const cwd = await mkdtemp(join(tmpdir(), "freeflow-pi-local-disabled-"));
1124
- try {
1125
- await mkdir(join(cwd, ".freeflow"));
1126
- await writeFile(
1127
- join(cwd, ".freeflow/config.json"),
1128
- JSON.stringify(
1129
- {
1130
- enabled: true,
1131
- defaultMode: "workflow",
1132
- },
1133
- null,
1134
- 2,
1135
- ),
1136
- "utf8",
1137
- );
1138
- await writeFile(join(cwd, ".freeflow/local.json"), JSON.stringify({ enabled: false }, null, 2), "utf8");
1139
-
1140
- const state = await readCapabilityState(cwd, undefined, PIFLOW_HOST);
1141
- assert.equal(state.configured, true);
1142
- assert.equal(state.enabled, false);
1143
- assert.equal(state.configSources.enabled, "local");
1144
- } finally {
1145
- await rm(cwd, { recursive: true, force: true });
1146
- }
1147
- });
1148
-
1149
- test("Pi master Freeflow toggle disables skills and capabilities", async () => {
1150
- const cwd = await mkdtemp(join(tmpdir(), "freeflow-pi-master-disabled-"));
1151
- try {
1152
- await mkdir(join(cwd, ".freeflow"));
1153
- await writeFile(
1154
- join(cwd, ".freeflow/config.json"),
1155
- JSON.stringify(
1156
- {
1157
- enabled: false,
1158
- defaultMode: "workflow",
1159
- skills: { enabled: true },
1160
- },
1161
- null,
1162
- 2,
1163
- ),
1164
- "utf8",
1165
- );
1166
-
1167
- const { handlers } = loadExtension();
1168
- const resources = await handlers.get("resources_discover")({ cwd }, context(cwd));
1169
- assert.deepEqual(resources.skillPaths, []);
1170
-
1171
- const result = await handlers.get("before_agent_start")({ systemPrompt: "base prompt" }, context(cwd));
1172
- assert.equal(result.systemPrompt, "base prompt");
1173
- assert.equal(result.message, undefined);
1174
-
1175
- const routed = await handlers.get("tool_result")(
1176
- {
1177
- toolName: "read",
1178
- input: { path: "large.txt" },
1179
- content: [{ type: "text", text: "line 1\nline 2" }],
1180
- isError: false,
1181
- },
1182
- context(cwd),
1183
- );
1184
- assert.equal(routed, undefined);
1185
- } finally {
1186
- await rm(cwd, { recursive: true, force: true });
1187
- }
1188
- });
1189
-
1190
- test("Pi skills toggle suppresses workflow skills without enabling retired router tools", async () => {
1191
- const cwd = await mkdtemp(join(tmpdir(), "freeflow-pi-skills-disabled-router-on-"));
1192
- try {
1193
- await mkdir(join(cwd, ".freeflow"));
1194
- await writeFile(
1195
- join(cwd, ".freeflow/config.json"),
1196
- JSON.stringify(
1197
- {
1198
- defaultMode: "workflow",
1199
- skills: { enabled: false },
1200
- },
1201
- null,
1202
- 2,
1203
- ),
1204
- "utf8",
1205
- );
1206
-
1207
604
  const { handlers, activeToolNames } = loadExtension();
1208
- const resources = await handlers.get("resources_discover")({ cwd }, context(cwd));
1209
- assert.deepEqual(resources.skillPaths, []);
1210
-
1211
- const ctx = context(cwd);
1212
- const result = await handlers.get("before_agent_start")({ systemPrompt: "base prompt" }, ctx);
1213
- assert.equal(result.message, undefined);
1214
- assert.match(result.systemPrompt, /# Freeflow Interaction Contract/);
1215
- assert.doesNotMatch(result.systemPrompt, /# Freeflow Runtime Kernel/);
1216
- assert.doesNotMatch(result.systemPrompt, /## Loaded Workflow Skill/);
1217
- assert.doesNotMatch(result.systemPrompt, /## Loaded Decision Gate Skill/);
1218
- assert.doesNotMatch(result.systemPrompt, /## Discovery-light/);
1219
- assert.doesNotMatch(result.systemPrompt, /Repository default mode:/);
1220
- assert.doesNotMatch(result.systemPrompt, /Personal default override:/);
1221
- assert.doesNotMatch(result.systemPrompt, /Configured default mode:/);
1222
- assert.doesNotMatch(result.systemPrompt, /Resolved mode:/);
1223
- assert.doesNotMatch(result.systemPrompt, /Effective Freeflow mode:/);
1224
- const providerContext = await handlers.get("context")({ messages: [] }, ctx);
1225
- assert.match(providerContext.messages.at(-1).content, /Default mode: `workflow`/);
1226
- assert.match(providerContext.messages.at(-1).content, /Active mode: `inactive`/);
1227
- assert.match(providerContext.messages.at(-1).content, /Skills: inactive/);
1228
- assert.doesNotMatch(result.systemPrompt, /Output Router|freeflow_(search|run|batch)/i);
1229
- assert.equal(ctx.statuses.at(-1).value, "freeflow: interaction");
1230
- assert.ok(!activeToolNames().includes("freeflow_context"));
1231
- } finally {
1232
- await rm(cwd, { recursive: true, force: true });
1233
- }
1234
- });
1235
-
1236
- test("Pi interaction contract can be disabled independently while Workflow stays enabled", async () => {
1237
- const cwd = await mkdtemp(join(tmpdir(), "freeflow-pi-interaction-disabled-skills-on-"));
1238
- try {
1239
- await mkdir(join(cwd, ".freeflow"));
1240
- await writeFile(
1241
- join(cwd, ".freeflow/config.json"),
1242
- JSON.stringify({ defaultMode: "workflow", interactionContract: false }, null, 2),
1243
- "utf8",
1244
- );
1245
- const { handlers } = loadExtension();
1246
- const result = await handlers.get("before_agent_start")(
1247
- { prompt: "hello", systemPrompt: "base prompt" },
1248
- context(cwd),
1249
- );
1250
- assert.doesNotMatch(result.systemPrompt, /# Freeflow Interaction Contract/);
1251
- assert.equal(result.message, undefined);
1252
- assert.match(result.systemPrompt, /# Freeflow Stable Guidance/);
1253
- assert.match(result.systemPrompt, /## Shared Terms/);
1254
- assert.doesNotMatch(result.systemPrompt, /# Freeflow Interaction Contract/);
1255
- } finally {
1256
- await rm(cwd, { recursive: true, force: true });
1257
- }
1258
- });
1259
-
1260
- // Regression for the "everything disabled except Freeflow control plane" state.
1261
- // The model should know Freeflow exists and can be reconfigured, but should not receive workflow behavior.
1262
- // `defaultMode` is dormant while Skills are off.
1263
- test("Pi all-disabled capability state injects only Freeflow control-plane status", async () => {
1264
- const cwd = await mkdtemp(join(tmpdir(), "freeflow-pi-all-capabilities-disabled-"));
1265
- try {
1266
- await mkdir(join(cwd, ".freeflow"));
1267
- await writeFile(
1268
- join(cwd, ".freeflow/config.json"),
1269
- JSON.stringify(
1270
- {
1271
- defaultMode: "workflow",
1272
- interactionContract: false,
1273
- skills: { enabled: false },
1274
- },
1275
- null,
1276
- 2,
1277
- ),
1278
- "utf8",
1279
- );
1280
-
1281
- const { handlers, commands, activeToolNames } = loadExtension();
1282
605
  const ctx = context(cwd);
606
+ const resources = await handlers.get("resources_discover")({ cwd }, ctx);
607
+ assert.deepEqual(resources.skillPaths, []);
1283
608
  const result = await handlers.get("before_agent_start")({ systemPrompt: "base prompt" }, ctx);
1284
-
1285
- assert.match(result.systemPrompt, /# Freeflow Stable Guidance/);
1286
- assert.match(result.systemPrompt, /## Mode/);
1287
- assert.doesNotMatch(result.systemPrompt, /## Shared Terms/);
1288
- assert.doesNotMatch(result.systemPrompt, /# Freeflow Interaction Contract/);
609
+ assert.equal(result.systemPrompt, "base prompt");
1289
610
  const providerContext = await handlers.get("context")({ messages: [] }, ctx);
1290
- assert.match(providerContext.messages.at(-1).content, /Default mode: `workflow`/);
1291
- assert.match(providerContext.messages.at(-1).content, /Active mode: `inactive`/);
1292
- assert.match(providerContext.messages.at(-1).content, /Interaction Contract: inactive/);
1293
- assert.match(providerContext.messages.at(-1).content, /Skills: inactive/);
1294
- assert.doesNotMatch(result.systemPrompt, /# Freeflow Runtime Kernel/);
1295
- assert.doesNotMatch(result.systemPrompt, /## Loaded Mode Contract Skill/);
1296
- assert.doesNotMatch(result.systemPrompt, /## Loaded Workflow Skill/);
1297
- assert.doesNotMatch(result.systemPrompt, /## Loaded Decision Gate Skill/);
1298
- assert.doesNotMatch(result.systemPrompt, /Output Router|freeflow_(search|run|batch)/i);
1299
- assert.equal(ctx.statuses.at(-1).value, "freeflow: idle");
1300
- assert.ok(!activeToolNames().includes("freeflow_context"));
1301
-
1302
- const freeflowCommand = commands.find((command) => command.name === "freeflow");
1303
- assert.ok(freeflowCommand);
1304
- const modeCtx = context(cwd);
1305
- await freeflowCommand.definition.handler("mode strict-workflow", modeCtx);
1306
- assert.match(modeCtx.notifications.at(-1).message, /Freeflow modes are inactive because Skills are disabled/);
1307
- assert.match(modeCtx.notifications.at(-1).message, /Configured default mode: workflow \(repository default\)/);
1308
- } finally {
1309
- await rm(cwd, { recursive: true, force: true });
1310
- }
1311
- });
1312
-
1313
- test("Pi /freeflow mode is the only mode command and opens a dedicated selector", async () => {
1314
- const cwd = await mkdtemp(join(tmpdir(), "freeflow-pi-mode-command-"));
1315
- try {
1316
- await mkdir(join(cwd, ".freeflow"));
1317
- const configPath = join(cwd, ".freeflow/config.json");
1318
- const configText = JSON.stringify({ defaultMode: "workflow" }, null, 2);
1319
- await writeFile(configPath, configText, "utf8");
1320
-
1321
- const { commands, entries } = loadExtension();
1322
- const freeflowCommand = commands.find((command) => command.name === "freeflow");
1323
- assert.ok(freeflowCommand);
1324
- assert.ok(!commands.some((command) => command.name === "workflow"));
1325
-
1326
- const alreadyDefaultCtx = context(cwd);
1327
- await freeflowCommand.definition.handler("mode workflow", alreadyDefaultCtx);
1328
- assert.equal(entries.length, 0);
1329
- assert.match(
1330
- alreadyDefaultCtx.notifications.at(-1).message,
1331
- /already in workflow mode from the configured default.*No session override was created/,
1332
- );
1333
-
1334
- const modeCtx = context(cwd);
1335
- modeCtx.ui.custom = async (factory) => {
1336
- let result;
1337
- const component = factory({ requestRender() {} }, testTheme, {}, (value) => {
1338
- result = value;
1339
- });
1340
- const text = renderText(component);
1341
- assert.match(text, /Freeflow Mode/);
1342
- assert.match(text, /Use configured default\s+workflow from repository/);
1343
- assert.match(text, /conversation\s+Discussion and read-only inspection/);
1344
- assert.match(text, /strict-workflow\s+Stronger decision and evidence pressure for high-risk work/);
1345
-
1346
- component.handleInput("\u001b[B");
1347
- component.handleInput("\u001b[B");
1348
- component.handleInput("\u001b[B");
1349
- component.handleInput("\r");
1350
- return result;
1351
- };
1352
-
1353
- await freeflowCommand.definition.handler("mode", modeCtx);
1354
- assert.deepEqual(entries.at(-1), {
1355
- customType: "freeflow-mode",
1356
- data: { currentMode: "strict-workflow" },
1357
- });
1358
- assert.equal(modeCtx.statuses.length, 1);
1359
- assert.equal(modeCtx.statuses.at(-1).value, "freeflow: interaction · strict-workflow (session)");
1360
- assert.match(modeCtx.notifications.at(-1).message, /Stored in Pi session history/);
1361
- assert.match(
1362
- modeCtx.notifications.at(-1).message,
1363
- /\.freeflow\/local\.json and \.freeflow\/config\.json were not changed/,
1364
- );
1365
- assert.equal(await readFile(configPath, "utf8"), configText);
1366
- assert.equal(modeCtx.reloads.length, 0);
1367
-
1368
- const alreadySessionCtx = context(cwd);
1369
- await freeflowCommand.definition.handler("mode strict-workflow", alreadySessionCtx);
1370
- assert.equal(entries.length, 1);
1371
- assert.match(alreadySessionCtx.notifications.at(-1).message, /already in strict-workflow mode for this Pi session/);
1372
-
1373
- const resetCtx = context(cwd);
1374
- await freeflowCommand.definition.handler("mode reset", resetCtx);
1375
- assert.deepEqual(entries.at(-1), {
1376
- customType: "freeflow-mode",
1377
- data: { currentMode: null },
1378
- });
1379
- assert.equal(resetCtx.statuses.at(-1).value, "freeflow: interaction · workflow");
1380
- assert.match(resetCtx.notifications.at(-1).message, /reset to configured default: workflow \(repository default\)/);
1381
- assert.match(resetCtx.notifications.at(-1).message, /Session override cleared/);
1382
- assert.match(
1383
- resetCtx.notifications.at(-1).message,
1384
- /\.freeflow\/local\.json and \.freeflow\/config\.json were not changed/,
1385
- );
1386
- assert.equal(entries.length, 2);
1387
-
1388
- const alreadyResetCtx = context(cwd);
1389
- await freeflowCommand.definition.handler("mode reset", alreadyResetCtx);
1390
- assert.equal(entries.length, 2);
1391
- assert.match(alreadyResetCtx.notifications.at(-1).message, /already using the configured default: workflow/);
1392
-
1393
- const nonTuiCtx = context(cwd);
1394
- await freeflowCommand.definition.handler("mode", nonTuiCtx);
1395
- assert.match(
1396
- nonTuiCtx.notifications.at(-1).message,
1397
- /Freeflow mode is workflow \(configured default workflow \(repository default\)\)/,
611
+ assert.match(lastRuntimeState(providerContext.messages).content, /Freeflow: inactive/);
612
+ assert.doesNotMatch(
613
+ lastRuntimeState(providerContext.messages).content,
614
+ /Default mode|Active mode|Interaction Contract|Skills/,
1398
615
  );
1399
- assert.doesNotMatch(nonTuiCtx.notifications.at(-1).message, /requires Pi TUI/);
1400
- } finally {
1401
- await rm(cwd, { recursive: true, force: true });
1402
- }
1403
- });
1404
-
1405
- test("Pi selector commands explain the TUI requirement in RPC mode without mutation", async () => {
1406
- const cwd = await mkdtemp(join(tmpdir(), "freeflow-pi-rpc-selector-guidance-"));
1407
- try {
1408
- await mkdir(join(cwd, ".freeflow"));
1409
- const configPath = join(cwd, ".freeflow/config.json");
1410
- const configText = JSON.stringify({ defaultMode: "workflow" }, null, 2);
1411
- await writeFile(configPath, configText, "utf8");
1412
-
1413
- const { commands } = loadExtension();
1414
- const freeflowCommand = commands.find((command) => command.name === "freeflow");
1415
- assert.ok(freeflowCommand);
1416
-
1417
- for (const args of ["settings", "mode"]) {
1418
- const ctx = context(cwd);
1419
- ctx.mode = "rpc";
1420
- ctx.hasUI = true;
1421
- let customCalls = 0;
1422
- ctx.ui.custom = async () => {
1423
- customCalls += 1;
1424
- };
1425
-
1426
- await freeflowCommand.definition.handler(args, ctx);
1427
-
1428
- assert.equal(customCalls, 0);
1429
- assert.match(ctx.notifications.at(-1).message, /requires? Pi TUI mode/);
1430
- }
1431
-
1432
- assert.equal(await readFile(configPath, "utf8"), configText);
1433
- assert.equal((await readModeState(cwd)).sessionMode, null);
1434
-
1435
- const explicitModeCtx = context(cwd);
1436
- explicitModeCtx.mode = "rpc";
1437
- explicitModeCtx.hasUI = true;
1438
- await freeflowCommand.definition.handler("mode conversation", explicitModeCtx);
1439
- assert.equal((await readModeState(cwd)).sessionMode, "conversation");
1440
- } finally {
1441
- restoreModeOverride(context(cwd));
1442
- await rm(cwd, { recursive: true, force: true });
1443
- }
1444
- });
1445
-
1446
- test("Pi selector commands fail explicitly when notifications are unavailable", async () => {
1447
- const cwd = await mkdtemp(join(tmpdir(), "freeflow-pi-print-selector-guidance-"));
1448
- try {
1449
- await mkdir(join(cwd, ".freeflow"));
1450
- const configPath = join(cwd, ".freeflow/config.json");
1451
- const configText = JSON.stringify({ defaultMode: "workflow" }, null, 2);
1452
- await writeFile(configPath, configText, "utf8");
1453
-
1454
- const { commands } = loadExtension();
1455
- const freeflowCommand = commands.find((command) => command.name === "freeflow");
1456
- assert.ok(freeflowCommand);
1457
-
1458
- for (const mode of ["json", "print"]) {
1459
- for (const args of ["settings", "mode"]) {
1460
- const ctx = context(cwd);
1461
- ctx.mode = mode;
1462
- ctx.hasUI = false;
1463
- ctx.ui.custom = async () => undefined;
1464
- await assert.rejects(freeflowCommand.definition.handler(args, ctx), /requires? Pi TUI mode/);
1465
- }
1466
- }
1467
-
1468
- assert.equal(await readFile(configPath, "utf8"), configText);
1469
- assert.equal((await readModeState(cwd)).sessionMode, null);
1470
- } finally {
1471
- restoreModeOverride(context(cwd));
1472
- await rm(cwd, { recursive: true, force: true });
1473
- }
1474
- });
1475
-
1476
- test("Pi non-TUI selector guidance precedes inactive and invalid configuration state", async () => {
1477
- const cases = [
1478
- {
1479
- name: "disabled",
1480
- args: "mode",
1481
- repository: JSON.stringify({ enabled: false, defaultMode: "workflow" }),
1482
- },
1483
- {
1484
- name: "skills-off",
1485
- args: "mode",
1486
- repository: JSON.stringify({ defaultMode: "workflow", skills: { enabled: false } }),
1487
- },
1488
- {
1489
- name: "invalid-repository",
1490
- args: "settings",
1491
- repository: "{ invalid\n",
1492
- },
1493
- {
1494
- name: "invalid-local",
1495
- args: "settings",
1496
- repository: JSON.stringify({ defaultMode: "workflow" }),
1497
- local: "{ invalid\n",
1498
- },
1499
- ];
1500
-
1501
- for (const [index, testCase] of cases.entries()) {
1502
- const cwd = await mkdtemp(join(tmpdir(), `freeflow-pi-non-tui-${testCase.name}-`));
1503
- try {
1504
- await mkdir(join(cwd, ".freeflow"));
1505
- const configPath = join(cwd, ".freeflow/config.json");
1506
- const localPath = join(cwd, ".freeflow/local.json");
1507
- await writeFile(configPath, testCase.repository, "utf8");
1508
- if (testCase.local) await writeFile(localPath, testCase.local, "utf8");
1509
-
1510
- const { commands } = loadExtension();
1511
- const freeflowCommand = commands.find((command) => command.name === "freeflow");
1512
- assert.ok(freeflowCommand);
1513
-
1514
- const rpcCtx = context(cwd);
1515
- rpcCtx.mode = "rpc";
1516
- rpcCtx.hasUI = true;
1517
- let customCalls = 0;
1518
- rpcCtx.ui.custom = async () => {
1519
- customCalls += 1;
1520
- };
1521
- await freeflowCommand.definition.handler(testCase.args, rpcCtx);
1522
- assert.equal(customCalls, 0);
1523
- assert.match(rpcCtx.notifications.at(-1).message, /requires? Pi TUI mode/);
1524
-
1525
- const nonUiCtx = context(cwd);
1526
- nonUiCtx.mode = index % 2 === 0 ? "json" : "print";
1527
- nonUiCtx.hasUI = false;
1528
- nonUiCtx.ui.custom = async () => undefined;
1529
- await assert.rejects(freeflowCommand.definition.handler(testCase.args, nonUiCtx), /requires? Pi TUI mode/);
1530
-
1531
- assert.equal(await readFile(configPath, "utf8"), testCase.repository);
1532
- if (testCase.local) assert.equal(await readFile(localPath, "utf8"), testCase.local);
1533
- assert.equal((await readModeState(cwd)).sessionMode, null);
1534
- } finally {
1535
- restoreModeOverride(context(cwd));
1536
- await rm(cwd, { recursive: true, force: true });
1537
- }
1538
- }
1539
- });
1540
-
1541
- test("Pi statusline reports only effective Freeflow runtime state", async () => {
1542
- const cwd = await mkdtemp(join(tmpdir(), "freeflow-pi-statusline-state-"));
1543
- try {
1544
- await mkdir(join(cwd, ".freeflow"));
1545
- const configPath = join(cwd, ".freeflow/config.json");
1546
- const { pi, handlers, commands } = loadExtension();
1547
- const beforeAgentStart = handlers.get("before_agent_start");
1548
- const sessionStart = handlers.get("session_start");
1549
- const freeflowCommand = commands.find((command) => command.name === "freeflow");
1550
- assert.ok(beforeAgentStart);
1551
- assert.ok(sessionStart);
1552
- assert.ok(freeflowCommand);
1553
-
1554
- const statusFor = async (config) => {
1555
- if (config === null) {
1556
- await rm(configPath, { force: true });
1557
- } else if (typeof config === "string") {
1558
- await writeFile(configPath, config, "utf8");
1559
- } else {
1560
- await writeFile(configPath, JSON.stringify(config, null, 2), "utf8");
1561
- }
1562
- const ctx = context(cwd);
1563
- await sessionStart({ reason: "reload" }, ctx);
1564
- await beforeAgentStart({ systemPrompt: "base prompt" }, ctx);
1565
- return ctx.statuses.at(-1).value;
1566
- };
1567
-
1568
- assert.equal(await statusFor(null), "freeflow: setup needed");
1569
- assert.equal(await statusFor("{ invalid"), "freeflow: config error");
1570
- assert.equal(await statusFor({ enabled: false, defaultMode: "workflow" }), "freeflow: off");
1571
- assert.equal(await statusFor({ defaultMode: "workflow", skills: { enabled: false } }), "freeflow: interaction");
1572
- assert.equal(
1573
- await statusFor({
1574
- defaultMode: "workflow",
1575
- skills: { enabled: false },
1576
- }),
1577
- "freeflow: interaction",
1578
- );
1579
- assert.equal(
1580
- await statusFor({
1581
- defaultMode: "workflow",
1582
- }),
1583
- "freeflow: interaction · workflow",
1584
- );
1585
-
1586
- const masterOverrideCtx = context(cwd);
1587
- await setSessionCoreOverride("enabled", false, masterOverrideCtx, pi);
1588
- await beforeAgentStart({ systemPrompt: "base prompt" }, masterOverrideCtx);
1589
- assert.equal(masterOverrideCtx.statuses.at(-1).value, "freeflow: off (session)");
1590
- await resetSessionOverrides(masterOverrideCtx, pi);
1591
-
1592
- const interactionOverrideCtx = context(cwd);
1593
- await setSessionCoreOverride("interactionContract", false, interactionOverrideCtx, pi);
1594
- const interactionRuntime = await beforeAgentStart({ systemPrompt: "base prompt" }, interactionOverrideCtx);
1595
- assert.doesNotMatch(interactionRuntime.systemPrompt, /Interaction contract:/);
1596
- const interactionProviderContext = await handlers.get("context")({ messages: [] }, interactionOverrideCtx);
1597
- assert.match(interactionProviderContext.messages.at(-1).content, /Interaction Contract: inactive/);
1598
- assert.equal(interactionOverrideCtx.statuses.at(-1).value, "freeflow: interaction off (session) · workflow");
1599
- await resetSessionOverrides(interactionOverrideCtx, pi);
1600
-
1601
- const skillsOverrideCtx = context(cwd);
1602
- await setSessionCoreOverride("skillsEnabled", false, skillsOverrideCtx, pi);
1603
- await beforeAgentStart({ systemPrompt: "base prompt" }, skillsOverrideCtx);
1604
- assert.equal(skillsOverrideCtx.statuses.at(-1).value, "freeflow: interaction · skills off (session)");
1605
- await resetSessionOverrides(skillsOverrideCtx, pi);
1606
-
1607
- const modeCtx = context(cwd);
1608
- await freeflowCommand.definition.handler("mode conversation", modeCtx);
1609
- assert.equal(modeCtx.statuses.at(-1).value, "freeflow: interaction · conversation (session)");
1610
- await freeflowCommand.definition.handler("mode reset", context(cwd));
1611
- } finally {
1612
- await rm(cwd, { recursive: true, force: true });
1613
- }
1614
- });
1615
-
1616
- test("Pi /freeflow command toggles master switch and blocks inactive settings rows", async () => {
1617
- const cwd = await mkdtemp(join(tmpdir(), "freeflow-pi-freeflow-settings-"));
1618
- try {
1619
- await mkdir(join(cwd, ".freeflow"));
1620
- await writeFile(
1621
- join(cwd, ".freeflow/config.json"),
1622
- JSON.stringify(
1623
- {
1624
- enabled: false,
1625
- defaultMode: "workflow",
1626
- },
1627
- null,
1628
- 2,
1629
- ),
1630
- "utf8",
1631
- );
1632
-
1633
- const { commands } = loadExtension();
1634
- const freeflowCommand = commands.find((command) => command.name === "freeflow");
1635
- assert.ok(freeflowCommand);
1636
-
1637
- const settingsCtx = context(cwd);
1638
- settingsCtx.ui.custom = async (factory, options) => {
1639
- assert.equal(options, undefined);
1640
- let result;
1641
- const dimTheme = {
1642
- ...testTheme,
1643
- fg(color, text) {
1644
- return color === "dim" ? `[dim]${text}[/dim]` : text;
1645
- },
1646
- };
1647
- const component = factory({ requestRender() {} }, dimTheme, {}, (value) => {
1648
- result = value;
1649
- });
1650
- const rootText = renderText(component);
1651
- assert.match(rootText, /^─+/);
1652
- assert.match(rootText, /Freeflow Settings/);
1653
- assert.doesNotMatch(rootText, /Output Router|Native safety net/);
1654
- assert.match(rootText, /\[dim\]Interaction Contract/);
1655
- assert.match(rootText, /\[dim\]Skills/);
1656
- assert.match(rootText, /Freeflow Context/);
1657
- component.handleInput("\u001b[B"); // Interaction Contract row is inactive while Freeflow is off.
1658
- component.handleInput("\r");
1659
- component.handleInput("\u001b");
1660
- return result;
1661
- };
1662
-
1663
- await freeflowCommand.definition.handler("settings", settingsCtx);
1664
- const afterInactiveEdit = JSON.parse(await readFile(join(cwd, ".freeflow/config.json"), "utf8"));
1665
- assert.deepEqual(afterInactiveEdit, {
1666
- enabled: false,
1667
- defaultMode: "workflow",
1668
- });
1669
- assert.equal(settingsCtx.reloads.length, 0);
1670
-
1671
- const enableCtx = context(cwd);
1672
- await freeflowCommand.definition.handler("enable", enableCtx);
1673
- const afterEnable = JSON.parse(await readFile(join(cwd, ".freeflow/config.json"), "utf8"));
1674
- assert.equal(afterEnable.enabled, undefined);
1675
- assert.equal(afterEnable.defaultMode, "workflow");
1676
- assert.equal(afterEnable.outputRouter, undefined);
1677
- assert.equal(enableCtx.reloads.length, 1);
1678
- } finally {
1679
- await rm(cwd, { recursive: true, force: true });
1680
- }
1681
- });
1682
-
1683
- test("Pi statusline uses one umbrella context label for either enabled context feature", async () => {
1684
- const cwd = await mkdtemp(join(tmpdir(), "freeflow-pi-context-status-"));
1685
- try {
1686
- await mkdir(join(cwd, ".freeflow"));
1687
- await writeFile(
1688
- join(cwd, ".freeflow/config.json"),
1689
- JSON.stringify({ contextVirtualization: true, conversationHistory: true }, null, 2),
1690
- "utf8",
1691
- );
1692
- const { handlers } = loadExtension();
1693
- const ctx = context(cwd);
1694
- await handlers.get("session_start")({ reason: "startup" }, ctx);
1695
- const status = ctx.statuses.at(-1).value;
1696
- assert.match(status, /context/);
1697
- assert.doesNotMatch(status, /context virtualization|conversation history/);
1698
- } finally {
1699
- await rm(cwd, { recursive: true, force: true });
1700
- }
1701
- });
1702
-
1703
- test("Pi /freeflow disable applies live gates before reload completes", async () => {
1704
- const cwd = await mkdtemp(join(tmpdir(), "freeflow-pi-disable-live-gates-"));
1705
- try {
1706
- await mkdir(join(cwd, ".freeflow"));
1707
- await writeFile(
1708
- join(cwd, ".freeflow/config.json"),
1709
- JSON.stringify(
1710
- {
1711
- defaultMode: "workflow",
1712
- },
1713
- null,
1714
- 2,
1715
- ),
1716
- "utf8",
1717
- );
1718
-
1719
- const { commands } = loadExtension();
1720
- const freeflowCommand = commands.find((command) => command.name === "freeflow");
1721
- assert.ok(freeflowCommand);
1722
- const ctx = context(cwd);
1723
- await freeflowCommand.definition.handler("disable", ctx);
1724
-
1725
- assert.equal(ctx.reloads.length, 1);
1726
- assert.equal(ctx.statuses.at(-1).value, "freeflow: off");
1727
- } finally {
1728
- await rm(cwd, { recursive: true, force: true });
1729
- }
1730
- });
1731
-
1732
- test("Pi /freeflow settings marks default mode dormant when Skills are disabled", async () => {
1733
- const cwd = await mkdtemp(join(tmpdir(), "freeflow-pi-settings-skills-off-mode-dormant-"));
1734
- try {
1735
- await mkdir(join(cwd, ".freeflow"));
1736
- await writeFile(
1737
- join(cwd, ".freeflow/config.json"),
1738
- JSON.stringify({ defaultMode: "workflow", skills: { enabled: false } }, null, 2),
1739
- "utf8",
1740
- );
1741
-
1742
- const { commands } = loadExtension();
1743
- const freeflowCommand = commands.find((command) => command.name === "freeflow");
1744
- assert.ok(freeflowCommand);
1745
-
1746
- const settingsCtx = context(cwd);
1747
- settingsCtx.ui.custom = async (factory) => {
1748
- let result;
1749
- const component = factory({ requestRender() {} }, testTheme, {}, (value) => {
1750
- result = value;
1751
- });
1752
- const rootText = renderText(component);
1753
- assert.match(rootText, /Skills\s+disabled \(repository\)/);
1754
- assert.match(rootText, /Cognitive Routing\s+disabled \(5\) disabled · inactive/);
1755
- assert.match(rootText, /Session mode\s+default \(workflow · repository · inactive\)/);
1756
- assert.match(rootText, /Default mode\s+workflow \(repository · inactive\)/);
1757
-
1758
- component.handleInput("\u001b[B");
1759
- component.handleInput("\u001b[B");
1760
- component.handleInput("\u001b[B");
1761
- component.handleInput("\u001b[B");
1762
- component.handleInput("\r");
1763
- const choiceText = renderText(component);
1764
- assert.match(choiceText, /Freeflow Settings · Personal overrides › Default mode/);
1765
- assert.match(choiceText, /conversation\s+Discussion and read-only inspection/);
1766
- assert.match(choiceText, /workflow\s+Adaptive workflow for consequential work/);
1767
- assert.match(choiceText, /strict-workflow\s+Stronger decision and evidence pressure for high-risk work/);
1768
- component.handleInput("\u001b[B");
1769
- component.handleInput("\u001b[B");
1770
- component.handleInput("\u001b[B");
1771
- component.handleInput("\r");
1772
- component.handleInput("\u001b");
1773
- return result;
1774
- };
1775
-
1776
- await freeflowCommand.definition.handler("settings", settingsCtx);
1777
- const repositoryAfter = JSON.parse(await readFile(join(cwd, ".freeflow/config.json"), "utf8"));
1778
- const localAfter = JSON.parse(await readFile(join(cwd, ".freeflow/local.json"), "utf8"));
1779
- assert.equal(repositoryAfter.defaultMode, "workflow");
1780
- assert.equal(repositoryAfter.skills.enabled, false);
1781
- assert.equal(localAfter.defaultMode, "strict-workflow");
1782
- assert.equal(settingsCtx.reloads.length, 1);
1783
- } finally {
1784
- await rm(cwd, { recursive: true, force: true });
1785
- }
1786
- });
1787
-
1788
- test("Pi /freeflow settings toggles the interaction contract independently", async () => {
1789
- const cwd = await mkdtemp(join(tmpdir(), "freeflow-pi-settings-interaction-contract-"));
1790
- try {
1791
- await mkdir(join(cwd, ".freeflow"));
1792
- const configPath = join(cwd, ".freeflow/config.json");
1793
- await writeFile(configPath, JSON.stringify({ defaultMode: "workflow" }, null, 2), "utf8");
1794
- await execFileAsync("git", ["init", "-q", cwd]);
1795
- const { commands, handlers } = loadExtension();
1796
- const freeflowCommand = commands.find((command) => command.name === "freeflow");
1797
- assert.ok(freeflowCommand);
1798
- const settingsCtx = context(cwd);
1799
- settingsCtx.ui.custom = async (factory) => {
1800
- let result;
1801
- const component = factory({ requestRender() {} }, testTheme, {}, (value) => {
1802
- result = value;
1803
- });
1804
- assert.match(renderText(component), /Interaction Contract\s+enabled \(builtin\)/);
1805
- component.handleInput("\u001b[B");
1806
- component.handleInput("\r");
1807
- component.handleInput("\u001b[B");
1808
- component.handleInput("\u001b[B");
1809
- component.handleInput("\r");
1810
- component.handleInput("\u001b");
1811
- return result;
1812
- };
1813
- await freeflowCommand.definition.handler("settings", settingsCtx);
1814
- const after = JSON.parse(await readFile(configPath, "utf8"));
1815
- const localAfter = JSON.parse(await readFile(join(cwd, ".freeflow/local.json"), "utf8"));
1816
- assert.equal(after.interactionContract, undefined);
1817
- assert.equal(after.skills, undefined);
1818
- assert.equal(localAfter.interactionContract, false);
1819
- await execFileAsync("git", ["-C", cwd, "check-ignore", "-q", "--", ".freeflow/local.json"]);
1820
- assert.equal(settingsCtx.reloads.length, 1);
1821
- const runtime = await handlers.get("before_agent_start")(
1822
- { prompt: "hello", systemPrompt: "base prompt" },
1823
- context(cwd),
1824
- );
1825
- assert.doesNotMatch(runtime.systemPrompt, /# Freeflow Interaction Contract/);
1826
- assert.equal(runtime.message, undefined);
1827
- const enableCtx = context(cwd);
1828
- enableCtx.ui.custom = async (factory) => {
1829
- let result;
1830
- const component = factory({ requestRender() {} }, testTheme, {}, (value) => {
1831
- result = value;
1832
- });
1833
- assert.match(renderText(component), /Interaction Contract\s+disabled \(local\)/);
1834
- component.handleInput("\u001b[B");
1835
- component.handleInput("\r");
1836
- component.handleInput("\u001b[B");
1837
- component.handleInput("\r");
1838
- component.handleInput("\u001b");
1839
- return result;
1840
- };
1841
- await freeflowCommand.definition.handler("settings", enableCtx);
1842
- const reenabled = JSON.parse(await readFile(configPath, "utf8"));
1843
- assert.equal(reenabled.interactionContract, undefined);
1844
- await assert.rejects(readFile(join(cwd, ".freeflow/local.json"), "utf8"), (error) => error?.code === "ENOENT");
1845
- assert.equal(enableCtx.reloads.length, 1);
1846
- } finally {
1847
- await rm(cwd, { recursive: true, force: true });
1848
- }
1849
- });
1850
-
1851
- test("Pi session settings override Freeflow without changing config", async () => {
1852
- const cwd = await mkdtemp(join(tmpdir(), "freeflow-pi-session-settings-"));
1853
- try {
1854
- await mkdir(join(cwd, ".freeflow"));
1855
- const configPath = join(cwd, ".freeflow/config.json");
1856
- const configText = JSON.stringify({ defaultMode: "workflow" }, null, 2);
1857
- await writeFile(configPath, configText, "utf8");
1858
-
1859
- const { commands, entries } = loadExtension();
1860
- const freeflowCommand = commands.find((command) => command.name === "freeflow");
1861
- assert.ok(freeflowCommand);
1862
- const settingsCtx = context(cwd);
1863
- settingsCtx.ui.custom = async (factory) => {
1864
- let result;
1865
- const component = factory({ requestRender() {} }, testTheme, {}, (value) => {
1866
- result = value;
1867
- });
1868
- const rootText = renderText(component);
1869
- assert.match(rootText, /Freeflow Settings · Session overrides/);
1870
- assert.match(rootText, /Freeflow\s+enabled \(builtin\)/);
1871
- assert.match(rootText, /Interaction Contract\s+enabled \(builtin\)/);
1872
- assert.match(rootText, /Skills\s+enabled \(builtin\)/);
1873
- assert.match(rootText, /Mode\s+default \(workflow · repository\)/);
1874
- assert.match(rootText, /Cognitive Routing\s+auto unavailable · inactive/);
1875
- assert.match(rootText, /Reset session overrides\s+available/);
1876
-
1877
- component.handleInput("\r");
1878
- const choices = renderText(component);
1879
- assert.match(choices, /Inherit configured value/);
1880
- assert.match(choices, /Enabled for this session/);
1881
- assert.match(choices, /Disabled for this session/);
1882
- component.handleInput("\u001b[B");
1883
- component.handleInput("\u001b[B");
1884
- component.handleInput("\r");
1885
- component.handleInput("\u001b");
1886
- return result;
1887
- };
1888
-
1889
- await freeflowCommand.definition.handler("settings session", settingsCtx);
1890
- const capabilityState = await readCapabilityState(cwd, undefined, PIFLOW_HOST);
1891
- assert.equal(capabilityState.enabled, false);
1892
- assert.equal(capabilityState.configSources.enabled, "session");
1893
- assert.equal(settingsCtx.reloads.length, 1);
1894
- assert.deepEqual(entries.at(-1), {
1895
- customType: "freeflow-session-overrides",
1896
- data: { overrides: { enabled: false } },
1897
- });
1898
- assert.equal(await readFile(configPath, "utf8"), configText);
1899
- await assert.rejects(readFile(join(cwd, ".freeflow/local.json"), "utf8"), (error) => error?.code === "ENOENT");
1900
-
1901
- const resetCtx = context(cwd);
1902
- resetCtx.ui.custom = async (factory) => {
1903
- let result;
1904
- const component = factory({ requestRender() {} }, testTheme, {}, (value) => {
1905
- result = value;
1906
- });
1907
- const rootText = renderText(component);
1908
- assert.match(rootText, /Freeflow\s+disabled \(session\)/);
1909
- assert.match(rootText, /Interaction Contract\s+enabled \(builtin\) · inactive/);
1910
- for (let index = 0; index < 5; index += 1) component.handleInput("\u001b[B");
1911
- component.handleInput("\r");
1912
- assert.match(renderText(component), /Reset all session overrides/);
1913
- component.handleInput("\r");
1914
- component.handleInput("\u001b");
1915
- return result;
1916
- };
1917
- await freeflowCommand.definition.handler("settings session", resetCtx);
1918
- assert.equal((await readCapabilityState(cwd, undefined, PIFLOW_HOST)).enabled, true);
1919
- assert.equal(resetCtx.reloads.length, 1);
1920
- assert.deepEqual(entries.at(-1), {
1921
- customType: "freeflow-session-overrides",
1922
- data: { overrides: {} },
1923
- });
1924
- assert.equal(await readFile(configPath, "utf8"), configText);
1925
- } finally {
1926
- restoreModeOverride(context(cwd));
1927
- await rm(cwd, { recursive: true, force: true });
1928
- }
1929
- });
1930
-
1931
- test("Pi settings refuse to overwrite an invalid local override", async () => {
1932
- const cwd = await mkdtemp(join(tmpdir(), "freeflow-pi-invalid-local-settings-"));
1933
- try {
1934
- await mkdir(join(cwd, ".freeflow"));
1935
- await writeFile(join(cwd, ".freeflow/config.json"), JSON.stringify({ defaultMode: "workflow" }, null, 2), "utf8");
1936
- const localPath = join(cwd, ".freeflow/local.json");
1937
- await writeFile(localPath, "{ invalid\n", "utf8");
1938
-
1939
- const { commands } = loadExtension();
1940
- const freeflowCommand = commands.find((command) => command.name === "freeflow");
1941
- assert.ok(freeflowCommand);
1942
- const ctx = context(cwd);
1943
- ctx.ui.custom = async () => {
1944
- assert.fail("settings UI must not open for invalid local config");
1945
- };
1946
-
1947
- await freeflowCommand.definition.handler("settings", ctx);
1948
- assert.equal(await readFile(localPath, "utf8"), "{ invalid\n");
1949
- assert.match(ctx.notifications.at(-1).message, /local\.json is invalid; repair or remove it/);
1950
- assert.equal(ctx.reloads.length, 0);
1951
- } finally {
1952
- await rm(cwd, { recursive: true, force: true });
1953
- }
1954
- });
1955
-
1956
- test("Pi personal settings refuse to write a tracked local override", async () => {
1957
- const cwd = await mkdtemp(join(tmpdir(), "freeflow-pi-tracked-local-settings-"));
1958
- try {
1959
- await mkdir(join(cwd, ".freeflow"));
1960
- await writeFile(join(cwd, ".freeflow/config.json"), JSON.stringify({ defaultMode: "workflow" }, null, 2), "utf8");
1961
- await writeFile(join(cwd, ".freeflow/local.json"), "{}\n", "utf8");
1962
- await execFileAsync("git", ["init", "-q", cwd]);
1963
- await execFileAsync("git", ["-C", cwd, "add", ".freeflow/local.json"]);
1964
-
1965
- const { commands } = loadExtension();
1966
- const freeflowCommand = commands.find((command) => command.name === "freeflow");
1967
- assert.ok(freeflowCommand);
1968
- const ctx = context(cwd);
1969
- ctx.ui.custom = async (factory) => {
1970
- let result;
1971
- const component = factory({ requestRender() {} }, testTheme, {}, (value) => {
1972
- result = value;
1973
- });
1974
- component.handleInput("\u001b[B");
1975
- component.handleInput("\r");
1976
- component.handleInput("\u001b[B");
1977
- component.handleInput("\u001b[B");
1978
- component.handleInput("\r");
1979
- component.handleInput("\u001b");
1980
- return result;
1981
- };
1982
-
1983
- await freeflowCommand.definition.handler("settings", ctx);
1984
- assert.deepEqual(JSON.parse(await readFile(join(cwd, ".freeflow/local.json"), "utf8")), {});
1985
- assert.match(ctx.notifications.at(-1).message, /local\.json is tracked by git/);
1986
- assert.equal(ctx.reloads.length, 0);
1987
- } finally {
1988
- await rm(cwd, { recursive: true, force: true });
1989
- }
1990
- });
1991
-
1992
- test("Pi settings do not report save or reload success after a mixed write failure", async () => {
1993
- const cwd = await mkdtemp(join(tmpdir(), "freeflow-pi-mixed-settings-write-"));
1994
- try {
1995
- await mkdir(join(cwd, ".freeflow"));
1996
- await writeFile(join(cwd, ".freeflow/config.json"), JSON.stringify({ defaultMode: "workflow" }), "utf8");
1997
- await writeFile(join(cwd, ".freeflow/local.json"), "{}\n", "utf8");
1998
- await execFileAsync("git", ["init", "-q", cwd]);
1999
- await execFileAsync("git", ["-C", cwd, "add", ".freeflow/local.json"]);
2000
-
2001
- const { commands } = loadExtension();
2002
- const freeflowCommand = commands.find((command) => command.name === "freeflow");
2003
- assert.ok(freeflowCommand);
2004
- const ctx = context(cwd);
2005
- ctx.ui.custom = async (factory) => {
2006
- let result;
2007
- const component = factory({ requestRender() {} }, testTheme, {}, (value) => {
2008
- result = value;
2009
- });
2010
-
2011
- for (let index = 0; index < 3; index++) component.handleInput("\u001b[B");
2012
- component.handleInput("\r");
2013
- component.handleInput("\u001b[B");
2014
- component.handleInput("\r"); // Session mode succeeds.
2015
- await component.waitForWrites();
2016
-
2017
- component.handleInput("\u001b[B");
2018
- component.handleInput("\r");
2019
- component.handleInput("\u001b[B");
2020
- component.handleInput("\u001b[B");
2021
- component.handleInput("\r"); // Tracked local config write fails.
2022
- await component.waitForWrites();
2023
- component.handleInput("\u001b");
2024
- return result;
2025
- };
2026
-
2027
- await freeflowCommand.definition.handler("settings", ctx);
2028
- assert.equal((await readModeState(cwd)).sessionMode, "conversation");
2029
- assert.equal(ctx.reloads.length, 0);
2030
- assert.match(ctx.notifications.map((notification) => notification.message).join("\n"), /Write failed:/);
2031
- assert.doesNotMatch(
2032
- ctx.notifications.map((notification) => notification.message).join("\n"),
2033
- /personal overrides saved/,
2034
- );
2035
- assert.deepEqual(JSON.parse(await readFile(join(cwd, ".freeflow/local.json"), "utf8")), {});
2036
- } finally {
2037
- restoreModeOverride(context(cwd));
2038
- await rm(cwd, { recursive: true, force: true });
2039
- }
2040
- });
2041
-
2042
- test("Pi /freeflow settings repo edits shared defaults and shows local effective sources", async () => {
2043
- const cwd = await mkdtemp(join(tmpdir(), "freeflow-pi-repository-settings-"));
2044
- try {
2045
- await mkdir(join(cwd, ".freeflow"));
2046
- const repositoryPath = join(cwd, ".freeflow/config.json");
2047
- const localPath = join(cwd, ".freeflow/local.json");
2048
- await writeFile(repositoryPath, JSON.stringify({ defaultMode: "workflow" }, null, 2), "utf8");
2049
- await writeFile(
2050
- localPath,
2051
- JSON.stringify(
2052
- {
2053
- defaultMode: "conversation",
2054
- processing: { unsafeUnsandboxed: { enabled: true } },
2055
- },
2056
- null,
2057
- 2,
2058
- ),
2059
- "utf8",
2060
- );
2061
-
2062
- const { commands } = loadExtension();
2063
- const freeflowCommand = commands.find((command) => command.name === "freeflow");
2064
- assert.ok(freeflowCommand);
2065
- const ctx = context(cwd);
2066
- ctx.ui.custom = async (factory) => {
2067
- let result;
2068
- const component = factory({ requestRender() {} }, testTheme, {}, (value) => {
2069
- result = value;
2070
- });
2071
- const rootText = renderText(component);
2072
- assert.match(rootText, /Freeflow Repository Settings · modifies \.freeflow\/config\.json/);
2073
- assert.match(rootText, /Default mode\s+workflow \(effective conversation · local\)/);
2074
- for (let index = 0; index < 4; index++) {
2075
- component.handleInput("\u001b[B");
2076
- }
2077
- component.handleInput("\r");
2078
- component.handleInput("\u001b[B");
2079
- component.handleInput("\r");
2080
- component.handleInput("\u001b");
2081
- return result;
2082
- };
2083
-
2084
- await freeflowCommand.definition.handler("settings repo", ctx);
2085
- const repositoryAfter = JSON.parse(await readFile(repositoryPath, "utf8"));
2086
- const localAfter = JSON.parse(await readFile(localPath, "utf8"));
2087
- assert.equal(repositoryAfter.defaultMode, "strict-workflow");
2088
- assert.equal(localAfter.defaultMode, "conversation");
2089
- assert.equal(localAfter.processing.unsafeUnsandboxed.enabled, true);
2090
- assert.equal(ctx.reloads.length, 1);
2091
- } finally {
2092
- await rm(cwd, { recursive: true, force: true });
2093
- }
2094
- });
2095
-
2096
- test("PiFlow shows unset Cognitive Routing presets as not configured and edits them while disabled", async () => {
2097
- const cwd = await mkdtemp(join(tmpdir(), "freeflow-piflow-cognitive-not-configured-"));
2098
- try {
2099
- await mkdir(join(cwd, ".freeflow"));
2100
- await writeFile(join(cwd, ".freeflow/config.json"), JSON.stringify({ defaultMode: "workflow" }), "utf8");
2101
- const { commands } = loadExtension();
2102
- const freeflowCommand = commands.find((command) => command.name === "freeflow");
2103
- assert.ok(freeflowCommand);
2104
- const settingsCtx = context(cwd);
2105
- settingsCtx.isIdle = () => true;
2106
- settingsCtx.modelRegistry = cognitiveRoutingModelRegistry();
2107
- settingsCtx.ui.custom = async (factory) => {
2108
- let result;
2109
- const component = factory({ requestRender() {} }, testTheme, {}, (value) => {
2110
- result = value;
2111
- });
2112
- const rootText = renderText(component);
2113
- assert.match(rootText, /Cognitive Routing\s+disabled \(5\) disabled/);
2114
- assert.doesNotMatch(rootText, /Output Router/);
2115
- for (let index = 0; index < 5; index += 1) component.handleInput("\u001b[B");
2116
- component.handleInput("\r");
2117
- assert.match(renderText(component), /Enabled\s+disabled/);
2118
- assert.match(renderText(component), /Standard preset\s+not configured/);
2119
- assert.match(renderText(component), /Reasoning preset\s+not configured/);
2120
-
2121
- component.handleInput("\u001b[B");
2122
- component.handleInput("\r");
2123
- component.handleInput("\u001b[B");
2124
- component.handleInput("\r");
2125
- component.handleInput("\r");
2126
- component.handleInput("\r");
2127
- await component.waitForWrites();
2128
- return result;
2129
- };
2130
-
2131
- await freeflowCommand.definition.handler("settings", settingsCtx);
2132
- const saved = JSON.parse(await readFile(join(cwd, ".freeflow/local.json"), "utf8"));
2133
- assert.deepEqual(saved.cognitiveRouting.profiles.standard, {
2134
- provider: "test",
2135
- model: "model-a",
2136
- thinkingLevel: "off",
2137
- });
2138
- assert.equal(saved.cognitiveRouting.enabled, undefined);
2139
- } finally {
2140
- await rm(cwd, { recursive: true, force: true });
2141
- }
2142
- });
2143
-
2144
- test("PiFlow settings refresh the Cognitive Routing group after enabling it", async () => {
2145
- const cwd = await mkdtemp(join(tmpdir(), "freeflow-piflow-cognitive-hot-reload-"));
2146
- try {
2147
- await mkdir(join(cwd, ".freeflow"));
2148
- await writeFile(
2149
- join(cwd, ".freeflow/config.json"),
2150
- JSON.stringify({
2151
- defaultMode: "workflow",
2152
- cognitiveRouting: {
2153
- enabled: false,
2154
- profiles: {
2155
- standard: { provider: "test", model: "model-a", thinkingLevel: "low" },
2156
- reasoning: { provider: "test", model: "model-b", thinkingLevel: "high" },
2157
- },
2158
- },
2159
- }),
2160
- "utf8",
2161
- );
2162
- const { commands } = loadExtension();
2163
- const freeflowCommand = commands.find((command) => command.name === "freeflow");
2164
- assert.ok(freeflowCommand);
2165
- const settingsCtx = context(cwd);
2166
- settingsCtx.isIdle = () => true;
2167
- settingsCtx.modelRegistry = cognitiveRoutingModelRegistry();
2168
- settingsCtx.ui.custom = async (factory) => {
2169
- let result;
2170
- const component = factory({ requestRender() {} }, testTheme, {}, (value) => {
2171
- result = value;
2172
- });
2173
- assert.match(renderText(component), /Cognitive Routing\s+disabled \(5\) disabled/);
2174
- for (let index = 0; index < 5; index += 1) component.handleInput("\u001b[B");
2175
- component.handleInput("\r");
2176
- component.handleInput("\r");
2177
- component.handleInput("\u001b[B");
2178
- component.handleInput("\r");
2179
- await component.waitForWrites();
2180
- component.handleInput("\u001b");
2181
- component.handleInput("\u001b");
2182
- assert.match(renderText(component), /Cognitive Routing\s+enabled \(5\) configured/);
2183
- component.handleInput("\u001b");
2184
- return result;
2185
- };
2186
-
2187
- await freeflowCommand.definition.handler("settings", settingsCtx);
2188
- const saved = JSON.parse(await readFile(join(cwd, ".freeflow/local.json"), "utf8"));
2189
- assert.equal(saved.cognitiveRouting.enabled, true);
2190
- assert.equal(settingsCtx.reloads.length, 1);
2191
- } finally {
2192
- await rm(cwd, { recursive: true, force: true });
2193
- }
2194
- });
2195
-
2196
- test("Pi Freeflow Context parent summary refreshes after a child feature changes", async () => {
2197
- const cwd = await mkdtemp(join(tmpdir(), "freeflow-pi-context-settings-hot-reload-"));
2198
- try {
2199
- await mkdir(join(cwd, ".freeflow"));
2200
- await writeFile(
2201
- join(cwd, ".freeflow/config.json"),
2202
- JSON.stringify({ contextVirtualization: true }, null, 2),
2203
- "utf8",
2204
- );
2205
-
2206
- const { commands } = loadExtension();
2207
- const freeflowCommand = commands.find((command) => command.name === "freeflow");
2208
- assert.ok(freeflowCommand);
2209
- const settingsCtx = context(cwd);
2210
- settingsCtx.ui.custom = async (factory) => {
2211
- let result;
2212
- const component = factory({ requestRender() {} }, testTheme, {}, (value) => {
2213
- result = value;
2214
- });
2215
- assert.match(renderText(component), /Freeflow Context\s+enabled \(2\) 1\/2 enabled/);
2216
-
2217
- for (let index = 0; index < 6; index += 1) component.handleInput("\u001b[B");
2218
- component.handleInput("\r");
2219
- component.handleInput("\r");
2220
- component.handleInput("\u001b[B");
2221
- component.handleInput("\u001b[B");
2222
- component.handleInput("\r");
2223
- await component.waitForWrites();
2224
- component.handleInput("\u001b");
2225
-
2226
- assert.match(renderText(component), /Freeflow Context\s+disabled \(2\) 0\/2 enabled/);
2227
- component.handleInput("\u001b");
2228
- return result;
2229
- };
2230
-
2231
- await freeflowCommand.definition.handler("settings", settingsCtx);
2232
- const local = JSON.parse(await readFile(join(cwd, ".freeflow/local.json"), "utf8"));
2233
- assert.equal(local.contextVirtualization, false);
2234
- assert.equal(settingsCtx.reloads.length, 1);
2235
- } finally {
2236
- await rm(cwd, { recursive: true, force: true });
2237
- }
2238
- });
2239
-
2240
- function cognitiveRoutingModelRegistry() {
2241
- const models = [
2242
- { provider: "test", id: "model-a", name: "Model A", reasoning: true },
2243
- { provider: "test", id: "model-b", name: "Model B", reasoning: true },
2244
- ];
2245
- const supported = new Map([
2246
- ["model-a", new Set(["off", "low", "medium", "high"])],
2247
- ["model-b", new Set(["off", "low", "medium", "high", "max"])],
2248
- ]);
2249
- return {
2250
- getAvailable() {
2251
- return models;
2252
- },
2253
- find(provider, id) {
2254
- return models.find((model) => model.provider === provider && model.id === id);
2255
- },
2256
- async getApiKeyAndHeaders() {
2257
- return { ok: true };
2258
- },
2259
- clampThinkingLevel(model, level) {
2260
- return supported.get(model.id)?.has(level) ? level : "off";
2261
- },
2262
- };
2263
- }
2264
-
2265
- test("PiFlow settings persist independent new-session control and profile overrides", async () => {
2266
- const cwd = await mkdtemp(join(tmpdir(), "freeflow-pi-cognitive-session-start-settings-"));
2267
- try {
2268
- await mkdir(join(cwd, ".freeflow"));
2269
- await writeFile(
2270
- join(cwd, ".freeflow/config.json"),
2271
- JSON.stringify(
2272
- {
2273
- defaultMode: "workflow",
2274
- cognitiveRouting: {
2275
- enabled: true,
2276
- profiles: {
2277
- standard: { provider: "test", model: "model-a", thinkingLevel: "low" },
2278
- reasoning: { provider: "test", model: "model-b", thinkingLevel: "high" },
2279
- },
2280
- },
2281
- },
2282
- null,
2283
- 2,
2284
- ),
2285
- "utf8",
2286
- );
2287
-
2288
- const { commands } = loadExtension();
2289
- const freeflowCommand = commands.find((command) => command.name === "freeflow");
2290
- assert.ok(freeflowCommand);
2291
- const settingsCtx = context(cwd);
2292
- settingsCtx.isIdle = () => true;
2293
- settingsCtx.modelRegistry = cognitiveRoutingModelRegistry();
2294
- settingsCtx.ui.custom = async (factory) => {
2295
- let result;
2296
- const component = factory({ requestRender() {} }, testTheme, {}, (value) => {
2297
- result = value;
2298
- });
2299
- for (let index = 0; index < 5; index += 1) component.handleInput("\u001b[B");
2300
- component.handleInput("\r");
2301
- assert.match(renderText(component), /Session start control\s+automatic \(builtin\)/);
2302
- assert.match(renderText(component), /Session start profile\s+standard \(builtin\)/);
2303
-
2304
- for (let index = 0; index < 3; index += 1) component.handleInput("\u001b[B");
2305
- component.handleInput("\r");
2306
- component.handleInput("\u001b[B");
2307
- component.handleInput("\u001b[B");
2308
- component.handleInput("\r");
2309
- await component.waitForWrites();
2310
-
2311
- component.handleInput("\u001b");
2312
- for (let index = 0; index < 5; index += 1) component.handleInput("\u001b[B");
2313
- component.handleInput("\r");
2314
- for (let index = 0; index < 4; index += 1) component.handleInput("\u001b[B");
2315
- component.handleInput("\r");
2316
- component.handleInput("\u001b[B");
2317
- component.handleInput("\u001b[B");
2318
- component.handleInput("\r");
2319
- await component.waitForWrites();
2320
-
2321
- component.handleInput("\u001b");
2322
- for (let index = 0; index < 5; index += 1) component.handleInput("\u001b[B");
2323
- component.handleInput("\r");
2324
- assert.match(renderText(component), /Session start control\s+manual \(local\)/);
2325
- assert.match(renderText(component), /Session start profile\s+reasoning \(local\)/);
2326
- component.handleInput("\u001b");
2327
- component.handleInput("\u001b");
2328
- return result;
2329
- };
2330
-
2331
- await freeflowCommand.definition.handler("settings", settingsCtx);
2332
- const saved = JSON.parse(await readFile(join(cwd, ".freeflow/local.json"), "utf8"));
2333
- assert.deepEqual(saved.cognitiveRouting.sessionStart, { control: "manual", profile: "reasoning" });
2334
- assert.equal(settingsCtx.reloads.length, 1);
2335
- } finally {
2336
- await rm(cwd, { recursive: true, force: true });
2337
- }
2338
- });
2339
-
2340
- test("Pi Cognitive Routing settings save a complete authenticated model and effort preset atomically", async () => {
2341
- const cwd = await mkdtemp(join(tmpdir(), "freeflow-pi-cognitive-settings-"));
2342
- try {
2343
- await mkdir(join(cwd, ".freeflow"));
2344
- await writeFile(
2345
- join(cwd, ".freeflow/config.json"),
2346
- JSON.stringify(
2347
- {
2348
- defaultMode: "workflow",
2349
- cognitiveRouting: {
2350
- enabled: true,
2351
- profiles: {
2352
- standard: { provider: "test", model: "model-a", thinkingLevel: "low" },
2353
- reasoning: { provider: "test", model: "model-b", thinkingLevel: "high" },
2354
- },
2355
- },
2356
- },
2357
- null,
2358
- 2,
2359
- ),
2360
- );
2361
-
2362
- const { commands } = loadExtension();
2363
- const freeflowCommand = commands.find((command) => command.name === "freeflow");
2364
- assert.ok(freeflowCommand);
2365
- const settingsCtx = context(cwd);
2366
- settingsCtx.isIdle = () => true;
2367
- settingsCtx.modelRegistry = cognitiveRoutingModelRegistry();
2368
- settingsCtx.ui.custom = async (factory) => {
2369
- let result;
2370
- const component = factory({ requestRender() {} }, testTheme, {}, (value) => {
2371
- result = value;
2372
- });
2373
- for (let index = 0; index < 5; index++) component.handleInput("\u001b[B");
2374
- component.handleInput("\r"); // Cognitive Routing group.
2375
- component.handleInput("\u001b[B");
2376
- component.handleInput("\r"); // Standard preset wizard.
2377
- component.handleInput("\u001b[B");
2378
- component.handleInput("\r"); // model-b.
2379
- for (let index = 0; index < 4; index++) component.handleInput("\u001b[B");
2380
- component.handleInput("\r"); // max.
2381
- component.handleInput("\r"); // confirm save.
2382
- await component.waitForWrites();
2383
- return result;
2384
- };
2385
-
2386
- await freeflowCommand.definition.handler("settings repo", settingsCtx);
2387
- const saved = JSON.parse(await readFile(join(cwd, ".freeflow/config.json"), "utf8"));
2388
- assert.deepEqual(saved.cognitiveRouting.profiles.standard, {
2389
- provider: "test",
2390
- model: "model-b",
2391
- thinkingLevel: "max",
2392
- });
2393
- assert.equal(saved.cognitiveRouting.profiles.reasoning.thinkingLevel, "high");
2394
- assert.equal(settingsCtx.reloads.length, 1);
616
+ assert.ok(!activeToolNames().includes("freeflow_context"));
2395
617
  } finally {
2396
618
  await rm(cwd, { recursive: true, force: true });
2397
619
  }
2398
620
  });
2399
621
 
2400
- test("Pi Cognitive Routing preset cancel leaves the previous preset unchanged", async () => {
2401
- const cwd = await mkdtemp(join(tmpdir(), "freeflow-pi-cognitive-cancel-"));
622
+ test("Pi settings expose no mode, Skills, or Interaction Contract controls", async () => {
623
+ const cwd = await configuredRepo({ enabled: false });
2402
624
  try {
2403
- await mkdir(join(cwd, ".freeflow"));
2404
- const original = {
2405
- defaultMode: "workflow",
2406
- cognitiveRouting: {
2407
- enabled: true,
2408
- profiles: {
2409
- standard: { provider: "test", model: "model-a", thinkingLevel: "low" },
2410
- reasoning: { provider: "test", model: "model-b", thinkingLevel: "high" },
2411
- },
2412
- },
2413
- };
2414
- await writeFile(join(cwd, ".freeflow/config.json"), JSON.stringify(original, null, 2));
2415
-
2416
625
  const { commands } = loadExtension();
2417
626
  const freeflowCommand = commands.find((command) => command.name === "freeflow");
2418
627
  assert.ok(freeflowCommand);
2419
- const settingsCtx = context(cwd);
2420
- settingsCtx.isIdle = () => true;
2421
- settingsCtx.modelRegistry = cognitiveRoutingModelRegistry();
2422
- settingsCtx.ui.custom = async (factory) => {
628
+ const ctx = context(cwd);
629
+ ctx.ui.custom = async (factory) => {
2423
630
  let result;
2424
631
  const component = factory({ requestRender() {} }, testTheme, {}, (value) => {
2425
632
  result = value;
2426
633
  });
2427
- for (let index = 0; index < 5; index++) component.handleInput("\u001b[B");
2428
- component.handleInput("\r");
2429
- component.handleInput("\u001b[B");
2430
- component.handleInput("\r");
2431
- component.handleInput("\u001b[B");
2432
- component.handleInput("\r");
2433
- component.handleInput("\u001b"); // Cancel at the effort step.
2434
- component.handleInput("\u001b");
634
+ const text = renderText(component);
635
+ assert.match(text, /Freeflow Settings/);
636
+ assert.match(text, /Freeflow Context/);
637
+ assert.doesNotMatch(text, /Interaction Contract|Skills|Session mode|Default mode|Mode/);
2435
638
  component.handleInput("\u001b");
2436
639
  return result;
2437
640
  };
2438
-
2439
- await freeflowCommand.definition.handler("settings repo", settingsCtx);
2440
- assert.deepEqual(JSON.parse(await readFile(join(cwd, ".freeflow/config.json"), "utf8")), original);
2441
- assert.equal(settingsCtx.reloads.length, 0);
2442
- } finally {
2443
- await rm(cwd, { recursive: true, force: true });
2444
- }
2445
- });
2446
-
2447
- test("Pi Cognitive Routing settings refuse mid-run mutation", async () => {
2448
- const cwd = await mkdtemp(join(tmpdir(), "freeflow-pi-cognitive-busy-"));
2449
- try {
2450
- await mkdir(join(cwd, ".freeflow"));
2451
- const original = JSON.stringify({ defaultMode: "workflow" }, null, 2);
2452
- await writeFile(join(cwd, ".freeflow/config.json"), original);
2453
- const { commands } = loadExtension();
2454
- const freeflowCommand = commands.find((command) => command.name === "freeflow");
2455
- assert.ok(freeflowCommand);
2456
- const settingsCtx = context(cwd);
2457
- settingsCtx.isIdle = () => false;
2458
- settingsCtx.ui.custom = async () => {
2459
- throw new Error("settings UI must not open while Pi is running");
2460
- };
2461
-
2462
- await freeflowCommand.definition.handler("settings repo", settingsCtx);
2463
- assert.match(settingsCtx.notifications.at(-1).message, /only while Pi is idle/);
2464
- assert.equal(await readFile(join(cwd, ".freeflow/config.json"), "utf8"), original);
641
+ await freeflowCommand.definition.handler("settings", ctx);
642
+ assert.equal(ctx.reloads.length, 0);
2465
643
  } finally {
2466
644
  await rm(cwd, { recursive: true, force: true });
2467
645
  }
2468
646
  });
2469
647
 
2470
- test("Pi settings recheck idle state before committing a selection", async () => {
2471
- const cwd = await mkdtemp(join(tmpdir(), "freeflow-pi-settings-busy-commit-"));
648
+ test("Pi enable and disable commands mutate only the master switch", async () => {
649
+ const cwd = await configuredRepo();
2472
650
  try {
2473
- await mkdir(join(cwd, ".freeflow"));
2474
- const original = JSON.stringify({ defaultMode: "workflow" }, null, 2);
2475
- await writeFile(join(cwd, ".freeflow/config.json"), original);
2476
651
  const { commands } = loadExtension();
2477
652
  const freeflowCommand = commands.find((command) => command.name === "freeflow");
2478
653
  assert.ok(freeflowCommand);
2479
- const settingsCtx = context(cwd);
2480
- let idle = true;
2481
- settingsCtx.isIdle = () => idle;
2482
- settingsCtx.ui.custom = async (factory) => {
2483
- let result;
2484
- const component = factory({ requestRender() {} }, testTheme, {}, (value) => {
2485
- result = value;
2486
- });
2487
- idle = false;
2488
- for (let index = 0; index < 4; index++) component.handleInput("\u001b[B");
2489
- component.handleInput("\r");
2490
- component.handleInput("\u001b[B");
2491
- component.handleInput("\r");
2492
- await component.waitForWrites();
2493
- component.handleInput("\u001b");
2494
- return result;
2495
- };
2496
-
2497
- await freeflowCommand.definition.handler("settings repo", settingsCtx);
2498
- assert.match(settingsCtx.notifications.at(-1).message, /only while Pi is idle/);
2499
- assert.equal(await readFile(join(cwd, ".freeflow/config.json"), "utf8"), original);
2500
- } finally {
2501
- await rm(cwd, { recursive: true, force: true });
2502
- }
2503
- });
654
+ const disableCtx = context(cwd);
655
+ await freeflowCommand.definition.handler("disable", disableCtx);
656
+ assert.deepEqual(JSON.parse(await readFile(join(cwd, ".freeflow/config.json"), "utf8")), { enabled: false });
657
+ assert.equal(disableCtx.statuses.at(-1).value, "freeflow: off");
658
+ assert.equal(disableCtx.reloads.length, 1);
2504
659
 
2505
- test("Pi before_agent_start does not inject retired Output Router context", async () => {
2506
- const cwd = await mkdtemp(join(tmpdir(), "freeflow-pi-router-default-disabled-"));
2507
- try {
2508
- await mkdir(join(cwd, ".freeflow"));
2509
- await writeFile(join(cwd, ".freeflow/config.json"), JSON.stringify({ defaultMode: "workflow" }, null, 2), "utf8");
2510
-
2511
- const { handlers } = loadExtension();
2512
- const beforeAgentStart = handlers.get("before_agent_start");
2513
- assert.ok(beforeAgentStart);
2514
-
2515
- const result = await beforeAgentStart({ systemPrompt: "base prompt" }, context(cwd));
2516
-
2517
- assert.match(result.systemPrompt, /# Freeflow Interaction Contract/);
2518
- assert.doesNotMatch(result.systemPrompt, /# Freeflow Runtime Kernel/);
2519
- assert.doesNotMatch(result.systemPrompt, /## Loaded Mode Contract Skill/);
2520
- assert.doesNotMatch(result.systemPrompt, /## Loaded Workflow Skill/);
2521
- assert.doesNotMatch(result.systemPrompt, /## Loaded Decision Gate Skill/);
2522
- assert.doesNotMatch(result.systemPrompt, /## Discovery-light/);
2523
- assert.doesNotMatch(result.systemPrompt, /## Loaded Output Router Skill/);
2524
- assert.doesNotMatch(result.systemPrompt, /freeflow_search/);
2525
-
2526
- assert.doesNotMatch(result.systemPrompt, /Legacy `FFRESULT`/);
2527
- assert.doesNotMatch(result.systemPrompt, /# Context Locality/);
2528
- assert.doesNotMatch(result.systemPrompt, /freeflow_run/);
2529
- assert.doesNotMatch(result.systemPrompt, /freeflow_capture/);
2530
- assert.doesNotMatch(result.systemPrompt, /freeflow_search action=transform/);
2531
- assert.doesNotMatch(result.systemPrompt, /Native tools stay direct/);
2532
- assert.doesNotMatch(result.systemPrompt, /## Loaded Workflow Map/);
2533
- assert.doesNotMatch(result.systemPrompt, /## Loaded Output Router Safety Policy/);
2534
- assert.doesNotMatch(result.systemPrompt, /Do not silently summarize or compress exactness-sensitive output/);
2535
- assert.doesNotMatch(result.systemPrompt, /large native read\/bash outputs may be vaulted/);
2536
- assert.doesNotMatch(result.systemPrompt, /Output-router config note/);
660
+ const enableCtx = context(cwd);
661
+ await freeflowCommand.definition.handler("enable", enableCtx);
662
+ assert.deepEqual(JSON.parse(await readFile(join(cwd, ".freeflow/config.json"), "utf8")), {});
663
+ assert.equal(enableCtx.reloads.length, 1);
2537
664
  } finally {
2538
665
  await rm(cwd, { recursive: true, force: true });
2539
666
  }
2540
667
  });
2541
668
 
2542
- test("Pi before_agent_start keeps the per-turn system context to the compact interaction contract", async () => {
2543
- const cwd = await mkdtemp(join(tmpdir(), "freeflow-pi-interaction-contract-"));
669
+ test("Pi statusline uses one umbrella context label for either enabled context feature", async () => {
670
+ const cwd = await configuredRepo({ contextVirtualization: true, conversationHistory: true });
2544
671
  try {
2545
- await mkdir(join(cwd, ".freeflow"));
2546
- await writeFile(join(cwd, ".freeflow/config.json"), JSON.stringify({ defaultMode: "workflow" }, null, 2), "utf8");
2547
-
2548
672
  const { handlers } = loadExtension();
2549
- const beforeAgentStart = handlers.get("before_agent_start");
2550
- assert.ok(beforeAgentStart);
2551
-
2552
- const result = await beforeAgentStart({ systemPrompt: "base prompt" }, context(cwd));
2553
-
2554
- assert.match(result.systemPrompt, /# Freeflow Stable Guidance/);
2555
- assert.match(result.systemPrompt, /# Freeflow Interaction Contract/);
2556
- assert.match(result.systemPrompt, /## Shared Terms/);
2557
- assert.match(result.systemPrompt, /## Workflow Cue/);
2558
- assert.doesNotMatch(result.systemPrompt, /# Freeflow Runtime Kernel/);
2559
- assert.doesNotMatch(result.systemPrompt, /## Conversation Mode Boundary/);
2560
- assert.doesNotMatch(result.systemPrompt, /## Strict Workflow Overlay/);
2561
- assert.match(result.systemPrompt, /Treat questions, criticism, examples, hypotheses, and tentative ideas as/);
2562
- assert.match(result.systemPrompt, /When a turn mixes a direct question with a request or approval to/);
2563
- assert.match(result.systemPrompt, /With a clear action request, recommend brief discussion/);
2564
- assert.match(result.systemPrompt, /self-review/);
2565
- assert.doesNotMatch(result.systemPrompt, /final assurance|standing authorization/i);
2566
- assert.doesNotMatch(result.systemPrompt, /## Loaded Mode Contract Skill/);
2567
- assert.doesNotMatch(result.systemPrompt, /## Loaded Workflow Skill/);
2568
- assert.doesNotMatch(result.systemPrompt, /## Loaded Decision Gate Skill/);
2569
- assert.doesNotMatch(result.systemPrompt, /## Discovery-light/);
673
+ const ctx = context(cwd);
674
+ await handlers.get("session_start")({ reason: "startup" }, ctx);
675
+ assert.equal(ctx.statuses.at(-1).value, "freeflow: context");
2570
676
  } finally {
2571
677
  await rm(cwd, { recursive: true, force: true });
2572
678
  }
2573
679
  });
2574
680
 
2575
- test("Pi exposes Workflow through discovery rather than a persistent bootstrap", async () => {
2576
- const cwd = await mkdtemp(join(tmpdir(), "freeflow-pi-workflow-discovery-"));
681
+ test("Pi settings refuse to overwrite an invalid local override", async () => {
682
+ const cwd = await configuredRepo();
2577
683
  try {
2578
- await mkdir(join(cwd, ".freeflow"));
2579
- await writeFile(join(cwd, ".freeflow/config.json"), JSON.stringify({ defaultMode: "workflow" }), "utf8");
2580
-
2581
- const { handlers } = loadExtension();
684
+ const localPath = join(cwd, ".freeflow/local.json");
685
+ await writeFile(localPath, "{ invalid\n", "utf8");
686
+ const { commands } = loadExtension();
687
+ const freeflowCommand = commands.find((command) => command.name === "freeflow");
688
+ assert.ok(freeflowCommand);
2582
689
  const ctx = context(cwd);
2583
- const resources = await handlers.get("resources_discover")({ cwd }, ctx);
2584
- assert.ok(resources.skillPaths.some((path) => path.endsWith("/skills/workflow/SKILL.md")));
2585
-
2586
- const result = await handlers.get("before_agent_start")({ prompt: "hi", systemPrompt: "base prompt" }, ctx);
2587
- assert.equal(result.message, undefined);
2588
- assert.match(result.systemPrompt, /## Workflow Cue/);
2589
- assert.doesNotMatch(result.systemPrompt, /^# Workflow$/m);
2590
- assert.doesNotMatch(result.systemPrompt, /freeflow-workflow-bootstrap|Freeflow Workflow Bootstrap/);
690
+ ctx.ui.custom = async () => assert.fail("settings UI must not open for invalid local config");
691
+ await freeflowCommand.definition.handler("settings", ctx);
692
+ assert.equal(await readFile(localPath, "utf8"), "{ invalid\n");
693
+ assert.match(ctx.notifications.at(-1).message, /local\.json is invalid; repair or remove it/);
2591
694
  } finally {
2592
695
  await rm(cwd, { recursive: true, force: true });
2593
696
  }
2594
697
  });
2595
698
 
2596
- test("Pi suppresses persisted Workflow context while Skills are disabled", async () => {
2597
- const cwd = await mkdtemp(join(tmpdir(), "freeflow-pi-workflow-bootstrap-disabled-"));
699
+ test("Pi personal settings refuse to write a tracked local override", async () => {
700
+ const cwd = await configuredRepo();
2598
701
  try {
2599
- await mkdir(join(cwd, ".freeflow"));
2600
- const configPath = join(cwd, ".freeflow/config.json");
2601
- await writeFile(
2602
- configPath,
2603
- JSON.stringify({ defaultMode: "workflow", skills: { enabled: false } }, null, 2),
2604
- "utf8",
2605
- );
2606
-
2607
- const { handlers } = loadExtension();
2608
- const contextHandler = handlers.get("context");
2609
- assert.ok(contextHandler);
2610
-
2611
- const workflowMessage = {
2612
- role: "custom",
2613
- customType: "freeflow-workflow-bootstrap",
2614
- content: "# Freeflow Workflow Bootstrap",
2615
- display: false,
2616
- timestamp: Date.now(),
2617
- };
2618
- const userMessage = {
2619
- role: "user",
2620
- content: "hello",
2621
- timestamp: Date.now(),
702
+ await writeFile(join(cwd, ".freeflow/local.json"), "{}\n", "utf8");
703
+ await execFileAsync("git", ["init", "-q", cwd]);
704
+ await execFileAsync("git", ["-C", cwd, "add", ".freeflow/local.json"]);
705
+ const { commands } = loadExtension();
706
+ const freeflowCommand = commands.find((command) => command.name === "freeflow");
707
+ assert.ok(freeflowCommand);
708
+ const ctx = context(cwd);
709
+ ctx.ui.custom = async (factory) => {
710
+ let result;
711
+ const component = factory({ requestRender() {} }, testTheme, {}, (value) => {
712
+ result = value;
713
+ });
714
+ component.handleInput("\r");
715
+ component.handleInput("\u001b[B");
716
+ component.handleInput("\r");
717
+ component.handleInput("\u001b");
718
+ return result;
2622
719
  };
2623
- const disabled = await contextHandler({ messages: [workflowMessage, userMessage] }, context(cwd));
2624
- assert.deepEqual(
2625
- disabled.messages.filter((message) => message.customType !== "freeflow-runtime-state"),
2626
- [userMessage],
720
+ await freeflowCommand.definition.handler("settings", ctx);
721
+ assert.deepEqual(JSON.parse(await readFile(join(cwd, ".freeflow/local.json"), "utf8")), {});
722
+ assert.match(
723
+ ctx.notifications.map((notification) => notification.message).join("\n"),
724
+ /local\.json is tracked by git/,
2627
725
  );
2628
- assert.equal(disabled.messages.at(-1).customType, "freeflow-runtime-state");
2629
-
2630
- await writeFile(configPath, JSON.stringify({ defaultMode: "workflow" }, null, 2), "utf8");
2631
- const enabled = await contextHandler({ messages: [workflowMessage, userMessage] }, context(cwd));
2632
- assert.ok(enabled);
2633
- assert.deepEqual(
2634
- enabled.messages.filter((message) => message.customType !== "freeflow-runtime-state"),
2635
- [userMessage],
726
+ assert.equal(ctx.reloads.length, 0);
727
+ assert.doesNotMatch(
728
+ ctx.notifications.map((notification) => notification.message).join("\n"),
729
+ /personal overrides saved/,
2636
730
  );
2637
- assert.equal(enabled.messages.at(-1).customType, "freeflow-runtime-state");
2638
731
  } finally {
2639
732
  await rm(cwd, { recursive: true, force: true });
2640
733
  }
2641
734
  });
2642
735
 
2643
- test("Pi suppresses persisted Cognitive Routing bootstrap context when routing is disabled", async () => {
2644
- const cwd = await mkdtemp(join(tmpdir(), "freeflow-pi-cognitive-bootstrap-disabled-"));
736
+ test("Pi Cognitive Routing settings preserve complete presets", async () => {
737
+ const cwd = await configuredRepo({
738
+ cognitiveRouting: {
739
+ enabled: true,
740
+ profiles: {
741
+ standard: { provider: "test", model: "model-a", thinkingLevel: "low" },
742
+ reasoning: { provider: "test", model: "model-b", thinkingLevel: "high" },
743
+ },
744
+ },
745
+ });
2645
746
  try {
2646
- await mkdir(join(cwd, ".freeflow"));
2647
- await writeFile(join(cwd, ".freeflow/config.json"), JSON.stringify({ defaultMode: "workflow" }, null, 2), "utf8");
2648
-
2649
- const { handlers } = loadExtension();
2650
- const contextHandler = handlers.get("context");
2651
- assert.ok(contextHandler);
2652
-
2653
- const cognitiveMessage = {
2654
- role: "custom",
2655
- customType: "freeflow-cognitive-routing-bootstrap",
2656
- content: "# Freeflow Cognitive Routing Bootstrap",
2657
- display: false,
2658
- timestamp: Date.now(),
2659
- };
2660
- const userMessage = {
2661
- role: "user",
2662
- content: "hello",
2663
- timestamp: Date.now(),
747
+ const { commands } = loadExtension();
748
+ const freeflowCommand = commands.find((command) => command.name === "freeflow");
749
+ assert.ok(freeflowCommand);
750
+ const settingsCtx = context(cwd);
751
+ settingsCtx.isIdle = () => true;
752
+ settingsCtx.modelRegistry = cognitiveRoutingModelRegistry();
753
+ settingsCtx.ui.custom = async (factory) => {
754
+ let result;
755
+ const component = factory({ requestRender() {} }, testTheme, {}, (value) => {
756
+ result = value;
757
+ });
758
+ component.handleInput("\u001b[B");
759
+ component.handleInput("\r");
760
+ component.handleInput("\u001b[B");
761
+ component.handleInput("\r");
762
+ component.handleInput("\u001b[B");
763
+ component.handleInput("\r");
764
+ for (let index = 0; index < 4; index++) component.handleInput("\u001b[B");
765
+ component.handleInput("\r");
766
+ component.handleInput("\r");
767
+ await component.waitForWrites();
768
+ return result;
2664
769
  };
2665
- const disabled = await contextHandler({ messages: [cognitiveMessage, userMessage] }, context(cwd));
2666
- assert.deepEqual(
2667
- disabled.messages.filter((message) => message.customType !== "freeflow-runtime-state"),
2668
- [userMessage],
2669
- );
2670
- assert.equal(disabled.messages.at(-1).customType, "freeflow-runtime-state");
2671
- } finally {
2672
- await rm(cwd, { recursive: true, force: true });
2673
- }
2674
- });
2675
-
2676
- test("Pi before_agent_start injects the Freeflow interaction contract on every turn", async () => {
2677
- const cwd = await mkdtemp(join(tmpdir(), "freeflow-pi-core-context-"));
2678
- try {
2679
- await mkdir(join(cwd, ".freeflow"));
2680
- await writeFile(
2681
- join(cwd, ".freeflow/config.json"),
2682
- JSON.stringify({ defaultMode: "workflow", contextVirtualization: true, conversationHistory: true }, null, 2),
2683
- "utf8",
2684
- );
2685
-
2686
- const { handlers } = loadExtension();
2687
- const beforeAgentStart = handlers.get("before_agent_start");
2688
- assert.ok(beforeAgentStart);
2689
-
2690
- const first = await beforeAgentStart({ systemPrompt: "base prompt" }, context(cwd));
2691
- const second = await beforeAgentStart({ systemPrompt: "base prompt" }, context(cwd));
2692
-
2693
- for (const result of [first, second]) {
2694
- assert.match(result.systemPrompt, /# Freeflow Stable Guidance/);
2695
- assert.match(result.systemPrompt, /## Shared Terms/);
2696
- assert.match(result.systemPrompt, /## Context Virtualization Cue/);
2697
- assert.match(result.systemPrompt, /## Conversation History Cue/);
2698
- assert.match(result.systemPrompt, /# Freeflow Interaction Contract/);
2699
- assert.doesNotMatch(result.systemPrompt, /# Freeflow Runtime Kernel/);
2700
- assert.match(result.systemPrompt, /Treat questions, criticism, examples, hypotheses, and tentative ideas as/);
2701
- assert.doesNotMatch(result.systemPrompt, /## Freeflow Runtime Priority/);
2702
- assert.doesNotMatch(result.systemPrompt, /## Loaded Mode Contract Skill/);
2703
- assert.doesNotMatch(result.systemPrompt, /## Loaded Workflow Skill/);
2704
- assert.doesNotMatch(result.systemPrompt, /## Loaded Decision Gate Skill/);
2705
- assert.doesNotMatch(result.systemPrompt, /## Discovery-light/);
2706
- assert.doesNotMatch(result.systemPrompt, /Output Router|freeflow_(search|run|batch)/i);
2707
- assert.doesNotMatch(result.systemPrompt, /## Loaded Workflow Map/);
2708
- }
2709
- } finally {
2710
- await rm(cwd, { recursive: true, force: true });
2711
- }
2712
- });
2713
-
2714
- test("Pi separates stable Freeflow guidance from volatile provider runtime state", async () => {
2715
- const cwd = await mkdtemp(join(tmpdir(), "freeflow-pi-runtime-state-layering-"));
2716
- try {
2717
- await mkdir(join(cwd, ".freeflow"));
2718
- await writeFile(
2719
- join(cwd, ".freeflow/config.json"),
2720
- JSON.stringify(
2721
- {
2722
- defaultMode: "workflow",
2723
- contextVirtualization: true,
2724
- conversationHistory: true,
2725
- },
2726
- null,
2727
- 2,
2728
- ),
2729
- "utf8",
2730
- );
2731
-
2732
- const { handlers } = loadExtension();
2733
- const beforeAgentStart = handlers.get("before_agent_start");
2734
- const contextHandler = handlers.get("context");
2735
- const ctx = context(cwd);
2736
- const before = await beforeAgentStart({ systemPrompt: "base prompt" }, ctx);
2737
-
2738
- assert.match(before.systemPrompt, /## Mode/);
2739
- assert.match(before.systemPrompt, /Use the latest extension-generated Freeflow Runtime State/);
2740
- assert.match(before.systemPrompt, /## Context Virtualization Cue/);
2741
- assert.match(before.systemPrompt, /## Conversation History Cue/);
2742
- assert.doesNotMatch(before.systemPrompt, /# Automatic Routing Kernel/);
2743
- assert.doesNotMatch(before.systemPrompt, /Repository default mode:/);
2744
- assert.doesNotMatch(before.systemPrompt, /Configured default mode:/);
2745
- assert.doesNotMatch(before.systemPrompt, /Effective Freeflow mode:/);
2746
- assert.doesNotMatch(before.systemPrompt, /- Interaction contract: enabled/);
2747
-
2748
- assert.equal(before.message, undefined);
2749
-
2750
- const firstContext = await contextHandler({ messages: [] }, ctx);
2751
- assert.equal(firstContext.messages.length, 1);
2752
- assert.equal(firstContext.messages[0].customType, "freeflow-runtime-state");
2753
- assert.equal(
2754
- firstContext.messages[0].content,
2755
- [
2756
- "# Freeflow Runtime State",
2757
- "",
2758
- "This is extension-generated runtime state. Use it to interpret the stable Freeflow guidance.",
2759
- "",
2760
- "Default mode: `workflow`",
2761
- "Active mode: `workflow`",
2762
- "",
2763
- "Capabilities:",
2764
- "- Interaction Contract: active",
2765
- "- Skills: active",
2766
- "- Context Virtualization: active",
2767
- "- Conversation History: active",
2768
- "- Cognitive Routing: inactive",
2769
- "",
2770
- "Cognitive Routing:",
2771
- "- Control: `unavailable`",
2772
- "- Profile: `unavailable`",
2773
- ].join("\n"),
2774
- );
2775
-
2776
- const repeatedContext = await contextHandler({ messages: firstContext.messages }, ctx);
2777
- assert.equal(repeatedContext.messages.length, 1);
2778
- assert.equal(repeatedContext.messages[0].customType, "freeflow-runtime-state");
770
+ await freeflowCommand.definition.handler("settings repo", settingsCtx);
771
+ const saved = JSON.parse(await readFile(join(cwd, ".freeflow/config.json"), "utf8"));
772
+ assert.deepEqual(saved.cognitiveRouting.profiles.standard, {
773
+ provider: "test",
774
+ model: "model-b",
775
+ thinkingLevel: "max",
776
+ });
777
+ assert.equal(settingsCtx.reloads.length, 1);
2779
778
  } finally {
2780
779
  await rm(cwd, { recursive: true, force: true });
2781
780
  }
2782
781
  });
2783
782
 
2784
- test("Pi session_start and session_compact keep the interaction contract on later turns", async () => {
2785
- const cwd = await mkdtemp(join(tmpdir(), "freeflow-pi-session-cache-"));
783
+ test("Pi filters persisted Workflow and Cognitive Routing bootstrap entries without creating them", async () => {
784
+ const cwd = await configuredRepo();
2786
785
  try {
2787
- await mkdir(join(cwd, ".freeflow"));
2788
- await writeFile(join(cwd, ".freeflow/config.json"), JSON.stringify({ defaultMode: "workflow" }, null, 2), "utf8");
2789
-
2790
786
  const { handlers } = loadExtension();
2791
- const beforeAgentStart = handlers.get("before_agent_start");
2792
- const sessionStart = handlers.get("session_start");
2793
- const sessionCompact = handlers.get("session_compact");
2794
- assert.ok(beforeAgentStart);
2795
- assert.ok(sessionStart);
2796
- assert.ok(sessionCompact);
2797
-
2798
- await beforeAgentStart({ systemPrompt: "base prompt" }, context(cwd));
2799
- const afterFirst = await beforeAgentStart({ systemPrompt: "base prompt" }, context(cwd));
2800
- assert.match(afterFirst.systemPrompt, /# Freeflow Interaction Contract/);
2801
-
2802
- await sessionCompact({ reason: "manual" }, context(cwd));
2803
- const afterCompact = await beforeAgentStart({ systemPrompt: "base prompt" }, context(cwd));
2804
- assert.match(afterCompact.systemPrompt, /# Freeflow Interaction Contract/);
2805
- assert.doesNotMatch(afterCompact.systemPrompt, /## Loaded Workflow Skill/);
2806
- assert.doesNotMatch(afterCompact.systemPrompt, /Output Router|freeflow_(search|run|batch)/i);
2807
-
2808
- await sessionStart({ reason: "resume" }, context(cwd));
2809
- const afterResume = await beforeAgentStart({ systemPrompt: "base prompt" }, context(cwd));
2810
- assert.match(afterResume.systemPrompt, /# Freeflow Interaction Contract/);
2811
- assert.doesNotMatch(afterResume.systemPrompt, /## Loaded Workflow Skill/);
2812
- assert.doesNotMatch(afterResume.systemPrompt, /Output Router|freeflow_(search|run|batch)/i);
2813
- } finally {
2814
- await rm(cwd, { recursive: true, force: true });
2815
- }
2816
- });
2817
-
2818
- test("Pi ignores retired router config without affecting core activation", async () => {
2819
- const cwd = await mkdtemp(join(tmpdir(), "freeflow-pi-retired-router-config-"));
2820
- try {
2821
- await mkdir(join(cwd, ".freeflow"));
2822
- await writeFile(
2823
- join(cwd, ".freeflow/config.json"),
2824
- JSON.stringify({
2825
- defaultMode: "workflow",
2826
- outputRouter: { postToolRouting: "always" },
2827
- }),
2828
- "utf8",
787
+ const userMessage = { role: "user", content: "hello", timestamp: Date.now() };
788
+ const messages = [
789
+ { role: "custom", customType: "freeflow-workflow-bootstrap", content: "old workflow", display: false },
790
+ { role: "custom", customType: "freeflow-cognitive-routing-bootstrap", content: "old routing", display: false },
791
+ userMessage,
792
+ ];
793
+ const result = await handlers.get("context")({ messages }, context(cwd));
794
+ assert.deepEqual(
795
+ result.messages.filter((message) => message.customType !== "freeflow-runtime-state"),
796
+ [userMessage],
2829
797
  );
2830
-
2831
- const { handlers } = loadExtension();
2832
- const notifications = [];
2833
- const ctx = context(cwd);
2834
- ctx.ui.notify = (message, level) => notifications.push({ message, level });
2835
- await handlers.get("session_start")({}, ctx);
2836
- const result = await handlers.get("before_agent_start")({ systemPrompt: "base prompt" }, ctx);
2837
-
2838
- assert.equal(notifications.length, 0);
2839
- assert.match(result.systemPrompt, /# Freeflow Interaction Contract/);
2840
- assert.doesNotMatch(result.systemPrompt, /Output Router|freeflow_(search|run|batch)/i);
798
+ const runtimeStateIndex = result.messages.findIndex((message) => message.customType === "freeflow-runtime-state");
799
+ assert.equal(runtimeStateIndex, result.messages.findIndex((message) => message.role === "user") - 1);
2841
800
  } finally {
2842
801
  await rm(cwd, { recursive: true, force: true });
2843
802
  }
2844
803
  });
2845
804
 
2846
- test("Pi appends the interaction contract after existing project context", async () => {
2847
- const cwd = await mkdtemp(join(tmpdir(), "freeflow-pi-already-core-"));
805
+ test("Pi preserves the host prompt when a mandatory prompt file is missing", async () => {
806
+ const root = await mkdtemp(join(tmpdir(), "freeflow-pi-missing-prompt-"));
807
+ const cwd = await configuredRepo();
2848
808
  try {
2849
- await mkdir(join(cwd, ".freeflow"));
2850
- await writeFile(
2851
- join(cwd, ".freeflow/config.json"),
2852
- JSON.stringify({
2853
- defaultMode: "workflow",
2854
- }),
2855
- "utf8",
2856
- );
2857
- const { handlers } = loadExtension();
2858
- const beforeAgentStart = handlers.get("before_agent_start");
2859
-
2860
- const existingPrompt = "# Project Instructions\n\nKeep existing repo guidance.";
2861
- const result = await beforeAgentStart({ systemPrompt: existingPrompt }, context(cwd));
2862
-
2863
- assert.match(result.systemPrompt, /^# Project Instructions/);
2864
- assert.match(result.systemPrompt, /# Freeflow Interaction Contract/);
2865
- assert.doesNotMatch(result.systemPrompt, /# Freeflow Runtime Kernel/);
2866
- assert.doesNotMatch(result.systemPrompt, /## Loaded Workflow Skill/);
2867
- assert.doesNotMatch(result.systemPrompt, /## Discovery-light/);
2868
- assert.doesNotMatch(result.systemPrompt, /Output Router|freeflow_(search|run|batch)/i);
809
+ await mkdir(join(root, "pi-extension"), { recursive: true });
810
+ await execFileAsync("cp", ["-R", join(process.cwd(), "pi-extension", "dist"), join(root, "pi-extension")]);
811
+ await execFileAsync("cp", ["-R", join(process.cwd(), "runtime"), root]);
812
+ await execFileAsync("ln", ["-s", join(process.cwd(), "node_modules"), join(root, "node_modules")]);
813
+ await execFileAsync("rm", [join(root, "runtime", "prompts", "interaction-contract.md")]);
814
+ const extension = (
815
+ await import(`${new URL(`file://${join(root, "pi-extension", "dist", "index.js")}`).href}?missing=${Date.now()}`)
816
+ ).default;
817
+ const { handlers } = loadExtension(extension);
818
+ const before = await handlers.get("before_agent_start")({ systemPrompt: "base prompt" }, context(cwd));
819
+ assert.equal(before.systemPrompt, "base prompt");
820
+ const resources = await handlers.get("resources_discover")({ cwd }, context(cwd));
821
+ assert.deepEqual(resources.skillPaths, []);
2869
822
  } finally {
2870
823
  await rm(cwd, { recursive: true, force: true });
824
+ await rm(root, { recursive: true, force: true });
2871
825
  }
2872
826
  });
2873
827
 
2874
- test("Pi keeps core capability context alongside the interaction contract", async () => {
2875
- const cwd = await mkdtemp(join(tmpdir(), "freeflow-pi-already-full-"));
2876
- try {
2877
- await mkdir(join(cwd, ".freeflow"));
2878
- await writeFile(
2879
- join(cwd, ".freeflow/config.json"),
2880
- JSON.stringify({
2881
- defaultMode: "workflow",
2882
- }),
2883
- "utf8",
2884
- );
2885
- const { handlers } = loadExtension();
2886
- const beforeAgentStart = handlers.get("before_agent_start");
2887
-
2888
- const result = await beforeAgentStart({ systemPrompt: "base prompt" }, context(cwd));
2889
-
2890
- assert.match(result.systemPrompt, /# Freeflow Interaction Contract/);
2891
- assert.doesNotMatch(result.systemPrompt, /# Freeflow Runtime Kernel/);
2892
- assert.doesNotMatch(result.systemPrompt, /## Loaded Workflow Skill/);
2893
- assert.doesNotMatch(result.systemPrompt, /## Discovery-light/);
2894
- assert.doesNotMatch(result.systemPrompt, /Output Router|freeflow_(search|run|batch)/i);
2895
- assert.doesNotMatch(result.systemPrompt, /Capture raw evidence before transformation/);
2896
- } finally {
2897
- await rm(cwd, { recursive: true, force: true });
2898
- }
2899
- });
828
+ function cognitiveRoutingModelRegistry() {
829
+ const models = [
830
+ { provider: "test", id: "model-a", name: "Model A", reasoning: true },
831
+ { provider: "test", id: "model-b", name: "Model B", reasoning: true },
832
+ ];
833
+ const supported = new Map([
834
+ ["model-a", new Set(["off", "low", "medium", "high"])],
835
+ ["model-b", new Set(["off", "low", "medium", "high", "max"])],
836
+ ]);
837
+ return {
838
+ getAvailable() {
839
+ return models;
840
+ },
841
+ find(provider, id) {
842
+ return models.find((model) => model.provider === provider && model.id === id);
843
+ },
844
+ async getApiKeyAndHeaders() {
845
+ return { ok: true };
846
+ },
847
+ clampThinkingLevel(model, level) {
848
+ return supported.get(model.id)?.has(level) ? level : "off";
849
+ },
850
+ };
851
+ }