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