@hassangameryt/freeflow 0.7.1 → 0.7.2
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 +28 -23
- package/capabilities/cognitive-routing/SKILL.md +94 -311
- 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 +120 -0
- package/pi-extension/dist/cognitive-routing-v2/projection.js +321 -0
- package/pi-extension/dist/cognitive-routing-v2/render.js +231 -0
- package/pi-extension/dist/cognitive-routing-v2/runtime.js +1585 -0
- package/pi-extension/dist/cognitive-routing-v2/schemas.js +107 -0
- package/pi-extension/dist/cognitive-routing-v2/state.js +612 -0
- package/pi-extension/dist/cognitive-routing-v2/tools.js +91 -0
- package/pi-extension/dist/cognitive-routing-v2/types.js +44 -0
- package/pi-extension/dist/freeflow-context/tool.js +5 -0
- package/pi-extension/dist/index.js +279 -624
- package/pi-extension/dist/runtime/runtime-context.js +63 -13
- package/pi-extension/dist/session-sources/events.js +195 -0
- package/pi-extension/dist/session-sources/read-only-session.js +148 -0
- package/pi-extension/dist/session-sources/sources.js +233 -0
- package/pi-extension/dist/settings/settings-ui.js +44 -117
- package/pi-extension/src/cognitive-routing-v2/budget.ts +42 -0
- package/pi-extension/src/cognitive-routing-v2/config.ts +143 -0
- package/pi-extension/src/cognitive-routing-v2/projection.ts +365 -0
- package/pi-extension/src/cognitive-routing-v2/render.ts +256 -0
- package/pi-extension/src/cognitive-routing-v2/runtime.ts +1634 -0
- package/pi-extension/src/cognitive-routing-v2/schemas.ts +109 -0
- package/pi-extension/src/cognitive-routing-v2/state.ts +622 -0
- package/pi-extension/src/cognitive-routing-v2/tools.ts +93 -0
- package/pi-extension/src/cognitive-routing-v2/types.ts +222 -0
- package/pi-extension/src/freeflow-context/tool.ts +5 -0
- package/pi-extension/src/index.ts +285 -674
- package/pi-extension/src/runtime/runtime-context.ts +75 -14
- package/pi-extension/src/session-sources/events.ts +208 -0
- package/pi-extension/src/session-sources/read-only-session.ts +170 -0
- package/pi-extension/src/session-sources/sources.ts +263 -0
- package/pi-extension/src/settings/settings-ui.ts +54 -131
- package/pi-extension/tests/cognitive-routing-v2/communication.test.js +484 -0
- package/pi-extension/tests/cognitive-routing-v2/compaction-input.test.js +183 -0
- package/pi-extension/tests/cognitive-routing-v2/native.test.js +441 -0
- package/pi-extension/tests/cognitive-routing-v2/performance.test.js +87 -0
- package/pi-extension/tests/cognitive-routing-v2/persistence.test.js +245 -0
- package/pi-extension/tests/cognitive-routing-v2/projection.test.js +241 -0
- package/pi-extension/tests/cognitive-routing-v2/recovery.test.js +236 -0
- package/pi-extension/tests/cognitive-routing-v2/render.test.js +230 -0
- package/pi-extension/tests/cognitive-routing-v2/runtime.test.js +153 -0
- package/pi-extension/tests/cognitive-routing-v2/stabilization.test.js +521 -0
- package/pi-extension/tests/cognitive-routing-v2/state.test.js +275 -0
- package/pi-extension/tests/context-virtualization/extension.test.js +16 -0
- package/pi-extension/tests/fixtures/routing-native.js +188 -0
- package/pi-extension/tests/integration/mode-free-core.test.js +3 -2
- package/pi-extension/tests/integration/pi-extension.test.js +154 -37
- package/pi-extension/tests/integration/prompt-architecture.test.js +87 -6
- package/pi-extension/tests/integration/unaffected-regressions.test.js +77 -10
- 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,6 +5,7 @@ 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";
|
|
10
11
|
import {
|
|
@@ -15,7 +16,7 @@ import {
|
|
|
15
16
|
setFreeflowStatus,
|
|
16
17
|
setSessionCoreOverride,
|
|
17
18
|
} from "../../dist/runtime/runtime-context.js";
|
|
18
|
-
import { PIFLOW_HOST } from "../
|
|
19
|
+
import { PIFLOW_HOST } from "../fixtures/pi-host.js";
|
|
19
20
|
|
|
20
21
|
const execFileAsync = promisify(execFile);
|
|
21
22
|
|
|
@@ -153,7 +154,7 @@ test("keeps Runtime State before the latest user message during context refreshe
|
|
|
153
154
|
const conversation = [
|
|
154
155
|
{ role: "user", content: "release 0.6.1" },
|
|
155
156
|
{ role: "assistant", content: "delegating" },
|
|
156
|
-
{ role: "toolResult", toolName: "
|
|
157
|
+
{ role: "toolResult", toolName: "freeflow_delegate", content: [] },
|
|
157
158
|
];
|
|
158
159
|
const first = await handlers.get("context")({ messages: conversation }, ctx);
|
|
159
160
|
const firstUserIndex = first.messages.findIndex((message) => message.role === "user");
|
|
@@ -187,6 +188,90 @@ test("keeps Runtime State before the latest user message during context refreshe
|
|
|
187
188
|
}
|
|
188
189
|
});
|
|
189
190
|
|
|
191
|
+
test("new routing tools and projection state follow the configured contract", async () => {
|
|
192
|
+
const cases = [
|
|
193
|
+
{ projection: false, expectedMode: "disabled", exposesProjection: false },
|
|
194
|
+
// Projection can be effective while the Coordinator correctly lacks Executor-only tools.
|
|
195
|
+
{ projection: true, expectedMode: "enabled", exposesProjection: false },
|
|
196
|
+
];
|
|
197
|
+
|
|
198
|
+
for (const scenario of cases) {
|
|
199
|
+
const cwd = await configuredRepo({
|
|
200
|
+
cognitiveRouting: {
|
|
201
|
+
enabled: true,
|
|
202
|
+
projection: scenario.projection,
|
|
203
|
+
profiles: {
|
|
204
|
+
coordinator: { provider: "test", model: "model-a", thinking: "low" },
|
|
205
|
+
executor: { provider: "test", model: "model-b", thinking: "high" },
|
|
206
|
+
},
|
|
207
|
+
},
|
|
208
|
+
});
|
|
209
|
+
try {
|
|
210
|
+
let liveContext;
|
|
211
|
+
const loaded = loadExtension(freeflowExtension, null, {
|
|
212
|
+
appendEntry(customType, data) {
|
|
213
|
+
liveContext.sessionManager.appendCustomEntry(customType, data);
|
|
214
|
+
},
|
|
215
|
+
async setModel(model) {
|
|
216
|
+
liveContext.model = model;
|
|
217
|
+
return true;
|
|
218
|
+
},
|
|
219
|
+
setThinkingLevel(level) {
|
|
220
|
+
liveContext.thinkingLevel = level;
|
|
221
|
+
},
|
|
222
|
+
});
|
|
223
|
+
const ctx = context(cwd);
|
|
224
|
+
liveContext = ctx;
|
|
225
|
+
ctx.model = { provider: "test", id: "return" };
|
|
226
|
+
ctx.thinkingLevel = "medium";
|
|
227
|
+
ctx.modelRegistry = cognitiveRoutingModelRegistry();
|
|
228
|
+
ctx.isIdle = () => true;
|
|
229
|
+
ctx.sessionManager = SessionManager.create(cwd, join(cwd, "sessions"));
|
|
230
|
+
// Reconciliation needs genuine persisted ancestry, not a fake mutable branch array.
|
|
231
|
+
ctx.sessionManager.appendMessage({
|
|
232
|
+
role: "assistant",
|
|
233
|
+
content: [{ type: "text", text: "fixture seed" }],
|
|
234
|
+
timestamp: 1,
|
|
235
|
+
stopReason: "stop",
|
|
236
|
+
});
|
|
237
|
+
await loaded.handlers.get("session_start")({ type: "session_start", reason: "startup" }, ctx);
|
|
238
|
+
await loaded.handlers.get("before_agent_start")({ systemPrompt: "base prompt" }, ctx);
|
|
239
|
+
|
|
240
|
+
const toolNames = loaded.tools.map((tool) => tool.name);
|
|
241
|
+
assert.deepEqual(
|
|
242
|
+
toolNames.filter((name) =>
|
|
243
|
+
["freeflow_delegate", "freeflow_return", "freeflow_unit", "freeflow_project"].includes(name),
|
|
244
|
+
),
|
|
245
|
+
["freeflow_delegate", "freeflow_return", "freeflow_unit", "freeflow_project"],
|
|
246
|
+
);
|
|
247
|
+
const delegate = loaded.tools.find((tool) => tool.name === "freeflow_delegate");
|
|
248
|
+
const returning = loaded.tools.find((tool) => tool.name === "freeflow_return");
|
|
249
|
+
assert.deepEqual(
|
|
250
|
+
delegate.parameters.oneOf.map((branch) => branch.properties.operation.enum[0]),
|
|
251
|
+
["assign", "replace"],
|
|
252
|
+
);
|
|
253
|
+
assert.deepEqual(
|
|
254
|
+
returning.parameters.oneOf.map((branch) => branch.properties.operation.enum[0]),
|
|
255
|
+
["submit", "retry"],
|
|
256
|
+
);
|
|
257
|
+
assert.equal(loaded.activeToolNames().includes("freeflow_project"), scenario.exposesProjection);
|
|
258
|
+
|
|
259
|
+
const providerContext = await loaded.handlers.get("context")({ messages: [] }, ctx);
|
|
260
|
+
assert.match(providerContext.messages[0].content, new RegExp("Projection: `" + scenario.expectedMode + "`"));
|
|
261
|
+
|
|
262
|
+
await loaded.commands.find((command) => command.name === "freeflow").definition.handler("profile executor", ctx);
|
|
263
|
+
const manualContext = await loaded.handlers.get("context")({ messages: [] }, ctx);
|
|
264
|
+
assert.match(
|
|
265
|
+
manualContext.messages[0].content,
|
|
266
|
+
new RegExp("Projection: `" + (scenario.projection ? "manual-bypass" : "disabled") + "`"),
|
|
267
|
+
);
|
|
268
|
+
assert.equal(loaded.activeToolNames().includes("freeflow_project"), false);
|
|
269
|
+
} finally {
|
|
270
|
+
await rm(cwd, { recursive: true, force: true });
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
});
|
|
274
|
+
|
|
190
275
|
test("Pi registers the remaining Freeflow commands without mode controls or retired router tools", () => {
|
|
191
276
|
const { commands, shortcuts, tools } = loadExtension();
|
|
192
277
|
const commandNames = commands.map((command) => command.name);
|
|
@@ -199,12 +284,14 @@ test("Pi registers the remaining Freeflow commands without mode controls or reti
|
|
|
199
284
|
assert.ok(!commandNames.includes("discover"));
|
|
200
285
|
assert.ok(!commandNames.includes("execute-plan"));
|
|
201
286
|
assert.ok(!commandNames.includes("workflow"));
|
|
287
|
+
assert.deepEqual(shortcuts, []);
|
|
288
|
+
assert.ok(tools.some((tool) => tool.name === "freeflow_context"));
|
|
202
289
|
assert.deepEqual(
|
|
203
|
-
|
|
204
|
-
["
|
|
290
|
+
toolNames.filter((name) => name.startsWith("freeflow_")),
|
|
291
|
+
["freeflow_delegate", "freeflow_return", "freeflow_unit", "freeflow_project", "freeflow_context"],
|
|
205
292
|
);
|
|
206
|
-
assert.ok(
|
|
207
|
-
assert.ok(
|
|
293
|
+
assert.ok(!toolNames.includes("freeflow_switch_profile"));
|
|
294
|
+
assert.ok(!toolNames.includes("freeflow_cognitive_routing_history"));
|
|
208
295
|
assert.ok(freeflowCommand);
|
|
209
296
|
assert.ok(!freeflowCommand.definition.getArgumentCompletions("").some((item) => item.value === "mode"));
|
|
210
297
|
assert.deepEqual(freeflowCommand.definition.getArgumentCompletions("mode "), []);
|
|
@@ -213,22 +300,27 @@ test("Pi registers the remaining Freeflow commands without mode controls or reti
|
|
|
213
300
|
);
|
|
214
301
|
});
|
|
215
302
|
|
|
216
|
-
test("
|
|
303
|
+
test("PiFlow keeps Cognitive Routing unavailable while exposing its configuration", async () => {
|
|
217
304
|
const cwd = await configuredRepo({
|
|
218
305
|
cognitiveRouting: {
|
|
219
306
|
enabled: true,
|
|
220
307
|
profiles: {
|
|
221
|
-
|
|
222
|
-
|
|
308
|
+
coordinator: { provider: "test", model: "coordinator", thinking: "high" },
|
|
309
|
+
executor: { provider: "test", model: "executor", thinking: "max" },
|
|
223
310
|
},
|
|
224
311
|
},
|
|
225
312
|
});
|
|
226
313
|
try {
|
|
227
|
-
const { commands, handlers, shortcuts, tools } = loadExtension(freeflowExtension,
|
|
314
|
+
const { commands, handlers, shortcuts, tools, activeToolNames } = loadExtension(freeflowExtension, PIFLOW_HOST);
|
|
228
315
|
const freeflowCommand = commands.find((command) => command.name === "freeflow");
|
|
229
316
|
assert.ok(freeflowCommand);
|
|
230
317
|
assert.deepEqual(shortcuts, []);
|
|
318
|
+
assert.deepEqual(
|
|
319
|
+
tools.filter((tool) => tool.name.startsWith("freeflow_")).map((tool) => tool.name),
|
|
320
|
+
["freeflow_delegate", "freeflow_return", "freeflow_unit", "freeflow_project", "freeflow_context"],
|
|
321
|
+
);
|
|
231
322
|
assert.ok(!tools.some((tool) => tool.name === "freeflow_switch_profile"));
|
|
323
|
+
assert.ok(!tools.some((tool) => tool.name === "freeflow_cognitive_routing_history"));
|
|
232
324
|
assert.ok(!freeflowCommand.definition.getArgumentCompletions("").some((item) => item.value === "profile"));
|
|
233
325
|
const capabilityState = await readCapabilityState(cwd, undefined, undefined);
|
|
234
326
|
assert.equal(capabilityState.cognitiveRouting.enabled, true);
|
|
@@ -237,7 +329,8 @@ test("unsupported Pi hosts keep Cognitive Routing unavailable while exposing its
|
|
|
237
329
|
|
|
238
330
|
const ctx = context(cwd);
|
|
239
331
|
await handlers.get("session_start")({ type: "session_start" }, ctx);
|
|
240
|
-
assert.match(ctx.statuses.at(-1).value, /cognitive blocked
|
|
332
|
+
assert.match(ctx.statuses.at(-1).value, /cognitive blocked ·/);
|
|
333
|
+
assert.ok(!activeToolNames().some((name) => name.startsWith("freeflow_delegate") || name === "freeflow_return"));
|
|
241
334
|
const before = await handlers.get("before_agent_start")({ systemPrompt: "base" }, ctx);
|
|
242
335
|
assert.doesNotMatch(before.systemPrompt, /Cognitive Routing/);
|
|
243
336
|
} finally {
|
|
@@ -250,8 +343,8 @@ test("normal Pi settings expose active Cognitive Routing configuration", async (
|
|
|
250
343
|
cognitiveRouting: {
|
|
251
344
|
enabled: true,
|
|
252
345
|
profiles: {
|
|
253
|
-
|
|
254
|
-
|
|
346
|
+
coordinator: { provider: "test", model: "model-a", thinking: "low" },
|
|
347
|
+
executor: { provider: "test", model: "model-b", thinking: "high" },
|
|
255
348
|
},
|
|
256
349
|
},
|
|
257
350
|
});
|
|
@@ -273,7 +366,7 @@ test("normal Pi settings expose active Cognitive Routing configuration", async (
|
|
|
273
366
|
settingsCtx.ui.custom = async (factory) => {
|
|
274
367
|
const component = factory({ requestRender() {} }, testTheme, {}, () => {});
|
|
275
368
|
const rootText = renderText(component);
|
|
276
|
-
assert.match(rootText, /Cognitive Routing\s+enabled \(
|
|
369
|
+
assert.match(rootText, /Cognitive Routing\s+enabled \(4\) active/);
|
|
277
370
|
assert.doesNotMatch(rootText, /PiFlow only/);
|
|
278
371
|
return undefined;
|
|
279
372
|
};
|
|
@@ -296,14 +389,14 @@ test("Pi statusline reports only dynamic Cognitive Routing and context state", (
|
|
|
296
389
|
conversationHistory: { effective: false },
|
|
297
390
|
cognitiveRouting: { enabled: true, effective: true, blockingReason: null },
|
|
298
391
|
},
|
|
299
|
-
{ effective: true, activeProfile: "
|
|
392
|
+
{ effective: true, activeProfile: "executor", controlMode: "automatic" },
|
|
300
393
|
readyFreeflowContext,
|
|
301
394
|
);
|
|
302
|
-
assert.equal(ctx.statuses.at(-1).value, "freeflow:
|
|
395
|
+
assert.equal(ctx.statuses.at(-1).value, "freeflow: executor · automatic · context");
|
|
303
396
|
assert.doesNotMatch(ctx.statuses.at(-1).value, /interaction|workflow|mode|skills/i);
|
|
304
397
|
});
|
|
305
398
|
|
|
306
|
-
test("Pi statusline
|
|
399
|
+
test("Pi statusline defaults pending activation to the Coordinator profile", () => {
|
|
307
400
|
const ctx = context();
|
|
308
401
|
setFreeflowStatus(
|
|
309
402
|
ctx,
|
|
@@ -316,7 +409,6 @@ test("Pi statusline shows the configured Cognitive Routing profile while activat
|
|
|
316
409
|
cognitiveRouting: {
|
|
317
410
|
enabled: true,
|
|
318
411
|
effective: true,
|
|
319
|
-
sessionStart: { profile: "reasoning" },
|
|
320
412
|
blockingReason: null,
|
|
321
413
|
},
|
|
322
414
|
},
|
|
@@ -324,7 +416,7 @@ test("Pi statusline shows the configured Cognitive Routing profile while activat
|
|
|
324
416
|
readyFreeflowContext,
|
|
325
417
|
{ cognitiveRoutingStartupPending: true },
|
|
326
418
|
);
|
|
327
|
-
assert.equal(ctx.statuses.at(-1).value, "freeflow:
|
|
419
|
+
assert.equal(ctx.statuses.at(-1).value, "freeflow: coordinator · pending");
|
|
328
420
|
});
|
|
329
421
|
|
|
330
422
|
test("Pi statusline keeps an inactive Cognitive Routing runtime blocked", () => {
|
|
@@ -339,20 +431,20 @@ test("Pi statusline keeps an inactive Cognitive Routing runtime blocked", () =>
|
|
|
339
431
|
conversationHistory: { effective: false },
|
|
340
432
|
cognitiveRouting: { enabled: true, effective: true, blockingReason: null },
|
|
341
433
|
},
|
|
342
|
-
{ effective: false, activeProfile: "
|
|
434
|
+
{ effective: false, activeProfile: "coordinator", controlMode: "automatic" },
|
|
343
435
|
readyFreeflowContext,
|
|
344
436
|
{ cognitiveRoutingStartupPending: true },
|
|
345
437
|
);
|
|
346
438
|
assert.equal(ctx.statuses.at(-1).value, "freeflow: cognitive blocked · runtime_inactive");
|
|
347
439
|
});
|
|
348
440
|
|
|
349
|
-
test("PiFlow
|
|
441
|
+
test("PiFlow keeps configured routing unavailable before the first prompt", async () => {
|
|
350
442
|
const cwd = await configuredRepo({
|
|
351
443
|
cognitiveRouting: {
|
|
352
444
|
enabled: true,
|
|
353
445
|
profiles: {
|
|
354
|
-
|
|
355
|
-
|
|
446
|
+
coordinator: { provider: "test", model: "model-a", thinking: "low" },
|
|
447
|
+
executor: { provider: "test", model: "model-b", thinking: "high" },
|
|
356
448
|
},
|
|
357
449
|
},
|
|
358
450
|
});
|
|
@@ -361,7 +453,7 @@ test("PiFlow empty configured sessions show a pending profile before first promp
|
|
|
361
453
|
const ctx = context(cwd);
|
|
362
454
|
ctx.modelRegistry = cognitiveRoutingModelRegistry();
|
|
363
455
|
await handlers.get("session_start")({ type: "session_start", reason: "startup" }, ctx);
|
|
364
|
-
assert.
|
|
456
|
+
assert.match(ctx.statuses.at(-1).value, /cognitive blocked ·/);
|
|
365
457
|
} finally {
|
|
366
458
|
await rm(cwd, { recursive: true, force: true });
|
|
367
459
|
}
|
|
@@ -377,8 +469,16 @@ test("Pi exposes bypass scope argument completions", () => {
|
|
|
377
469
|
]);
|
|
378
470
|
});
|
|
379
471
|
|
|
380
|
-
test("Pi describes the mode-free Freeflow argument surface", () => {
|
|
381
|
-
const { commands } = loadExtension(
|
|
472
|
+
test("Pi describes the mode-free Freeflow argument surface and manual profile controls", () => {
|
|
473
|
+
const { commands } = loadExtension(
|
|
474
|
+
freeflowExtension,
|
|
475
|
+
{},
|
|
476
|
+
{
|
|
477
|
+
appendEntry() {},
|
|
478
|
+
setModel() {},
|
|
479
|
+
setThinkingLevel() {},
|
|
480
|
+
},
|
|
481
|
+
);
|
|
382
482
|
const freeflowCommand = commands.find((command) => command.name === "freeflow");
|
|
383
483
|
assert.ok(freeflowCommand);
|
|
384
484
|
assert.deepEqual(freeflowCommand.definition.getArgumentCompletions(""), [
|
|
@@ -386,9 +486,16 @@ test("Pi describes the mode-free Freeflow argument surface", () => {
|
|
|
386
486
|
{ value: "status", label: "status", description: "Show effective Freeflow state" },
|
|
387
487
|
{ value: "context", label: "context", description: "Inspect Freeflow Context" },
|
|
388
488
|
{ value: "profile", label: "profile", description: "Hold or release Cognitive Routing profile control" },
|
|
489
|
+
{ value: "resume", label: "resume", description: "Resume the current saved routing responsibility" },
|
|
389
490
|
{ value: "enable", label: "enable", description: "Enable Freeflow for this repository" },
|
|
390
491
|
{ value: "disable", label: "disable", description: "Disable Freeflow for this repository" },
|
|
391
492
|
]);
|
|
493
|
+
assert.deepEqual(freeflowCommand.definition.getArgumentCompletions("profile "), [
|
|
494
|
+
{ value: "profile coordinator", label: "coordinator", description: "Hold Coordinator manually" },
|
|
495
|
+
{ value: "profile executor", label: "executor", description: "Hold Executor manually" },
|
|
496
|
+
{ value: "profile auto", label: "auto", description: "Return to automatic Coordinator reconciliation" },
|
|
497
|
+
{ value: "profile history", label: "history", description: "Read routing observations" },
|
|
498
|
+
]);
|
|
392
499
|
assert.deepEqual(freeflowCommand.definition.getArgumentCompletions("mode "), []);
|
|
393
500
|
assert.deepEqual(freeflowCommand.definition.getArgumentCompletions("context "), [
|
|
394
501
|
{ value: "context status", label: "status", description: "Show Freeflow Context state" },
|
|
@@ -398,7 +505,7 @@ test("Pi describes the mode-free Freeflow argument surface", () => {
|
|
|
398
505
|
]);
|
|
399
506
|
});
|
|
400
507
|
|
|
401
|
-
test("Pi exposes
|
|
508
|
+
test("Pi exposes 24 base skills without TDD, a mode skill, or compatibility aliases", async () => {
|
|
402
509
|
const cwd = await configuredRepo();
|
|
403
510
|
try {
|
|
404
511
|
const { handlers, commands, sentMessages, sentMessageOptions } = loadExtension();
|
|
@@ -408,8 +515,9 @@ test("Pi exposes 25 base skills without a mode skill or compatibility aliases",
|
|
|
408
515
|
assert.ok(match, `unexpected skill path: ${path}`);
|
|
409
516
|
return match[1];
|
|
410
517
|
});
|
|
411
|
-
assert.equal(skillNames.length,
|
|
518
|
+
assert.equal(skillNames.length, 24);
|
|
412
519
|
assert.ok(skillNames.includes("workflow"));
|
|
520
|
+
assert.ok(!skillNames.includes("tdd"));
|
|
413
521
|
assert.ok(!skillNames.includes("mode-contract"));
|
|
414
522
|
assert.ok(!skillNames.includes("discover"));
|
|
415
523
|
assert.ok(!skillNames.includes("execute-plan"));
|
|
@@ -738,13 +846,23 @@ test("Pi Cognitive Routing settings preserve complete presets", async () => {
|
|
|
738
846
|
cognitiveRouting: {
|
|
739
847
|
enabled: true,
|
|
740
848
|
profiles: {
|
|
741
|
-
|
|
742
|
-
|
|
849
|
+
coordinator: { provider: "test", model: "model-a", thinking: "low" },
|
|
850
|
+
executor: { provider: "test", model: "model-b", thinking: "high" },
|
|
743
851
|
},
|
|
744
852
|
},
|
|
745
853
|
});
|
|
746
854
|
try {
|
|
747
|
-
const { commands } = loadExtension(
|
|
855
|
+
const { commands } = loadExtension(
|
|
856
|
+
freeflowExtension,
|
|
857
|
+
{},
|
|
858
|
+
{
|
|
859
|
+
appendEntry() {},
|
|
860
|
+
async setModel() {
|
|
861
|
+
return true;
|
|
862
|
+
},
|
|
863
|
+
setThinkingLevel() {},
|
|
864
|
+
},
|
|
865
|
+
);
|
|
748
866
|
const freeflowCommand = commands.find((command) => command.name === "freeflow");
|
|
749
867
|
assert.ok(freeflowCommand);
|
|
750
868
|
const settingsCtx = context(cwd);
|
|
@@ -759,9 +877,8 @@ test("Pi Cognitive Routing settings preserve complete presets", async () => {
|
|
|
759
877
|
component.handleInput("\r");
|
|
760
878
|
component.handleInput("\u001b[B");
|
|
761
879
|
component.handleInput("\r");
|
|
762
|
-
component.handleInput("\u001b[B");
|
|
763
880
|
component.handleInput("\r");
|
|
764
|
-
for (let index = 0; index <
|
|
881
|
+
for (let index = 0; index < 1; index++) component.handleInput("\u001b[B");
|
|
765
882
|
component.handleInput("\r");
|
|
766
883
|
component.handleInput("\r");
|
|
767
884
|
await component.waitForWrites();
|
|
@@ -769,10 +886,10 @@ test("Pi Cognitive Routing settings preserve complete presets", async () => {
|
|
|
769
886
|
};
|
|
770
887
|
await freeflowCommand.definition.handler("settings repo", settingsCtx);
|
|
771
888
|
const saved = JSON.parse(await readFile(join(cwd, ".freeflow/config.json"), "utf8"));
|
|
772
|
-
assert.deepEqual(saved.cognitiveRouting.profiles.
|
|
889
|
+
assert.deepEqual(saved.cognitiveRouting.profiles.executor, {
|
|
773
890
|
provider: "test",
|
|
774
891
|
model: "model-b",
|
|
775
|
-
|
|
892
|
+
thinking: "max",
|
|
776
893
|
});
|
|
777
894
|
assert.equal(settingsCtx.reloads.length, 1);
|
|
778
895
|
} finally {
|
|
@@ -827,8 +944,8 @@ test("Pi preserves the host prompt when a mandatory prompt file is missing", asy
|
|
|
827
944
|
|
|
828
945
|
function cognitiveRoutingModelRegistry() {
|
|
829
946
|
const models = [
|
|
830
|
-
{ provider: "test", id: "model-a", name: "Model A", reasoning: true },
|
|
831
|
-
{ provider: "test", id: "model-b", name: "Model B", reasoning: true },
|
|
947
|
+
{ provider: "test", id: "model-a", name: "Model A", reasoning: true, contextWindow: 128000 },
|
|
948
|
+
{ provider: "test", id: "model-b", name: "Model B", reasoning: true, contextWindow: 128000 },
|
|
832
949
|
];
|
|
833
950
|
const supported = new Map([
|
|
834
951
|
["model-a", new Set(["off", "low", "medium", "high"])],
|
|
@@ -6,11 +6,12 @@ import { tmpdir } from "node:os";
|
|
|
6
6
|
import test from "node:test";
|
|
7
7
|
|
|
8
8
|
import freeflowExtension from "../../dist/index.js";
|
|
9
|
-
import { PIFLOW_HOST } from "../
|
|
9
|
+
import { PIFLOW_HOST } from "../fixtures/pi-host.js";
|
|
10
10
|
|
|
11
|
-
function context(cwd) {
|
|
11
|
+
function context(cwd, systemPrompt = "") {
|
|
12
12
|
return {
|
|
13
13
|
cwd,
|
|
14
|
+
getSystemPrompt: () => systemPrompt,
|
|
14
15
|
sessionManager: {
|
|
15
16
|
getEntries: () => [],
|
|
16
17
|
getBranch: () => [],
|
|
@@ -65,20 +66,44 @@ function lastRuntimeState(messages) {
|
|
|
65
66
|
}
|
|
66
67
|
|
|
67
68
|
test("re-entry recovery is stable and capability-neutral", async () => {
|
|
68
|
-
const [core, cognitiveRouting, conversationHistory] = await Promise.all([
|
|
69
|
+
const [core, cognitiveRouting, conversationHistory, routingSkill] = await Promise.all([
|
|
69
70
|
readFile(join(process.cwd(), "runtime", "prompts", "core.md"), "utf8"),
|
|
70
71
|
readFile(join(process.cwd(), "runtime", "prompts", "cognitive-routing.md"), "utf8"),
|
|
71
72
|
readFile(join(process.cwd(), "runtime", "prompts", "conversation-history.md"), "utf8"),
|
|
73
|
+
readFile(join(process.cwd(), "capabilities", "cognitive-routing", "SKILL.md"), "utf8"),
|
|
72
74
|
]);
|
|
73
75
|
|
|
76
|
+
assert.match(core, /## Load The Selected Method/);
|
|
77
|
+
assert.match(core, /Before applying a selected skill, read its current body/);
|
|
74
78
|
assert.match(core, /## Recover After Context Loss/);
|
|
75
79
|
assert.match(core, /latest Freeflow Runtime State/);
|
|
76
|
-
assert.
|
|
77
|
-
assert.match(
|
|
80
|
+
assert.match(core, /recovery is not a bypass/);
|
|
81
|
+
assert.match(core, /evidence of prior approval; check that the approval still applies/);
|
|
82
|
+
assert.match(core, /choose or retain the current owner/);
|
|
83
|
+
assert.match(core, /apply its method and gather or produce evidence/);
|
|
84
|
+
assert.match(core, /read the complete `full` record/);
|
|
85
|
+
assert.match(core, /Make material gaps, contradictions, deferrals, and user-owned decisions explicit/);
|
|
86
|
+
assert.match(cognitiveRouting, /Before relying on automatic routing, read the complete cognitive-routing skill/);
|
|
87
|
+
assert.match(cognitiveRouting, /This bootstrap read is the only environment call/);
|
|
88
|
+
assert.match(cognitiveRouting, /If unavailable, stop and report the missing method/);
|
|
89
|
+
// The approved compact cue keeps bootstrap/control boundaries; the loaded
|
|
90
|
+
// method owns detailed role and handoff policy. Keep both obligations checked.
|
|
78
91
|
assert.match(
|
|
79
92
|
cognitiveRouting,
|
|
80
|
-
|
|
93
|
+
/Under Manual or inactive routing, stop applying the automatic split and follow ordinary Workflow/,
|
|
81
94
|
);
|
|
95
|
+
assert.match(cognitiveRouting, /Use the latest Runtime State for control, profile, and current responsibility/);
|
|
96
|
+
assert.match(cognitiveRouting, /Coordinator and Executor in one agent\/session/);
|
|
97
|
+
assert.match(routingSkill, /Automatic delegation, role restrictions, and projection are bypassed/);
|
|
98
|
+
assert.match(routingSkill, /Put the actual contract inside `freeflow_delegate/);
|
|
99
|
+
assert.match(routingSkill, /submits the actual report and stops ordinary task work/);
|
|
100
|
+
assert.match(cognitiveRouting, /project every completed skill and instructional-reference read/);
|
|
101
|
+
assert.match(routingSkill, /Attach recovery to the existing responsibility/);
|
|
102
|
+
assert.match(
|
|
103
|
+
routingSkill,
|
|
104
|
+
/Returned work may use an attached recovery-only phase only if the runtime actually exposes it; otherwise inspect saved communication and stop if insufficient/,
|
|
105
|
+
);
|
|
106
|
+
assert.match(routingSkill, /Do not simulate recovery by replacing the assignment or report/);
|
|
82
107
|
assert.doesNotMatch(
|
|
83
108
|
conversationHistory,
|
|
84
109
|
/Current user direction, live source truth, and present runtime state remain authoritative/,
|
|
@@ -98,8 +123,10 @@ test("composes the mandatory core fragments, optional capabilities, discovery, a
|
|
|
98
123
|
const order = [
|
|
99
124
|
"# Freeflow Stable Guidance",
|
|
100
125
|
"## Shared Terms",
|
|
126
|
+
"## Load The Selected Method",
|
|
101
127
|
"## Recover After Context Loss",
|
|
102
128
|
"## Three Nested Loops",
|
|
129
|
+
"## Evidence And Judgment",
|
|
103
130
|
"## Workflow Cue",
|
|
104
131
|
"## Action Selection Cue",
|
|
105
132
|
"## Supported Exit",
|
|
@@ -112,6 +139,18 @@ test("composes the mandatory core fragments, optional capabilities, discovery, a
|
|
|
112
139
|
order,
|
|
113
140
|
[...order].sort((a, b) => a - b),
|
|
114
141
|
);
|
|
142
|
+
assert.match(
|
|
143
|
+
prompt,
|
|
144
|
+
/Before applying a selected skill, read its current body when its exact method is absent from context/,
|
|
145
|
+
);
|
|
146
|
+
assert.match(prompt, /Verification establishes what direct evidence proves at the observed boundary/);
|
|
147
|
+
assert.match(
|
|
148
|
+
prompt,
|
|
149
|
+
/Review judges whether work or an artifact is aligned, correct, suitable, and sufficiently evidenced/,
|
|
150
|
+
);
|
|
151
|
+
assert.match(prompt, /Interpret requested intent, not sentence form/);
|
|
152
|
+
assert.match(prompt, /answer the question before any action that depends on it/);
|
|
153
|
+
assert.match(prompt, /An unresolved question does not automatically suspend independent, clearly authorized work/);
|
|
115
154
|
assert.doesNotMatch(prompt, /## Mode\b|strict-workflow|conversation mode|workflow mode/);
|
|
116
155
|
assert.doesNotMatch(prompt, /Skills prompt/);
|
|
117
156
|
assert.doesNotMatch(prompt, /# Workflow\n/);
|
|
@@ -124,6 +163,8 @@ test("composes the mandatory core fragments, optional capabilities, discovery, a
|
|
|
124
163
|
assert.match(runtimeState.content, /Context Virtualization: active/);
|
|
125
164
|
assert.match(runtimeState.content, /Conversation History: active/);
|
|
126
165
|
assert.match(runtimeState.content, /Cognitive Routing: inactive/);
|
|
166
|
+
assert.match(runtimeState.content, /Control: `unavailable`/);
|
|
167
|
+
assert.match(runtimeState.content, /Projection: `disabled`/);
|
|
127
168
|
assert.doesNotMatch(runtimeState.content, /Default mode|Active mode|Interaction Contract|Skills/);
|
|
128
169
|
|
|
129
170
|
const resources = await handlers.get("resources_discover")({ cwd }, ctx);
|
|
@@ -137,6 +178,46 @@ test("composes the mandatory core fragments, optional capabilities, discovery, a
|
|
|
137
178
|
}
|
|
138
179
|
});
|
|
139
180
|
|
|
181
|
+
test("subagents keep Freeflow core and base skills without optional capabilities", async () => {
|
|
182
|
+
const cwd = await configuredRepo({
|
|
183
|
+
contextVirtualization: true,
|
|
184
|
+
conversationHistory: true,
|
|
185
|
+
cognitiveRouting: {
|
|
186
|
+
enabled: true,
|
|
187
|
+
profiles: {
|
|
188
|
+
coordinator: { provider: "test", model: "coordinator", thinking: "high" },
|
|
189
|
+
executor: { provider: "test", model: "executor", thinking: "max" },
|
|
190
|
+
},
|
|
191
|
+
},
|
|
192
|
+
});
|
|
193
|
+
try {
|
|
194
|
+
const { handlers, activeToolNames } = loadExtension(freeflowExtension, null);
|
|
195
|
+
const ctx = context(cwd, "<!-- freeflow-subagent-capabilities: disabled -->");
|
|
196
|
+
await handlers.get("session_start")({ type: "session_start" }, ctx);
|
|
197
|
+
|
|
198
|
+
const before = await handlers.get("before_agent_start")({ systemPrompt: "base prompt" }, ctx);
|
|
199
|
+
assert.match(before.systemPrompt, /# Freeflow Stable Guidance/);
|
|
200
|
+
assert.match(before.systemPrompt, /# Freeflow Interaction Contract/);
|
|
201
|
+
assert.doesNotMatch(before.systemPrompt, /## Cognitive Routing Cue/);
|
|
202
|
+
assert.doesNotMatch(before.systemPrompt, /## Context Virtualization Cue/);
|
|
203
|
+
assert.doesNotMatch(before.systemPrompt, /## Conversation History Cue/);
|
|
204
|
+
|
|
205
|
+
const resources = await handlers.get("resources_discover")({ cwd }, ctx);
|
|
206
|
+
assert.ok(resources.skillPaths.some((path) => path.endsWith("/skills/action-selection/SKILL.md")));
|
|
207
|
+
assert.ok(!resources.skillPaths.some((path) => path.includes("/capabilities/")));
|
|
208
|
+
|
|
209
|
+
const providerContext = await handlers.get("context")({ messages: [] }, ctx);
|
|
210
|
+
const runtimeState = lastRuntimeState(providerContext.messages);
|
|
211
|
+
assert.match(runtimeState.content, /Freeflow: active/);
|
|
212
|
+
assert.match(runtimeState.content, /Context Virtualization: inactive/);
|
|
213
|
+
assert.match(runtimeState.content, /Conversation History: inactive/);
|
|
214
|
+
assert.match(runtimeState.content, /Cognitive Routing: inactive/);
|
|
215
|
+
assert.ok(!activeToolNames().includes("freeflow_context"));
|
|
216
|
+
} finally {
|
|
217
|
+
await rm(cwd, { recursive: true, force: true });
|
|
218
|
+
}
|
|
219
|
+
});
|
|
220
|
+
|
|
140
221
|
test("provider context reuses the before-agent surface until the next provider turn", async () => {
|
|
141
222
|
const cwd = await configuredRepo({ contextVirtualization: true });
|
|
142
223
|
try {
|