@hassangameryt/freeflow 0.7.2 → 0.7.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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 +147 -127
- package/capabilities/cognitive-routing/SKILL.md +162 -93
- package/gemini-extension.json +1 -1
- package/package.json +1 -1
- package/pi-extension/dist/cognitive-routing-v2/config.js +35 -19
- package/pi-extension/dist/cognitive-routing-v2/projection.js +53 -44
- package/pi-extension/dist/cognitive-routing-v2/render.js +31 -10
- package/pi-extension/dist/cognitive-routing-v2/runtime.js +701 -105
- package/pi-extension/dist/cognitive-routing-v2/schemas.js +90 -24
- package/pi-extension/dist/cognitive-routing-v2/state.js +243 -27
- package/pi-extension/dist/cognitive-routing-v2/tools.js +19 -50
- package/pi-extension/dist/cognitive-routing-v2/types.js +5 -1
- package/pi-extension/dist/context-virtualization/runtime.js +29 -7
- package/pi-extension/dist/freeflow-context/tool.js +7 -6
- package/pi-extension/dist/index.js +36 -15
- 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 +29 -10
- package/pi-extension/dist/session-sources/events.js +22 -0
- package/pi-extension/dist/session-sources/provenance.js +32 -0
- package/pi-extension/dist/session-sources/sources.js +51 -7
- package/pi-extension/dist/settings/settings-ui.js +247 -47
- package/pi-extension/src/cognitive-routing-v2/config.ts +49 -19
- package/pi-extension/src/cognitive-routing-v2/projection.ts +53 -43
- package/pi-extension/src/cognitive-routing-v2/render.ts +31 -10
- package/pi-extension/src/cognitive-routing-v2/runtime.ts +712 -105
- package/pi-extension/src/cognitive-routing-v2/schemas.ts +95 -24
- package/pi-extension/src/cognitive-routing-v2/state.ts +244 -27
- package/pi-extension/src/cognitive-routing-v2/tools.ts +19 -50
- package/pi-extension/src/cognitive-routing-v2/types.ts +35 -3
- package/pi-extension/src/context-virtualization/runtime.ts +30 -8
- package/pi-extension/src/freeflow-context/tool.ts +7 -6
- package/pi-extension/src/index.ts +36 -14
- 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 +32 -10
- package/pi-extension/src/session-sources/events.ts +22 -0
- package/pi-extension/src/session-sources/provenance.ts +39 -0
- package/pi-extension/src/session-sources/sources.ts +77 -6
- package/pi-extension/src/settings/settings-ui.ts +276 -46
- 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 +94 -1
- package/pi-extension/tests/cognitive-routing-v2/delegation.test.js +557 -0
- package/pi-extension/tests/cognitive-routing-v2/native.test.js +34 -1
- package/pi-extension/tests/cognitive-routing-v2/persistence.test.js +55 -0
- package/pi-extension/tests/cognitive-routing-v2/recovery.test.js +800 -2
- package/pi-extension/tests/cognitive-routing-v2/render.test.js +15 -1
- package/pi-extension/tests/cognitive-routing-v2/runtime.test.js +273 -3
- package/pi-extension/tests/cognitive-routing-v2/state.test.js +312 -1
- package/pi-extension/tests/context-virtualization/extension.test.js +20 -10
- package/pi-extension/tests/context-virtualization/runtime.test.js +17 -3
- package/pi-extension/tests/fixtures/routing-native.js +4 -1
- package/pi-extension/tests/integration/mode-free-core.test.js +4 -3
- package/pi-extension/tests/integration/pi-extension.test.js +508 -29
- package/pi-extension/tests/integration/prompt-architecture.test.js +56 -40
- package/pi-extension/tests/integration/unaffected-regressions.test.js +3 -3
- 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 -4
- package/skills/design-for-depth/SKILL.md +1 -1
- package/skills/design-for-depth/references/implementation-decisions.md +3 -3
- package/skills/discuss/SKILL.md +2 -2
|
@@ -8,6 +8,7 @@ import { promisify } from "node:util";
|
|
|
8
8
|
import { SessionManager } from "@earendil-works/pi-coding-agent";
|
|
9
9
|
|
|
10
10
|
import freeflowExtension from "../../dist/index.js";
|
|
11
|
+
import { handleFreeflowCommand } from "../../dist/settings/settings-ui.js";
|
|
11
12
|
import {
|
|
12
13
|
readCapabilityState,
|
|
13
14
|
readFreeflowConfigLayers,
|
|
@@ -17,6 +18,7 @@ import {
|
|
|
17
18
|
setSessionCoreOverride,
|
|
18
19
|
} from "../../dist/runtime/runtime-context.js";
|
|
19
20
|
import { PIFLOW_HOST } from "../fixtures/pi-host.js";
|
|
21
|
+
import { matches } from "../../dist/cognitive-routing-v2/schemas.js";
|
|
20
22
|
|
|
21
23
|
const execFileAsync = promisify(execFile);
|
|
22
24
|
|
|
@@ -250,28 +252,74 @@ test("new routing tools and projection state follow the configured contract", as
|
|
|
250
252
|
delegate.parameters.oneOf.map((branch) => branch.properties.operation.enum[0]),
|
|
251
253
|
["assign", "replace"],
|
|
252
254
|
);
|
|
255
|
+
assert.deepEqual(delegate.parameters.properties.worker.enum, ["helper", "executor"]);
|
|
256
|
+
assert.equal(
|
|
257
|
+
delegate.parameters.oneOf.every((branch) => !branch.required.includes("worker")),
|
|
258
|
+
true,
|
|
259
|
+
);
|
|
253
260
|
assert.deepEqual(
|
|
254
261
|
returning.parameters.oneOf.map((branch) => branch.properties.operation.enum[0]),
|
|
255
|
-
["submit", "retry"],
|
|
262
|
+
["submit", "supplement", "retry"],
|
|
256
263
|
);
|
|
257
|
-
assert.equal(loaded.activeToolNames().includes("freeflow_project"),
|
|
264
|
+
assert.equal(loaded.activeToolNames().includes("freeflow_project"), true);
|
|
258
265
|
|
|
259
266
|
const providerContext = await loaded.handlers.get("context")({ messages: [] }, ctx);
|
|
267
|
+
assert.match(providerContext.messages[0].content, /Delegation: `executor`/);
|
|
260
268
|
assert.match(providerContext.messages[0].content, new RegExp("Projection: `" + scenario.expectedMode + "`"));
|
|
261
269
|
|
|
262
270
|
await loaded.commands.find((command) => command.name === "freeflow").definition.handler("profile executor", ctx);
|
|
263
271
|
const manualContext = await loaded.handlers.get("context")({ messages: [] }, ctx);
|
|
264
272
|
assert.match(
|
|
265
|
-
manualContext.messages
|
|
273
|
+
manualContext.messages.findLast((message) => message.customType === "freeflow-runtime-state").content,
|
|
266
274
|
new RegExp("Projection: `" + (scenario.projection ? "manual-bypass" : "disabled") + "`"),
|
|
267
275
|
);
|
|
268
|
-
assert.equal(loaded.activeToolNames().includes("freeflow_project"),
|
|
276
|
+
assert.equal(loaded.activeToolNames().includes("freeflow_project"), true);
|
|
269
277
|
} finally {
|
|
270
278
|
await rm(cwd, { recursive: true, force: true });
|
|
271
279
|
}
|
|
272
280
|
}
|
|
273
281
|
});
|
|
274
282
|
|
|
283
|
+
test("Pi describes strict evidence-selection operation shapes", () => {
|
|
284
|
+
const { tools } = loadExtension();
|
|
285
|
+
const project = tools.find((tool) => tool.name === "freeflow_project");
|
|
286
|
+
assert.ok(project);
|
|
287
|
+
assert.deepEqual(
|
|
288
|
+
project.parameters.oneOf.map((branch) => branch.properties.operation.enum[0]),
|
|
289
|
+
["inspect", "add", "remove"],
|
|
290
|
+
);
|
|
291
|
+
assert.deepEqual(Object.keys(project.parameters.oneOf[1].properties), ["operation", "refs"]);
|
|
292
|
+
assert.deepEqual(Object.keys(project.parameters.oneOf[2].properties), ["operation", "refs", "reason"]);
|
|
293
|
+
assert.match(project.description, /reason is remove-only/i);
|
|
294
|
+
assert.match(project.promptGuidelines.join(" "), /reason is required only for remove and invalid for add\/inspect/i);
|
|
295
|
+
assert.match(
|
|
296
|
+
project.promptGuidelines.join(" "),
|
|
297
|
+
/for add.*never add refs marked not offered.*for remove.*currently selected or unresolved/i,
|
|
298
|
+
);
|
|
299
|
+
assert.match(project.promptGuidelines.join(" "), /add exact eligible visible refs directly/i);
|
|
300
|
+
assert.doesNotMatch(project.promptGuidelines.join(" "), /inspect first/i);
|
|
301
|
+
assert.match(project.parameters.properties.reason.description, /only for remove/i);
|
|
302
|
+
assert.match(project.parameters.oneOf[1].properties.refs.description, /never add a ref marked not offered/i);
|
|
303
|
+
assert.match(
|
|
304
|
+
project.parameters.oneOf[2].properties.refs.description,
|
|
305
|
+
/currently selected or unresolved.*non-selectable ref may be removed/i,
|
|
306
|
+
);
|
|
307
|
+
assert.match(
|
|
308
|
+
project.parameters.properties.refs.description,
|
|
309
|
+
/For add.*eligible.*For remove.*currently selected or unresolved/i,
|
|
310
|
+
);
|
|
311
|
+
|
|
312
|
+
assert.equal(
|
|
313
|
+
matches({ operation: "add", refs: ["ctx:example"], reason: "selection explanation" }, project.parameters),
|
|
314
|
+
false,
|
|
315
|
+
);
|
|
316
|
+
assert.equal(matches({ operation: "add", refs: ["ctx:example"] }, project.parameters), true);
|
|
317
|
+
assert.equal(
|
|
318
|
+
matches({ operation: "remove", refs: ["ctx:example"], reason: "selection explanation" }, project.parameters),
|
|
319
|
+
true,
|
|
320
|
+
);
|
|
321
|
+
});
|
|
322
|
+
|
|
275
323
|
test("Pi registers the remaining Freeflow commands without mode controls or retired router tools", () => {
|
|
276
324
|
const { commands, shortcuts, tools } = loadExtension();
|
|
277
325
|
const commandNames = commands.map((command) => command.name);
|
|
@@ -330,9 +378,9 @@ test("PiFlow keeps Cognitive Routing unavailable while exposing its configuratio
|
|
|
330
378
|
const ctx = context(cwd);
|
|
331
379
|
await handlers.get("session_start")({ type: "session_start" }, ctx);
|
|
332
380
|
assert.match(ctx.statuses.at(-1).value, /cognitive blocked ·/);
|
|
333
|
-
assert.ok(
|
|
381
|
+
assert.ok(activeToolNames().includes("freeflow_delegate"));
|
|
334
382
|
const before = await handlers.get("before_agent_start")({ systemPrompt: "base" }, ctx);
|
|
335
|
-
assert.
|
|
383
|
+
assert.match(before.systemPrompt, /guidance is dormant/);
|
|
336
384
|
} finally {
|
|
337
385
|
await rm(cwd, { recursive: true, force: true });
|
|
338
386
|
}
|
|
@@ -366,7 +414,7 @@ test("normal Pi settings expose active Cognitive Routing configuration", async (
|
|
|
366
414
|
settingsCtx.ui.custom = async (factory) => {
|
|
367
415
|
const component = factory({ requestRender() {} }, testTheme, {}, () => {});
|
|
368
416
|
const rootText = renderText(component);
|
|
369
|
-
assert.match(rootText, /Cognitive Routing\s+enabled \(
|
|
417
|
+
assert.match(rootText, /Cognitive Routing\s+enabled \(6\) active/);
|
|
370
418
|
assert.doesNotMatch(rootText, /PiFlow only/);
|
|
371
419
|
return undefined;
|
|
372
420
|
};
|
|
@@ -377,6 +425,180 @@ test("normal Pi settings expose active Cognitive Routing configuration", async (
|
|
|
377
425
|
}
|
|
378
426
|
});
|
|
379
427
|
|
|
428
|
+
test("Pi settings persist delegation mode without rewriting complete presets", async () => {
|
|
429
|
+
const initial = {
|
|
430
|
+
cognitiveRouting: {
|
|
431
|
+
enabled: true,
|
|
432
|
+
profiles: {
|
|
433
|
+
coordinator: { provider: "test", model: "model-a", thinking: "low" },
|
|
434
|
+
helper: { provider: "test", model: "model-b", thinking: "high" },
|
|
435
|
+
executor: { provider: "test", model: "model-b", thinking: "max" },
|
|
436
|
+
},
|
|
437
|
+
},
|
|
438
|
+
};
|
|
439
|
+
const cwd = await configuredRepo(initial);
|
|
440
|
+
try {
|
|
441
|
+
const { commands } = loadExtension(freeflowExtension, null, {
|
|
442
|
+
appendEntry() {},
|
|
443
|
+
async setModel() {
|
|
444
|
+
return true;
|
|
445
|
+
},
|
|
446
|
+
setThinkingLevel() {},
|
|
447
|
+
});
|
|
448
|
+
const command = commands.find((candidate) => candidate.name === "freeflow");
|
|
449
|
+
const settingsCtx = context(cwd);
|
|
450
|
+
settingsCtx.isIdle = () => true;
|
|
451
|
+
settingsCtx.modelRegistry = cognitiveRoutingModelRegistry();
|
|
452
|
+
settingsCtx.ui.custom = async (factory) => {
|
|
453
|
+
let result;
|
|
454
|
+
const component = factory({ requestRender() {} }, testTheme, {}, (value) => {
|
|
455
|
+
result = value;
|
|
456
|
+
});
|
|
457
|
+
component.handleInput("\u001b[B");
|
|
458
|
+
component.handleInput("\r");
|
|
459
|
+
component.handleInput("\u001b[B");
|
|
460
|
+
component.handleInput("\r");
|
|
461
|
+
component.handleInput("\u001b[B");
|
|
462
|
+
component.handleInput("\u001b[B");
|
|
463
|
+
component.handleInput("\r");
|
|
464
|
+
await component.waitForWrites();
|
|
465
|
+
component.handleInput("\u001b");
|
|
466
|
+
component.handleInput("\u001b");
|
|
467
|
+
return result;
|
|
468
|
+
};
|
|
469
|
+
await command.definition.handler("settings repo", settingsCtx);
|
|
470
|
+
const saved = JSON.parse(await readFile(join(cwd, ".freeflow/config.json"), "utf8"));
|
|
471
|
+
assert.equal(saved.cognitiveRouting.delegation, "both");
|
|
472
|
+
assert.deepEqual(saved.cognitiveRouting.profiles, initial.cognitiveRouting.profiles);
|
|
473
|
+
} finally {
|
|
474
|
+
await rm(cwd, { recursive: true, force: true });
|
|
475
|
+
}
|
|
476
|
+
});
|
|
477
|
+
|
|
478
|
+
test("personal delegation override and inherit preserve repository mode and other local presets", async () => {
|
|
479
|
+
const repository = {
|
|
480
|
+
cognitiveRouting: {
|
|
481
|
+
enabled: true,
|
|
482
|
+
delegation: "helper",
|
|
483
|
+
profiles: {
|
|
484
|
+
coordinator: { provider: "test", model: "model-a", thinking: "low" },
|
|
485
|
+
helper: { provider: "test", model: "model-b", thinking: "high" },
|
|
486
|
+
executor: { provider: "test", model: "model-b", thinking: "max" },
|
|
487
|
+
},
|
|
488
|
+
},
|
|
489
|
+
};
|
|
490
|
+
const local = {
|
|
491
|
+
contextVirtualization: true,
|
|
492
|
+
cognitiveRouting: {
|
|
493
|
+
profiles: {
|
|
494
|
+
helper: { provider: "test", model: "model-b", thinking: "medium" },
|
|
495
|
+
},
|
|
496
|
+
},
|
|
497
|
+
};
|
|
498
|
+
const cwd = await configuredRepo(repository);
|
|
499
|
+
const repositoryPath = join(cwd, ".freeflow/config.json");
|
|
500
|
+
const localPath = join(cwd, ".freeflow/local.json");
|
|
501
|
+
await writeFile(localPath, JSON.stringify(local, null, 2), "utf8");
|
|
502
|
+
try {
|
|
503
|
+
const { commands, pi } = loadExtension(freeflowExtension, null, {
|
|
504
|
+
appendEntry() {},
|
|
505
|
+
async setModel() {
|
|
506
|
+
return true;
|
|
507
|
+
},
|
|
508
|
+
setThinkingLevel() {},
|
|
509
|
+
});
|
|
510
|
+
const command = commands.find((candidate) => candidate.name === "freeflow");
|
|
511
|
+
const settingsCtx = context(cwd);
|
|
512
|
+
settingsCtx.isIdle = () => true;
|
|
513
|
+
settingsCtx.modelRegistry = cognitiveRoutingModelRegistry();
|
|
514
|
+
|
|
515
|
+
const chooseDelegation = async (direction, commit = true) => {
|
|
516
|
+
settingsCtx.ui.custom = async (factory) => {
|
|
517
|
+
let result;
|
|
518
|
+
const component = factory({ requestRender() {} }, testTheme, {}, (value) => {
|
|
519
|
+
result = value;
|
|
520
|
+
});
|
|
521
|
+
component.handleInput("\u001b[B");
|
|
522
|
+
component.handleInput("\r");
|
|
523
|
+
component.handleInput("\u001b[B");
|
|
524
|
+
component.handleInput("\r");
|
|
525
|
+
component.handleInput(direction);
|
|
526
|
+
component.handleInput(commit ? "\r" : "\u001b");
|
|
527
|
+
if (commit) await component.waitForWrites();
|
|
528
|
+
component.handleInput("\u001b");
|
|
529
|
+
component.handleInput("\u001b");
|
|
530
|
+
return result;
|
|
531
|
+
};
|
|
532
|
+
await command.definition.handler("settings", settingsCtx);
|
|
533
|
+
};
|
|
534
|
+
|
|
535
|
+
await chooseDelegation("\u001b[B");
|
|
536
|
+
let saved = JSON.parse(await readFile(localPath, "utf8"));
|
|
537
|
+
assert.equal(saved.cognitiveRouting.delegation, "executor");
|
|
538
|
+
assert.deepEqual(saved.cognitiveRouting.profiles, local.cognitiveRouting.profiles);
|
|
539
|
+
assert.deepEqual(JSON.parse(await readFile(repositoryPath, "utf8")), repository);
|
|
540
|
+
assert.equal((await readCapabilityState(cwd, settingsCtx, pi.host)).cognitiveRouting.delegation, "executor");
|
|
541
|
+
|
|
542
|
+
await chooseDelegation("\u001b[A");
|
|
543
|
+
saved = JSON.parse(await readFile(localPath, "utf8"));
|
|
544
|
+
assert.equal(saved.cognitiveRouting.delegation, undefined);
|
|
545
|
+
assert.deepEqual(saved.cognitiveRouting.profiles, local.cognitiveRouting.profiles);
|
|
546
|
+
assert.equal((await readCapabilityState(cwd, settingsCtx, pi.host)).cognitiveRouting.delegation, "helper");
|
|
547
|
+
|
|
548
|
+
const beforeCancel = await readFile(localPath, "utf8");
|
|
549
|
+
await chooseDelegation("\u001b[B", false);
|
|
550
|
+
assert.equal(await readFile(localPath, "utf8"), beforeCancel);
|
|
551
|
+
assert.deepEqual(JSON.parse(await readFile(repositoryPath, "utf8")), repository);
|
|
552
|
+
} finally {
|
|
553
|
+
await rm(cwd, { recursive: true, force: true });
|
|
554
|
+
}
|
|
555
|
+
});
|
|
556
|
+
|
|
557
|
+
test("both mode session settings expose Helper and both existing profile presets", async () => {
|
|
558
|
+
const cwd = await configuredRepo({
|
|
559
|
+
cognitiveRouting: {
|
|
560
|
+
enabled: true,
|
|
561
|
+
delegation: "both",
|
|
562
|
+
profiles: {
|
|
563
|
+
coordinator: { provider: "test", model: "model-a", thinking: "low" },
|
|
564
|
+
helper: { provider: "test", model: "model-b", thinking: "high" },
|
|
565
|
+
executor: { provider: "test", model: "model-b", thinking: "max" },
|
|
566
|
+
},
|
|
567
|
+
},
|
|
568
|
+
});
|
|
569
|
+
try {
|
|
570
|
+
const { commands } = loadExtension(freeflowExtension, null, {
|
|
571
|
+
appendEntry() {},
|
|
572
|
+
async setModel() {
|
|
573
|
+
return true;
|
|
574
|
+
},
|
|
575
|
+
setThinkingLevel() {},
|
|
576
|
+
});
|
|
577
|
+
const command = commands.find((candidate) => candidate.name === "freeflow");
|
|
578
|
+
const settingsCtx = context(cwd);
|
|
579
|
+
settingsCtx.isIdle = () => true;
|
|
580
|
+
settingsCtx.modelRegistry = cognitiveRoutingModelRegistry();
|
|
581
|
+
settingsCtx.ui.custom = async (factory) => {
|
|
582
|
+
let result;
|
|
583
|
+
const component = factory({ requestRender() {} }, testTheme, {}, (value) => {
|
|
584
|
+
result = value;
|
|
585
|
+
});
|
|
586
|
+
component.handleInput("\u001b[B");
|
|
587
|
+
component.handleInput("\u001b[B");
|
|
588
|
+
component.handleInput("\r");
|
|
589
|
+
const presets = component.render(180).join("\n");
|
|
590
|
+
assert.match(presets, /Coordinator preset/);
|
|
591
|
+
assert.match(presets, /Helper preset/);
|
|
592
|
+
assert.match(presets, /Executor preset/);
|
|
593
|
+
result = { changed: false };
|
|
594
|
+
return result;
|
|
595
|
+
};
|
|
596
|
+
await command.definition.handler("settings session", settingsCtx);
|
|
597
|
+
} finally {
|
|
598
|
+
await rm(cwd, { recursive: true, force: true });
|
|
599
|
+
}
|
|
600
|
+
});
|
|
601
|
+
|
|
380
602
|
test("Pi statusline reports only dynamic Cognitive Routing and context state", () => {
|
|
381
603
|
const ctx = context();
|
|
382
604
|
setFreeflowStatus(
|
|
@@ -492,7 +714,8 @@ test("Pi describes the mode-free Freeflow argument surface and manual profile co
|
|
|
492
714
|
]);
|
|
493
715
|
assert.deepEqual(freeflowCommand.definition.getArgumentCompletions("profile "), [
|
|
494
716
|
{ value: "profile coordinator", label: "coordinator", description: "Hold Coordinator manually" },
|
|
495
|
-
{ value: "profile
|
|
717
|
+
{ value: "profile helper", label: "helper", description: "Hold Helper manually when enabled" },
|
|
718
|
+
{ value: "profile executor", label: "executor", description: "Hold Executor manually when enabled" },
|
|
496
719
|
{ value: "profile auto", label: "auto", description: "Return to automatic Coordinator reconciliation" },
|
|
497
720
|
{ value: "profile history", label: "history", description: "Read routing observations" },
|
|
498
721
|
]);
|
|
@@ -510,11 +733,13 @@ test("Pi exposes 24 base skills without TDD, a mode skill, or compatibility alia
|
|
|
510
733
|
try {
|
|
511
734
|
const { handlers, commands, sentMessages, sentMessageOptions } = loadExtension();
|
|
512
735
|
const resources = await handlers.get("resources_discover")({ cwd }, context(cwd));
|
|
513
|
-
const skillNames = resources.skillPaths
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
736
|
+
const skillNames = resources.skillPaths
|
|
737
|
+
.filter((path) => path.includes("/skills/"))
|
|
738
|
+
.map((path) => {
|
|
739
|
+
const match = path.match(/[\\/]skills[\\/]([^\\/]+)\/SKILL\.md$/);
|
|
740
|
+
assert.ok(match, `unexpected skill path: ${path}`);
|
|
741
|
+
return match[1];
|
|
742
|
+
});
|
|
518
743
|
assert.equal(skillNames.length, 24);
|
|
519
744
|
assert.ok(skillNames.includes("workflow"));
|
|
520
745
|
assert.ok(!skillNames.includes("tdd"));
|
|
@@ -560,10 +785,11 @@ test("Pi keeps Freeflow inactive until repository activation exists", async () =
|
|
|
560
785
|
try {
|
|
561
786
|
const { handlers } = loadExtension();
|
|
562
787
|
const resources = await handlers.get("resources_discover")({ cwd }, context(cwd));
|
|
563
|
-
assert.equal(resources.skillPaths.length,
|
|
564
|
-
assert.
|
|
788
|
+
assert.equal(resources.skillPaths.length, 27);
|
|
789
|
+
assert.ok(resources.skillPaths.some((path) => path.endsWith("/skills/setup-freeflow/SKILL.md")));
|
|
565
790
|
const result = await handlers.get("before_agent_start")({ systemPrompt: "base prompt" }, context(cwd));
|
|
566
|
-
assert.
|
|
791
|
+
assert.ok(result.systemPrompt.startsWith("base prompt\n\n"));
|
|
792
|
+
assert.match(result.systemPrompt, /guidance is dormant/);
|
|
567
793
|
} finally {
|
|
568
794
|
await rm(cwd, { recursive: true, force: true });
|
|
569
795
|
}
|
|
@@ -588,10 +814,11 @@ test("Pi treats invalid configuration as inactive", async () => {
|
|
|
588
814
|
try {
|
|
589
815
|
const { handlers, commands } = loadExtension();
|
|
590
816
|
const resources = await handlers.get("resources_discover")({ cwd }, context(cwd));
|
|
591
|
-
assert.equal(resources.skillPaths.length,
|
|
592
|
-
assert.
|
|
817
|
+
assert.equal(resources.skillPaths.length, 27);
|
|
818
|
+
assert.ok(resources.skillPaths.some((path) => path.endsWith("/skills/setup-freeflow/SKILL.md")));
|
|
593
819
|
const result = await handlers.get("before_agent_start")({ systemPrompt: "base prompt" }, context(cwd));
|
|
594
|
-
assert.
|
|
820
|
+
assert.ok(result.systemPrompt.startsWith("base prompt\n\n"));
|
|
821
|
+
assert.match(result.systemPrompt, /guidance is dormant/);
|
|
595
822
|
const freeflowCommand = commands.find((command) => command.name === "freeflow");
|
|
596
823
|
const statusCtx = context(cwd);
|
|
597
824
|
await freeflowCommand.definition.handler("status", statusCtx);
|
|
@@ -686,8 +913,8 @@ test("Pi fails closed when an existing local override is invalid", async () => {
|
|
|
686
913
|
assert.equal(state.enabled, false);
|
|
687
914
|
const { handlers } = loadExtension();
|
|
688
915
|
const resources = await handlers.get("resources_discover")({ cwd }, context(cwd));
|
|
689
|
-
assert.equal(resources.skillPaths.length,
|
|
690
|
-
assert.
|
|
916
|
+
assert.equal(resources.skillPaths.length, 27);
|
|
917
|
+
assert.ok(resources.skillPaths.some((path) => path.endsWith("/skills/setup-freeflow/SKILL.md")));
|
|
691
918
|
} finally {
|
|
692
919
|
await rm(cwd, { recursive: true, force: true });
|
|
693
920
|
}
|
|
@@ -706,22 +933,23 @@ test("Pi local enablement overrides the repository master switch", async () => {
|
|
|
706
933
|
}
|
|
707
934
|
});
|
|
708
935
|
|
|
709
|
-
test("Pi master Freeflow toggle
|
|
936
|
+
test("Pi master Freeflow toggle makes features inactive while preserving their reference surface", async () => {
|
|
710
937
|
const cwd = await configuredRepo({ enabled: false });
|
|
711
938
|
try {
|
|
712
939
|
const { handlers, activeToolNames } = loadExtension();
|
|
713
940
|
const ctx = context(cwd);
|
|
714
941
|
const resources = await handlers.get("resources_discover")({ cwd }, ctx);
|
|
715
|
-
assert.
|
|
942
|
+
assert.equal(resources.skillPaths.length, 27);
|
|
716
943
|
const result = await handlers.get("before_agent_start")({ systemPrompt: "base prompt" }, ctx);
|
|
717
|
-
assert.
|
|
944
|
+
assert.ok(result.systemPrompt.startsWith("base prompt\n\n"));
|
|
945
|
+
assert.match(result.systemPrompt, /guidance is dormant/);
|
|
718
946
|
const providerContext = await handlers.get("context")({ messages: [] }, ctx);
|
|
719
947
|
assert.match(lastRuntimeState(providerContext.messages).content, /Freeflow: inactive/);
|
|
720
948
|
assert.doesNotMatch(
|
|
721
949
|
lastRuntimeState(providerContext.messages).content,
|
|
722
950
|
/Default mode|Active mode|Interaction Contract|Skills/,
|
|
723
951
|
);
|
|
724
|
-
assert.ok(
|
|
952
|
+
assert.ok(activeToolNames().includes("freeflow_context"));
|
|
725
953
|
} finally {
|
|
726
954
|
await rm(cwd, { recursive: true, force: true });
|
|
727
955
|
}
|
|
@@ -875,7 +1103,7 @@ test("Pi Cognitive Routing settings preserve complete presets", async () => {
|
|
|
875
1103
|
});
|
|
876
1104
|
component.handleInput("\u001b[B");
|
|
877
1105
|
component.handleInput("\r");
|
|
878
|
-
component.handleInput("\u001b[B");
|
|
1106
|
+
for (let index = 0; index < 4; index++) component.handleInput("\u001b[B");
|
|
879
1107
|
component.handleInput("\r");
|
|
880
1108
|
component.handleInput("\r");
|
|
881
1109
|
for (let index = 0; index < 1; index++) component.handleInput("\u001b[B");
|
|
@@ -897,6 +1125,256 @@ test("Pi Cognitive Routing settings preserve complete presets", async () => {
|
|
|
897
1125
|
}
|
|
898
1126
|
});
|
|
899
1127
|
|
|
1128
|
+
test("Pi session settings expose both routing preset wizards without changing config files", async () => {
|
|
1129
|
+
const cwd = await configuredRepo({
|
|
1130
|
+
cognitiveRouting: {
|
|
1131
|
+
enabled: true,
|
|
1132
|
+
profiles: {
|
|
1133
|
+
coordinator: { provider: "test", model: "model-a", thinking: "low" },
|
|
1134
|
+
executor: { provider: "test", model: "model-b", thinking: "high" },
|
|
1135
|
+
},
|
|
1136
|
+
},
|
|
1137
|
+
});
|
|
1138
|
+
try {
|
|
1139
|
+
const configPath = join(cwd, ".freeflow/config.json");
|
|
1140
|
+
const localPath = join(cwd, ".freeflow/local.json");
|
|
1141
|
+
const originalConfig = await readFile(configPath, "utf8");
|
|
1142
|
+
const originalLocal = await readFile(localPath, "utf8").catch(() => undefined);
|
|
1143
|
+
const { commands } = loadExtension(
|
|
1144
|
+
freeflowExtension,
|
|
1145
|
+
{},
|
|
1146
|
+
{
|
|
1147
|
+
async setModel() {
|
|
1148
|
+
return true;
|
|
1149
|
+
},
|
|
1150
|
+
setThinkingLevel() {},
|
|
1151
|
+
},
|
|
1152
|
+
);
|
|
1153
|
+
const freeflowCommand = commands.find((command) => command.name === "freeflow");
|
|
1154
|
+
assert.ok(freeflowCommand);
|
|
1155
|
+
const settingsCtx = context(cwd);
|
|
1156
|
+
settingsCtx.isIdle = () => true;
|
|
1157
|
+
settingsCtx.modelRegistry = cognitiveRoutingModelRegistry();
|
|
1158
|
+
settingsCtx.ui.custom = async (factory) => {
|
|
1159
|
+
let result;
|
|
1160
|
+
const component = factory({ requestRender() {} }, testTheme, {}, (value) => {
|
|
1161
|
+
result = value;
|
|
1162
|
+
});
|
|
1163
|
+
const rendered = component.render(180).join("\\n");
|
|
1164
|
+
assert.match(rendered, /Cognitive Routing presets/);
|
|
1165
|
+
component.handleInput("\u001b[B");
|
|
1166
|
+
component.handleInput("\u001b[B");
|
|
1167
|
+
component.handleInput("\r");
|
|
1168
|
+
const presetRendered = component.render(180).join("\\n");
|
|
1169
|
+
assert.match(presetRendered, /Coordinator preset/);
|
|
1170
|
+
assert.match(presetRendered, /Executor preset/);
|
|
1171
|
+
result = { changed: false };
|
|
1172
|
+
await component.waitForWrites();
|
|
1173
|
+
return result;
|
|
1174
|
+
};
|
|
1175
|
+
await freeflowCommand.definition.handler("settings session", settingsCtx);
|
|
1176
|
+
assert.equal(await readFile(configPath, "utf8"), originalConfig);
|
|
1177
|
+
assert.equal(await readFile(localPath, "utf8").catch(() => undefined), originalLocal);
|
|
1178
|
+
} finally {
|
|
1179
|
+
await rm(cwd, { recursive: true, force: true });
|
|
1180
|
+
}
|
|
1181
|
+
});
|
|
1182
|
+
|
|
1183
|
+
test("Pi session preset wizard applies a complete pair without writing config", async () => {
|
|
1184
|
+
const cwd = await configuredRepo({
|
|
1185
|
+
cognitiveRouting: {
|
|
1186
|
+
enabled: true,
|
|
1187
|
+
profiles: {
|
|
1188
|
+
coordinator: { provider: "test", model: "model-a", thinking: "low" },
|
|
1189
|
+
executor: { provider: "test", model: "model-b", thinking: "high" },
|
|
1190
|
+
},
|
|
1191
|
+
},
|
|
1192
|
+
});
|
|
1193
|
+
try {
|
|
1194
|
+
const configPath = join(cwd, ".freeflow/config.json");
|
|
1195
|
+
const originalConfig = await readFile(configPath, "utf8");
|
|
1196
|
+
const calls = [];
|
|
1197
|
+
const controller = {
|
|
1198
|
+
state: () => ({ effective: true, controlMode: "automatic", activeProfile: "coordinator" }),
|
|
1199
|
+
sessionProfileOverrides: () => ({}),
|
|
1200
|
+
setManualProfile: async () => ({ status: "active" }),
|
|
1201
|
+
setAutomaticControl: async () => ({ status: "automatic" }),
|
|
1202
|
+
setSessionProfileOverride: async (profile, override) => {
|
|
1203
|
+
calls.push({ profile, override });
|
|
1204
|
+
return { status: "active" };
|
|
1205
|
+
},
|
|
1206
|
+
resetSessionProfileOverrides: async () => ({ status: "unchanged" }),
|
|
1207
|
+
};
|
|
1208
|
+
const settingsCtx = context(cwd);
|
|
1209
|
+
settingsCtx.isIdle = () => true;
|
|
1210
|
+
settingsCtx.modelRegistry = cognitiveRoutingModelRegistry();
|
|
1211
|
+
const pi = {
|
|
1212
|
+
host: {},
|
|
1213
|
+
appendEntry() {},
|
|
1214
|
+
async setModel() {
|
|
1215
|
+
return true;
|
|
1216
|
+
},
|
|
1217
|
+
setThinkingLevel() {},
|
|
1218
|
+
};
|
|
1219
|
+
settingsCtx.ui.custom = async (factory) => {
|
|
1220
|
+
let finish;
|
|
1221
|
+
const component = factory({ requestRender() {} }, testTheme, {}, (value) => {
|
|
1222
|
+
finish = value;
|
|
1223
|
+
});
|
|
1224
|
+
component.handleInput("\u001b[B");
|
|
1225
|
+
component.handleInput("\u001b[B");
|
|
1226
|
+
component.handleInput("\r");
|
|
1227
|
+
component.handleInput("\r");
|
|
1228
|
+
component.handleInput("\u001b[B");
|
|
1229
|
+
component.handleInput("\r");
|
|
1230
|
+
component.handleInput("\u001b[B");
|
|
1231
|
+
component.handleInput("\r");
|
|
1232
|
+
component.handleInput("\r");
|
|
1233
|
+
await component.waitForWrites();
|
|
1234
|
+
finish?.({ changed: false });
|
|
1235
|
+
return { changed: false };
|
|
1236
|
+
};
|
|
1237
|
+
await handleFreeflowCommand("settings session", settingsCtx, async () => {}, pi, controller);
|
|
1238
|
+
assert.deepEqual(calls, [
|
|
1239
|
+
{
|
|
1240
|
+
profile: "coordinator",
|
|
1241
|
+
override: { provider: "test", modelId: "model-a", thinking: "low" },
|
|
1242
|
+
},
|
|
1243
|
+
]);
|
|
1244
|
+
assert.equal(await readFile(configPath, "utf8"), originalConfig);
|
|
1245
|
+
} finally {
|
|
1246
|
+
await rm(cwd, { recursive: true, force: true });
|
|
1247
|
+
}
|
|
1248
|
+
});
|
|
1249
|
+
|
|
1250
|
+
test("both mode session wizard applies a Helper pair without writing config", async () => {
|
|
1251
|
+
const cwd = await configuredRepo({
|
|
1252
|
+
cognitiveRouting: {
|
|
1253
|
+
enabled: true,
|
|
1254
|
+
delegation: "both",
|
|
1255
|
+
profiles: {
|
|
1256
|
+
coordinator: { provider: "test", model: "model-a", thinking: "low" },
|
|
1257
|
+
helper: { provider: "test", model: "model-b", thinking: "high" },
|
|
1258
|
+
executor: { provider: "test", model: "model-b", thinking: "max" },
|
|
1259
|
+
},
|
|
1260
|
+
},
|
|
1261
|
+
});
|
|
1262
|
+
try {
|
|
1263
|
+
const configPath = join(cwd, ".freeflow/config.json");
|
|
1264
|
+
const originalConfig = await readFile(configPath, "utf8");
|
|
1265
|
+
const calls = [];
|
|
1266
|
+
const controller = {
|
|
1267
|
+
state: () => ({ effective: true, controlMode: "automatic", activeProfile: "coordinator" }),
|
|
1268
|
+
sessionProfileOverrides: () => ({}),
|
|
1269
|
+
setManualProfile: async () => ({ status: "active" }),
|
|
1270
|
+
setAutomaticControl: async () => ({ status: "automatic" }),
|
|
1271
|
+
setSessionProfileOverride: async (profile, override) => {
|
|
1272
|
+
calls.push({ profile, override });
|
|
1273
|
+
return { status: "stored" };
|
|
1274
|
+
},
|
|
1275
|
+
resetSessionProfileOverrides: async () => ({ status: "unchanged" }),
|
|
1276
|
+
};
|
|
1277
|
+
const settingsCtx = context(cwd);
|
|
1278
|
+
settingsCtx.isIdle = () => true;
|
|
1279
|
+
settingsCtx.modelRegistry = cognitiveRoutingModelRegistry();
|
|
1280
|
+
const pi = {
|
|
1281
|
+
host: {},
|
|
1282
|
+
appendEntry() {},
|
|
1283
|
+
async setModel() {
|
|
1284
|
+
return true;
|
|
1285
|
+
},
|
|
1286
|
+
setThinkingLevel() {},
|
|
1287
|
+
};
|
|
1288
|
+
settingsCtx.ui.custom = async (factory) => {
|
|
1289
|
+
let finish;
|
|
1290
|
+
const component = factory({ requestRender() {} }, testTheme, {}, (value) => {
|
|
1291
|
+
finish = value;
|
|
1292
|
+
});
|
|
1293
|
+
component.handleInput("\u001b[B");
|
|
1294
|
+
component.handleInput("\u001b[B");
|
|
1295
|
+
component.handleInput("\r");
|
|
1296
|
+
component.handleInput("\u001b[B");
|
|
1297
|
+
component.handleInput("\r");
|
|
1298
|
+
component.handleInput("\u001b[B");
|
|
1299
|
+
component.handleInput("\r");
|
|
1300
|
+
component.handleInput("\u001b[B");
|
|
1301
|
+
component.handleInput("\r");
|
|
1302
|
+
component.handleInput("\r");
|
|
1303
|
+
await component.waitForWrites();
|
|
1304
|
+
finish?.({ changed: false });
|
|
1305
|
+
return { changed: false };
|
|
1306
|
+
};
|
|
1307
|
+
await handleFreeflowCommand("settings session", settingsCtx, async () => {}, pi, controller);
|
|
1308
|
+
assert.deepEqual(calls, [
|
|
1309
|
+
{
|
|
1310
|
+
profile: "helper",
|
|
1311
|
+
override: { provider: "test", modelId: "model-a", thinking: "low" },
|
|
1312
|
+
},
|
|
1313
|
+
]);
|
|
1314
|
+
assert.equal(await readFile(configPath, "utf8"), originalConfig);
|
|
1315
|
+
} finally {
|
|
1316
|
+
await rm(cwd, { recursive: true, force: true });
|
|
1317
|
+
}
|
|
1318
|
+
});
|
|
1319
|
+
|
|
1320
|
+
test("session reset preserves core overrides when routing reset fails", async () => {
|
|
1321
|
+
const cwd = await configuredRepo({
|
|
1322
|
+
cognitiveRouting: {
|
|
1323
|
+
enabled: true,
|
|
1324
|
+
profiles: {
|
|
1325
|
+
coordinator: { provider: "test", model: "model-a", thinking: "low" },
|
|
1326
|
+
executor: { provider: "test", model: "model-b", thinking: "high" },
|
|
1327
|
+
},
|
|
1328
|
+
},
|
|
1329
|
+
});
|
|
1330
|
+
try {
|
|
1331
|
+
const configPath = join(cwd, ".freeflow/config.json");
|
|
1332
|
+
const originalConfig = await readFile(configPath, "utf8");
|
|
1333
|
+
const settingsCtx = context(cwd);
|
|
1334
|
+
settingsCtx.isIdle = () => true;
|
|
1335
|
+
settingsCtx.modelRegistry = cognitiveRoutingModelRegistry();
|
|
1336
|
+
const pi = {
|
|
1337
|
+
host: {},
|
|
1338
|
+
appendEntry() {},
|
|
1339
|
+
async setModel() {
|
|
1340
|
+
return true;
|
|
1341
|
+
},
|
|
1342
|
+
setThinkingLevel() {},
|
|
1343
|
+
};
|
|
1344
|
+
await setSessionCoreOverride("contextVirtualization", true, settingsCtx, pi);
|
|
1345
|
+
const controller = {
|
|
1346
|
+
state: () => ({ effective: true, controlMode: "automatic", activeProfile: "coordinator" }),
|
|
1347
|
+
sessionProfileOverrides: () => ({}),
|
|
1348
|
+
setManualProfile: async () => ({ status: "active" }),
|
|
1349
|
+
setAutomaticControl: async () => ({ status: "automatic" }),
|
|
1350
|
+
setSessionProfileOverride: async () => ({ status: "stored" }),
|
|
1351
|
+
resetSessionProfileOverrides: async () => ({ status: "blocked", reason: "fixture failure" }),
|
|
1352
|
+
};
|
|
1353
|
+
settingsCtx.ui.custom = async (factory) => {
|
|
1354
|
+
let finish;
|
|
1355
|
+
const component = factory({ requestRender() {} }, testTheme, {}, (value) => {
|
|
1356
|
+
finish = value;
|
|
1357
|
+
});
|
|
1358
|
+
component.handleInput("\u001b[B");
|
|
1359
|
+
component.handleInput("\u001b[B");
|
|
1360
|
+
component.handleInput("\u001b[B");
|
|
1361
|
+
component.handleInput("\r");
|
|
1362
|
+
component.handleInput("\r");
|
|
1363
|
+
await component.waitForWrites();
|
|
1364
|
+
finish?.({ changed: false });
|
|
1365
|
+
return { changed: false };
|
|
1366
|
+
};
|
|
1367
|
+
await handleFreeflowCommand("settings session", settingsCtx, async () => {}, pi, controller);
|
|
1368
|
+
const state = await readCapabilityState(cwd, settingsCtx, pi.host);
|
|
1369
|
+
assert.equal(state.contextVirtualization.effective, true);
|
|
1370
|
+
assert.equal(state.sessionOverrides.contextVirtualization, true);
|
|
1371
|
+
assert.equal(await readFile(configPath, "utf8"), originalConfig);
|
|
1372
|
+
assert.match(settingsCtx.notifications.at(-1)?.message ?? "", /fixture failure/);
|
|
1373
|
+
} finally {
|
|
1374
|
+
await rm(cwd, { recursive: true, force: true });
|
|
1375
|
+
}
|
|
1376
|
+
});
|
|
1377
|
+
|
|
900
1378
|
test("Pi filters persisted Workflow and Cognitive Routing bootstrap entries without creating them", async () => {
|
|
901
1379
|
const cwd = await configuredRepo();
|
|
902
1380
|
try {
|
|
@@ -919,7 +1397,7 @@ test("Pi filters persisted Workflow and Cognitive Routing bootstrap entries with
|
|
|
919
1397
|
}
|
|
920
1398
|
});
|
|
921
1399
|
|
|
922
|
-
test("Pi preserves the host prompt when a mandatory
|
|
1400
|
+
test("Pi preserves the host prompt prefix and dormant contract when a mandatory file is missing", async () => {
|
|
923
1401
|
const root = await mkdtemp(join(tmpdir(), "freeflow-pi-missing-prompt-"));
|
|
924
1402
|
const cwd = await configuredRepo();
|
|
925
1403
|
try {
|
|
@@ -933,9 +1411,10 @@ test("Pi preserves the host prompt when a mandatory prompt file is missing", asy
|
|
|
933
1411
|
).default;
|
|
934
1412
|
const { handlers } = loadExtension(extension);
|
|
935
1413
|
const before = await handlers.get("before_agent_start")({ systemPrompt: "base prompt" }, context(cwd));
|
|
936
|
-
assert.
|
|
1414
|
+
assert.ok(before.systemPrompt.startsWith("base prompt\n\n"));
|
|
1415
|
+
assert.match(before.systemPrompt, /guidance is dormant/);
|
|
937
1416
|
const resources = await handlers.get("resources_discover")({ cwd }, context(cwd));
|
|
938
|
-
assert.
|
|
1417
|
+
assert.equal(resources.skillPaths.length, 27);
|
|
939
1418
|
} finally {
|
|
940
1419
|
await rm(cwd, { recursive: true, force: true });
|
|
941
1420
|
await rm(root, { recursive: true, force: true });
|