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