@hassangameryt/freeflow 0.4.0 → 0.6.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 -1
- package/.codex-plugin/plugin.json +2 -1
- package/README.md +150 -155
- package/capabilities/cognitive-routing/SKILL.md +229 -0
- package/capabilities/context-virtualization/SKILL.md +103 -0
- package/capabilities/conversation-history/SKILL.md +53 -0
- package/command-surface.json +17 -13
- package/hooks/freeflow-runtime-context.mjs +393 -159
- package/hooks/hooks.json +26 -1
- package/hooks/tests/check-runtime-context-hook.sh +223 -54
- package/package.json +25 -16
- package/pi-extension/dist/cognitive-routing/commands.js +96 -0
- package/pi-extension/dist/cognitive-routing/config.js +236 -0
- package/pi-extension/dist/cognitive-routing/controller.js +862 -0
- package/pi-extension/dist/cognitive-routing/history.js +439 -0
- package/pi-extension/dist/cognitive-routing/host.js +126 -0
- package/pi-extension/dist/cognitive-routing/runtime.js +43 -0
- package/pi-extension/dist/cognitive-routing/tool.js +211 -0
- package/pi-extension/dist/cognitive-routing/types.js +6 -0
- package/pi-extension/dist/context-virtualization/commands.js +89 -0
- package/pi-extension/dist/context-virtualization/projector.js +42 -0
- package/pi-extension/dist/context-virtualization/runtime.js +356 -0
- package/pi-extension/dist/context-virtualization/state.js +57 -0
- package/pi-extension/dist/context-virtualization/types.js +10 -0
- package/pi-extension/dist/conversation-history/passages.js +66 -0
- package/pi-extension/dist/conversation-history/ranking.js +105 -0
- package/pi-extension/dist/conversation-history/runtime.js +226 -0
- package/pi-extension/dist/conversation-history/search.js +56 -0
- package/pi-extension/dist/conversation-history/types.js +1 -0
- package/pi-extension/dist/freeflow-context/resolver.js +120 -0
- package/pi-extension/dist/freeflow-context/runtime.js +80 -0
- package/pi-extension/dist/freeflow-context/source-projector.js +95 -0
- package/pi-extension/dist/freeflow-context/tool.js +308 -0
- package/pi-extension/dist/freeflow-context/types.js +12 -0
- package/pi-extension/dist/freeflow-context/visibility.js +28 -0
- package/pi-extension/dist/index.js +516 -98
- package/pi-extension/dist/runtime/runtime-context.js +318 -271
- package/pi-extension/dist/runtime/runtime-identity.js +5 -0
- package/pi-extension/dist/settings/settings-tui.js +78 -0
- package/pi-extension/dist/settings/settings-ui.js +593 -563
- package/pi-extension/dist/ui/text-component.js +57 -0
- package/pi-extension/src/cognitive-routing/commands.ts +119 -0
- package/pi-extension/src/cognitive-routing/config.ts +307 -0
- package/pi-extension/src/cognitive-routing/controller.ts +1118 -0
- package/pi-extension/src/cognitive-routing/history.ts +638 -0
- package/pi-extension/src/cognitive-routing/host.ts +172 -0
- package/pi-extension/src/cognitive-routing/runtime.ts +57 -0
- package/pi-extension/src/cognitive-routing/tool.ts +272 -0
- package/pi-extension/src/cognitive-routing/types.ts +89 -0
- package/pi-extension/src/context-virtualization/commands.ts +111 -0
- package/pi-extension/src/context-virtualization/projector.ts +62 -0
- package/pi-extension/src/context-virtualization/runtime.ts +406 -0
- package/pi-extension/src/context-virtualization/state.ts +79 -0
- package/pi-extension/src/context-virtualization/types.ts +92 -0
- package/pi-extension/src/conversation-history/passages.ts +81 -0
- package/pi-extension/src/conversation-history/ranking.ts +155 -0
- package/pi-extension/src/conversation-history/runtime.ts +250 -0
- package/pi-extension/src/conversation-history/search.ts +79 -0
- package/pi-extension/src/conversation-history/types.ts +31 -0
- package/pi-extension/src/freeflow-context/resolver.ts +139 -0
- package/pi-extension/src/freeflow-context/runtime.ts +100 -0
- package/pi-extension/src/freeflow-context/source-projector.ts +106 -0
- package/pi-extension/src/freeflow-context/tool.ts +360 -0
- package/pi-extension/src/freeflow-context/types.ts +70 -0
- package/pi-extension/src/freeflow-context/visibility.ts +53 -0
- package/pi-extension/src/index.ts +553 -101
- package/pi-extension/src/runtime/runtime-context.ts +339 -281
- package/pi-extension/src/runtime/runtime-identity.ts +8 -0
- package/pi-extension/src/settings/settings-tui.ts +93 -0
- package/pi-extension/src/settings/settings-ui.ts +700 -613
- package/pi-extension/src/ui/text-component.ts +66 -0
- package/pi-extension/tests/cognitive-routing/commands.test.js +134 -0
- package/pi-extension/tests/cognitive-routing/controller.test.js +1099 -0
- package/pi-extension/tests/cognitive-routing/extension-lifecycle.test.js +1071 -0
- package/pi-extension/tests/cognitive-routing/history.test.js +274 -0
- package/pi-extension/tests/cognitive-routing/host-fixture.js +4 -0
- package/pi-extension/tests/cognitive-routing/host-identity.test.js +63 -0
- package/pi-extension/tests/cognitive-routing/profile-resolution.test.js +236 -0
- package/pi-extension/tests/cognitive-routing/tool.test.js +230 -0
- package/pi-extension/tests/context-virtualization/extension.test.js +749 -0
- package/pi-extension/tests/context-virtualization/projector.test.js +110 -0
- package/pi-extension/tests/context-virtualization/runtime.test.js +216 -0
- package/pi-extension/tests/conversation-history/runtime.test.js +98 -0
- package/pi-extension/tests/conversation-history/search.test.js +97 -0
- package/pi-extension/tests/integration/pi-extension.test.js +928 -2047
- package/pi-extension/tests/integration/prompt-architecture.test.js +305 -0
- package/runtime/prompts/cognitive-routing.md +9 -0
- package/runtime/prompts/context-virtualization.md +3 -0
- package/runtime/prompts/conversation-history.md +3 -0
- package/runtime/prompts/core.md +13 -0
- package/runtime/prompts/interaction-contract.md +7 -0
- package/runtime/prompts/skills.md +65 -0
- package/skills/action-selection/SKILL.md +91 -0
- package/skills/bypass/SKILL.md +3 -2
- package/skills/commit-work/SKILL.md +1 -1
- package/skills/commit-work/references/staging-decisions.md +1 -1
- package/skills/decision-gate/SKILL.md +1 -1
- package/skills/design-for-depth/SKILL.md +14 -33
- package/skills/design-for-depth/references/design-pressure-signals.md +1 -1
- package/skills/design-for-depth/references/interface-design-loop.md +1 -1
- package/skills/design-for-depth/references/software-design-philosophy.md +1 -1
- package/skills/diagnose-failure/SKILL.md +20 -14
- package/skills/diagnose-failure/references/diagnostic-loop-catalog.md +1 -3
- package/skills/diagnose-failure/references/{flaky-and-performance.md → flaky-and-performance-diagnosis.md} +1 -1
- package/skills/discuss/SKILL.md +34 -22
- package/skills/discuss/references/discussion-continuity.md +71 -0
- package/skills/evaluate-skill/SKILL.md +5 -5
- package/skills/evaluate-skill/references/{definition-schema.md → evaluation-definition-schema.md} +42 -2
- package/skills/evaluate-skill/references/execution-and-evidence.md +8 -3
- package/skills/evaluate-skill/scripts/lib/definitions.mjs +114 -3
- package/skills/evaluate-skill/scripts/lib/grade-interaction.mjs +156 -1
- package/skills/evaluate-skill/scripts/lib/grade.mjs +9 -1
- package/skills/evaluate-skill/scripts/lib/pi.mjs +147 -32
- package/skills/evaluate-skill/scripts/lib/rpc.mjs +4 -4
- package/skills/evaluate-skill/scripts/lib/runner.mjs +36 -14
- package/skills/evaluate-skill/scripts/lib/sandbox.mjs +92 -0
- package/skills/evaluate-skill/scripts/pi-guard.mjs +1 -1
- package/skills/evaluate-skill/scripts/pi-observer.mjs +66 -0
- package/skills/evaluate-skill/scripts/skill-eval.mjs +6 -5
- package/skills/execute-work/SKILL.md +18 -15
- package/skills/execute-work/references/code-practices.md +1 -1
- package/skills/execute-work/references/execute-work-edges.md +42 -0
- package/skills/finish-branch/SKILL.md +2 -2
- package/skills/finish-branch/references/integration-options.md +1 -1
- package/skills/handoff/SKILL.md +3 -3
- package/skills/launch-work/SKILL.md +1 -1
- package/skills/launch-work/references/launch-readiness.md +2 -2
- package/skills/migration-work/SKILL.md +2 -2
- package/skills/migration-work/references/migration-lifecycle.md +1 -1
- package/skills/mode-contract/SKILL.md +32 -10
- package/skills/release-work/SKILL.md +1 -1
- package/skills/release-work/references/release-evidence.md +1 -1
- package/skills/review-artifact/SKILL.md +28 -26
- package/skills/review-work/SKILL.md +28 -26
- package/skills/review-work/references/{reviewer-prompt.md → independent-work-reviewer-contract.md} +1 -1
- package/skills/review-work/references/security-risk-lens.md +1 -1
- package/skills/setup-freeflow/SKILL.md +6 -10
- package/skills/setup-freeflow/references/activation-contract.md +5 -10
- package/skills/setup-freeflow/references/host-setup.md +3 -7
- package/skills/simplify-code/SKILL.md +1 -1
- package/skills/simplify-code/references/simplification-patterns.md +1 -1
- package/skills/tdd/SKILL.md +18 -38
- package/skills/tdd/references/test-design.md +2 -2
- package/skills/track-work/SKILL.md +362 -58
- package/skills/track-work/scripts/lib/codec.mjs +570 -0
- package/skills/track-work/scripts/lib/commands.mjs +568 -0
- package/skills/track-work/scripts/lib/contract.mjs +303 -0
- package/skills/track-work/scripts/lib/model.mjs +506 -0
- package/skills/track-work/scripts/lib/operations.mjs +965 -0
- package/skills/track-work/scripts/lib/record-store.mjs +264 -0
- package/skills/track-work/scripts/lib/result.mjs +37 -0
- package/skills/track-work/scripts/lib/rewrites.mjs +740 -0
- package/skills/track-work/scripts/lib/schema.mjs +277 -0
- package/skills/track-work/scripts/lib/source-inventory.mjs +162 -0
- package/skills/track-work/scripts/lib/views.mjs +443 -0
- package/skills/track-work/scripts/lib/workspace.mjs +134 -0
- package/skills/track-work/scripts/working-record.mjs +122 -0
- package/skills/verify-work/SKILL.md +14 -2
- package/skills/verify-work/references/browser-runtime-evidence.md +1 -1
- package/skills/verify-work/references/integration-evidence.md +1 -1
- package/skills/verify-work/references/performance-evidence.md +1 -1
- package/skills/workflow/SKILL.md +105 -53
- package/skills/workflow/references/domain-skill-composition.md +22 -42
- package/skills/write-plan/SKILL.md +1 -1
- package/skills/write-skill/SKILL.md +3 -2
- package/skills/write-skill/references/progressive-disclosure.md +17 -3
- package/pi-extension/dist/output-router/host-producer-identification.js +0 -232
- package/pi-extension/dist/output-router/native-safety-net.js +0 -216
- package/pi-extension/dist/output-router/observed-tool-routing.js +0 -122
- package/pi-extension/dist/output-router/renderers.js +0 -1023
- package/pi-extension/dist/output-router/router-tools.js +0 -554
- package/pi-extension/dist/output-router/schemas.js +0 -383
- package/pi-extension/dist/output-router/status.js +0 -744
- package/pi-extension/dist/output-router/utils.js +0 -502
- package/pi-extension/src/output-router/host-producer-identification.ts +0 -256
- package/pi-extension/src/output-router/native-safety-net.ts +0 -248
- package/pi-extension/src/output-router/observed-tool-routing.ts +0 -134
- package/pi-extension/src/output-router/renderers.ts +0 -1108
- package/pi-extension/src/output-router/router-tools.ts +0 -579
- package/pi-extension/src/output-router/schemas.ts +0 -387
- package/pi-extension/src/output-router/status.ts +0 -791
- package/pi-extension/src/output-router/utils.ts +0 -554
- package/pi-extension/tests/output-router/observed-producers.test.js +0 -132
- package/pi-extension/tests/output-router/search.test.js +0 -722
- package/router/dist/benchmarks/benchmark-harness.d.ts +0 -35
- package/router/dist/benchmarks/benchmark-harness.js +0 -92
- package/router/dist/benchmarks/benchmarks.d.ts +0 -109
- package/router/dist/benchmarks/benchmarks.js +0 -979
- package/router/dist/benchmarks/codex-qa-benchmarks.d.ts +0 -95
- package/router/dist/benchmarks/codex-qa-benchmarks.js +0 -500
- package/router/dist/benchmarks/command-benchmarks.d.ts +0 -143
- package/router/dist/benchmarks/command-benchmarks.js +0 -698
- package/router/dist/benchmarks/context-mode-normalized-benchmarks.d.ts +0 -82
- package/router/dist/benchmarks/context-mode-normalized-benchmarks.js +0 -773
- package/router/dist/benchmarks/context-mode-real-deep-benchmark.d.ts +0 -132
- package/router/dist/benchmarks/context-mode-real-deep-benchmark.js +0 -1380
- package/router/dist/benchmarks/context-saving-baseline.d.ts +0 -60
- package/router/dist/benchmarks/context-saving-baseline.js +0 -470
- package/router/dist/benchmarks/index-benchmarks.d.ts +0 -103
- package/router/dist/benchmarks/index-benchmarks.js +0 -862
- package/router/dist/benchmarks/storage-policy-benchmarks.d.ts +0 -101
- package/router/dist/benchmarks/storage-policy-benchmarks.js +0 -618
- package/router/dist/benchmarks/vault-index-benchmarks.d.ts +0 -56
- package/router/dist/benchmarks/vault-index-benchmarks.js +0 -322
- package/router/dist/config/config.d.ts +0 -103
- package/router/dist/config/config.js +0 -632
- package/router/dist/config/router-contract.d.ts +0 -10
- package/router/dist/config/router-contract.js +0 -54
- package/router/dist/config/schema.d.ts +0 -27
- package/router/dist/config/schema.js +0 -648
- package/router/dist/config/types.d.ts +0 -573
- package/router/dist/config/types.js +0 -41
- package/router/dist/evidence/bounded-evidence.d.ts +0 -40
- package/router/dist/evidence/bounded-evidence.js +0 -323
- package/router/dist/evidence/evidence-range-selector.d.ts +0 -24
- package/router/dist/evidence/evidence-range-selector.js +0 -232
- package/router/dist/evidence/evidence-search.d.ts +0 -27
- package/router/dist/evidence/evidence-search.js +0 -490
- package/router/dist/evidence/evidence.d.ts +0 -38
- package/router/dist/evidence/evidence.js +0 -200
- package/router/dist/evidence/failure-contracts.d.ts +0 -37
- package/router/dist/evidence/failure-contracts.js +0 -166
- package/router/dist/evidence/line-ranges.d.ts +0 -24
- package/router/dist/evidence/line-ranges.js +0 -22
- package/router/dist/experiments/local-index.d.ts +0 -64
- package/router/dist/experiments/local-index.js +0 -581
- package/router/dist/index.d.ts +0 -30
- package/router/dist/index.js +0 -32
- package/router/dist/local/local-traversal.d.ts +0 -18
- package/router/dist/local/local-traversal.js +0 -334
- package/router/dist/processing/engine.d.ts +0 -132
- package/router/dist/processing/engine.js +0 -533
- package/router/dist/processing/reducers.d.ts +0 -280
- package/router/dist/processing/reducers.js +0 -1673
- package/router/dist/processing/renderers.d.ts +0 -31
- package/router/dist/processing/renderers.js +0 -195
- package/router/dist/processing/scripts.d.ts +0 -91
- package/router/dist/processing/scripts.js +0 -386
- package/router/dist/repo/repo-traversal.d.ts +0 -18
- package/router/dist/repo/repo-traversal.js +0 -245
- package/router/dist/routing/observed-reducers.d.ts +0 -41
- package/router/dist/routing/observed-reducers.js +0 -661
- package/router/dist/routing/observed-routing.d.ts +0 -26
- package/router/dist/routing/observed-routing.js +0 -378
- package/router/dist/routing/parsers.d.ts +0 -16
- package/router/dist/routing/parsers.js +0 -521
- package/router/dist/routing/run-filters.d.ts +0 -60
- package/router/dist/routing/run-filters.js +0 -238
- package/router/dist/routing/run-reducers.d.ts +0 -49
- package/router/dist/routing/run-reducers.js +0 -122
- package/router/dist/sandbox/adapter-roots.d.ts +0 -46
- package/router/dist/sandbox/adapter-roots.js +0 -66
- package/router/dist/sandbox/eryx-python-adapter.d.ts +0 -13
- package/router/dist/sandbox/eryx-python-adapter.js +0 -868
- package/router/dist/sandbox/jq-wasm-adapter.d.ts +0 -9
- package/router/dist/sandbox/jq-wasm-adapter.js +0 -429
- package/router/dist/sandbox/quickjs-wasi-adapter.d.ts +0 -13
- package/router/dist/sandbox/quickjs-wasi-adapter.js +0 -384
- package/router/dist/sandbox/script-sandbox.d.ts +0 -134
- package/router/dist/sandbox/script-sandbox.js +0 -337
- package/router/dist/setup/script-transform-adapters.d.ts +0 -32
- package/router/dist/setup/script-transform-adapters.js +0 -278
- package/router/dist/tools/batch.d.ts +0 -33
- package/router/dist/tools/batch.js +0 -586
- package/router/dist/tools/run.d.ts +0 -62
- package/router/dist/tools/run.js +0 -1304
- package/router/dist/tools/search.d.ts +0 -66
- package/router/dist/tools/search.js +0 -1364
- package/router/dist/transform/engine.d.ts +0 -168
- package/router/dist/transform/engine.js +0 -2072
- package/router/dist/vault/vault-index.d.ts +0 -125
- package/router/dist/vault/vault-index.js +0 -416
- package/router/dist/vault/vault.d.ts +0 -138
- package/router/dist/vault/vault.js +0 -630
- package/runtime/interaction-contract.md +0 -16
- package/skills/discuss/references/checkpoints.md +0 -66
- package/skills/execute-work/references/execution-loop.md +0 -77
- package/skills/output-router/SKILL.md +0 -206
- package/skills/output-router/references/safety-policy.md +0 -40
- package/skills/setup-freeflow/references/output-router-setup.md +0 -110
- package/skills/track-work/references/working-record-schema.md +0 -378
- package/skills/workflow/references/workflow-loop.md +0 -92
- /package/skills/handoff/references/{templates.md → handoff-templates.md} +0 -0
- /package/skills/review-artifact/references/{reviewer-prompt.md → independent-artifact-reviewer-contract.md} +0 -0
|
@@ -11,30 +11,37 @@ import {
|
|
|
11
11
|
readCapabilityState,
|
|
12
12
|
readFreeflowConfigLayers,
|
|
13
13
|
readModeState,
|
|
14
|
-
readOutputRouterConfig,
|
|
15
14
|
resetSessionOverrides,
|
|
15
|
+
setModeStatus,
|
|
16
16
|
restoreModeOverride,
|
|
17
17
|
setSessionCoreOverride,
|
|
18
18
|
setSessionMode,
|
|
19
19
|
} from "../../dist/runtime/runtime-context.js";
|
|
20
|
-
import {
|
|
20
|
+
import { PIFLOW_HOST } from "../cognitive-routing/host-fixture.js";
|
|
21
21
|
|
|
22
22
|
const execFileAsync = promisify(execFile);
|
|
23
23
|
|
|
24
|
-
function loadExtension() {
|
|
24
|
+
function loadExtension(host = PIFLOW_HOST) {
|
|
25
25
|
const handlers = new Map();
|
|
26
26
|
const tools = [];
|
|
27
27
|
const commands = [];
|
|
28
|
+
const shortcuts = [];
|
|
28
29
|
const entries = [];
|
|
29
30
|
const sentMessages = [];
|
|
30
31
|
let activeToolNames;
|
|
31
32
|
const pi = {
|
|
33
|
+
host,
|
|
32
34
|
registerTool(tool) {
|
|
33
|
-
tools.
|
|
35
|
+
const index = tools.findIndex((existing) => existing.name === tool.name);
|
|
36
|
+
if (index >= 0) tools[index] = tool;
|
|
37
|
+
else tools.push(tool);
|
|
34
38
|
},
|
|
35
39
|
registerCommand(name, definition) {
|
|
36
40
|
commands.push({ name, definition });
|
|
37
41
|
},
|
|
42
|
+
registerShortcut(shortcut, definition) {
|
|
43
|
+
shortcuts.push({ shortcut, definition });
|
|
44
|
+
},
|
|
38
45
|
on(event, handler) {
|
|
39
46
|
handlers.set(event, handler);
|
|
40
47
|
},
|
|
@@ -64,6 +71,7 @@ function loadExtension() {
|
|
|
64
71
|
handlers,
|
|
65
72
|
tools,
|
|
66
73
|
commands,
|
|
74
|
+
shortcuts,
|
|
67
75
|
entries,
|
|
68
76
|
sentMessages,
|
|
69
77
|
activeToolNames: () => activeToolNames ?? tools.map((tool) => tool.name),
|
|
@@ -120,26 +128,239 @@ function renderText(component, width = 120) {
|
|
|
120
128
|
return component.render(width).join("\n");
|
|
121
129
|
}
|
|
122
130
|
|
|
123
|
-
test("Pi registers
|
|
124
|
-
const { commands, tools } = loadExtension();
|
|
131
|
+
test("Pi registers Freeflow commands without retired router tools", () => {
|
|
132
|
+
const { commands, shortcuts, tools } = loadExtension();
|
|
125
133
|
const commandNames = commands.map((command) => command.name);
|
|
126
134
|
const toolNames = tools.map((tool) => tool.name);
|
|
127
135
|
|
|
128
136
|
assert.ok(commandNames.includes("freeflow"));
|
|
129
|
-
assert.ok(commandNames.includes("output-router"));
|
|
130
|
-
for (const command of ["discuss", "
|
|
137
|
+
assert.ok(!commandNames.includes("output-router"));
|
|
138
|
+
for (const command of ["discuss", "track-work", "execute-work"]) {
|
|
131
139
|
assert.ok(commandNames.includes(command));
|
|
132
140
|
}
|
|
141
|
+
assert.ok(!commandNames.includes("discover"));
|
|
142
|
+
assert.ok(!commandNames.includes("execute-plan"));
|
|
133
143
|
assert.ok(!commandNames.includes("workflow"));
|
|
134
|
-
assert.
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
144
|
+
assert.deepEqual(
|
|
145
|
+
shortcuts.map(({ shortcut }) => shortcut),
|
|
146
|
+
["ctrl+shift+r", "ctrl+shift+a"],
|
|
147
|
+
);
|
|
148
|
+
assert.ok(toolNames.includes("freeflow_context"));
|
|
149
|
+
assert.deepEqual(tools.find((tool) => tool.name === "freeflow_context").parameters.oneOf, []);
|
|
150
|
+
assert.ok(toolNames.includes("freeflow_switch_profile"));
|
|
151
|
+
assert.ok(
|
|
152
|
+
!toolNames.some((name) => ["freeflow_status", "freeflow_search", "freeflow_run", "freeflow_batch"].includes(name)),
|
|
153
|
+
);
|
|
138
154
|
assert.ok(!toolNames.includes("freeflow_retrieve"));
|
|
139
|
-
assert.ok(!toolNames.includes("freeflow_search action=transform"));
|
|
140
155
|
assert.ok(!toolNames.includes("freeflow_capture"));
|
|
141
156
|
});
|
|
142
157
|
|
|
158
|
+
test("normal Pi keeps Cognitive Routing disabled while showing its configuration", async () => {
|
|
159
|
+
const cwd = await mkdtemp(join(tmpdir(), "freeflow-pi-normal-runtime-"));
|
|
160
|
+
try {
|
|
161
|
+
await mkdir(join(cwd, ".freeflow"));
|
|
162
|
+
await writeFile(join(cwd, ".freeflow/config.json"), JSON.stringify({ defaultMode: "workflow" }), "utf8");
|
|
163
|
+
await writeFile(
|
|
164
|
+
join(cwd, ".freeflow/local.json"),
|
|
165
|
+
JSON.stringify({
|
|
166
|
+
cognitiveRouting: {
|
|
167
|
+
enabled: true,
|
|
168
|
+
profiles: {
|
|
169
|
+
standard: { provider: "test", model: "standard", thinkingLevel: "high" },
|
|
170
|
+
reasoning: { provider: "test", model: "reasoning", thinkingLevel: "max" },
|
|
171
|
+
},
|
|
172
|
+
},
|
|
173
|
+
}),
|
|
174
|
+
"utf8",
|
|
175
|
+
);
|
|
176
|
+
const { commands, handlers, shortcuts, tools } = loadExtension(null);
|
|
177
|
+
const freeflowCommand = commands.find((command) => command.name === "freeflow");
|
|
178
|
+
assert.ok(freeflowCommand);
|
|
179
|
+
assert.deepEqual(shortcuts, []);
|
|
180
|
+
assert.ok(!tools.some((tool) => tool.name === "freeflow_switch_profile"));
|
|
181
|
+
assert.ok(!freeflowCommand.definition.getArgumentCompletions("").some((item) => item.value === "profile"));
|
|
182
|
+
const capabilityState = await readCapabilityState(
|
|
183
|
+
cwd,
|
|
184
|
+
{ modelRegistry: cognitiveRoutingModelRegistry() },
|
|
185
|
+
undefined,
|
|
186
|
+
);
|
|
187
|
+
assert.equal(capabilityState.cognitiveRouting.enabled, true);
|
|
188
|
+
assert.equal(capabilityState.cognitiveRouting.effective, false);
|
|
189
|
+
assert.equal(capabilityState.cognitiveRouting.blockingReason.code, "runtime_disabled");
|
|
190
|
+
|
|
191
|
+
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
|
+
await handlers.get("session_start")({ type: "session_start" }, ctx);
|
|
195
|
+
assert.match(ctx.statuses.at(-1).value, /cognitive disabled · PiFlow only/);
|
|
196
|
+
const beforeAgentStart = await handlers.get("before_agent_start")({ systemPrompt: "base" }, ctx);
|
|
197
|
+
assert.doesNotMatch(
|
|
198
|
+
`${beforeAgentStart?.systemPrompt ?? ""}\n${beforeAgentStart?.message ?? ""}`,
|
|
199
|
+
/Cognitive Routing/i,
|
|
200
|
+
);
|
|
201
|
+
ctx.ui.custom = async (factory) => {
|
|
202
|
+
let result;
|
|
203
|
+
const component = factory({ requestRender() {} }, testTheme, {}, (value) => {
|
|
204
|
+
result = value;
|
|
205
|
+
});
|
|
206
|
+
const rootText = renderText(component);
|
|
207
|
+
assert.match(rootText, /Cognitive Routing/);
|
|
208
|
+
assert.match(rootText, /PiFlow only/);
|
|
209
|
+
component.handleInput("\u001b[B");
|
|
210
|
+
component.handleInput("\r");
|
|
211
|
+
component.handleInput("\u001b[B");
|
|
212
|
+
component.handleInput("\u001b[B");
|
|
213
|
+
component.handleInput("\r");
|
|
214
|
+
await component.waitForWrites();
|
|
215
|
+
assert.doesNotMatch(renderText(component), /Cognitive Routing\s+enabled \(5\) configured · inactive/);
|
|
216
|
+
for (let index = 0; index < 5; index += 1) component.handleInput("\u001b[B");
|
|
217
|
+
component.handleInput("\r");
|
|
218
|
+
const cognitiveText = renderText(component);
|
|
219
|
+
assert.match(cognitiveText, /Freeflow Settings · Personal overrides › Cognitive Routing/);
|
|
220
|
+
assert.match(cognitiveText, /Enabled\s+enabled \(local · inactive\)/);
|
|
221
|
+
assert.match(cognitiveText, /Standard preset\s+test\/standard · high \(local · inactive\)/);
|
|
222
|
+
assert.match(cognitiveText, /Reasoning preset\s+test\/reasoning · max \(local · inactive\)/);
|
|
223
|
+
component.handleInput("\r");
|
|
224
|
+
assert.match(renderText(component), /Freeflow Settings · Personal overrides › Cognitive Routing/);
|
|
225
|
+
assert.doesNotMatch(renderText(component), /Inherit repository/);
|
|
226
|
+
component.handleInput("\u001b");
|
|
227
|
+
return result;
|
|
228
|
+
};
|
|
229
|
+
await freeflowCommand.definition.handler("settings", ctx);
|
|
230
|
+
} finally {
|
|
231
|
+
await rm(cwd, { recursive: true, force: true });
|
|
232
|
+
}
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
test("PiFlow statusline reports the current cognitive profile and control mode", () => {
|
|
236
|
+
const ctx = context();
|
|
237
|
+
setModeStatus(
|
|
238
|
+
ctx,
|
|
239
|
+
{ effectiveMode: "workflow", currentMode: null },
|
|
240
|
+
{
|
|
241
|
+
configured: true,
|
|
242
|
+
enabled: true,
|
|
243
|
+
configSources: { interactionContract: "builtin", skillsEnabled: "builtin", enabled: "builtin" },
|
|
244
|
+
interactionContract: { effective: true },
|
|
245
|
+
skills: { effective: true },
|
|
246
|
+
cognitiveRouting: { enabled: true, effective: true, blockingReason: { code: "disabled" } },
|
|
247
|
+
},
|
|
248
|
+
{ effective: true, activeProfile: "reasoning", controlMode: "automatic" },
|
|
249
|
+
);
|
|
250
|
+
assert.equal(ctx.statuses.at(-1).value, "freeflow: interaction · workflow · reasoning · automatic");
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
test("PiFlow statusline shows the configured standard profile pending first prompt activation", () => {
|
|
254
|
+
const ctx = context();
|
|
255
|
+
setModeStatus(
|
|
256
|
+
ctx,
|
|
257
|
+
{ effectiveMode: "workflow", currentMode: null },
|
|
258
|
+
{
|
|
259
|
+
configured: true,
|
|
260
|
+
enabled: true,
|
|
261
|
+
configSources: { interactionContract: "builtin", skillsEnabled: "builtin", enabled: "builtin" },
|
|
262
|
+
interactionContract: { effective: true },
|
|
263
|
+
skills: { effective: true },
|
|
264
|
+
cognitiveRouting: { enabled: true, effective: true, blockingReason: null },
|
|
265
|
+
},
|
|
266
|
+
undefined,
|
|
267
|
+
{ cognitiveRoutingStartupPending: true },
|
|
268
|
+
);
|
|
269
|
+
assert.equal(ctx.statuses.at(-1).value, "freeflow: interaction · workflow · standard · pending");
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
test("PiFlow statusline shows the configured startup profile while pending", () => {
|
|
273
|
+
const ctx = context();
|
|
274
|
+
setModeStatus(
|
|
275
|
+
ctx,
|
|
276
|
+
{ effectiveMode: "workflow", currentMode: null },
|
|
277
|
+
{
|
|
278
|
+
configured: true,
|
|
279
|
+
enabled: true,
|
|
280
|
+
configSources: { interactionContract: "builtin", skillsEnabled: "builtin", enabled: "builtin" },
|
|
281
|
+
interactionContract: { effective: true },
|
|
282
|
+
skills: { effective: true },
|
|
283
|
+
cognitiveRouting: {
|
|
284
|
+
enabled: true,
|
|
285
|
+
effective: true,
|
|
286
|
+
sessionStart: { control: "manual", profile: "reasoning" },
|
|
287
|
+
blockingReason: null,
|
|
288
|
+
},
|
|
289
|
+
},
|
|
290
|
+
undefined,
|
|
291
|
+
{ cognitiveRoutingStartupPending: true },
|
|
292
|
+
);
|
|
293
|
+
assert.equal(ctx.statuses.at(-1).value, "freeflow: interaction · workflow · reasoning · pending");
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
test("PiFlow statusline keeps an inactive runtime blocked instead of showing startup pending", () => {
|
|
297
|
+
const ctx = context();
|
|
298
|
+
setModeStatus(
|
|
299
|
+
ctx,
|
|
300
|
+
{ effectiveMode: "workflow", currentMode: null },
|
|
301
|
+
{
|
|
302
|
+
configured: true,
|
|
303
|
+
enabled: true,
|
|
304
|
+
configSources: { interactionContract: "builtin", skillsEnabled: "builtin", enabled: "builtin" },
|
|
305
|
+
interactionContract: { effective: true },
|
|
306
|
+
skills: { effective: true },
|
|
307
|
+
cognitiveRouting: { enabled: true, effective: true, blockingReason: null },
|
|
308
|
+
},
|
|
309
|
+
{ effective: false, activeProfile: "standard", controlMode: "automatic" },
|
|
310
|
+
{ cognitiveRoutingStartupPending: true },
|
|
311
|
+
);
|
|
312
|
+
assert.equal(ctx.statuses.at(-1).value, "freeflow: interaction · workflow · cognitive blocked · runtime_inactive");
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
test("PiFlow statusline preserves startup suppression when the host selected a model", () => {
|
|
316
|
+
const ctx = context();
|
|
317
|
+
ctx.modelStateProvenance = { explicitModel: true };
|
|
318
|
+
setModeStatus(
|
|
319
|
+
ctx,
|
|
320
|
+
{ effectiveMode: "workflow", currentMode: null },
|
|
321
|
+
{
|
|
322
|
+
configured: true,
|
|
323
|
+
enabled: true,
|
|
324
|
+
configSources: { interactionContract: "builtin", skillsEnabled: "builtin", enabled: "builtin" },
|
|
325
|
+
interactionContract: { effective: true },
|
|
326
|
+
skills: { effective: true },
|
|
327
|
+
cognitiveRouting: { enabled: true, effective: true, blockingReason: null },
|
|
328
|
+
},
|
|
329
|
+
undefined,
|
|
330
|
+
{ cognitiveRoutingStartupPending: true },
|
|
331
|
+
);
|
|
332
|
+
assert.equal(ctx.statuses.at(-1).value, "freeflow: interaction · workflow · cognitive blocked · runtime_inactive");
|
|
333
|
+
});
|
|
334
|
+
|
|
335
|
+
test("PiFlow empty configured sessions show standard pending before first prompt", async () => {
|
|
336
|
+
const cwd = await mkdtemp(join(tmpdir(), "freeflow-pi-pending-cognitive-status-"));
|
|
337
|
+
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
|
+
const { handlers } = loadExtension();
|
|
354
|
+
const ctx = context(cwd);
|
|
355
|
+
ctx.modelRegistry = cognitiveRoutingModelRegistry();
|
|
356
|
+
|
|
357
|
+
await handlers.get("session_start")({ type: "session_start", reason: "startup" }, ctx);
|
|
358
|
+
assert.equal(ctx.statuses.at(-1).value, "freeflow: interaction · workflow · standard · pending");
|
|
359
|
+
} finally {
|
|
360
|
+
await rm(cwd, { recursive: true, force: true });
|
|
361
|
+
}
|
|
362
|
+
});
|
|
363
|
+
|
|
143
364
|
test("Pi exposes bypass scope argument completions", () => {
|
|
144
365
|
const { commands } = loadExtension();
|
|
145
366
|
const bypassCommand = commands.find((command) => command.name === "bypass");
|
|
@@ -155,20 +376,52 @@ test("Pi exposes bypass scope argument completions", () => {
|
|
|
155
376
|
assert.deepEqual(bypassCommand.definition.getArgumentCompletions("unknown"), []);
|
|
156
377
|
});
|
|
157
378
|
|
|
158
|
-
test("Pi describes Freeflow
|
|
379
|
+
test("Pi describes Freeflow argument completions", () => {
|
|
159
380
|
const { commands } = loadExtension();
|
|
160
381
|
const freeflowCommand = commands.find((command) => command.name === "freeflow");
|
|
161
|
-
const outputRouterCommand = commands.find((command) => command.name === "output-router");
|
|
162
382
|
assert.ok(freeflowCommand);
|
|
163
|
-
assert.ok(outputRouterCommand);
|
|
164
383
|
|
|
165
384
|
assert.deepEqual(freeflowCommand.definition.getArgumentCompletions(""), [
|
|
166
385
|
{ value: "settings", label: "settings", description: "Open personal override settings" },
|
|
167
386
|
{ value: "status", label: "status", description: "Show effective Freeflow state" },
|
|
387
|
+
{ value: "context", label: "context", description: "Inspect Freeflow Context" },
|
|
168
388
|
{ value: "mode", label: "mode", description: "Select a temporary session mode" },
|
|
389
|
+
{ value: "profile", label: "profile", description: "Hold or release Cognitive Routing profile control" },
|
|
169
390
|
{ value: "enable", label: "enable", description: "Enable Freeflow for this repository" },
|
|
170
391
|
{ value: "disable", label: "disable", description: "Disable Freeflow for this repository" },
|
|
171
392
|
]);
|
|
393
|
+
assert.deepEqual(freeflowCommand.definition.getArgumentCompletions("profile "), [
|
|
394
|
+
{
|
|
395
|
+
value: "profile standard",
|
|
396
|
+
label: "standard",
|
|
397
|
+
description: "Hold standard profile manually",
|
|
398
|
+
},
|
|
399
|
+
{
|
|
400
|
+
value: "profile reasoning",
|
|
401
|
+
label: "reasoning",
|
|
402
|
+
description: "Hold reasoning profile manually",
|
|
403
|
+
},
|
|
404
|
+
{
|
|
405
|
+
value: "profile auto",
|
|
406
|
+
label: "auto",
|
|
407
|
+
description: "Release the manual hold without changing the model",
|
|
408
|
+
},
|
|
409
|
+
{
|
|
410
|
+
value: "profile history",
|
|
411
|
+
label: "history",
|
|
412
|
+
description: "Show Cognitive Routing transition history",
|
|
413
|
+
},
|
|
414
|
+
{
|
|
415
|
+
value: "profile history active",
|
|
416
|
+
label: "history active",
|
|
417
|
+
description: "Show current-branch Cognitive Routing history",
|
|
418
|
+
},
|
|
419
|
+
{
|
|
420
|
+
value: "profile history anomalies",
|
|
421
|
+
label: "history anomalies",
|
|
422
|
+
description: "Show Cognitive Routing history anomalies",
|
|
423
|
+
},
|
|
424
|
+
]);
|
|
172
425
|
assert.deepEqual(freeflowCommand.definition.getArgumentCompletions("settings "), [
|
|
173
426
|
{ value: "settings session", label: "session", description: "Override Freeflow for this Pi session" },
|
|
174
427
|
{ value: "settings local", label: "local", description: "Edit personal overrides for this repository" },
|
|
@@ -188,15 +441,15 @@ test("Pi describes Freeflow and Output Router argument completions", () => {
|
|
|
188
441
|
description: "Clear the session override and use the configured default",
|
|
189
442
|
},
|
|
190
443
|
]);
|
|
191
|
-
assert.deepEqual(
|
|
192
|
-
{ value: "
|
|
193
|
-
{ value: "
|
|
194
|
-
{ value: "
|
|
195
|
-
{ value: "
|
|
444
|
+
assert.deepEqual(freeflowCommand.definition.getArgumentCompletions("context "), [
|
|
445
|
+
{ value: "context status", label: "status", description: "Show Freeflow Context state" },
|
|
446
|
+
{ value: "context list", label: "list", description: "List archived context projections" },
|
|
447
|
+
{ value: "context restore", label: "restore", description: "Restore one or more context references" },
|
|
448
|
+
{ value: "context reset all", label: "reset all", description: "Reset projection decisions on the active branch" },
|
|
196
449
|
]);
|
|
197
450
|
});
|
|
198
451
|
|
|
199
|
-
test("Pi exposes canonical model skills
|
|
452
|
+
test("Pi exposes canonical model skills without compatibility aliases", async () => {
|
|
200
453
|
const cwd = await mkdtemp(join(tmpdir(), "freeflow-pi-canonical-skills-"));
|
|
201
454
|
try {
|
|
202
455
|
await mkdir(join(cwd, ".freeflow"));
|
|
@@ -209,25 +462,29 @@ test("Pi exposes canonical model skills and maps published command aliases", asy
|
|
|
209
462
|
assert.ok(match, `unexpected skill path: ${path}`);
|
|
210
463
|
return match[1];
|
|
211
464
|
});
|
|
212
|
-
|
|
465
|
+
assert.equal(skillNames.length, 26);
|
|
466
|
+
for (const skill of ["action-selection", "discuss", "execute-work", "track-work"]) {
|
|
213
467
|
assert.ok(skillNames.includes(skill));
|
|
214
468
|
}
|
|
469
|
+
assert.ok(!skillNames.includes("output-router"));
|
|
215
470
|
assert.ok(!skillNames.includes("discover"));
|
|
216
471
|
assert.ok(!skillNames.includes("execute-plan"));
|
|
217
472
|
await Promise.all(resources.skillPaths.map((path) => readFile(path, "utf8")));
|
|
218
473
|
|
|
219
474
|
for (const [commandName, expectedSkill] of [
|
|
475
|
+
["action-selection", "action-selection"],
|
|
220
476
|
["discuss", "discuss"],
|
|
221
|
-
["discover", "discuss"],
|
|
222
477
|
["track-work", "track-work"],
|
|
223
478
|
["execute-work", "execute-work"],
|
|
224
|
-
["execute-plan", "execute-work"],
|
|
225
479
|
]) {
|
|
226
480
|
const command = commands.find((candidate) => candidate.name === commandName);
|
|
227
481
|
assert.ok(command);
|
|
228
482
|
await command.definition.handler(undefined, context(cwd));
|
|
229
483
|
assert.equal(sentMessages.at(-1), `/skill:${expectedSkill}`);
|
|
230
484
|
}
|
|
485
|
+
for (const alias of ["discover", "execute-plan"]) {
|
|
486
|
+
assert.ok(!commands.some((candidate) => candidate.name === alias));
|
|
487
|
+
}
|
|
231
488
|
} finally {
|
|
232
489
|
await rm(cwd, { recursive: true, force: true });
|
|
233
490
|
}
|
|
@@ -236,7 +493,7 @@ test("Pi exposes canonical model skills and maps published command aliases", asy
|
|
|
236
493
|
test("Pi keeps Freeflow inactive until setup config exists", async () => {
|
|
237
494
|
const cwd = await mkdtemp(join(tmpdir(), "freeflow-pi-missing-setup-"));
|
|
238
495
|
try {
|
|
239
|
-
const { handlers
|
|
496
|
+
const { handlers } = loadExtension();
|
|
240
497
|
const resourcesDiscover = handlers.get("resources_discover");
|
|
241
498
|
const beforeAgentStart = handlers.get("before_agent_start");
|
|
242
499
|
assert.ok(resourcesDiscover);
|
|
@@ -248,10 +505,6 @@ test("Pi keeps Freeflow inactive until setup config exists", async () => {
|
|
|
248
505
|
|
|
249
506
|
const result = await beforeAgentStart({ systemPrompt: "base prompt" }, context(cwd));
|
|
250
507
|
assert.equal(result.systemPrompt, "base prompt");
|
|
251
|
-
assert.ok(!activeToolNames().includes("freeflow_status"));
|
|
252
|
-
assert.ok(!activeToolNames().includes("freeflow_search"));
|
|
253
|
-
assert.ok(!activeToolNames().includes("freeflow_run"));
|
|
254
|
-
assert.ok(!activeToolNames().includes("freeflow_batch"));
|
|
255
508
|
} finally {
|
|
256
509
|
await rm(cwd, { recursive: true, force: true });
|
|
257
510
|
}
|
|
@@ -269,7 +522,6 @@ test("Pi treats invalid Freeflow setup config as inactive", async () => {
|
|
|
269
522
|
enabled: "false",
|
|
270
523
|
interactionContract: "on",
|
|
271
524
|
skills: { enabled: "no" },
|
|
272
|
-
outputRouter: { enabled: true },
|
|
273
525
|
},
|
|
274
526
|
null,
|
|
275
527
|
2,
|
|
@@ -277,15 +529,13 @@ test("Pi treats invalid Freeflow setup config as inactive", async () => {
|
|
|
277
529
|
"utf8",
|
|
278
530
|
);
|
|
279
531
|
|
|
280
|
-
const { handlers, commands
|
|
532
|
+
const { handlers, commands } = loadExtension();
|
|
281
533
|
const resources = await handlers.get("resources_discover")({ cwd }, context(cwd));
|
|
282
534
|
assert.equal(resources.skillPaths.length, 1);
|
|
283
535
|
assert.match(resources.skillPaths[0], /setup-freeflow\/SKILL\.md$/);
|
|
284
536
|
|
|
285
537
|
const result = await handlers.get("before_agent_start")({ systemPrompt: "base prompt" }, context(cwd));
|
|
286
538
|
assert.equal(result.systemPrompt, "base prompt");
|
|
287
|
-
assert.ok(!activeToolNames().includes("freeflow_status"));
|
|
288
|
-
assert.ok(!activeToolNames().includes("freeflow_search"));
|
|
289
539
|
|
|
290
540
|
const freeflowCommand = commands.find((command) => command.name === "freeflow");
|
|
291
541
|
const statusCtx = context(cwd);
|
|
@@ -356,20 +606,22 @@ test("Pi layers local core overrides over repository defaults with source eviden
|
|
|
356
606
|
assert.deepEqual(layers.coreConfig, {
|
|
357
607
|
enabled: true,
|
|
358
608
|
interactionContract: false,
|
|
609
|
+
contextVirtualization: false,
|
|
610
|
+
conversationHistory: false,
|
|
359
611
|
skills: { enabled: false },
|
|
360
612
|
defaultMode: "strict-workflow",
|
|
361
613
|
});
|
|
362
614
|
assert.deepEqual(layers.sources, {
|
|
363
615
|
enabled: "local",
|
|
364
616
|
interactionContract: "repository",
|
|
617
|
+
contextVirtualization: "builtin",
|
|
618
|
+
conversationHistory: "builtin",
|
|
365
619
|
skillsEnabled: "local",
|
|
366
620
|
defaultMode: "local",
|
|
367
621
|
});
|
|
368
622
|
assert.equal(layers.local.parsed.processing.unsafeUnsandboxed.enabled, true);
|
|
369
|
-
const routerConfig = await readOutputRouterConfig(cwd);
|
|
370
|
-
assert.equal(routerConfig.localConfig.processing.unsafeUnsandboxed.enabled, true);
|
|
371
623
|
|
|
372
|
-
const capabilityState = await readCapabilityState(cwd);
|
|
624
|
+
const capabilityState = await readCapabilityState(cwd, undefined, PIFLOW_HOST);
|
|
373
625
|
assert.equal(capabilityState.enabled, true);
|
|
374
626
|
assert.equal(capabilityState.interactionContract.effective, false);
|
|
375
627
|
assert.equal(capabilityState.skills.effective, false);
|
|
@@ -403,7 +655,7 @@ test("Pi layers branch-aware session core overrides above configured values", as
|
|
|
403
655
|
|
|
404
656
|
let result = await setSessionCoreOverride("enabled", false, ctx, pi);
|
|
405
657
|
assert.equal(result.changed, true);
|
|
406
|
-
let capabilityState = await readCapabilityState(cwd);
|
|
658
|
+
let capabilityState = await readCapabilityState(cwd, undefined, PIFLOW_HOST);
|
|
407
659
|
assert.equal(capabilityState.enabled, false);
|
|
408
660
|
assert.equal(capabilityState.configSources.enabled, "session");
|
|
409
661
|
assert.deepEqual(capabilityState.sessionOverrides, { enabled: false });
|
|
@@ -419,7 +671,7 @@ test("Pi layers branch-aware session core overrides above configured values", as
|
|
|
419
671
|
await setSessionCoreOverride("enabled", null, ctx, pi);
|
|
420
672
|
await setSessionCoreOverride("interactionContract", false, ctx, pi);
|
|
421
673
|
await setSessionCoreOverride("skillsEnabled", false, ctx, pi);
|
|
422
|
-
capabilityState = await readCapabilityState(cwd);
|
|
674
|
+
capabilityState = await readCapabilityState(cwd, undefined, PIFLOW_HOST);
|
|
423
675
|
assert.equal(capabilityState.enabled, true);
|
|
424
676
|
assert.equal(capabilityState.interactionContract.effective, false);
|
|
425
677
|
assert.equal(capabilityState.skills.effective, false);
|
|
@@ -429,7 +681,7 @@ test("Pi layers branch-aware session core overrides above configured values", as
|
|
|
429
681
|
const reset = await resetSessionOverrides(ctx, pi);
|
|
430
682
|
assert.equal(reset.changed, true);
|
|
431
683
|
assert.equal(reset.reloadRequired, true);
|
|
432
|
-
capabilityState = await readCapabilityState(cwd);
|
|
684
|
+
capabilityState = await readCapabilityState(cwd, undefined, PIFLOW_HOST);
|
|
433
685
|
assert.deepEqual(capabilityState.sessionOverrides, {});
|
|
434
686
|
assert.equal(capabilityState.interactionContract.effective, true);
|
|
435
687
|
assert.equal(capabilityState.skills.effective, true);
|
|
@@ -449,12 +701,12 @@ test("Pi session enablement can override configured off but cannot bypass activa
|
|
|
449
701
|
const ctx = context(cwd);
|
|
450
702
|
await setSessionCoreOverride("enabled", true, ctx, pi);
|
|
451
703
|
|
|
452
|
-
let capabilityState = await readCapabilityState(cwd);
|
|
704
|
+
let capabilityState = await readCapabilityState(cwd, undefined, PIFLOW_HOST);
|
|
453
705
|
assert.equal(capabilityState.configured, false);
|
|
454
706
|
assert.equal(capabilityState.enabled, false);
|
|
455
707
|
|
|
456
708
|
await writeFile(join(cwd, ".freeflow/config.json"), "{ invalid", "utf8");
|
|
457
|
-
capabilityState = await readCapabilityState(cwd);
|
|
709
|
+
capabilityState = await readCapabilityState(cwd, undefined, PIFLOW_HOST);
|
|
458
710
|
assert.equal(capabilityState.configured, false);
|
|
459
711
|
assert.equal(capabilityState.enabled, false);
|
|
460
712
|
|
|
@@ -463,7 +715,7 @@ test("Pi session enablement can override configured off but cannot bypass activa
|
|
|
463
715
|
JSON.stringify({ enabled: false, defaultMode: "workflow" }),
|
|
464
716
|
"utf8",
|
|
465
717
|
);
|
|
466
|
-
capabilityState = await readCapabilityState(cwd);
|
|
718
|
+
capabilityState = await readCapabilityState(cwd, undefined, PIFLOW_HOST);
|
|
467
719
|
assert.equal(capabilityState.configured, true);
|
|
468
720
|
assert.equal(capabilityState.enabled, true);
|
|
469
721
|
assert.equal(capabilityState.configSources.enabled, "session");
|
|
@@ -496,7 +748,7 @@ test("Pi restores session core overrides from the active branch", async () => {
|
|
|
496
748
|
const { handlers } = loadExtension();
|
|
497
749
|
await handlers.get("session_start")({ reason: "resume" }, context(cwd, allEntries, activeBranchEntries));
|
|
498
750
|
|
|
499
|
-
let capabilityState = await readCapabilityState(cwd);
|
|
751
|
+
let capabilityState = await readCapabilityState(cwd, undefined, PIFLOW_HOST);
|
|
500
752
|
assert.equal(capabilityState.enabled, true);
|
|
501
753
|
assert.equal(capabilityState.interactionContract.effective, false);
|
|
502
754
|
assert.deepEqual(capabilityState.sessionOverrides, { interactionContract: false });
|
|
@@ -509,7 +761,7 @@ test("Pi restores session core overrides from the active branch", async () => {
|
|
|
509
761
|
},
|
|
510
762
|
];
|
|
511
763
|
await handlers.get("session_tree")({}, context(cwd, allEntries, switchedBranchEntries));
|
|
512
|
-
capabilityState = await readCapabilityState(cwd);
|
|
764
|
+
capabilityState = await readCapabilityState(cwd, undefined, PIFLOW_HOST);
|
|
513
765
|
assert.equal(capabilityState.enabled, false);
|
|
514
766
|
assert.deepEqual(capabilityState.sessionOverrides, { enabled: false });
|
|
515
767
|
} finally {
|
|
@@ -536,25 +788,29 @@ test("Pi compact mode context distinguishes layered defaults, session state, and
|
|
|
536
788
|
assert.ok(freeflowCommand);
|
|
537
789
|
|
|
538
790
|
let result = await beforeAgentStart({ systemPrompt: "base prompt" }, context(cwd));
|
|
539
|
-
assert.match(result.systemPrompt, /
|
|
540
|
-
assert.
|
|
541
|
-
assert.
|
|
542
|
-
assert.
|
|
543
|
-
assert.match(
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
assert.
|
|
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/);
|
|
548
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`/);
|
|
549
805
|
|
|
550
806
|
await freeflowCommand.definition.handler("mode conversation", context(cwd));
|
|
551
807
|
result = await beforeAgentStart({ systemPrompt: "base prompt" }, context(cwd));
|
|
552
|
-
assert.
|
|
553
|
-
assert.
|
|
554
|
-
assert.
|
|
555
|
-
assert.
|
|
556
|
-
assert.
|
|
557
|
-
assert.
|
|
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/);
|
|
558
814
|
assert.doesNotMatch(result.systemPrompt, /## Strict Workflow Overlay/);
|
|
559
815
|
|
|
560
816
|
await freeflowCommand.definition.handler("mode reset", context(cwd));
|
|
@@ -597,9 +853,13 @@ test("Pi restores layered session mode on resume and preserves it through compac
|
|
|
597
853
|
|
|
598
854
|
await sessionCompact({ reason: "manual" }, ctx);
|
|
599
855
|
let result = await beforeAgentStart({ prompt: "continue", systemPrompt: "base prompt" }, ctx);
|
|
600
|
-
assert.
|
|
601
|
-
assert.
|
|
602
|
-
assert.
|
|
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`/);
|
|
603
863
|
|
|
604
864
|
await writeFile(
|
|
605
865
|
localPath,
|
|
@@ -620,9 +880,13 @@ test("Pi restores layered session mode on resume and preserves it through compac
|
|
|
620
880
|
assert.equal(modeState.active, false);
|
|
621
881
|
assert.equal(modeState.effectiveMode, null);
|
|
622
882
|
result = await beforeAgentStart({ prompt: "continue", systemPrompt: "base prompt" }, ctx);
|
|
623
|
-
assert.
|
|
624
|
-
assert.
|
|
625
|
-
assert.
|
|
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/);
|
|
626
890
|
|
|
627
891
|
sessionEntries.push({
|
|
628
892
|
type: "custom",
|
|
@@ -719,7 +983,10 @@ test("Pi ordinary prompts and direct skill calls do not change mode state", asyn
|
|
|
719
983
|
assert.equal(modeState.sessionMode, null);
|
|
720
984
|
assert.equal(modeState.resolvedMode, "strict-workflow");
|
|
721
985
|
assert.equal(modeState.effectiveMode, "strict-workflow");
|
|
722
|
-
assert.match(
|
|
986
|
+
assert.match(
|
|
987
|
+
result.systemPrompt,
|
|
988
|
+
/Task type, skill selection, direct skill calls, usefulness, or new evidence does not change it/,
|
|
989
|
+
);
|
|
723
990
|
assert.deepEqual(entries, []);
|
|
724
991
|
|
|
725
992
|
await freeflowCommand.definition.handler("mode conversation", context(cwd));
|
|
@@ -733,8 +1000,11 @@ test("Pi ordinary prompts and direct skill calls do not change mode state", asyn
|
|
|
733
1000
|
assert.equal(modeState.resolvedMode, "conversation");
|
|
734
1001
|
assert.equal(modeState.effectiveMode, "conversation");
|
|
735
1002
|
result = await beforeAgentStart({ prompt: "/commit-work", systemPrompt: "base prompt" }, context(cwd));
|
|
736
|
-
assert.
|
|
737
|
-
assert.
|
|
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`/);
|
|
738
1008
|
} finally {
|
|
739
1009
|
restoreModeOverride(context(cwd));
|
|
740
1010
|
await rm(cwd, { recursive: true, force: true });
|
|
@@ -752,12 +1022,16 @@ test("Pi layered core config inherits omitted values and built-in defaults", asy
|
|
|
752
1022
|
assert.deepEqual(layers.coreConfig, {
|
|
753
1023
|
enabled: true,
|
|
754
1024
|
interactionContract: false,
|
|
1025
|
+
contextVirtualization: false,
|
|
1026
|
+
conversationHistory: false,
|
|
755
1027
|
skills: { enabled: true },
|
|
756
1028
|
defaultMode: "workflow",
|
|
757
1029
|
});
|
|
758
1030
|
assert.deepEqual(layers.sources, {
|
|
759
1031
|
enabled: "builtin",
|
|
760
1032
|
interactionContract: "local",
|
|
1033
|
+
contextVirtualization: "builtin",
|
|
1034
|
+
conversationHistory: "builtin",
|
|
761
1035
|
skillsEnabled: "builtin",
|
|
762
1036
|
defaultMode: "builtin",
|
|
763
1037
|
});
|
|
@@ -779,7 +1053,7 @@ test("Pi requires repository activation even when local overrides exist", async
|
|
|
779
1053
|
const layers = await readFreeflowConfigLayers(cwd);
|
|
780
1054
|
assert.equal(layers.repositoryConfigured, false);
|
|
781
1055
|
assert.equal(layers.configured, false);
|
|
782
|
-
const capabilityState = await readCapabilityState(cwd);
|
|
1056
|
+
const capabilityState = await readCapabilityState(cwd, undefined, PIFLOW_HOST);
|
|
783
1057
|
assert.equal(capabilityState.enabled, false);
|
|
784
1058
|
assert.equal(capabilityState.skills.effective, false);
|
|
785
1059
|
|
|
@@ -807,7 +1081,7 @@ test("Pi fails closed when an existing local override is invalid", async () => {
|
|
|
807
1081
|
assert.equal(layers.blockingConfigPath, layers.local.path);
|
|
808
1082
|
assert.match(layers.parseError, /JSON/);
|
|
809
1083
|
|
|
810
|
-
const capabilityState = await readCapabilityState(cwd);
|
|
1084
|
+
const capabilityState = await readCapabilityState(cwd, undefined, PIFLOW_HOST);
|
|
811
1085
|
assert.equal(capabilityState.configured, false);
|
|
812
1086
|
assert.equal(capabilityState.enabled, false);
|
|
813
1087
|
assert.equal(capabilityState.interactionContract.effective, false);
|
|
@@ -815,13 +1089,12 @@ test("Pi fails closed when an existing local override is invalid", async () => {
|
|
|
815
1089
|
assert.equal(capabilityState.localConfigValid, false);
|
|
816
1090
|
assert.match(capabilityState.localConfigParseError, /JSON/);
|
|
817
1091
|
|
|
818
|
-
const { handlers
|
|
1092
|
+
const { handlers } = loadExtension();
|
|
819
1093
|
const resources = await handlers.get("resources_discover")({ cwd }, context(cwd));
|
|
820
1094
|
assert.equal(resources.skillPaths.length, 1);
|
|
821
1095
|
assert.match(resources.skillPaths[0], /setup-freeflow\/SKILL\.md$/);
|
|
822
1096
|
const result = await handlers.get("before_agent_start")({ systemPrompt: "base prompt" }, context(cwd));
|
|
823
1097
|
assert.equal(result.systemPrompt, "base prompt");
|
|
824
|
-
assert.ok(!activeToolNames().includes("freeflow_status"));
|
|
825
1098
|
} finally {
|
|
826
1099
|
await rm(cwd, { recursive: true, force: true });
|
|
827
1100
|
}
|
|
@@ -838,7 +1111,7 @@ test("Pi fails closed when a local core override has an invalid type", async ()
|
|
|
838
1111
|
assert.equal(layers.configured, false);
|
|
839
1112
|
assert.equal(layers.local.valid, false);
|
|
840
1113
|
assert.equal(layers.parseError, "skills.enabled must be a boolean");
|
|
841
|
-
const state = await readCapabilityState(cwd);
|
|
1114
|
+
const state = await readCapabilityState(cwd, undefined, PIFLOW_HOST);
|
|
842
1115
|
assert.equal(state.enabled, false);
|
|
843
1116
|
assert.equal(state.skills.effective, false);
|
|
844
1117
|
} finally {
|
|
@@ -856,7 +1129,6 @@ test("Pi local enabled false overrides an enabled repository master switch", asy
|
|
|
856
1129
|
{
|
|
857
1130
|
enabled: true,
|
|
858
1131
|
defaultMode: "workflow",
|
|
859
|
-
outputRouter: { enabled: true },
|
|
860
1132
|
},
|
|
861
1133
|
null,
|
|
862
1134
|
2,
|
|
@@ -865,17 +1137,16 @@ test("Pi local enabled false overrides an enabled repository master switch", asy
|
|
|
865
1137
|
);
|
|
866
1138
|
await writeFile(join(cwd, ".freeflow/local.json"), JSON.stringify({ enabled: false }, null, 2), "utf8");
|
|
867
1139
|
|
|
868
|
-
const state = await readCapabilityState(cwd);
|
|
1140
|
+
const state = await readCapabilityState(cwd, undefined, PIFLOW_HOST);
|
|
869
1141
|
assert.equal(state.configured, true);
|
|
870
1142
|
assert.equal(state.enabled, false);
|
|
871
1143
|
assert.equal(state.configSources.enabled, "local");
|
|
872
|
-
assert.equal(state.outputRouter.enabled, false);
|
|
873
1144
|
} finally {
|
|
874
1145
|
await rm(cwd, { recursive: true, force: true });
|
|
875
1146
|
}
|
|
876
1147
|
});
|
|
877
1148
|
|
|
878
|
-
test("Pi master Freeflow toggle disables skills
|
|
1149
|
+
test("Pi master Freeflow toggle disables skills and capabilities", async () => {
|
|
879
1150
|
const cwd = await mkdtemp(join(tmpdir(), "freeflow-pi-master-disabled-"));
|
|
880
1151
|
try {
|
|
881
1152
|
await mkdir(join(cwd, ".freeflow"));
|
|
@@ -886,7 +1157,6 @@ test("Pi master Freeflow toggle disables skills, capabilities, and routing", asy
|
|
|
886
1157
|
enabled: false,
|
|
887
1158
|
defaultMode: "workflow",
|
|
888
1159
|
skills: { enabled: true },
|
|
889
|
-
outputRouter: { enabled: true, postToolRouting: "safety-net" },
|
|
890
1160
|
},
|
|
891
1161
|
null,
|
|
892
1162
|
2,
|
|
@@ -894,18 +1164,13 @@ test("Pi master Freeflow toggle disables skills, capabilities, and routing", asy
|
|
|
894
1164
|
"utf8",
|
|
895
1165
|
);
|
|
896
1166
|
|
|
897
|
-
const { handlers
|
|
1167
|
+
const { handlers } = loadExtension();
|
|
898
1168
|
const resources = await handlers.get("resources_discover")({ cwd }, context(cwd));
|
|
899
1169
|
assert.deepEqual(resources.skillPaths, []);
|
|
900
1170
|
|
|
901
1171
|
const result = await handlers.get("before_agent_start")({ systemPrompt: "base prompt" }, context(cwd));
|
|
902
|
-
assert.
|
|
903
|
-
assert.
|
|
904
|
-
assert.doesNotMatch(result.systemPrompt, /# Freeflow Runtime Kernel/);
|
|
905
|
-
assert.doesNotMatch(result.systemPrompt, /## Loaded Workflow Skill/);
|
|
906
|
-
assert.doesNotMatch(result.systemPrompt, /## Loaded Output Router Skill/);
|
|
907
|
-
assert.ok(!activeToolNames().includes("freeflow_status"));
|
|
908
|
-
assert.ok(!activeToolNames().includes("freeflow_search"));
|
|
1172
|
+
assert.equal(result.systemPrompt, "base prompt");
|
|
1173
|
+
assert.equal(result.message, undefined);
|
|
909
1174
|
|
|
910
1175
|
const routed = await handlers.get("tool_result")(
|
|
911
1176
|
{
|
|
@@ -922,7 +1187,7 @@ test("Pi master Freeflow toggle disables skills, capabilities, and routing", asy
|
|
|
922
1187
|
}
|
|
923
1188
|
});
|
|
924
1189
|
|
|
925
|
-
test("Pi skills toggle suppresses workflow skills
|
|
1190
|
+
test("Pi skills toggle suppresses workflow skills without enabling retired router tools", async () => {
|
|
926
1191
|
const cwd = await mkdtemp(join(tmpdir(), "freeflow-pi-skills-disabled-router-on-"));
|
|
927
1192
|
try {
|
|
928
1193
|
await mkdir(join(cwd, ".freeflow"));
|
|
@@ -932,7 +1197,6 @@ test("Pi skills toggle suppresses workflow skills while allowing enabled router
|
|
|
932
1197
|
{
|
|
933
1198
|
defaultMode: "workflow",
|
|
934
1199
|
skills: { enabled: false },
|
|
935
|
-
outputRouter: { enabled: true },
|
|
936
1200
|
},
|
|
937
1201
|
null,
|
|
938
1202
|
2,
|
|
@@ -952,18 +1216,18 @@ test("Pi skills toggle suppresses workflow skills while allowing enabled router
|
|
|
952
1216
|
assert.doesNotMatch(result.systemPrompt, /## Loaded Workflow Skill/);
|
|
953
1217
|
assert.doesNotMatch(result.systemPrompt, /## Loaded Decision Gate Skill/);
|
|
954
1218
|
assert.doesNotMatch(result.systemPrompt, /## Discovery-light/);
|
|
955
|
-
assert.
|
|
956
|
-
assert.
|
|
957
|
-
assert.
|
|
958
|
-
assert.
|
|
959
|
-
assert.
|
|
960
|
-
|
|
961
|
-
assert.match(
|
|
962
|
-
assert.
|
|
963
|
-
assert.
|
|
964
|
-
assert.
|
|
965
|
-
assert.
|
|
966
|
-
assert.ok(activeToolNames().includes("
|
|
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"));
|
|
967
1231
|
} finally {
|
|
968
1232
|
await rm(cwd, { recursive: true, force: true });
|
|
969
1233
|
}
|
|
@@ -984,16 +1248,17 @@ test("Pi interaction contract can be disabled independently while Workflow stays
|
|
|
984
1248
|
context(cwd),
|
|
985
1249
|
);
|
|
986
1250
|
assert.doesNotMatch(result.systemPrompt, /# Freeflow Interaction Contract/);
|
|
987
|
-
assert.
|
|
988
|
-
assert.
|
|
989
|
-
assert.match(result.
|
|
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/);
|
|
990
1255
|
} finally {
|
|
991
1256
|
await rm(cwd, { recursive: true, force: true });
|
|
992
1257
|
}
|
|
993
1258
|
});
|
|
994
1259
|
|
|
995
1260
|
// Regression for the "everything disabled except Freeflow control plane" state.
|
|
996
|
-
// The model should know Freeflow exists and can be reconfigured, but should not receive workflow
|
|
1261
|
+
// The model should know Freeflow exists and can be reconfigured, but should not receive workflow behavior.
|
|
997
1262
|
// `defaultMode` is dormant while Skills are off.
|
|
998
1263
|
test("Pi all-disabled capability state injects only Freeflow control-plane status", async () => {
|
|
999
1264
|
const cwd = await mkdtemp(join(tmpdir(), "freeflow-pi-all-capabilities-disabled-"));
|
|
@@ -1017,24 +1282,22 @@ test("Pi all-disabled capability state injects only Freeflow control-plane statu
|
|
|
1017
1282
|
const ctx = context(cwd);
|
|
1018
1283
|
const result = await handlers.get("before_agent_start")({ systemPrompt: "base prompt" }, ctx);
|
|
1019
1284
|
|
|
1020
|
-
assert.match(result.systemPrompt, /# Freeflow
|
|
1021
|
-
assert.match(
|
|
1022
|
-
|
|
1023
|
-
/Freeflow is enabled for this repo, but no model-facing capabilities are enabled/,
|
|
1024
|
-
);
|
|
1025
|
-
assert.match(result.systemPrompt, /Repository default mode: `workflow` \(repository\)/);
|
|
1026
|
-
assert.match(result.systemPrompt, /Resolved mode: `workflow` \(inactive because Skills are disabled\)/);
|
|
1027
|
-
assert.match(result.systemPrompt, /Effective Freeflow mode: `none`/);
|
|
1028
|
-
assert.doesNotMatch(result.systemPrompt, /# Freeflow Runtime Context/);
|
|
1285
|
+
assert.match(result.systemPrompt, /# Freeflow Stable Guidance/);
|
|
1286
|
+
assert.match(result.systemPrompt, /## Mode/);
|
|
1287
|
+
assert.doesNotMatch(result.systemPrompt, /## Shared Terms/);
|
|
1029
1288
|
assert.doesNotMatch(result.systemPrompt, /# Freeflow Interaction Contract/);
|
|
1289
|
+
const providerContext = await handlers.get("context")({ messages: [] }, ctx);
|
|
1290
|
+
assert.match(providerContext.messages.at(-1).content, /Default mode: `workflow`/);
|
|
1291
|
+
assert.match(providerContext.messages.at(-1).content, /Active mode: `inactive`/);
|
|
1292
|
+
assert.match(providerContext.messages.at(-1).content, /Interaction Contract: inactive/);
|
|
1293
|
+
assert.match(providerContext.messages.at(-1).content, /Skills: inactive/);
|
|
1030
1294
|
assert.doesNotMatch(result.systemPrompt, /# Freeflow Runtime Kernel/);
|
|
1031
1295
|
assert.doesNotMatch(result.systemPrompt, /## Loaded Mode Contract Skill/);
|
|
1032
1296
|
assert.doesNotMatch(result.systemPrompt, /## Loaded Workflow Skill/);
|
|
1033
1297
|
assert.doesNotMatch(result.systemPrompt, /## Loaded Decision Gate Skill/);
|
|
1034
|
-
assert.doesNotMatch(result.systemPrompt,
|
|
1298
|
+
assert.doesNotMatch(result.systemPrompt, /Output Router|freeflow_(search|run|batch)/i);
|
|
1035
1299
|
assert.equal(ctx.statuses.at(-1).value, "freeflow: idle");
|
|
1036
|
-
assert.ok(activeToolNames().includes("
|
|
1037
|
-
assert.ok(!activeToolNames().includes("freeflow_search"));
|
|
1300
|
+
assert.ok(!activeToolNames().includes("freeflow_context"));
|
|
1038
1301
|
|
|
1039
1302
|
const freeflowCommand = commands.find((command) => command.name === "freeflow");
|
|
1040
1303
|
assert.ok(freeflowCommand);
|
|
@@ -1310,16 +1573,14 @@ test("Pi statusline reports only effective Freeflow runtime state", async () =>
|
|
|
1310
1573
|
await statusFor({
|
|
1311
1574
|
defaultMode: "workflow",
|
|
1312
1575
|
skills: { enabled: false },
|
|
1313
|
-
outputRouter: { enabled: true },
|
|
1314
1576
|
}),
|
|
1315
|
-
"freeflow: interaction
|
|
1577
|
+
"freeflow: interaction",
|
|
1316
1578
|
);
|
|
1317
1579
|
assert.equal(
|
|
1318
1580
|
await statusFor({
|
|
1319
1581
|
defaultMode: "workflow",
|
|
1320
|
-
outputRouter: { enabled: true },
|
|
1321
1582
|
}),
|
|
1322
|
-
"freeflow: interaction · workflow
|
|
1583
|
+
"freeflow: interaction · workflow",
|
|
1323
1584
|
);
|
|
1324
1585
|
|
|
1325
1586
|
const masterOverrideCtx = context(cwd);
|
|
@@ -1331,22 +1592,21 @@ test("Pi statusline reports only effective Freeflow runtime state", async () =>
|
|
|
1331
1592
|
const interactionOverrideCtx = context(cwd);
|
|
1332
1593
|
await setSessionCoreOverride("interactionContract", false, interactionOverrideCtx, pi);
|
|
1333
1594
|
const interactionRuntime = await beforeAgentStart({ systemPrompt: "base prompt" }, interactionOverrideCtx);
|
|
1334
|
-
assert.
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
);
|
|
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");
|
|
1339
1599
|
await resetSessionOverrides(interactionOverrideCtx, pi);
|
|
1340
1600
|
|
|
1341
1601
|
const skillsOverrideCtx = context(cwd);
|
|
1342
1602
|
await setSessionCoreOverride("skillsEnabled", false, skillsOverrideCtx, pi);
|
|
1343
1603
|
await beforeAgentStart({ systemPrompt: "base prompt" }, skillsOverrideCtx);
|
|
1344
|
-
assert.equal(skillsOverrideCtx.statuses.at(-1).value, "freeflow: interaction · skills off (session)
|
|
1604
|
+
assert.equal(skillsOverrideCtx.statuses.at(-1).value, "freeflow: interaction · skills off (session)");
|
|
1345
1605
|
await resetSessionOverrides(skillsOverrideCtx, pi);
|
|
1346
1606
|
|
|
1347
1607
|
const modeCtx = context(cwd);
|
|
1348
1608
|
await freeflowCommand.definition.handler("mode conversation", modeCtx);
|
|
1349
|
-
assert.equal(modeCtx.statuses.at(-1).value, "freeflow: interaction · conversation (session)
|
|
1609
|
+
assert.equal(modeCtx.statuses.at(-1).value, "freeflow: interaction · conversation (session)");
|
|
1350
1610
|
await freeflowCommand.definition.handler("mode reset", context(cwd));
|
|
1351
1611
|
} finally {
|
|
1352
1612
|
await rm(cwd, { recursive: true, force: true });
|
|
@@ -1363,7 +1623,6 @@ test("Pi /freeflow command toggles master switch and blocks inactive settings ro
|
|
|
1363
1623
|
{
|
|
1364
1624
|
enabled: false,
|
|
1365
1625
|
defaultMode: "workflow",
|
|
1366
|
-
outputRouter: { enabled: true },
|
|
1367
1626
|
},
|
|
1368
1627
|
null,
|
|
1369
1628
|
2,
|
|
@@ -1391,12 +1650,10 @@ test("Pi /freeflow command toggles master switch and blocks inactive settings ro
|
|
|
1391
1650
|
const rootText = renderText(component);
|
|
1392
1651
|
assert.match(rootText, /^─+/);
|
|
1393
1652
|
assert.match(rootText, /Freeflow Settings/);
|
|
1394
|
-
assert.
|
|
1395
|
-
assert.match(rootText, /enabled \(22\) \(repository\) · inactive/);
|
|
1653
|
+
assert.doesNotMatch(rootText, /Output Router|Native safety net/);
|
|
1396
1654
|
assert.match(rootText, /\[dim\]Interaction Contract/);
|
|
1397
1655
|
assert.match(rootText, /\[dim\]Skills/);
|
|
1398
|
-
assert.match(rootText,
|
|
1399
|
-
assert.doesNotMatch(rootText, /Native safety net/);
|
|
1656
|
+
assert.match(rootText, /Freeflow Context/);
|
|
1400
1657
|
component.handleInput("\u001b[B"); // Interaction Contract row is inactive while Freeflow is off.
|
|
1401
1658
|
component.handleInput("\r");
|
|
1402
1659
|
component.handleInput("\u001b");
|
|
@@ -1408,7 +1665,6 @@ test("Pi /freeflow command toggles master switch and blocks inactive settings ro
|
|
|
1408
1665
|
assert.deepEqual(afterInactiveEdit, {
|
|
1409
1666
|
enabled: false,
|
|
1410
1667
|
defaultMode: "workflow",
|
|
1411
|
-
outputRouter: { enabled: true },
|
|
1412
1668
|
});
|
|
1413
1669
|
assert.equal(settingsCtx.reloads.length, 0);
|
|
1414
1670
|
|
|
@@ -1417,13 +1673,33 @@ test("Pi /freeflow command toggles master switch and blocks inactive settings ro
|
|
|
1417
1673
|
const afterEnable = JSON.parse(await readFile(join(cwd, ".freeflow/config.json"), "utf8"));
|
|
1418
1674
|
assert.equal(afterEnable.enabled, undefined);
|
|
1419
1675
|
assert.equal(afterEnable.defaultMode, "workflow");
|
|
1420
|
-
assert.equal(afterEnable.outputRouter
|
|
1676
|
+
assert.equal(afterEnable.outputRouter, undefined);
|
|
1421
1677
|
assert.equal(enableCtx.reloads.length, 1);
|
|
1422
1678
|
} finally {
|
|
1423
1679
|
await rm(cwd, { recursive: true, force: true });
|
|
1424
1680
|
}
|
|
1425
1681
|
});
|
|
1426
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
|
+
|
|
1427
1703
|
test("Pi /freeflow disable applies live gates before reload completes", async () => {
|
|
1428
1704
|
const cwd = await mkdtemp(join(tmpdir(), "freeflow-pi-disable-live-gates-"));
|
|
1429
1705
|
try {
|
|
@@ -1433,7 +1709,6 @@ test("Pi /freeflow disable applies live gates before reload completes", async ()
|
|
|
1433
1709
|
JSON.stringify(
|
|
1434
1710
|
{
|
|
1435
1711
|
defaultMode: "workflow",
|
|
1436
|
-
outputRouter: { enabled: true },
|
|
1437
1712
|
},
|
|
1438
1713
|
null,
|
|
1439
1714
|
2,
|
|
@@ -1441,19 +1716,14 @@ test("Pi /freeflow disable applies live gates before reload completes", async ()
|
|
|
1441
1716
|
"utf8",
|
|
1442
1717
|
);
|
|
1443
1718
|
|
|
1444
|
-
const { commands
|
|
1719
|
+
const { commands } = loadExtension();
|
|
1445
1720
|
const freeflowCommand = commands.find((command) => command.name === "freeflow");
|
|
1446
1721
|
assert.ok(freeflowCommand);
|
|
1447
|
-
assert.ok(activeToolNames().includes("freeflow_search"));
|
|
1448
|
-
|
|
1449
1722
|
const ctx = context(cwd);
|
|
1450
1723
|
await freeflowCommand.definition.handler("disable", ctx);
|
|
1451
1724
|
|
|
1452
1725
|
assert.equal(ctx.reloads.length, 1);
|
|
1453
|
-
assert.
|
|
1454
|
-
assert.ok(!activeToolNames().includes("freeflow_search"));
|
|
1455
|
-
assert.ok(!activeToolNames().includes("freeflow_run"));
|
|
1456
|
-
assert.ok(!activeToolNames().includes("freeflow_batch"));
|
|
1726
|
+
assert.equal(ctx.statuses.at(-1).value, "freeflow: off");
|
|
1457
1727
|
} finally {
|
|
1458
1728
|
await rm(cwd, { recursive: true, force: true });
|
|
1459
1729
|
}
|
|
@@ -1481,6 +1751,7 @@ test("Pi /freeflow settings marks default mode dormant when Skills are disabled"
|
|
|
1481
1751
|
});
|
|
1482
1752
|
const rootText = renderText(component);
|
|
1483
1753
|
assert.match(rootText, /Skills\s+disabled \(repository\)/);
|
|
1754
|
+
assert.match(rootText, /Cognitive Routing\s+disabled \(5\) disabled · inactive/);
|
|
1484
1755
|
assert.match(rootText, /Session mode\s+default \(workflow · repository · inactive\)/);
|
|
1485
1756
|
assert.match(rootText, /Default mode\s+workflow \(repository · inactive\)/);
|
|
1486
1757
|
|
|
@@ -1552,7 +1823,7 @@ test("Pi /freeflow settings toggles the interaction contract independently", asy
|
|
|
1552
1823
|
context(cwd),
|
|
1553
1824
|
);
|
|
1554
1825
|
assert.doesNotMatch(runtime.systemPrompt, /# Freeflow Interaction Contract/);
|
|
1555
|
-
assert.equal(runtime.message
|
|
1826
|
+
assert.equal(runtime.message, undefined);
|
|
1556
1827
|
const enableCtx = context(cwd);
|
|
1557
1828
|
enableCtx.ui.custom = async (factory) => {
|
|
1558
1829
|
let result;
|
|
@@ -1600,6 +1871,7 @@ test("Pi session settings override Freeflow without changing config", async () =
|
|
|
1600
1871
|
assert.match(rootText, /Interaction Contract\s+enabled \(builtin\)/);
|
|
1601
1872
|
assert.match(rootText, /Skills\s+enabled \(builtin\)/);
|
|
1602
1873
|
assert.match(rootText, /Mode\s+default \(workflow · repository\)/);
|
|
1874
|
+
assert.match(rootText, /Cognitive Routing\s+auto unavailable · inactive/);
|
|
1603
1875
|
assert.match(rootText, /Reset session overrides\s+available/);
|
|
1604
1876
|
|
|
1605
1877
|
component.handleInput("\r");
|
|
@@ -1615,7 +1887,7 @@ test("Pi session settings override Freeflow without changing config", async () =
|
|
|
1615
1887
|
};
|
|
1616
1888
|
|
|
1617
1889
|
await freeflowCommand.definition.handler("settings session", settingsCtx);
|
|
1618
|
-
const capabilityState = await readCapabilityState(cwd);
|
|
1890
|
+
const capabilityState = await readCapabilityState(cwd, undefined, PIFLOW_HOST);
|
|
1619
1891
|
assert.equal(capabilityState.enabled, false);
|
|
1620
1892
|
assert.equal(capabilityState.configSources.enabled, "session");
|
|
1621
1893
|
assert.equal(settingsCtx.reloads.length, 1);
|
|
@@ -1635,7 +1907,7 @@ test("Pi session settings override Freeflow without changing config", async () =
|
|
|
1635
1907
|
const rootText = renderText(component);
|
|
1636
1908
|
assert.match(rootText, /Freeflow\s+disabled \(session\)/);
|
|
1637
1909
|
assert.match(rootText, /Interaction Contract\s+enabled \(builtin\) · inactive/);
|
|
1638
|
-
for (let index = 0; index <
|
|
1910
|
+
for (let index = 0; index < 5; index += 1) component.handleInput("\u001b[B");
|
|
1639
1911
|
component.handleInput("\r");
|
|
1640
1912
|
assert.match(renderText(component), /Reset all session overrides/);
|
|
1641
1913
|
component.handleInput("\r");
|
|
@@ -1643,7 +1915,7 @@ test("Pi session settings override Freeflow without changing config", async () =
|
|
|
1643
1915
|
return result;
|
|
1644
1916
|
};
|
|
1645
1917
|
await freeflowCommand.definition.handler("settings session", resetCtx);
|
|
1646
|
-
assert.equal((await readCapabilityState(cwd)).enabled, true);
|
|
1918
|
+
assert.equal((await readCapabilityState(cwd, undefined, PIFLOW_HOST)).enabled, true);
|
|
1647
1919
|
assert.equal(resetCtx.reloads.length, 1);
|
|
1648
1920
|
assert.deepEqual(entries.at(-1), {
|
|
1649
1921
|
customType: "freeflow-session-overrides",
|
|
@@ -1742,8 +2014,7 @@ test("Pi settings do not report save or reload success after a mixed write failu
|
|
|
1742
2014
|
component.handleInput("\r"); // Session mode succeeds.
|
|
1743
2015
|
await component.waitForWrites();
|
|
1744
2016
|
|
|
1745
|
-
component.handleInput("\u001b[
|
|
1746
|
-
component.handleInput("\u001b[A");
|
|
2017
|
+
component.handleInput("\u001b[B");
|
|
1747
2018
|
component.handleInput("\r");
|
|
1748
2019
|
component.handleInput("\u001b[B");
|
|
1749
2020
|
component.handleInput("\u001b[B");
|
|
@@ -1822,347 +2093,177 @@ test("Pi /freeflow settings repo edits shared defaults and shows local effective
|
|
|
1822
2093
|
}
|
|
1823
2094
|
});
|
|
1824
2095
|
|
|
1825
|
-
test("
|
|
1826
|
-
const cwd = await mkdtemp(join(tmpdir(), "freeflow-
|
|
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-"));
|
|
1827
2098
|
try {
|
|
1828
2099
|
await mkdir(join(cwd, ".freeflow"));
|
|
1829
|
-
await writeFile(
|
|
1830
|
-
join(cwd, ".freeflow/config.json"),
|
|
1831
|
-
JSON.stringify({ defaultMode: "workflow", outputRouter: { enabled: true } }, null, 2),
|
|
1832
|
-
"utf8",
|
|
1833
|
-
);
|
|
1834
|
-
|
|
2100
|
+
await writeFile(join(cwd, ".freeflow/config.json"), JSON.stringify({ defaultMode: "workflow" }), "utf8");
|
|
1835
2101
|
const { commands } = loadExtension();
|
|
1836
2102
|
const freeflowCommand = commands.find((command) => command.name === "freeflow");
|
|
1837
2103
|
assert.ok(freeflowCommand);
|
|
1838
|
-
|
|
1839
2104
|
const settingsCtx = context(cwd);
|
|
2105
|
+
settingsCtx.isIdle = () => true;
|
|
2106
|
+
settingsCtx.modelRegistry = cognitiveRoutingModelRegistry();
|
|
1840
2107
|
settingsCtx.ui.custom = async (factory) => {
|
|
1841
2108
|
let result;
|
|
1842
2109
|
const component = factory({ requestRender() {} }, testTheme, {}, (value) => {
|
|
1843
2110
|
result = value;
|
|
1844
2111
|
});
|
|
1845
2112
|
const rootText = renderText(component);
|
|
1846
|
-
assert.match(rootText,
|
|
1847
|
-
assert.
|
|
1848
|
-
|
|
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/);
|
|
1849
2120
|
|
|
1850
|
-
|
|
1851
|
-
component.handleInput("\u001b[B");
|
|
1852
|
-
}
|
|
2121
|
+
component.handleInput("\u001b[B");
|
|
1853
2122
|
component.handleInput("\r");
|
|
1854
|
-
|
|
1855
|
-
|
|
1856
|
-
|
|
1857
|
-
component.handleInput("\
|
|
1858
|
-
component.
|
|
2123
|
+
component.handleInput("\u001b[B");
|
|
2124
|
+
component.handleInput("\r");
|
|
2125
|
+
component.handleInput("\r");
|
|
2126
|
+
component.handleInput("\r");
|
|
2127
|
+
await component.waitForWrites();
|
|
1859
2128
|
return result;
|
|
1860
2129
|
};
|
|
1861
2130
|
|
|
1862
2131
|
await freeflowCommand.definition.handler("settings", settingsCtx);
|
|
1863
|
-
|
|
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);
|
|
1864
2139
|
} finally {
|
|
1865
2140
|
await rm(cwd, { recursive: true, force: true });
|
|
1866
2141
|
}
|
|
1867
2142
|
});
|
|
1868
2143
|
|
|
1869
|
-
test("
|
|
1870
|
-
const cwd = await mkdtemp(join(tmpdir(), "freeflow-
|
|
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-"));
|
|
1871
2146
|
try {
|
|
1872
2147
|
await mkdir(join(cwd, ".freeflow"));
|
|
1873
|
-
await writeFile(
|
|
1874
|
-
|
|
1875
|
-
|
|
1876
|
-
|
|
1877
|
-
|
|
1878
|
-
|
|
1879
|
-
|
|
1880
|
-
|
|
1881
|
-
|
|
1882
|
-
|
|
1883
|
-
|
|
1884
|
-
|
|
1885
|
-
|
|
1886
|
-
|
|
1887
|
-
|
|
1888
|
-
|
|
1889
|
-
assert.
|
|
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
|
+
};
|
|
1890
2186
|
|
|
1891
|
-
|
|
1892
|
-
|
|
1893
|
-
assert.
|
|
1894
|
-
assert.
|
|
1895
|
-
assert.doesNotMatch(result.systemPrompt, /freeflow_search action=transform/);
|
|
1896
|
-
assert.doesNotMatch(result.systemPrompt, /Native tools stay direct/);
|
|
1897
|
-
assert.doesNotMatch(result.systemPrompt, /## Loaded Workflow Map/);
|
|
1898
|
-
assert.doesNotMatch(result.systemPrompt, /## Loaded Output Router Safety Policy/);
|
|
1899
|
-
assert.doesNotMatch(result.systemPrompt, /Do not silently summarize or compress exactness-sensitive output/);
|
|
1900
|
-
assert.doesNotMatch(result.systemPrompt, /large native read\/bash outputs may be vaulted/);
|
|
1901
|
-
assert.doesNotMatch(result.systemPrompt, /Output-router config note/);
|
|
1902
|
-
assert.ok(!activeToolNames().includes("freeflow_search"));
|
|
1903
|
-
assert.ok(!activeToolNames().includes("freeflow_run"));
|
|
1904
|
-
assert.ok(!activeToolNames().includes("freeflow_batch"));
|
|
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);
|
|
1905
2191
|
} finally {
|
|
1906
2192
|
await rm(cwd, { recursive: true, force: true });
|
|
1907
2193
|
}
|
|
1908
2194
|
});
|
|
1909
2195
|
|
|
1910
|
-
test("Pi
|
|
1911
|
-
const cwd = await mkdtemp(join(tmpdir(), "freeflow-pi-
|
|
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-"));
|
|
1912
2198
|
try {
|
|
1913
2199
|
await mkdir(join(cwd, ".freeflow"));
|
|
1914
|
-
await writeFile(
|
|
1915
|
-
|
|
1916
|
-
|
|
1917
|
-
|
|
1918
|
-
|
|
1919
|
-
|
|
1920
|
-
const result = await beforeAgentStart({ systemPrompt: "base prompt" }, context(cwd));
|
|
2200
|
+
await writeFile(
|
|
2201
|
+
join(cwd, ".freeflow/config.json"),
|
|
2202
|
+
JSON.stringify({ contextVirtualization: true }, null, 2),
|
|
2203
|
+
"utf8",
|
|
2204
|
+
);
|
|
1921
2205
|
|
|
1922
|
-
|
|
1923
|
-
|
|
1924
|
-
assert.
|
|
1925
|
-
|
|
1926
|
-
|
|
1927
|
-
|
|
1928
|
-
|
|
1929
|
-
|
|
1930
|
-
|
|
1931
|
-
|
|
1932
|
-
assert.doesNotMatch(result.systemPrompt, /## Loaded Decision Gate Skill/);
|
|
1933
|
-
assert.doesNotMatch(result.systemPrompt, /## Discovery-light/);
|
|
1934
|
-
} finally {
|
|
1935
|
-
await rm(cwd, { recursive: true, force: true });
|
|
1936
|
-
}
|
|
1937
|
-
});
|
|
1938
|
-
|
|
1939
|
-
test("Pi loads the full Workflow skill as one persistent first-turn message", async () => {
|
|
1940
|
-
const cwd = await mkdtemp(join(tmpdir(), "freeflow-pi-workflow-bootstrap-"));
|
|
1941
|
-
try {
|
|
1942
|
-
const workflowSource = await readFile(new URL("../../../skills/workflow/SKILL.md", import.meta.url), "utf8");
|
|
1943
|
-
const workflowLines = workflowSource.split(/\r?\n/);
|
|
1944
|
-
const workflowOwnerLine = workflowLines.find((line) => line.startsWith("The active agent owns "));
|
|
1945
|
-
const workflowSelfReviewLine = workflowLines.find((line) => line.startsWith("Self-review is silent "));
|
|
1946
|
-
assert.ok(workflowOwnerLine, "canonical Workflow owner sentinel missing");
|
|
1947
|
-
assert.ok(workflowSelfReviewLine, "canonical Workflow self-review sentinel missing");
|
|
1948
|
-
|
|
1949
|
-
await mkdir(join(cwd, ".freeflow"));
|
|
1950
|
-
await writeFile(join(cwd, ".freeflow/config.json"), JSON.stringify({ defaultMode: "workflow" }, null, 2), "utf8");
|
|
1951
|
-
|
|
1952
|
-
const { handlers } = loadExtension();
|
|
1953
|
-
const beforeAgentStart = handlers.get("before_agent_start");
|
|
1954
|
-
assert.ok(beforeAgentStart);
|
|
1955
|
-
|
|
1956
|
-
const sessionEntries = [];
|
|
1957
|
-
const ctx = context(cwd, sessionEntries);
|
|
1958
|
-
const first = await beforeAgentStart({ prompt: "hi", systemPrompt: "base prompt" }, ctx);
|
|
1959
|
-
|
|
1960
|
-
assert.equal(first.message.customType, "freeflow-workflow-bootstrap");
|
|
1961
|
-
assert.equal(first.message.display, false);
|
|
1962
|
-
assert.match(first.message.content, /^# Freeflow Workflow Bootstrap/);
|
|
1963
|
-
assert.match(first.message.content, /# Workflow/);
|
|
1964
|
-
assert.ok(first.message.content.includes(workflowOwnerLine));
|
|
1965
|
-
assert.ok(first.message.content.includes(workflowSelfReviewLine));
|
|
1966
|
-
assert.match(first.message.content, /evidence/i);
|
|
1967
|
-
assert.doesNotMatch(first.systemPrompt, /# Freeflow Workflow Bootstrap/);
|
|
1968
|
-
|
|
1969
|
-
sessionEntries.push({
|
|
1970
|
-
type: "custom_message",
|
|
1971
|
-
id: "workflow-bootstrap-entry",
|
|
1972
|
-
parentId: null,
|
|
1973
|
-
timestamp: new Date().toISOString(),
|
|
1974
|
-
customType: first.message.customType,
|
|
1975
|
-
content: first.message.content,
|
|
1976
|
-
display: first.message.display,
|
|
1977
|
-
details: first.message.details,
|
|
1978
|
-
});
|
|
1979
|
-
const later = await beforeAgentStart({ prompt: "implement the feature", systemPrompt: "base prompt" }, ctx);
|
|
1980
|
-
|
|
1981
|
-
assert.equal(later.message, undefined);
|
|
1982
|
-
assert.match(later.systemPrompt, /# Freeflow Interaction Contract/);
|
|
1983
|
-
assert.doesNotMatch(later.systemPrompt, /# Freeflow Runtime Kernel/);
|
|
1984
|
-
|
|
1985
|
-
const compactedContext = context(cwd, sessionEntries, []);
|
|
1986
|
-
const afterCompaction = await beforeAgentStart(
|
|
1987
|
-
{ prompt: "continue", systemPrompt: "base prompt" },
|
|
1988
|
-
compactedContext,
|
|
1989
|
-
);
|
|
1990
|
-
assert.equal(afterCompaction.message.customType, "freeflow-workflow-bootstrap");
|
|
1991
|
-
} finally {
|
|
1992
|
-
await rm(cwd, { recursive: true, force: true });
|
|
1993
|
-
}
|
|
1994
|
-
});
|
|
1995
|
-
|
|
1996
|
-
test("Pi suppresses persisted Workflow context while Skills are disabled", async () => {
|
|
1997
|
-
const cwd = await mkdtemp(join(tmpdir(), "freeflow-pi-workflow-bootstrap-disabled-"));
|
|
1998
|
-
try {
|
|
1999
|
-
await mkdir(join(cwd, ".freeflow"));
|
|
2000
|
-
const configPath = join(cwd, ".freeflow/config.json");
|
|
2001
|
-
await writeFile(
|
|
2002
|
-
configPath,
|
|
2003
|
-
JSON.stringify({ defaultMode: "workflow", skills: { enabled: false } }, null, 2),
|
|
2004
|
-
"utf8",
|
|
2005
|
-
);
|
|
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/);
|
|
2006
2216
|
|
|
2007
|
-
|
|
2008
|
-
|
|
2009
|
-
|
|
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");
|
|
2010
2225
|
|
|
2011
|
-
|
|
2012
|
-
|
|
2013
|
-
|
|
2014
|
-
content: "# Freeflow Workflow Bootstrap",
|
|
2015
|
-
display: false,
|
|
2016
|
-
timestamp: Date.now(),
|
|
2017
|
-
};
|
|
2018
|
-
const userMessage = {
|
|
2019
|
-
role: "user",
|
|
2020
|
-
content: "hello",
|
|
2021
|
-
timestamp: Date.now(),
|
|
2226
|
+
assert.match(renderText(component), /Freeflow Context\s+disabled \(2\) 0\/2 enabled/);
|
|
2227
|
+
component.handleInput("\u001b");
|
|
2228
|
+
return result;
|
|
2022
2229
|
};
|
|
2023
|
-
const disabled = await contextHandler({ messages: [workflowMessage, userMessage] }, context(cwd));
|
|
2024
|
-
assert.deepEqual(disabled.messages, [userMessage]);
|
|
2025
|
-
|
|
2026
|
-
await writeFile(configPath, JSON.stringify({ defaultMode: "workflow" }, null, 2), "utf8");
|
|
2027
|
-
const enabled = await contextHandler({ messages: [workflowMessage, userMessage] }, context(cwd));
|
|
2028
|
-
assert.equal(enabled, undefined);
|
|
2029
|
-
} finally {
|
|
2030
|
-
await rm(cwd, { recursive: true, force: true });
|
|
2031
|
-
}
|
|
2032
|
-
});
|
|
2033
|
-
|
|
2034
|
-
test("Pi before_agent_start injects the Freeflow interaction contract on every turn", async () => {
|
|
2035
|
-
const cwd = await mkdtemp(join(tmpdir(), "freeflow-pi-core-context-"));
|
|
2036
|
-
try {
|
|
2037
|
-
await mkdir(join(cwd, ".freeflow"));
|
|
2038
|
-
await writeFile(join(cwd, ".freeflow/config.json"), JSON.stringify({ defaultMode: "workflow" }, null, 2), "utf8");
|
|
2039
|
-
|
|
2040
|
-
const { handlers } = loadExtension();
|
|
2041
|
-
const beforeAgentStart = handlers.get("before_agent_start");
|
|
2042
|
-
assert.ok(beforeAgentStart);
|
|
2043
|
-
|
|
2044
|
-
const first = await beforeAgentStart({ systemPrompt: "base prompt" }, context(cwd));
|
|
2045
|
-
const second = await beforeAgentStart({ systemPrompt: "base prompt" }, context(cwd));
|
|
2046
|
-
|
|
2047
|
-
for (const result of [first, second]) {
|
|
2048
|
-
assert.match(result.systemPrompt, /# Freeflow Runtime Context/);
|
|
2049
|
-
assert.match(result.systemPrompt, /# Freeflow Interaction Contract/);
|
|
2050
|
-
assert.doesNotMatch(result.systemPrompt, /# Freeflow Runtime Kernel/);
|
|
2051
|
-
assert.match(result.systemPrompt, /Answer questions without inferring action/);
|
|
2052
|
-
assert.doesNotMatch(result.systemPrompt, /## Freeflow Runtime Priority/);
|
|
2053
|
-
assert.doesNotMatch(result.systemPrompt, /## Loaded Mode Contract Skill/);
|
|
2054
|
-
assert.doesNotMatch(result.systemPrompt, /## Loaded Workflow Skill/);
|
|
2055
|
-
assert.doesNotMatch(result.systemPrompt, /## Loaded Decision Gate Skill/);
|
|
2056
|
-
assert.doesNotMatch(result.systemPrompt, /## Discovery-light/);
|
|
2057
|
-
assert.match(result.systemPrompt, /Output router: disabled/);
|
|
2058
|
-
assert.doesNotMatch(result.systemPrompt, /## Loaded Output Router Skill/);
|
|
2059
|
-
assert.doesNotMatch(result.systemPrompt, /freeflow_search action=transform/);
|
|
2060
|
-
assert.doesNotMatch(result.systemPrompt, /## Freeflow Output Router Reminder/);
|
|
2061
|
-
assert.doesNotMatch(result.systemPrompt, /## Loaded Workflow Map/);
|
|
2062
|
-
assert.doesNotMatch(result.systemPrompt, /## Loaded Output Router Safety Policy/);
|
|
2063
|
-
}
|
|
2064
|
-
} finally {
|
|
2065
|
-
await rm(cwd, { recursive: true, force: true });
|
|
2066
|
-
}
|
|
2067
|
-
});
|
|
2068
|
-
|
|
2069
|
-
test("Pi session_start and session_compact keep the interaction contract on later turns", async () => {
|
|
2070
|
-
const cwd = await mkdtemp(join(tmpdir(), "freeflow-pi-session-cache-"));
|
|
2071
|
-
try {
|
|
2072
|
-
await mkdir(join(cwd, ".freeflow"));
|
|
2073
|
-
await writeFile(join(cwd, ".freeflow/config.json"), JSON.stringify({ defaultMode: "workflow" }, null, 2), "utf8");
|
|
2074
|
-
|
|
2075
|
-
const { handlers } = loadExtension();
|
|
2076
|
-
const beforeAgentStart = handlers.get("before_agent_start");
|
|
2077
|
-
const sessionStart = handlers.get("session_start");
|
|
2078
|
-
const sessionCompact = handlers.get("session_compact");
|
|
2079
|
-
assert.ok(beforeAgentStart);
|
|
2080
|
-
assert.ok(sessionStart);
|
|
2081
|
-
assert.ok(sessionCompact);
|
|
2082
|
-
|
|
2083
|
-
await beforeAgentStart({ systemPrompt: "base prompt" }, context(cwd));
|
|
2084
|
-
const afterFirst = await beforeAgentStart({ systemPrompt: "base prompt" }, context(cwd));
|
|
2085
|
-
assert.match(afterFirst.systemPrompt, /# Freeflow Interaction Contract/);
|
|
2086
|
-
|
|
2087
|
-
await sessionCompact({ reason: "manual" }, context(cwd));
|
|
2088
|
-
const afterCompact = await beforeAgentStart({ systemPrompt: "base prompt" }, context(cwd));
|
|
2089
|
-
assert.match(afterCompact.systemPrompt, /# Freeflow Interaction Contract/);
|
|
2090
|
-
assert.doesNotMatch(afterCompact.systemPrompt, /## Loaded Workflow Skill/);
|
|
2091
|
-
assert.doesNotMatch(afterCompact.systemPrompt, /## Loaded Output Router Skill/);
|
|
2092
|
-
|
|
2093
|
-
await sessionStart({ reason: "resume" }, context(cwd));
|
|
2094
|
-
const afterResume = await beforeAgentStart({ systemPrompt: "base prompt" }, context(cwd));
|
|
2095
|
-
assert.match(afterResume.systemPrompt, /# Freeflow Interaction Contract/);
|
|
2096
|
-
assert.doesNotMatch(afterResume.systemPrompt, /## Loaded Workflow Skill/);
|
|
2097
|
-
assert.doesNotMatch(afterResume.systemPrompt, /## Loaded Output Router Skill/);
|
|
2098
|
-
} finally {
|
|
2099
|
-
await rm(cwd, { recursive: true, force: true });
|
|
2100
|
-
}
|
|
2101
|
-
});
|
|
2102
|
-
|
|
2103
|
-
test("Pi capability config disables output-router context, active tools, and execution", async () => {
|
|
2104
|
-
const cwd = await mkdtemp(join(tmpdir(), "freeflow-pi-output-router-disabled-"));
|
|
2105
|
-
try {
|
|
2106
|
-
await mkdir(join(cwd, ".freeflow"));
|
|
2107
|
-
await writeFile(
|
|
2108
|
-
join(cwd, ".freeflow/config.json"),
|
|
2109
|
-
JSON.stringify({ defaultMode: "workflow", outputRouter: { enabled: false } }, null, 2),
|
|
2110
|
-
"utf8",
|
|
2111
|
-
);
|
|
2112
|
-
|
|
2113
|
-
const { handlers, tools, activeToolNames } = loadExtension();
|
|
2114
|
-
const beforeAgentStart = handlers.get("before_agent_start");
|
|
2115
|
-
assert.ok(beforeAgentStart);
|
|
2116
|
-
|
|
2117
|
-
const result = await beforeAgentStart({ systemPrompt: "base prompt" }, context(cwd));
|
|
2118
|
-
assert.match(result.systemPrompt, /Output router: disabled/);
|
|
2119
|
-
assert.doesNotMatch(result.systemPrompt, /## Loaded Output Router Skill/);
|
|
2120
|
-
assert.ok(!activeToolNames().includes("freeflow_search"));
|
|
2121
|
-
assert.ok(!activeToolNames().includes("freeflow_run"));
|
|
2122
|
-
assert.ok(!activeToolNames().includes("freeflow_batch"));
|
|
2123
|
-
|
|
2124
|
-
const searchTool = tools.find((tool) => tool.name === "freeflow_search");
|
|
2125
|
-
assert.ok(searchTool);
|
|
2126
|
-
const disabled = await searchTool.execute(
|
|
2127
|
-
"search-disabled",
|
|
2128
|
-
{ action: "locate", query: "x" },
|
|
2129
|
-
undefined,
|
|
2130
|
-
undefined,
|
|
2131
|
-
context(cwd),
|
|
2132
|
-
);
|
|
2133
|
-
assert.match(disabled.content[0].text, /freeflow_search\|disabled_by_config/);
|
|
2134
2230
|
|
|
2135
|
-
|
|
2136
|
-
const
|
|
2137
|
-
assert.equal(
|
|
2138
|
-
assert.
|
|
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);
|
|
2139
2235
|
} finally {
|
|
2140
2236
|
await rm(cwd, { recursive: true, force: true });
|
|
2141
2237
|
}
|
|
2142
2238
|
});
|
|
2143
2239
|
|
|
2144
|
-
|
|
2145
|
-
const
|
|
2146
|
-
|
|
2147
|
-
|
|
2148
|
-
|
|
2149
|
-
|
|
2150
|
-
|
|
2151
|
-
|
|
2152
|
-
|
|
2153
|
-
|
|
2154
|
-
|
|
2155
|
-
|
|
2156
|
-
|
|
2157
|
-
|
|
2158
|
-
|
|
2159
|
-
|
|
2160
|
-
|
|
2161
|
-
|
|
2162
|
-
}
|
|
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
|
+
}
|
|
2163
2264
|
|
|
2164
|
-
test("
|
|
2165
|
-
const cwd = await mkdtemp(join(tmpdir(), "freeflow-pi-
|
|
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-"));
|
|
2166
2267
|
try {
|
|
2167
2268
|
await mkdir(join(cwd, ".freeflow"));
|
|
2168
2269
|
await writeFile(
|
|
@@ -2170,7 +2271,13 @@ test("Pi output-router settings UI toggles multiple config values and reloads on
|
|
|
2170
2271
|
JSON.stringify(
|
|
2171
2272
|
{
|
|
2172
2273
|
defaultMode: "workflow",
|
|
2173
|
-
|
|
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
|
+
},
|
|
2174
2281
|
},
|
|
2175
2282
|
null,
|
|
2176
2283
|
2,
|
|
@@ -2179,46 +2286,59 @@ test("Pi output-router settings UI toggles multiple config values and reloads on
|
|
|
2179
2286
|
);
|
|
2180
2287
|
|
|
2181
2288
|
const { commands } = loadExtension();
|
|
2182
|
-
const
|
|
2183
|
-
assert.ok(
|
|
2184
|
-
|
|
2185
|
-
|
|
2186
|
-
|
|
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) => {
|
|
2187
2295
|
let result;
|
|
2188
2296
|
const component = factory({ requestRender() {} }, testTheme, {}, (value) => {
|
|
2189
2297
|
result = value;
|
|
2190
2298
|
});
|
|
2191
|
-
|
|
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");
|
|
2192
2308
|
component.handleInput("\r");
|
|
2193
|
-
component.handleInput("\u001b[A");
|
|
2194
|
-
component.handleInput("\r"); // Output Router enabled
|
|
2195
2309
|
await component.waitForWrites();
|
|
2196
|
-
|
|
2197
|
-
|
|
2198
|
-
|
|
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");
|
|
2199
2318
|
component.handleInput("\r");
|
|
2200
|
-
component.handleInput("\u001b[A");
|
|
2201
|
-
component.handleInput("\r"); // Script transform enabled
|
|
2202
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");
|
|
2203
2327
|
component.handleInput("\u001b");
|
|
2204
2328
|
return result;
|
|
2205
2329
|
};
|
|
2206
2330
|
|
|
2207
|
-
await
|
|
2208
|
-
|
|
2209
|
-
|
|
2210
|
-
assert.equal(
|
|
2211
|
-
assert.deepEqual(after.outputRouter.hints.generatedPathGlobs, ["graphify-out/**"]);
|
|
2212
|
-
assert.equal(after.outputRouter.generatedPaths, undefined);
|
|
2213
|
-
assert.equal(after.outputRouter.scriptTransform.enabled, true);
|
|
2214
|
-
assert.equal(ctx.reloads.length, 1);
|
|
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);
|
|
2215
2335
|
} finally {
|
|
2216
2336
|
await rm(cwd, { recursive: true, force: true });
|
|
2217
2337
|
}
|
|
2218
2338
|
});
|
|
2219
2339
|
|
|
2220
|
-
test("Pi
|
|
2221
|
-
const cwd = await mkdtemp(join(tmpdir(), "freeflow-pi-
|
|
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-"));
|
|
2222
2342
|
try {
|
|
2223
2343
|
await mkdir(join(cwd, ".freeflow"));
|
|
2224
2344
|
await writeFile(
|
|
@@ -2226,1727 +2346,498 @@ test("Pi output-router status summarizes master and subfeature state", async ()
|
|
|
2226
2346
|
JSON.stringify(
|
|
2227
2347
|
{
|
|
2228
2348
|
defaultMode: "workflow",
|
|
2229
|
-
|
|
2349
|
+
cognitiveRouting: {
|
|
2230
2350
|
enabled: true,
|
|
2231
|
-
|
|
2232
|
-
|
|
2233
|
-
|
|
2351
|
+
profiles: {
|
|
2352
|
+
standard: { provider: "test", model: "model-a", thinkingLevel: "low" },
|
|
2353
|
+
reasoning: { provider: "test", model: "model-b", thinkingLevel: "high" },
|
|
2354
|
+
},
|
|
2234
2355
|
},
|
|
2235
2356
|
},
|
|
2236
2357
|
null,
|
|
2237
2358
|
2,
|
|
2238
2359
|
),
|
|
2239
|
-
"utf8",
|
|
2240
2360
|
);
|
|
2241
2361
|
|
|
2242
2362
|
const { commands } = loadExtension();
|
|
2243
|
-
const
|
|
2244
|
-
assert.ok(
|
|
2245
|
-
|
|
2246
|
-
|
|
2247
|
-
|
|
2248
|
-
|
|
2249
|
-
|
|
2250
|
-
|
|
2251
|
-
|
|
2252
|
-
|
|
2253
|
-
|
|
2254
|
-
|
|
2255
|
-
|
|
2256
|
-
|
|
2257
|
-
|
|
2258
|
-
|
|
2259
|
-
|
|
2260
|
-
|
|
2261
|
-
|
|
2262
|
-
|
|
2263
|
-
|
|
2264
|
-
|
|
2265
|
-
await writeFile(configPath, configText, "utf8");
|
|
2266
|
-
|
|
2267
|
-
const { tools } = loadExtension();
|
|
2268
|
-
const statusTool = tools.find((tool) => tool.name === "freeflow_status");
|
|
2269
|
-
assert.ok(statusTool);
|
|
2270
|
-
|
|
2271
|
-
const result = await statusTool.execute("status-minimal", { action: "doctor" }, undefined, undefined, context(cwd));
|
|
2272
|
-
const report = JSON.parse(result.content[0].text);
|
|
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
|
+
};
|
|
2273
2385
|
|
|
2274
|
-
|
|
2275
|
-
|
|
2276
|
-
assert.
|
|
2277
|
-
|
|
2278
|
-
|
|
2279
|
-
|
|
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",
|
|
2280
2392
|
});
|
|
2281
|
-
assert.equal(
|
|
2282
|
-
assert.equal(
|
|
2283
|
-
assert.equal(report.effectiveConfig.outputRouter.profile, "standard");
|
|
2284
|
-
assert.equal(report.effectiveConfig.outputRouter.postToolRouting, "off");
|
|
2285
|
-
assert.equal("capture" in report.effectiveConfig, false);
|
|
2286
|
-
assert.equal("providers" in report.effectiveConfig, false);
|
|
2287
|
-
assert.equal(report.effectiveConfig.outputRouter.observedRouting.enabled, false);
|
|
2288
|
-
assert.deepEqual(report.effectiveConfig.outputRouter.observedRouting.mcp.servers, {});
|
|
2289
|
-
assert.equal(report.effectiveConfig.outputRouter.scriptTransform.enabled, false);
|
|
2290
|
-
assert.equal(report.effectiveLocalConfig.processing.unsafeUnsandboxed.enabled, false);
|
|
2291
|
-
assert.equal(report.processing.unsafeUnsandboxed.enabled, false);
|
|
2292
|
-
assert.equal(report.processing.unsafeUnsandboxed.status, "disabled");
|
|
2293
|
-
assert.equal(report.localConfigExists, false);
|
|
2294
|
-
assert.deepEqual(report.localConfigWarnings, []);
|
|
2295
|
-
assert.equal(report.scriptTransform.enabled, false);
|
|
2296
|
-
assert.equal(report.scriptTransform.executionStatus, "disabled");
|
|
2297
|
-
assert.ok(["available", "unavailable"].includes(report.scriptTransform.adapterStatus));
|
|
2298
|
-
assert.equal(report.scriptTransform.adapterContractVersion, 1);
|
|
2299
|
-
assert.deepEqual(report.scriptTransform.configuredLanguages, ["javascript", "python", "jq"]);
|
|
2300
|
-
const reportedLanguages = [
|
|
2301
|
-
...report.scriptTransform.availableLanguages,
|
|
2302
|
-
...report.scriptTransform.unavailableLanguages.map((entry) => entry.language),
|
|
2303
|
-
].sort();
|
|
2304
|
-
assert.deepEqual(reportedLanguages, ["javascript", "jq", "python"]);
|
|
2305
|
-
assert.ok(report.scriptTransform.requiredProofs.includes("network_access_denied"));
|
|
2306
|
-
assert.ok(
|
|
2307
|
-
report.scriptTransform.candidateMechanisms.some(
|
|
2308
|
-
(candidate) => candidate.id === "node-vm" && candidate.status === "rejected",
|
|
2309
|
-
),
|
|
2310
|
-
);
|
|
2311
|
-
assert.ok(
|
|
2312
|
-
report.scriptTransform.candidateMechanisms.some(
|
|
2313
|
-
(candidate) => candidate.id === "os-sandbox-adapter" && candidate.status === "candidate_unproven",
|
|
2314
|
-
),
|
|
2315
|
-
);
|
|
2316
|
-
assert.equal(report.scriptTransform.network, "off");
|
|
2317
|
-
assert.equal(report.scriptTransform.rawScriptPersistence, "disabled");
|
|
2318
|
-
assert.equal(report.observedRouting.host.name, "pi");
|
|
2319
|
-
assert.equal(report.observedRouting.host.outputReplacement, "available");
|
|
2320
|
-
assert.equal(report.vaultIndex.engine, "local-json-sidecar");
|
|
2321
|
-
assert.equal(typeof report.vaultIndex.available, "boolean");
|
|
2322
|
-
assert.equal(report.vaultIndex.degraded, false);
|
|
2323
|
-
assert.equal(report.vaultIndex.stale, false);
|
|
2324
|
-
assert.equal(report.vaultIndex.rebuildRecommended, false);
|
|
2325
|
-
assert.equal(typeof report.vaultIndex.entryCount, "number");
|
|
2326
|
-
assert.ok(report.vaultIndex.entryCount >= 0);
|
|
2327
|
-
assert.equal(report.observedRouting.unsupportedPersistenceModes.includes("redacted"), true);
|
|
2328
|
-
assert.deepEqual(report.configWarnings, []);
|
|
2329
|
-
assert.match(report.vault.root, /freeflow-router\/vault$/);
|
|
2330
|
-
assert.ok(
|
|
2331
|
-
[
|
|
2332
|
-
"writable",
|
|
2333
|
-
"missing_ancestor_writable",
|
|
2334
|
-
"missing_ancestor_unavailable",
|
|
2335
|
-
"not_directory",
|
|
2336
|
-
"not_writable",
|
|
2337
|
-
"unknown",
|
|
2338
|
-
].includes(report.vault.writability.status),
|
|
2339
|
-
);
|
|
2340
|
-
assert.equal(await readFile(configPath, "utf8"), configText);
|
|
2393
|
+
assert.equal(saved.cognitiveRouting.profiles.reasoning.thinkingLevel, "high");
|
|
2394
|
+
assert.equal(settingsCtx.reloads.length, 1);
|
|
2341
2395
|
} finally {
|
|
2342
2396
|
await rm(cwd, { recursive: true, force: true });
|
|
2343
2397
|
}
|
|
2344
2398
|
});
|
|
2345
2399
|
|
|
2346
|
-
test("Pi
|
|
2347
|
-
const cwd = await mkdtemp(join(tmpdir(), "freeflow-pi-
|
|
2400
|
+
test("Pi Cognitive Routing preset cancel leaves the previous preset unchanged", async () => {
|
|
2401
|
+
const cwd = await mkdtemp(join(tmpdir(), "freeflow-pi-cognitive-cancel-"));
|
|
2348
2402
|
try {
|
|
2349
2403
|
await mkdir(join(cwd, ".freeflow"));
|
|
2350
|
-
|
|
2351
|
-
|
|
2352
|
-
|
|
2404
|
+
const original = {
|
|
2405
|
+
defaultMode: "workflow",
|
|
2406
|
+
cognitiveRouting: {
|
|
2353
2407
|
enabled: true,
|
|
2354
|
-
|
|
2355
|
-
|
|
2356
|
-
|
|
2357
|
-
}),
|
|
2358
|
-
"utf8",
|
|
2359
|
-
);
|
|
2360
|
-
await writeFile(
|
|
2361
|
-
join(cwd, ".freeflow/local.json"),
|
|
2362
|
-
JSON.stringify({
|
|
2363
|
-
interactionContract: false,
|
|
2364
|
-
defaultMode: "strict-workflow",
|
|
2365
|
-
skills: { enabled: false },
|
|
2366
|
-
}),
|
|
2367
|
-
"utf8",
|
|
2368
|
-
);
|
|
2369
|
-
|
|
2370
|
-
const { tools } = loadExtension();
|
|
2371
|
-
const statusTool = tools.find((tool) => tool.name === "freeflow_status");
|
|
2372
|
-
assert.ok(statusTool);
|
|
2373
|
-
const result = await statusTool.execute(
|
|
2374
|
-
"status-layered-core",
|
|
2375
|
-
{ action: "status" },
|
|
2376
|
-
undefined,
|
|
2377
|
-
undefined,
|
|
2378
|
-
context(cwd),
|
|
2379
|
-
);
|
|
2380
|
-
const report = JSON.parse(result.content[0].text);
|
|
2381
|
-
|
|
2382
|
-
assert.equal(report.configValid, true);
|
|
2383
|
-
assert.equal(report.localConfigValid, true);
|
|
2384
|
-
assert.deepEqual(report.configuration.sources, {
|
|
2385
|
-
enabled: "repository",
|
|
2386
|
-
interactionContract: "local",
|
|
2387
|
-
skillsEnabled: "local",
|
|
2388
|
-
defaultMode: "local",
|
|
2389
|
-
});
|
|
2390
|
-
assert.equal(report.effectiveConfig.enabled, true);
|
|
2391
|
-
assert.equal(report.effectiveConfig.defaultMode, "strict-workflow");
|
|
2392
|
-
assert.deepEqual(report.effectiveConfig.interactionContract, {
|
|
2393
|
-
enabled: false,
|
|
2394
|
-
effective: false,
|
|
2395
|
-
});
|
|
2396
|
-
assert.deepEqual(report.effectiveConfig.skills, {
|
|
2397
|
-
enabled: false,
|
|
2398
|
-
effective: false,
|
|
2399
|
-
});
|
|
2400
|
-
assert.equal(report.mode.repositoryDefaultMode, "workflow");
|
|
2401
|
-
assert.equal(report.mode.repositoryDefaultModeSource, "repository");
|
|
2402
|
-
assert.equal(report.mode.personalDefaultMode, "strict-workflow");
|
|
2403
|
-
assert.equal(report.mode.defaultMode, "strict-workflow");
|
|
2404
|
-
assert.equal(report.mode.defaultModeSource, "local");
|
|
2405
|
-
assert.equal(report.mode.sessionMode, null);
|
|
2406
|
-
assert.equal(report.mode.active, false);
|
|
2407
|
-
assert.equal(report.mode.resolvedMode, "strict-workflow");
|
|
2408
|
-
assert.equal(report.mode.effectiveMode, null);
|
|
2409
|
-
|
|
2410
|
-
const expanded = renderText(statusTool.renderResult(result, { expanded: true }, testTheme));
|
|
2411
|
-
assert.match(expanded, /repository:\s+valid/);
|
|
2412
|
-
assert.match(expanded, /personal:\s+valid/);
|
|
2413
|
-
assert.match(expanded, /interactionContract:\s+false\s+\(local\)/);
|
|
2414
|
-
assert.match(expanded, /skills:\s+false\s+\(local\)/);
|
|
2415
|
-
assert.match(expanded, /active:\s+false/);
|
|
2416
|
-
assert.match(expanded, /effective:\s+none/);
|
|
2417
|
-
assert.match(expanded, /resolved:\s+strict-workflow/);
|
|
2418
|
-
assert.match(expanded, /repository default:\s+workflow\s+\(repository\)/);
|
|
2419
|
-
assert.match(expanded, /personal default override:\s+strict-workflow/);
|
|
2420
|
-
assert.match(expanded, /configured default:\s+strict-workflow\s+\(local\)/);
|
|
2421
|
-
} finally {
|
|
2422
|
-
await rm(cwd, { recursive: true, force: true });
|
|
2423
|
-
}
|
|
2424
|
-
});
|
|
2425
|
-
|
|
2426
|
-
test("Pi freeflow_status fails closed and explains invalid local core config", async () => {
|
|
2427
|
-
const cwd = await mkdtemp(join(tmpdir(), "freeflow-pi-status-invalid-local-"));
|
|
2428
|
-
try {
|
|
2429
|
-
await mkdir(join(cwd, ".freeflow"));
|
|
2430
|
-
await writeFile(join(cwd, ".freeflow/config.json"), JSON.stringify({ defaultMode: "workflow" }), "utf8");
|
|
2431
|
-
await writeFile(join(cwd, ".freeflow/local.json"), JSON.stringify({ skills: { enabled: "no" } }), "utf8");
|
|
2432
|
-
|
|
2433
|
-
const { tools } = loadExtension();
|
|
2434
|
-
const statusTool = tools.find((tool) => tool.name === "freeflow_status");
|
|
2435
|
-
assert.ok(statusTool);
|
|
2436
|
-
const result = await statusTool.execute(
|
|
2437
|
-
"status-invalid-local",
|
|
2438
|
-
{ action: "doctor" },
|
|
2439
|
-
undefined,
|
|
2440
|
-
undefined,
|
|
2441
|
-
context(cwd),
|
|
2442
|
-
);
|
|
2443
|
-
const report = JSON.parse(result.content[0].text);
|
|
2444
|
-
|
|
2445
|
-
assert.equal(report.configValid, false);
|
|
2446
|
-
assert.equal(report.repositoryConfigValid, true);
|
|
2447
|
-
assert.equal(report.localConfigValid, false);
|
|
2448
|
-
assert.equal(report.effectiveConfig.configured, false);
|
|
2449
|
-
assert.equal(report.effectiveConfig.enabled, false);
|
|
2450
|
-
assert.equal(report.mode.active, false);
|
|
2451
|
-
assert.match(report.configuration.personal.error, /skills\.enabled/);
|
|
2452
|
-
assert.ok(
|
|
2453
|
-
report.localConfigWarnings.some(
|
|
2454
|
-
(warning) => warning.includes(".freeflow/local.json is invalid") && warning.includes("runtime is inactive"),
|
|
2455
|
-
),
|
|
2456
|
-
);
|
|
2457
|
-
|
|
2458
|
-
const collapsed = renderText(statusTool.renderResult(result, { expanded: false }, testTheme));
|
|
2459
|
-
assert.match(collapsed, /freeflow off/);
|
|
2460
|
-
assert.match(collapsed, /mode dormant \(workflow\)/);
|
|
2461
|
-
assert.match(collapsed, /warnings=1/);
|
|
2462
|
-
} finally {
|
|
2463
|
-
await rm(cwd, { recursive: true, force: true });
|
|
2464
|
-
}
|
|
2465
|
-
});
|
|
2466
|
-
|
|
2467
|
-
test("Pi freeflow_status reports vault writability without creating directories", async () => {
|
|
2468
|
-
const cwd = await mkdtemp(join(tmpdir(), "freeflow-pi-status-vault-"));
|
|
2469
|
-
try {
|
|
2470
|
-
await mkdir(join(cwd, ".freeflow"));
|
|
2471
|
-
const nestedVault = join(cwd, "missing", "nested", "vault");
|
|
2472
|
-
await writeFile(
|
|
2473
|
-
join(cwd, ".freeflow/config.json"),
|
|
2474
|
-
JSON.stringify({
|
|
2475
|
-
defaultMode: "workflow",
|
|
2476
|
-
outputRouter: { vaultRoot: nestedVault },
|
|
2477
|
-
}),
|
|
2478
|
-
"utf8",
|
|
2479
|
-
);
|
|
2480
|
-
|
|
2481
|
-
const { tools } = loadExtension();
|
|
2482
|
-
const statusTool = tools.find((tool) => tool.name === "freeflow_status");
|
|
2483
|
-
const missingResult = await statusTool.execute(
|
|
2484
|
-
"status-vault-missing",
|
|
2485
|
-
{ action: "doctor" },
|
|
2486
|
-
undefined,
|
|
2487
|
-
undefined,
|
|
2488
|
-
context(cwd),
|
|
2489
|
-
);
|
|
2490
|
-
const missingReport = JSON.parse(missingResult.content[0].text);
|
|
2491
|
-
|
|
2492
|
-
assert.equal(missingReport.vault.writability.status, "missing_ancestor_writable");
|
|
2493
|
-
await assert.rejects(readFile(nestedVault, "utf8"));
|
|
2494
|
-
|
|
2495
|
-
const fileVault = join(cwd, "vault-file");
|
|
2496
|
-
await writeFile(fileVault, "not a directory", "utf8");
|
|
2497
|
-
await writeFile(
|
|
2498
|
-
join(cwd, ".freeflow/config.json"),
|
|
2499
|
-
JSON.stringify({
|
|
2500
|
-
defaultMode: "workflow",
|
|
2501
|
-
outputRouter: { enabled: true, vaultRoot: fileVault },
|
|
2502
|
-
}),
|
|
2503
|
-
"utf8",
|
|
2504
|
-
);
|
|
2505
|
-
const fileResult = await statusTool.execute(
|
|
2506
|
-
"status-vault-file",
|
|
2507
|
-
{ action: "doctor" },
|
|
2508
|
-
undefined,
|
|
2509
|
-
undefined,
|
|
2510
|
-
context(cwd),
|
|
2511
|
-
);
|
|
2512
|
-
const fileReport = JSON.parse(fileResult.content[0].text);
|
|
2513
|
-
assert.equal(fileReport.vault.writability.status, "not_directory");
|
|
2514
|
-
} finally {
|
|
2515
|
-
await rm(cwd, { recursive: true, force: true });
|
|
2516
|
-
}
|
|
2517
|
-
});
|
|
2518
|
-
|
|
2519
|
-
test("Pi freeflow_status reports local unsafe processing opt-in without shared config", async () => {
|
|
2520
|
-
const cwd = await mkdtemp(join(tmpdir(), "freeflow-pi-status-local-processing-"));
|
|
2521
|
-
try {
|
|
2522
|
-
await mkdir(join(cwd, ".freeflow"));
|
|
2523
|
-
await writeFile(join(cwd, ".freeflow/config.json"), JSON.stringify({ defaultMode: "workflow" }), "utf8");
|
|
2524
|
-
await writeFile(
|
|
2525
|
-
join(cwd, ".freeflow/local.json"),
|
|
2526
|
-
JSON.stringify({ processing: { unsafeUnsandboxed: { enabled: true } } }),
|
|
2527
|
-
"utf8",
|
|
2528
|
-
);
|
|
2529
|
-
|
|
2530
|
-
const { tools } = loadExtension();
|
|
2531
|
-
const statusTool = tools.find((tool) => tool.name === "freeflow_status");
|
|
2532
|
-
const result = await statusTool.execute(
|
|
2533
|
-
"status-local-processing",
|
|
2534
|
-
{ action: "doctor" },
|
|
2535
|
-
undefined,
|
|
2536
|
-
undefined,
|
|
2537
|
-
context(cwd),
|
|
2538
|
-
);
|
|
2539
|
-
const report = JSON.parse(result.content[0].text);
|
|
2540
|
-
|
|
2541
|
-
assert.equal(report.localConfigExists, true);
|
|
2542
|
-
assert.equal(report.effectiveLocalConfig.processing.unsafeUnsandboxed.enabled, true);
|
|
2543
|
-
assert.equal(report.processing.unsafeUnsandboxed.enabled, true);
|
|
2544
|
-
assert.equal(report.processing.unsafeUnsandboxed.status, "enabled_unsafe");
|
|
2545
|
-
assert.deepEqual(report.configWarnings, []);
|
|
2546
|
-
assert.deepEqual(report.localConfigWarnings, []);
|
|
2547
|
-
} finally {
|
|
2548
|
-
await rm(cwd, { recursive: true, force: true });
|
|
2549
|
-
}
|
|
2550
|
-
});
|
|
2551
|
-
|
|
2552
|
-
test("Pi freeflow_status warns that shared config cannot enable unsafe processing", async () => {
|
|
2553
|
-
const cwd = await mkdtemp(join(tmpdir(), "freeflow-pi-status-shared-processing-"));
|
|
2554
|
-
try {
|
|
2555
|
-
await mkdir(join(cwd, ".freeflow"));
|
|
2556
|
-
await writeFile(
|
|
2557
|
-
join(cwd, ".freeflow/config.json"),
|
|
2558
|
-
JSON.stringify({
|
|
2559
|
-
defaultMode: "workflow",
|
|
2560
|
-
processing: { unsafeUnsandboxed: { enabled: true } },
|
|
2561
|
-
}),
|
|
2562
|
-
"utf8",
|
|
2563
|
-
);
|
|
2564
|
-
|
|
2565
|
-
const { tools } = loadExtension();
|
|
2566
|
-
const statusTool = tools.find((tool) => tool.name === "freeflow_status");
|
|
2567
|
-
const result = await statusTool.execute(
|
|
2568
|
-
"status-shared-processing",
|
|
2569
|
-
{ action: "doctor" },
|
|
2570
|
-
undefined,
|
|
2571
|
-
undefined,
|
|
2572
|
-
context(cwd),
|
|
2573
|
-
);
|
|
2574
|
-
const report = JSON.parse(result.content[0].text);
|
|
2575
|
-
|
|
2576
|
-
assert.equal(report.effectiveLocalConfig.processing.unsafeUnsandboxed.enabled, false);
|
|
2577
|
-
assert.equal(report.processing.unsafeUnsandboxed.enabled, false);
|
|
2578
|
-
assert.ok(report.configWarnings.some((warning) => warning.includes("processing config is ignored")));
|
|
2579
|
-
} finally {
|
|
2580
|
-
await rm(cwd, { recursive: true, force: true });
|
|
2581
|
-
}
|
|
2582
|
-
});
|
|
2583
|
-
|
|
2584
|
-
test("Pi freeflow_status reports configured observed routing", async () => {
|
|
2585
|
-
const cwd = await mkdtemp(join(tmpdir(), "freeflow-pi-status-observed-"));
|
|
2586
|
-
try {
|
|
2587
|
-
await mkdir(join(cwd, ".freeflow"));
|
|
2588
|
-
await writeFile(
|
|
2589
|
-
join(cwd, ".freeflow/config.json"),
|
|
2590
|
-
JSON.stringify({
|
|
2591
|
-
defaultMode: "workflow",
|
|
2592
|
-
outputRouter: {
|
|
2593
|
-
enabled: true,
|
|
2594
|
-
observedRouting: {
|
|
2595
|
-
enabled: true,
|
|
2596
|
-
onRoutingFailure: "fail-open",
|
|
2597
|
-
mcp: {
|
|
2598
|
-
servers: {
|
|
2599
|
-
github: { enabled: true, persistence: "exact" },
|
|
2600
|
-
gmail: { enabled: true, persistence: "metadata-only" },
|
|
2601
|
-
},
|
|
2602
|
-
},
|
|
2603
|
-
web: { enabled: true, persistence: "exact" },
|
|
2604
|
-
fetch: { enabled: false },
|
|
2605
|
-
codeSearch: { enabled: true, persistence: "none" },
|
|
2606
|
-
},
|
|
2607
|
-
scriptTransform: {
|
|
2608
|
-
enabled: true,
|
|
2609
|
-
languages: ["python"],
|
|
2610
|
-
limits: {
|
|
2611
|
-
timeoutMs: 1000,
|
|
2612
|
-
maxInputBytes: 2048,
|
|
2613
|
-
maxOutputBytes: 4096,
|
|
2614
|
-
},
|
|
2615
|
-
},
|
|
2616
|
-
},
|
|
2617
|
-
}),
|
|
2618
|
-
"utf8",
|
|
2619
|
-
);
|
|
2620
|
-
|
|
2621
|
-
const { tools } = loadExtension();
|
|
2622
|
-
const statusTool = tools.find((tool) => tool.name === "freeflow_status");
|
|
2623
|
-
const result = await statusTool.execute(
|
|
2624
|
-
"status-observed",
|
|
2625
|
-
{ action: "doctor" },
|
|
2626
|
-
undefined,
|
|
2627
|
-
undefined,
|
|
2628
|
-
context(cwd),
|
|
2629
|
-
);
|
|
2630
|
-
const report = JSON.parse(result.content[0].text);
|
|
2631
|
-
|
|
2632
|
-
assert.equal(report.effectiveConfig.outputRouter.observedRouting.enabled, true);
|
|
2633
|
-
assert.equal(report.effectiveConfig.outputRouter.observedRouting.onRoutingFailure, "fail-open");
|
|
2634
|
-
assert.deepEqual(report.effectiveConfig.outputRouter.observedRouting.mcp.servers.github, {
|
|
2635
|
-
enabled: true,
|
|
2636
|
-
persistence: "exact",
|
|
2637
|
-
});
|
|
2638
|
-
assert.deepEqual(report.effectiveConfig.outputRouter.observedRouting.mcp.servers.gmail, {
|
|
2639
|
-
enabled: true,
|
|
2640
|
-
persistence: "metadata-only",
|
|
2641
|
-
});
|
|
2642
|
-
assert.deepEqual(report.effectiveConfig.outputRouter.observedRouting.web, {
|
|
2643
|
-
enabled: true,
|
|
2644
|
-
persistence: "exact",
|
|
2645
|
-
});
|
|
2646
|
-
assert.deepEqual(report.effectiveConfig.outputRouter.observedRouting.fetch, {
|
|
2647
|
-
enabled: false,
|
|
2648
|
-
persistence: "none",
|
|
2649
|
-
});
|
|
2650
|
-
assert.deepEqual(report.effectiveConfig.outputRouter.observedRouting.codeSearch, {
|
|
2651
|
-
enabled: true,
|
|
2652
|
-
persistence: "none",
|
|
2653
|
-
});
|
|
2654
|
-
assert.equal(report.observedRouting.enabled, true);
|
|
2655
|
-
assert.equal(report.observedRouting.mcp.configuredServerCount, 2);
|
|
2656
|
-
assert.equal(report.effectiveConfig.outputRouter.scriptTransform.enabled, true);
|
|
2657
|
-
assert.deepEqual(report.effectiveConfig.outputRouter.scriptTransform.languages, ["python"]);
|
|
2658
|
-
assert.equal(report.scriptTransform.enabled, true);
|
|
2659
|
-
assert.ok(["available", "adapter_unavailable"].includes(report.scriptTransform.executionStatus));
|
|
2660
|
-
assert.equal(typeof report.scriptTransform.adapterAvailable, "boolean");
|
|
2661
|
-
if (report.scriptTransform.adapterAvailable) {
|
|
2662
|
-
assert.ok(report.scriptTransform.availableLanguages.includes("python"));
|
|
2663
|
-
}
|
|
2664
|
-
assert.deepEqual(report.observedRouting.persistenceModes, ["exact", "metadata-only", "none"]);
|
|
2665
|
-
assert.deepEqual(report.configWarnings, []);
|
|
2666
|
-
} finally {
|
|
2667
|
-
await rm(cwd, { recursive: true, force: true });
|
|
2668
|
-
}
|
|
2669
|
-
});
|
|
2670
|
-
|
|
2671
|
-
test("Pi freeflow_status reports invalid config warnings and safe fallbacks", async () => {
|
|
2672
|
-
const cwd = await mkdtemp(join(tmpdir(), "freeflow-pi-status-invalid-"));
|
|
2673
|
-
try {
|
|
2674
|
-
await mkdir(join(cwd, ".freeflow"));
|
|
2675
|
-
await writeFile(
|
|
2676
|
-
join(cwd, ".freeflow/config.json"),
|
|
2677
|
-
JSON.stringify({
|
|
2678
|
-
defaultMode: "workflow",
|
|
2679
|
-
outputRouter: {
|
|
2680
|
-
enabled: "yes",
|
|
2681
|
-
profile: "future",
|
|
2682
|
-
scriptTransform: {
|
|
2683
|
-
enabled: "yes",
|
|
2684
|
-
sandbox: "none",
|
|
2685
|
-
languages: ["ruby"],
|
|
2686
|
-
network: "on",
|
|
2687
|
-
},
|
|
2688
|
-
observedRouting: {
|
|
2689
|
-
enabled: "yes",
|
|
2690
|
-
mcp: {
|
|
2691
|
-
servers: { github: { enabled: true, persistence: "redacted" } },
|
|
2692
|
-
},
|
|
2693
|
-
web: { enabled: true },
|
|
2694
|
-
},
|
|
2695
|
-
},
|
|
2696
|
-
capture: { freeflowMediated: "metadata-only", directHostTools: "raw" },
|
|
2697
|
-
providers: { enabled: [{ id: "serena", mode: "write" }] },
|
|
2698
|
-
}),
|
|
2699
|
-
"utf8",
|
|
2700
|
-
);
|
|
2701
|
-
|
|
2702
|
-
const { tools } = loadExtension();
|
|
2703
|
-
const statusTool = tools.find((tool) => tool.name === "freeflow_status");
|
|
2704
|
-
const result = await statusTool.execute("status-invalid", { action: "doctor" }, undefined, undefined, context(cwd));
|
|
2705
|
-
const report = JSON.parse(result.content[0].text);
|
|
2706
|
-
|
|
2707
|
-
assert.equal(report.effectiveConfig.outputRouter.enabled, false);
|
|
2708
|
-
assert.equal(report.effectiveConfig.outputRouter.profile, "standard");
|
|
2709
|
-
assert.equal("capture" in report.effectiveConfig, false);
|
|
2710
|
-
assert.equal("providers" in report.effectiveConfig, false);
|
|
2711
|
-
assert.equal(report.effectiveConfig.outputRouter.observedRouting.enabled, false);
|
|
2712
|
-
assert.equal(report.effectiveConfig.outputRouter.scriptTransform.enabled, false);
|
|
2713
|
-
assert.deepEqual(report.effectiveConfig.outputRouter.scriptTransform.languages, ["javascript", "python", "jq"]);
|
|
2714
|
-
assert.equal(report.effectiveConfig.outputRouter.observedRouting.mcp.servers.github.persistence, "metadata-only");
|
|
2715
|
-
assert.equal(report.effectiveConfig.outputRouter.observedRouting.web.persistence, "metadata-only");
|
|
2716
|
-
assert.ok(report.configWarnings.some((warning) => warning.includes("outputRouter.enabled")));
|
|
2717
|
-
assert.ok(report.configWarnings.some((warning) => warning.includes("outputRouter.profile")));
|
|
2718
|
-
assert.ok(report.configWarnings.some((warning) => warning.includes("outputRouter.scriptTransform.enabled")));
|
|
2719
|
-
assert.ok(report.configWarnings.some((warning) => warning.includes("outputRouter.scriptTransform.sandbox")));
|
|
2720
|
-
assert.ok(report.configWarnings.some((warning) => warning.includes("outputRouter.scriptTransform.languages")));
|
|
2721
|
-
assert.ok(report.configWarnings.some((warning) => warning.includes("outputRouter.scriptTransform.network")));
|
|
2722
|
-
assert.ok(report.configWarnings.some((warning) => warning.includes("outputRouter.observedRouting.enabled")));
|
|
2723
|
-
assert.ok(
|
|
2724
|
-
report.configWarnings.some(
|
|
2725
|
-
(warning) =>
|
|
2726
|
-
warning.includes("outputRouter.observedRouting.mcp.servers.github.persistence") &&
|
|
2727
|
-
warning.includes("redacted"),
|
|
2728
|
-
),
|
|
2729
|
-
);
|
|
2730
|
-
assert.ok(
|
|
2731
|
-
report.configWarnings.some((warning) => warning.includes("outputRouter.observedRouting.web.persistence")),
|
|
2732
|
-
);
|
|
2733
|
-
} finally {
|
|
2734
|
-
await rm(cwd, { recursive: true, force: true });
|
|
2735
|
-
}
|
|
2736
|
-
});
|
|
2737
|
-
|
|
2738
|
-
test("Pi freeflow_status migration recommendations are non-destructive", async () => {
|
|
2739
|
-
const cwd = await mkdtemp(join(tmpdir(), "freeflow-pi-status-migration-"));
|
|
2740
|
-
try {
|
|
2741
|
-
await mkdir(join(cwd, ".freeflow"));
|
|
2742
|
-
const configPath = join(cwd, ".freeflow/config.json");
|
|
2743
|
-
const configText = JSON.stringify(
|
|
2744
|
-
{
|
|
2745
|
-
defaultMode: "workflow",
|
|
2746
|
-
outputRouter: {
|
|
2747
|
-
enabled: true,
|
|
2748
|
-
profile: "standard",
|
|
2749
|
-
postToolRouting: "off",
|
|
2750
|
-
largeOutputBytes: 64000,
|
|
2751
|
-
largeOutputLines: 1000,
|
|
2752
|
-
vaultRoot: "~/.cache/freeflow-router/vault",
|
|
2753
|
-
vaultRetentionDays: 7,
|
|
2754
|
-
},
|
|
2755
|
-
capture: { freeflowMediated: "raw", directHostTools: "off" },
|
|
2756
|
-
providers: { enabled: [] },
|
|
2757
|
-
},
|
|
2758
|
-
null,
|
|
2759
|
-
2,
|
|
2760
|
-
);
|
|
2761
|
-
await writeFile(configPath, configText, "utf8");
|
|
2762
|
-
|
|
2763
|
-
const { tools } = loadExtension();
|
|
2764
|
-
const statusTool = tools.find((tool) => tool.name === "freeflow_status");
|
|
2765
|
-
const result = await statusTool.execute(
|
|
2766
|
-
"status-migration",
|
|
2767
|
-
{ action: "migration" },
|
|
2768
|
-
undefined,
|
|
2769
|
-
undefined,
|
|
2770
|
-
context(cwd),
|
|
2771
|
-
);
|
|
2772
|
-
const report = JSON.parse(result.content[0].text);
|
|
2773
|
-
|
|
2774
|
-
assert.equal(report.action, "migration");
|
|
2775
|
-
assert.equal(report.migration.applied, false);
|
|
2776
|
-
assert.equal(report.migration.requiresConfirmation, true);
|
|
2777
|
-
assert.ok(
|
|
2778
|
-
report.migration.recommendations.some((recommendation) => recommendation.path === "outputRouter.postToolRouting"),
|
|
2779
|
-
);
|
|
2780
|
-
assert.ok(report.migration.recommendations.some((recommendation) => recommendation.path === "capture"));
|
|
2781
|
-
assert.ok(report.migration.recommendations.some((recommendation) => recommendation.path === "providers"));
|
|
2782
|
-
assert.equal(await readFile(configPath, "utf8"), configText);
|
|
2783
|
-
} finally {
|
|
2784
|
-
await rm(cwd, { recursive: true, force: true });
|
|
2785
|
-
}
|
|
2786
|
-
});
|
|
2787
|
-
|
|
2788
|
-
test("Pi outputRouter.enabled=false suppresses router context and native safety net", async () => {
|
|
2789
|
-
const cwd = await mkdtemp(join(tmpdir(), "freeflow-pi-router-disabled-"));
|
|
2790
|
-
try {
|
|
2791
|
-
await mkdir(join(cwd, ".freeflow"));
|
|
2792
|
-
await writeFile(
|
|
2793
|
-
join(cwd, ".freeflow/config.json"),
|
|
2794
|
-
JSON.stringify({
|
|
2795
|
-
defaultMode: "workflow",
|
|
2796
|
-
outputRouter: {
|
|
2797
|
-
enabled: false,
|
|
2798
|
-
postToolRouting: "safety-net",
|
|
2799
|
-
largeOutputLines: 1,
|
|
2800
|
-
largeOutputBytes: 1,
|
|
2408
|
+
profiles: {
|
|
2409
|
+
standard: { provider: "test", model: "model-a", thinkingLevel: "low" },
|
|
2410
|
+
reasoning: { provider: "test", model: "model-b", thinkingLevel: "high" },
|
|
2801
2411
|
},
|
|
2802
|
-
}),
|
|
2803
|
-
"utf8",
|
|
2804
|
-
);
|
|
2805
|
-
|
|
2806
|
-
const { handlers } = loadExtension();
|
|
2807
|
-
const beforeAgentStart = handlers.get("before_agent_start");
|
|
2808
|
-
const result = await beforeAgentStart({ systemPrompt: "base prompt" }, context(cwd));
|
|
2809
|
-
assert.doesNotMatch(result.systemPrompt, /## Loaded Output Router Skill/);
|
|
2810
|
-
|
|
2811
|
-
const toolResult = await handlers.get("tool_result")(
|
|
2812
|
-
{
|
|
2813
|
-
type: "tool_result",
|
|
2814
|
-
toolName: "read",
|
|
2815
|
-
toolCallId: "read-router-disabled",
|
|
2816
|
-
input: { path: "large.txt" },
|
|
2817
|
-
content: [{ type: "text", text: "line 1\nline 2" }],
|
|
2818
|
-
details: undefined,
|
|
2819
|
-
isError: false,
|
|
2820
|
-
},
|
|
2821
|
-
context(cwd),
|
|
2822
|
-
);
|
|
2823
|
-
assert.equal(toolResult, undefined);
|
|
2824
|
-
} finally {
|
|
2825
|
-
await rm(cwd, { recursive: true, force: true });
|
|
2826
|
-
}
|
|
2827
|
-
});
|
|
2828
|
-
|
|
2829
|
-
test("Pi output-router context mentions native safety net only when config enables it", async () => {
|
|
2830
|
-
const cwd = await mkdtemp(join(tmpdir(), "freeflow-pi-config-"));
|
|
2831
|
-
try {
|
|
2832
|
-
await mkdir(join(cwd, ".freeflow"));
|
|
2833
|
-
await writeFile(
|
|
2834
|
-
join(cwd, ".freeflow/config.json"),
|
|
2835
|
-
JSON.stringify({
|
|
2836
|
-
defaultMode: "workflow",
|
|
2837
|
-
outputRouter: { enabled: true, postToolRouting: "safety-net" },
|
|
2838
|
-
}),
|
|
2839
|
-
"utf8",
|
|
2840
|
-
);
|
|
2841
|
-
|
|
2842
|
-
const { handlers } = loadExtension();
|
|
2843
|
-
const beforeAgentStart = handlers.get("before_agent_start");
|
|
2844
|
-
const result = await beforeAgentStart({ systemPrompt: "base prompt" }, context(cwd));
|
|
2845
|
-
|
|
2846
|
-
assert.match(result.systemPrompt, /large native read\/bash outputs may be vaulted/);
|
|
2847
|
-
assert.match(result.systemPrompt, /## Loaded Output Router Skill/);
|
|
2848
|
-
assert.doesNotMatch(result.systemPrompt, /## Loaded Output Router Safety Policy/);
|
|
2849
|
-
assert.match(result.systemPrompt, /freeflow_search/);
|
|
2850
|
-
assert.match(result.systemPrompt, /freeflow_search action=transform/);
|
|
2851
|
-
} finally {
|
|
2852
|
-
await rm(cwd, { recursive: true, force: true });
|
|
2853
|
-
}
|
|
2854
|
-
});
|
|
2855
|
-
|
|
2856
|
-
test("Pi freeflow_search renders compact and expanded routed evidence UI", () => {
|
|
2857
|
-
const { tools } = loadExtension();
|
|
2858
|
-
const searchTool = tools.find((tool) => tool.name === "freeflow_search");
|
|
2859
|
-
assert.ok(searchTool);
|
|
2860
|
-
assert.ok(searchTool.parameters.properties.action.enum.includes("get"));
|
|
2861
|
-
|
|
2862
|
-
const call = renderText(
|
|
2863
|
-
searchTool.renderCall(
|
|
2864
|
-
{
|
|
2865
|
-
action: "query",
|
|
2866
|
-
source: {
|
|
2867
|
-
kind: "repo",
|
|
2868
|
-
path: "docs/codex-cli-agent-harness/passes/2026-06-12-pass-3-sandboxing-and-permissions.md",
|
|
2869
|
-
},
|
|
2870
|
-
query: "SandboxPermissions Plain-language meaning",
|
|
2871
|
-
},
|
|
2872
|
-
testTheme,
|
|
2873
|
-
),
|
|
2874
|
-
200,
|
|
2875
|
-
);
|
|
2876
|
-
assert.match(call, /freeflow_search query repo/);
|
|
2877
|
-
assert.match(call, /SandboxPermissions/);
|
|
2878
|
-
|
|
2879
|
-
const toolResult = {
|
|
2880
|
-
content: [{ type: "text", text: "raw json should not be the visible UI" }],
|
|
2881
|
-
details: {
|
|
2882
|
-
result: {
|
|
2883
|
-
toolStatus: "ok",
|
|
2884
|
-
decisionId: "ffdec_test",
|
|
2885
|
-
preserve: "important",
|
|
2886
|
-
source: { kind: "repo", path: "docs/example.md" },
|
|
2887
|
-
routing: {
|
|
2888
|
-
status: "routed",
|
|
2889
|
-
route: "search",
|
|
2890
|
-
reason: "Deterministic test route.",
|
|
2891
|
-
},
|
|
2892
|
-
evidence: [
|
|
2893
|
-
{
|
|
2894
|
-
id: "ev_test",
|
|
2895
|
-
source: { kind: "repo", path: "docs/example.md" },
|
|
2896
|
-
path: "docs/example.md",
|
|
2897
|
-
lines: "523-527",
|
|
2898
|
-
excerpt: "### Sandbox Permissions\n\n`SandboxPermissions` is a per-command request shape.",
|
|
2899
|
-
why: "Matched exact heading and identifier.",
|
|
2900
|
-
window: "small",
|
|
2901
|
-
expandable: true,
|
|
2902
|
-
},
|
|
2903
|
-
],
|
|
2904
|
-
recovery: {
|
|
2905
|
-
how: "Use freeflow_search action=expand with evidenceId=ev_test.",
|
|
2906
|
-
evidenceId: "ev_test",
|
|
2907
|
-
},
|
|
2908
|
-
},
|
|
2909
|
-
},
|
|
2910
|
-
};
|
|
2911
|
-
|
|
2912
|
-
const collapsed = renderText(searchTool.renderResult(toolResult, { expanded: false }, testTheme));
|
|
2913
|
-
assert.match(collapsed, /1 evidence packet/);
|
|
2914
|
-
assert.match(collapsed, /docs\/example\.md:523-527/);
|
|
2915
|
-
assert.match(collapsed, /ctrl\+o to expand/);
|
|
2916
|
-
assert.doesNotMatch(collapsed, /raw json/);
|
|
2917
|
-
|
|
2918
|
-
const expanded = renderText(searchTool.renderResult(toolResult, { expanded: true }, testTheme));
|
|
2919
|
-
assert.match(expanded, /Source/);
|
|
2920
|
-
assert.match(expanded, /preserve: important/);
|
|
2921
|
-
assert.match(expanded, /Storage/);
|
|
2922
|
-
assert.match(expanded, /decisionId: ffdec_test/);
|
|
2923
|
-
assert.match(expanded, /Evidence/);
|
|
2924
|
-
assert.match(expanded, /evidenceId: ev_test/);
|
|
2925
|
-
assert.match(expanded, /source: repo docs\/example\.md/);
|
|
2926
|
-
assert.match(expanded, /expandable: true/);
|
|
2927
|
-
assert.match(expanded, /exact search: action=retrieve source.kind=repo lineRange=523-527 path=docs\/example\.md/);
|
|
2928
|
-
assert.match(expanded, /### Sandbox Permissions/);
|
|
2929
|
-
assert.match(expanded, /Recovery/);
|
|
2930
|
-
assert.match(expanded, /expand hint: freeflow_search action=expand evidenceId=ev_test/);
|
|
2931
|
-
});
|
|
2932
|
-
|
|
2933
|
-
test("Pi freeflow_run exposes declarative filter schema", () => {
|
|
2934
|
-
const { tools } = loadExtension();
|
|
2935
|
-
const runTool = tools.find((tool) => tool.name === "freeflow_run");
|
|
2936
|
-
assert.ok(runTool);
|
|
2937
|
-
|
|
2938
|
-
assert.deepEqual(runTool.parameters.oneOf, [{ required: ["command"] }, { required: ["script"] }]);
|
|
2939
|
-
|
|
2940
|
-
const script = runTool.parameters.properties.script;
|
|
2941
|
-
assert.equal(script.type, "object");
|
|
2942
|
-
assert.equal(script.additionalProperties, false);
|
|
2943
|
-
assert.deepEqual(script.properties.language.enum, ["javascript", "python", "jq"]);
|
|
2944
|
-
assert.equal(script.properties.code.minLength, 1);
|
|
2945
|
-
assert.equal(script.properties.limits.properties.timeoutMs.maximum, 30000);
|
|
2946
|
-
assert.deepEqual(script.required, ["language", "code"]);
|
|
2947
|
-
|
|
2948
|
-
const filters = runTool.parameters.properties.filters;
|
|
2949
|
-
assert.equal(filters.type, "object");
|
|
2950
|
-
assert.equal(filters.additionalProperties, false);
|
|
2951
|
-
assert.deepEqual(filters.properties.stream.enum, ["stdout", "stderr", "combined"]);
|
|
2952
|
-
assert.equal(filters.properties.include.items.minLength, 1);
|
|
2953
|
-
assert.equal(filters.properties.exclude.items.minLength, 1);
|
|
2954
|
-
assert.match(filters.properties.flags.pattern, /gimsu/);
|
|
2955
|
-
assert.equal(filters.properties.head.minimum, 1);
|
|
2956
|
-
assert.equal(filters.properties.tail.minimum, 1);
|
|
2957
|
-
assert.equal(filters.properties.maxLines.minimum, 1);
|
|
2958
|
-
assert.equal(filters.properties.maxBytes.minimum, 1);
|
|
2959
|
-
|
|
2960
|
-
const scriptFilter = runTool.parameters.properties.scriptFilter;
|
|
2961
|
-
assert.equal(scriptFilter.type, "object");
|
|
2962
|
-
assert.equal(scriptFilter.additionalProperties, false);
|
|
2963
|
-
assert.deepEqual(scriptFilter.properties.language.enum, ["javascript", "python", "jq"]);
|
|
2964
|
-
assert.equal(scriptFilter.properties.code.minLength, 1);
|
|
2965
|
-
assert.equal(scriptFilter.properties.limits.properties.timeoutMs.maximum, 30000);
|
|
2966
|
-
assert.deepEqual(scriptFilter.required, ["language", "code"]);
|
|
2967
|
-
});
|
|
2968
|
-
|
|
2969
|
-
test("Pi freeflow_run renders compact and expanded status, evidence, and vault UI", () => {
|
|
2970
|
-
const { tools } = loadExtension();
|
|
2971
|
-
const runTool = tools.find((tool) => tool.name === "freeflow_run");
|
|
2972
|
-
assert.ok(runTool);
|
|
2973
|
-
|
|
2974
|
-
const call = renderText(runTool.renderCall({ command: "npm test -- --runInBand", preserve: "important" }, testTheme));
|
|
2975
|
-
assert.match(call, /freeflow_run \$ npm test/);
|
|
2976
|
-
assert.match(call, /preserve=important/);
|
|
2977
|
-
|
|
2978
|
-
const toolResult = {
|
|
2979
|
-
content: [{ type: "text", text: "raw json should not be the visible UI" }],
|
|
2980
|
-
details: {
|
|
2981
|
-
result: {
|
|
2982
|
-
toolStatus: "ok",
|
|
2983
|
-
decisionId: "ffdec_run_test",
|
|
2984
|
-
preserve: "important",
|
|
2985
|
-
outputId: "ffout_test123",
|
|
2986
|
-
recordId: "ffrec_test123",
|
|
2987
|
-
execution: { status: "failed", exitCode: 1, durationMs: 842 },
|
|
2988
|
-
routing: {
|
|
2989
|
-
status: "routed",
|
|
2990
|
-
route: "run",
|
|
2991
|
-
reason: "Command failed; exact failure evidence was returned and raw output was vaulted before routing.",
|
|
2992
|
-
},
|
|
2993
|
-
summary: "Command failed with exitCode=1.",
|
|
2994
|
-
parser: {
|
|
2995
|
-
name: "test-runner",
|
|
2996
|
-
confidence: 0.92,
|
|
2997
|
-
fidelity: "exact",
|
|
2998
|
-
compressed: true,
|
|
2999
|
-
counts: { testsFailed: 1 },
|
|
3000
|
-
},
|
|
3001
|
-
persistence: {
|
|
3002
|
-
status: "vaulted",
|
|
3003
|
-
recoverability: "exact",
|
|
3004
|
-
recoveryOutputId: "ffout_test123",
|
|
3005
|
-
},
|
|
3006
|
-
filters: {
|
|
3007
|
-
stream: "stderr",
|
|
3008
|
-
include: ["AssertionError"],
|
|
3009
|
-
sourceLines: 2,
|
|
3010
|
-
selectedLines: 1,
|
|
3011
|
-
},
|
|
3012
|
-
scriptFilter: {
|
|
3013
|
-
status: "success",
|
|
3014
|
-
language: "javascript",
|
|
3015
|
-
label: "failures-only",
|
|
3016
|
-
rawOutputId: "ffout_test123",
|
|
3017
|
-
sourceAliases: ["stdout", "stderr", "combined"],
|
|
3018
|
-
outputId: "ffout_script123",
|
|
3019
|
-
operation: {
|
|
3020
|
-
kind: "script",
|
|
3021
|
-
language: "javascript",
|
|
3022
|
-
codeSha256: "sha256_abc",
|
|
3023
|
-
},
|
|
3024
|
-
},
|
|
3025
|
-
importantLines: [
|
|
3026
|
-
{
|
|
3027
|
-
stream: "stderr",
|
|
3028
|
-
lines: "14-16",
|
|
3029
|
-
excerpt: "AssertionError: expected false to equal true\nSTACK_BENCH_MARKER exact failure line",
|
|
3030
|
-
},
|
|
3031
|
-
],
|
|
3032
|
-
recovery: {
|
|
3033
|
-
how: "Use freeflow_search with source.kind=vault and outputId=ffout_test123 to recover exact command output.",
|
|
3034
|
-
outputId: "ffout_test123",
|
|
3035
|
-
},
|
|
3036
|
-
},
|
|
3037
|
-
},
|
|
3038
|
-
};
|
|
3039
|
-
|
|
3040
|
-
const collapsed = renderText(runTool.renderResult(toolResult, { expanded: false }, testTheme));
|
|
3041
|
-
assert.match(collapsed, /execution: failed/);
|
|
3042
|
-
assert.match(collapsed, /routing: routed/);
|
|
3043
|
-
assert.match(collapsed, /ffout_test123/);
|
|
3044
|
-
assert.match(collapsed, /ffout_script123/);
|
|
3045
|
-
assert.match(collapsed, /parser test-runner 0\.92/);
|
|
3046
|
-
assert.match(collapsed, /raw and script output recoverable from vault/);
|
|
3047
|
-
assert.match(collapsed, /ctrl\+o to expand/);
|
|
3048
|
-
assert.doesNotMatch(collapsed, /raw json/);
|
|
3049
|
-
|
|
3050
|
-
const expanded = renderText(
|
|
3051
|
-
runTool.renderResult(toolResult, { expanded: true }, testTheme, {
|
|
3052
|
-
args: { command: "npm test -- --runInBand" },
|
|
3053
|
-
}),
|
|
3054
|
-
);
|
|
3055
|
-
assert.match(expanded, /Status/);
|
|
3056
|
-
assert.match(expanded, /execution.status: failed/);
|
|
3057
|
-
assert.match(expanded, /Storage/);
|
|
3058
|
-
assert.match(expanded, /decisionId: ffdec_run_test/);
|
|
3059
|
-
assert.match(expanded, /recordId: ffrec_test123/);
|
|
3060
|
-
assert.match(expanded, /persistence: vaulted \/ exact/);
|
|
3061
|
-
assert.match(expanded, /Filters/);
|
|
3062
|
-
assert.match(expanded, /stream=stderr/);
|
|
3063
|
-
assert.match(expanded, /include=AssertionError/);
|
|
3064
|
-
assert.match(expanded, /selected=1\/2/);
|
|
3065
|
-
assert.match(expanded, /Script filter/);
|
|
3066
|
-
assert.match(expanded, /javascript:success/);
|
|
3067
|
-
assert.match(expanded, /rawOutputId: ffout_test123/);
|
|
3068
|
-
assert.match(expanded, /sources: stdout, stderr, combined/);
|
|
3069
|
-
assert.match(expanded, /sha256_abc/);
|
|
3070
|
-
assert.match(expanded, /Parser/);
|
|
3071
|
-
assert.match(expanded, /confidence: 0\.92/);
|
|
3072
|
-
assert.match(expanded, /counts:.*testsFailed/);
|
|
3073
|
-
assert.match(expanded, /Evidence/);
|
|
3074
|
-
assert.match(expanded, /AssertionError/);
|
|
3075
|
-
assert.match(expanded, /Vault recovery/);
|
|
3076
|
-
assert.match(expanded, /source.kind=vault/);
|
|
3077
|
-
assert.match(
|
|
3078
|
-
expanded,
|
|
3079
|
-
/exact search: action=retrieve source.kind=vault lineRange=14-16 stream=raw outputId=ffout_script123/,
|
|
3080
|
-
);
|
|
3081
|
-
assert.match(expanded, /raw command starting point: freeflow_search source.kind=vault outputId=ffout_test123/);
|
|
3082
|
-
assert.match(expanded, /details\.result/);
|
|
3083
|
-
});
|
|
3084
|
-
|
|
3085
|
-
test("Pi freeflow_run returns compact model-visible text with full structured details", async () => {
|
|
3086
|
-
const cwd = await mkdtemp(join(tmpdir(), "freeflow-pi-run-compact-text-"));
|
|
3087
|
-
try {
|
|
3088
|
-
await mkdir(join(cwd, ".freeflow"));
|
|
3089
|
-
await writeFile(
|
|
3090
|
-
join(cwd, ".freeflow/config.json"),
|
|
3091
|
-
JSON.stringify({
|
|
3092
|
-
defaultMode: "workflow",
|
|
3093
|
-
outputRouter: { enabled: true },
|
|
3094
|
-
}),
|
|
3095
|
-
"utf8",
|
|
3096
|
-
);
|
|
3097
|
-
const tools = [];
|
|
3098
|
-
const pi = {
|
|
3099
|
-
registerTool(tool) {
|
|
3100
|
-
tools.push(tool);
|
|
3101
|
-
},
|
|
3102
|
-
registerCommand() {},
|
|
3103
|
-
on() {},
|
|
3104
|
-
appendEntry() {},
|
|
3105
|
-
sendUserMessage() {},
|
|
3106
|
-
async exec() {
|
|
3107
|
-
return {
|
|
3108
|
-
stdout: "Tests: 1 failed, 24 passed, 25 total\n",
|
|
3109
|
-
stderr: "AssertionError: expected false to equal true\nSTACK_BENCH_MARKER exact failure line\n",
|
|
3110
|
-
code: 1,
|
|
3111
|
-
killed: false,
|
|
3112
|
-
};
|
|
3113
2412
|
},
|
|
3114
2413
|
};
|
|
3115
|
-
|
|
3116
|
-
const runTool = tools.find((tool) => tool.name === "freeflow_run");
|
|
3117
|
-
assert.ok(runTool);
|
|
3118
|
-
|
|
3119
|
-
const result = await runTool.execute(
|
|
3120
|
-
"tool-call",
|
|
3121
|
-
{ command: "npm test", goal: "verification" },
|
|
3122
|
-
undefined,
|
|
3123
|
-
undefined,
|
|
3124
|
-
context(cwd),
|
|
3125
|
-
);
|
|
2414
|
+
await writeFile(join(cwd, ".freeflow/config.json"), JSON.stringify(original, null, 2));
|
|
3126
2415
|
|
|
3127
|
-
const
|
|
3128
|
-
const
|
|
3129
|
-
|
|
3130
|
-
|
|
3131
|
-
|
|
3132
|
-
|
|
3133
|
-
|
|
3134
|
-
|
|
3135
|
-
|
|
3136
|
-
|
|
3137
|
-
|
|
3138
|
-
|
|
3139
|
-
|
|
3140
|
-
|
|
3141
|
-
|
|
3142
|
-
|
|
3143
|
-
|
|
3144
|
-
|
|
3145
|
-
|
|
3146
|
-
|
|
3147
|
-
|
|
3148
|
-
});
|
|
3149
|
-
|
|
3150
|
-
test("Pi freeflow_run forwards script producer and does not call host exec when sandbox is disabled", async () => {
|
|
3151
|
-
const cwd = await mkdtemp(join(tmpdir(), "freeflow-pi-run-script-producer-"));
|
|
3152
|
-
try {
|
|
3153
|
-
await mkdir(join(cwd, ".freeflow"));
|
|
3154
|
-
await writeFile(
|
|
3155
|
-
join(cwd, ".freeflow/config.json"),
|
|
3156
|
-
JSON.stringify({
|
|
3157
|
-
defaultMode: "workflow",
|
|
3158
|
-
outputRouter: { enabled: true },
|
|
3159
|
-
}),
|
|
3160
|
-
"utf8",
|
|
3161
|
-
);
|
|
3162
|
-
let execCalls = 0;
|
|
3163
|
-
const tools = [];
|
|
3164
|
-
const pi = {
|
|
3165
|
-
registerTool(tool) {
|
|
3166
|
-
tools.push(tool);
|
|
3167
|
-
},
|
|
3168
|
-
registerCommand() {},
|
|
3169
|
-
on() {},
|
|
3170
|
-
appendEntry() {},
|
|
3171
|
-
sendUserMessage() {},
|
|
3172
|
-
async exec() {
|
|
3173
|
-
execCalls += 1;
|
|
3174
|
-
throw new Error("host exec should not be called for sandboxed script producers");
|
|
3175
|
-
},
|
|
2416
|
+
const { commands } = loadExtension();
|
|
2417
|
+
const freeflowCommand = commands.find((command) => command.name === "freeflow");
|
|
2418
|
+
assert.ok(freeflowCommand);
|
|
2419
|
+
const settingsCtx = context(cwd);
|
|
2420
|
+
settingsCtx.isIdle = () => true;
|
|
2421
|
+
settingsCtx.modelRegistry = cognitiveRoutingModelRegistry();
|
|
2422
|
+
settingsCtx.ui.custom = async (factory) => {
|
|
2423
|
+
let result;
|
|
2424
|
+
const component = factory({ requestRender() {} }, testTheme, {}, (value) => {
|
|
2425
|
+
result = value;
|
|
2426
|
+
});
|
|
2427
|
+
for (let index = 0; index < 5; index++) component.handleInput("\u001b[B");
|
|
2428
|
+
component.handleInput("\r");
|
|
2429
|
+
component.handleInput("\u001b[B");
|
|
2430
|
+
component.handleInput("\r");
|
|
2431
|
+
component.handleInput("\u001b[B");
|
|
2432
|
+
component.handleInput("\r");
|
|
2433
|
+
component.handleInput("\u001b"); // Cancel at the effort step.
|
|
2434
|
+
component.handleInput("\u001b");
|
|
2435
|
+
component.handleInput("\u001b");
|
|
2436
|
+
return result;
|
|
3176
2437
|
};
|
|
3177
|
-
freeflowExtension(pi);
|
|
3178
|
-
const runTool = tools.find((tool) => tool.name === "freeflow_run");
|
|
3179
|
-
assert.ok(runTool);
|
|
3180
|
-
|
|
3181
|
-
const result = await runTool.execute(
|
|
3182
|
-
"tool-call-script-producer",
|
|
3183
|
-
{
|
|
3184
|
-
script: {
|
|
3185
|
-
language: "javascript",
|
|
3186
|
-
code: "RAW_PI_SCRIPT",
|
|
3187
|
-
label: "pi-script",
|
|
3188
|
-
},
|
|
3189
|
-
},
|
|
3190
|
-
undefined,
|
|
3191
|
-
undefined,
|
|
3192
|
-
context(cwd),
|
|
3193
|
-
);
|
|
3194
2438
|
|
|
3195
|
-
|
|
3196
|
-
assert.
|
|
3197
|
-
assert.equal(
|
|
3198
|
-
assert.equal(result.details.result.producer.kind, "script");
|
|
3199
|
-
assert.equal(result.details.result.producer.name, "pi-script");
|
|
3200
|
-
assert.equal(result.details.result.failure.kind, "script_transform_disabled");
|
|
3201
|
-
assert.equal(result.details.result.scriptProducer.status, "unavailable");
|
|
3202
|
-
assert.doesNotMatch(JSON.stringify(result), /RAW_PI_SCRIPT/);
|
|
2439
|
+
await freeflowCommand.definition.handler("settings repo", settingsCtx);
|
|
2440
|
+
assert.deepEqual(JSON.parse(await readFile(join(cwd, ".freeflow/config.json"), "utf8")), original);
|
|
2441
|
+
assert.equal(settingsCtx.reloads.length, 0);
|
|
3203
2442
|
} finally {
|
|
3204
2443
|
await rm(cwd, { recursive: true, force: true });
|
|
3205
2444
|
}
|
|
3206
2445
|
});
|
|
3207
2446
|
|
|
3208
|
-
test("Pi
|
|
3209
|
-
const cwd = await mkdtemp(join(tmpdir(), "freeflow-pi-
|
|
2447
|
+
test("Pi Cognitive Routing settings refuse mid-run mutation", async () => {
|
|
2448
|
+
const cwd = await mkdtemp(join(tmpdir(), "freeflow-pi-cognitive-busy-"));
|
|
3210
2449
|
try {
|
|
3211
2450
|
await mkdir(join(cwd, ".freeflow"));
|
|
3212
|
-
|
|
3213
|
-
|
|
3214
|
-
|
|
3215
|
-
|
|
3216
|
-
|
|
3217
|
-
|
|
3218
|
-
|
|
3219
|
-
)
|
|
3220
|
-
|
|
3221
|
-
const tools = [];
|
|
3222
|
-
const pi = {
|
|
3223
|
-
registerTool(tool) {
|
|
3224
|
-
tools.push(tool);
|
|
3225
|
-
},
|
|
3226
|
-
registerCommand() {},
|
|
3227
|
-
on() {},
|
|
3228
|
-
appendEntry() {},
|
|
3229
|
-
sendUserMessage() {},
|
|
3230
|
-
async exec(_bin, args) {
|
|
3231
|
-
const command = args?.[1] ?? "unknown";
|
|
3232
|
-
const prefix = command.includes("one") ? "ONE" : "TWO";
|
|
3233
|
-
return {
|
|
3234
|
-
stdout:
|
|
3235
|
-
Array.from({ length: 20 }, (_, index) => `${prefix}_VISIBLE_BATCH_SENTINEL_${index + 1}`).join("\n") + "\n",
|
|
3236
|
-
stderr: "",
|
|
3237
|
-
code: 0,
|
|
3238
|
-
killed: false,
|
|
3239
|
-
};
|
|
3240
|
-
},
|
|
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");
|
|
3241
2460
|
};
|
|
3242
|
-
freeflowExtension(pi);
|
|
3243
|
-
const runTool = tools.find((tool) => tool.name === "freeflow_run");
|
|
3244
|
-
const batchTool = tools.find((tool) => tool.name === "freeflow_batch");
|
|
3245
|
-
assert.ok(runTool);
|
|
3246
|
-
assert.ok(batchTool);
|
|
3247
|
-
|
|
3248
|
-
const batchCtx = context(cwd);
|
|
3249
|
-
batchCtx.sessionManager.getSessionId = () => "batch-compact";
|
|
3250
|
-
const batch = await batchTool.execute(
|
|
3251
|
-
"batch-call",
|
|
3252
|
-
{
|
|
3253
|
-
steps: [
|
|
3254
|
-
{ id: "one", kind: "run", input: { command: "fixture one" } },
|
|
3255
|
-
{ id: "two", kind: "run", input: { command: "fixture two" } },
|
|
3256
|
-
],
|
|
3257
|
-
},
|
|
3258
|
-
undefined,
|
|
3259
|
-
undefined,
|
|
3260
|
-
batchCtx,
|
|
3261
|
-
);
|
|
3262
2461
|
|
|
3263
|
-
|
|
3264
|
-
|
|
3265
|
-
|
|
3266
|
-
const separateTwo = await runTool.execute("run-two", { command: "fixture two" }, undefined, undefined, separateCtx);
|
|
3267
|
-
const separateVisible = `${separateOne.content[0].text}\n${separateTwo.content[0].text}`;
|
|
3268
|
-
const visibleText = batch.content[0].text;
|
|
3269
|
-
|
|
3270
|
-
assert.match(visibleText, /freeflow_batch\|routed/);
|
|
3271
|
-
assert.match(visibleText, /steps=2/);
|
|
3272
|
-
assert.match(visibleText, /details\.result\.steps/);
|
|
3273
|
-
assert.doesNotMatch(visibleText, /^\s*\{/);
|
|
3274
|
-
assert.doesNotMatch(visibleText, /VISIBLE_BATCH_SENTINEL/);
|
|
3275
|
-
assert.ok(Buffer.byteLength(visibleText, "utf8") < Buffer.byteLength(separateVisible, "utf8"));
|
|
3276
|
-
|
|
3277
|
-
const payload = batch.details.result;
|
|
3278
|
-
assert.equal(payload.stepCount, 2);
|
|
3279
|
-
assert.equal(payload.okCount, 2);
|
|
3280
|
-
assert.equal(payload.failedCount, 0);
|
|
3281
|
-
assert.equal(payload.steps[0].result.importantLines[0].excerpt.includes("ONE_VISIBLE_BATCH_SENTINEL_1"), true);
|
|
3282
|
-
assert.equal(payload.steps[1].result.importantLines[0].excerpt.includes("TWO_VISIBLE_BATCH_SENTINEL_1"), true);
|
|
3283
|
-
|
|
3284
|
-
const expanded = renderText(batchTool.renderResult(batch, { expanded: true }, testTheme));
|
|
3285
|
-
assert.match(expanded, /Steps/);
|
|
3286
|
-
assert.match(expanded, /#1 one/);
|
|
3287
|
-
assert.match(expanded, /#2 two/);
|
|
3288
|
-
assert.match(expanded, /details\.result\.steps/);
|
|
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);
|
|
3289
2465
|
} finally {
|
|
3290
2466
|
await rm(cwd, { recursive: true, force: true });
|
|
3291
2467
|
}
|
|
3292
2468
|
});
|
|
3293
2469
|
|
|
3294
|
-
test("Pi
|
|
3295
|
-
const cwd = await mkdtemp(join(tmpdir(), "freeflow-pi-
|
|
2470
|
+
test("Pi settings recheck idle state before committing a selection", async () => {
|
|
2471
|
+
const cwd = await mkdtemp(join(tmpdir(), "freeflow-pi-settings-busy-commit-"));
|
|
3296
2472
|
try {
|
|
3297
2473
|
await mkdir(join(cwd, ".freeflow"));
|
|
3298
|
-
|
|
3299
|
-
|
|
3300
|
-
|
|
3301
|
-
|
|
3302
|
-
|
|
3303
|
-
|
|
3304
|
-
|
|
3305
|
-
);
|
|
3306
|
-
|
|
3307
|
-
|
|
3308
|
-
|
|
3309
|
-
|
|
3310
|
-
|
|
3311
|
-
|
|
3312
|
-
|
|
3313
|
-
|
|
3314
|
-
|
|
3315
|
-
|
|
3316
|
-
|
|
3317
|
-
|
|
3318
|
-
|
|
3319
|
-
stderr: "",
|
|
3320
|
-
code: 0,
|
|
3321
|
-
killed: false,
|
|
3322
|
-
};
|
|
3323
|
-
},
|
|
2474
|
+
const original = JSON.stringify({ defaultMode: "workflow" }, null, 2);
|
|
2475
|
+
await writeFile(join(cwd, ".freeflow/config.json"), original);
|
|
2476
|
+
const { commands } = loadExtension();
|
|
2477
|
+
const freeflowCommand = commands.find((command) => command.name === "freeflow");
|
|
2478
|
+
assert.ok(freeflowCommand);
|
|
2479
|
+
const settingsCtx = context(cwd);
|
|
2480
|
+
let idle = true;
|
|
2481
|
+
settingsCtx.isIdle = () => idle;
|
|
2482
|
+
settingsCtx.ui.custom = async (factory) => {
|
|
2483
|
+
let result;
|
|
2484
|
+
const component = factory({ requestRender() {} }, testTheme, {}, (value) => {
|
|
2485
|
+
result = value;
|
|
2486
|
+
});
|
|
2487
|
+
idle = false;
|
|
2488
|
+
for (let index = 0; index < 4; index++) component.handleInput("\u001b[B");
|
|
2489
|
+
component.handleInput("\r");
|
|
2490
|
+
component.handleInput("\u001b[B");
|
|
2491
|
+
component.handleInput("\r");
|
|
2492
|
+
await component.waitForWrites();
|
|
2493
|
+
component.handleInput("\u001b");
|
|
2494
|
+
return result;
|
|
3324
2495
|
};
|
|
3325
|
-
freeflowExtension(pi);
|
|
3326
|
-
const batchTool = tools.find((tool) => tool.name === "freeflow_batch");
|
|
3327
|
-
assert.ok(batchTool);
|
|
3328
|
-
assert.ok(batchTool.parameters.properties.queries);
|
|
3329
|
-
|
|
3330
|
-
const batchCtx = context(cwd);
|
|
3331
|
-
batchCtx.sessionManager.getSessionId = () => "batch-query-render";
|
|
3332
|
-
const batch = await batchTool.execute(
|
|
3333
|
-
"batch-query-call",
|
|
3334
|
-
{
|
|
3335
|
-
queries: ["BATCH_QUERY_VISIBLE_FACT_99"],
|
|
3336
|
-
steps: [
|
|
3337
|
-
{
|
|
3338
|
-
id: "fact",
|
|
3339
|
-
kind: "run",
|
|
3340
|
-
input: { command: "fixture fact", preserve: "full" },
|
|
3341
|
-
},
|
|
3342
|
-
],
|
|
3343
|
-
},
|
|
3344
|
-
undefined,
|
|
3345
|
-
undefined,
|
|
3346
|
-
batchCtx,
|
|
3347
|
-
);
|
|
3348
2496
|
|
|
3349
|
-
|
|
3350
|
-
assert.match(
|
|
3351
|
-
assert.
|
|
3352
|
-
assert.equal(batch.details.result.queries[0].status, "answered");
|
|
3353
|
-
|
|
3354
|
-
const expanded = renderText(batchTool.renderResult(batch, { expanded: true }, testTheme));
|
|
3355
|
-
assert.match(expanded, /Query answers/);
|
|
3356
|
-
assert.match(expanded, /BATCH_QUERY_VISIBLE_FACT_99/);
|
|
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);
|
|
3357
2500
|
} finally {
|
|
3358
2501
|
await rm(cwd, { recursive: true, force: true });
|
|
3359
2502
|
}
|
|
3360
2503
|
});
|
|
3361
2504
|
|
|
3362
|
-
test("Pi
|
|
3363
|
-
const cwd = await mkdtemp(join(tmpdir(), "freeflow-pi-
|
|
2505
|
+
test("Pi before_agent_start does not inject retired Output Router context", async () => {
|
|
2506
|
+
const cwd = await mkdtemp(join(tmpdir(), "freeflow-pi-router-default-disabled-"));
|
|
3364
2507
|
try {
|
|
3365
2508
|
await mkdir(join(cwd, ".freeflow"));
|
|
3366
|
-
await writeFile(
|
|
3367
|
-
join(cwd, ".freeflow/config.json"),
|
|
3368
|
-
JSON.stringify({
|
|
3369
|
-
defaultMode: "workflow",
|
|
3370
|
-
outputRouter: {
|
|
3371
|
-
enabled: true,
|
|
3372
|
-
largeOutputLines: 1,
|
|
3373
|
-
largeOutputBytes: 10_000,
|
|
3374
|
-
vaultRoot: join(cwd, "vault"),
|
|
3375
|
-
},
|
|
3376
|
-
}),
|
|
3377
|
-
"utf8",
|
|
3378
|
-
);
|
|
3379
|
-
|
|
3380
|
-
const tools = [];
|
|
3381
|
-
const pi = {
|
|
3382
|
-
registerTool(tool) {
|
|
3383
|
-
tools.push(tool);
|
|
3384
|
-
},
|
|
3385
|
-
registerCommand() {},
|
|
3386
|
-
on() {},
|
|
3387
|
-
appendEntry() {},
|
|
3388
|
-
sendUserMessage() {},
|
|
3389
|
-
async exec() {
|
|
3390
|
-
return { stdout: "one\ntwo\n", stderr: "", code: 0, killed: false };
|
|
3391
|
-
},
|
|
3392
|
-
};
|
|
3393
|
-
freeflowExtension(pi);
|
|
3394
|
-
const runTool = tools.find((tool) => tool.name === "freeflow_run");
|
|
3395
|
-
assert.ok(runTool);
|
|
3396
|
-
|
|
3397
|
-
const result = await runTool.execute("tool-call", { command: "fixture" }, undefined, undefined, context(cwd));
|
|
3398
|
-
const visibleText = result.content[0].text;
|
|
3399
|
-
const payload = result.details.result;
|
|
3400
|
-
|
|
3401
|
-
assert.match(visibleText, /freeflow_run\|success/);
|
|
3402
|
-
assert.match(visibleText, /route=partial/);
|
|
3403
|
-
assert.match(visibleText, /raw=ffout_/);
|
|
3404
|
-
assert.doesNotMatch(visibleText, /^\s*\{/);
|
|
3405
|
-
assert.equal(payload.toolStatus, "ok");
|
|
3406
|
-
assert.equal(payload.routing.status, "partial");
|
|
3407
|
-
assert.ok(payload.recovery.outputId.startsWith("ffout_"));
|
|
3408
|
-
} finally {
|
|
3409
|
-
await rm(cwd, { recursive: true, force: true });
|
|
3410
|
-
}
|
|
3411
|
-
});
|
|
2509
|
+
await writeFile(join(cwd, ".freeflow/config.json"), JSON.stringify({ defaultMode: "workflow" }, null, 2), "utf8");
|
|
3412
2510
|
|
|
3413
|
-
|
|
3414
|
-
|
|
3415
|
-
|
|
3416
|
-
await mkdir(join(cwd, ".freeflow"));
|
|
3417
|
-
await mkdir(join(cwd, "custom-generated"));
|
|
3418
|
-
await writeFile(
|
|
3419
|
-
join(cwd, ".freeflow/config.json"),
|
|
3420
|
-
JSON.stringify({
|
|
3421
|
-
defaultMode: "workflow",
|
|
3422
|
-
outputRouter: {
|
|
3423
|
-
enabled: true,
|
|
3424
|
-
generatedPaths: ["custom-generated/**"],
|
|
3425
|
-
},
|
|
3426
|
-
}),
|
|
3427
|
-
"utf8",
|
|
3428
|
-
);
|
|
3429
|
-
await writeFile(join(cwd, "target.md"), "PI_GENERATED_HINT_MARKER source truth", "utf8");
|
|
3430
|
-
await writeFile(
|
|
3431
|
-
join(cwd, "custom-generated", "decoy.md"),
|
|
3432
|
-
`${"PI_GENERATED_HINT_MARKER source truth ".repeat(1000)}pihintsentinel`,
|
|
3433
|
-
"utf8",
|
|
3434
|
-
);
|
|
2511
|
+
const { handlers } = loadExtension();
|
|
2512
|
+
const beforeAgentStart = handlers.get("before_agent_start");
|
|
2513
|
+
assert.ok(beforeAgentStart);
|
|
3435
2514
|
|
|
3436
|
-
const {
|
|
3437
|
-
const searchTool = tools.find((tool) => tool.name === "freeflow_search");
|
|
3438
|
-
assert.ok(searchTool);
|
|
2515
|
+
const result = await beforeAgentStart({ systemPrompt: "base prompt" }, context(cwd));
|
|
3439
2516
|
|
|
3440
|
-
|
|
3441
|
-
|
|
3442
|
-
|
|
3443
|
-
|
|
3444
|
-
|
|
3445
|
-
|
|
3446
|
-
|
|
3447
|
-
|
|
3448
|
-
undefined,
|
|
3449
|
-
context(cwd),
|
|
3450
|
-
);
|
|
3451
|
-
const broadVisible = broad.content[0].text;
|
|
3452
|
-
const broadPayload = broad.details.result;
|
|
3453
|
-
assert.match(broadVisible, /freeflow_search\|routed/);
|
|
3454
|
-
assert.doesNotMatch(broadVisible, /^\s*\{/);
|
|
3455
|
-
assert.ok(
|
|
3456
|
-
Buffer.byteLength(broadVisible, "utf8") < Buffer.byteLength(JSON.stringify(broadPayload, null, 2), "utf8"),
|
|
3457
|
-
);
|
|
3458
|
-
assert.equal(broadPayload.evidence[0].path, "target.md");
|
|
3459
|
-
assert.doesNotMatch(broadPayload.evidence[0].excerpt, /pihintsentinel/);
|
|
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/);
|
|
3460
2525
|
|
|
3461
|
-
|
|
3462
|
-
|
|
3463
|
-
|
|
3464
|
-
|
|
3465
|
-
|
|
3466
|
-
|
|
3467
|
-
|
|
3468
|
-
|
|
3469
|
-
|
|
3470
|
-
|
|
3471
|
-
);
|
|
3472
|
-
const explicitPayload = explicit.details.result;
|
|
3473
|
-
assert.equal(explicitPayload.evidence[0].path, "custom-generated/decoy.md");
|
|
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/);
|
|
3474
2537
|
} finally {
|
|
3475
2538
|
await rm(cwd, { recursive: true, force: true });
|
|
3476
2539
|
}
|
|
3477
2540
|
});
|
|
3478
2541
|
|
|
3479
|
-
test("Pi
|
|
3480
|
-
const cwd = await mkdtemp(join(tmpdir(), "freeflow-pi-
|
|
2542
|
+
test("Pi before_agent_start keeps the per-turn system context to the compact interaction contract", async () => {
|
|
2543
|
+
const cwd = await mkdtemp(join(tmpdir(), "freeflow-pi-interaction-contract-"));
|
|
3481
2544
|
try {
|
|
3482
2545
|
await mkdir(join(cwd, ".freeflow"));
|
|
3483
|
-
|
|
3484
|
-
await writeFile(
|
|
3485
|
-
join(cwd, ".freeflow/config.json"),
|
|
3486
|
-
JSON.stringify({
|
|
3487
|
-
defaultMode: "workflow",
|
|
3488
|
-
outputRouter: { enabled: true, vaultRoot },
|
|
3489
|
-
}),
|
|
3490
|
-
"utf8",
|
|
3491
|
-
);
|
|
2546
|
+
await writeFile(join(cwd, ".freeflow/config.json"), JSON.stringify({ defaultMode: "workflow" }, null, 2), "utf8");
|
|
3492
2547
|
|
|
3493
|
-
const
|
|
3494
|
-
const
|
|
3495
|
-
|
|
3496
|
-
sourceKind: "mcp",
|
|
3497
|
-
raw: "PI_VAULT_WIDE_TARGET through registered Pi tool",
|
|
3498
|
-
producer: { kind: "mcp", server: "github", tool: "search_issues" },
|
|
3499
|
-
createdAt: "2026-06-16T00:00:00.000Z",
|
|
3500
|
-
});
|
|
2548
|
+
const { handlers } = loadExtension();
|
|
2549
|
+
const beforeAgentStart = handlers.get("before_agent_start");
|
|
2550
|
+
assert.ok(beforeAgentStart);
|
|
3501
2551
|
|
|
3502
|
-
const {
|
|
3503
|
-
const searchTool = tools.find((tool) => tool.name === "freeflow_search");
|
|
3504
|
-
assert.ok(searchTool);
|
|
3505
|
-
const ctx = context(cwd);
|
|
3506
|
-
ctx.sessionManager.getSessionId = () => sessionId;
|
|
2552
|
+
const result = await beforeAgentStart({ systemPrompt: "base prompt" }, context(cwd));
|
|
3507
2553
|
|
|
3508
|
-
|
|
3509
|
-
|
|
3510
|
-
|
|
3511
|
-
|
|
3512
|
-
|
|
3513
|
-
|
|
3514
|
-
|
|
3515
|
-
|
|
3516
|
-
|
|
3517
|
-
|
|
3518
|
-
|
|
3519
|
-
);
|
|
3520
|
-
|
|
3521
|
-
|
|
3522
|
-
assert.
|
|
3523
|
-
assert.
|
|
3524
|
-
assert.doesNotMatch(visibleText, /^\s*\{/);
|
|
3525
|
-
assert.equal(payload.toolStatus, "ok");
|
|
3526
|
-
assert.equal(payload.evidence[0].source.outputId, stored.outputId);
|
|
3527
|
-
assert.equal(payload.evidence[0].source.stream, "raw");
|
|
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/);
|
|
3528
2570
|
} finally {
|
|
3529
2571
|
await rm(cwd, { recursive: true, force: true });
|
|
3530
2572
|
}
|
|
3531
2573
|
});
|
|
3532
2574
|
|
|
3533
|
-
test("Pi
|
|
3534
|
-
const cwd = await mkdtemp(join(tmpdir(), "freeflow-pi-
|
|
2575
|
+
test("Pi exposes Workflow through discovery rather than a persistent bootstrap", async () => {
|
|
2576
|
+
const cwd = await mkdtemp(join(tmpdir(), "freeflow-pi-workflow-discovery-"));
|
|
3535
2577
|
try {
|
|
3536
2578
|
await mkdir(join(cwd, ".freeflow"));
|
|
3537
|
-
await writeFile(
|
|
3538
|
-
join(cwd, ".freeflow/config.json"),
|
|
3539
|
-
JSON.stringify({
|
|
3540
|
-
defaultMode: "workflow",
|
|
3541
|
-
outputRouter: { postToolRouting: "always" },
|
|
3542
|
-
}),
|
|
3543
|
-
"utf8",
|
|
3544
|
-
);
|
|
2579
|
+
await writeFile(join(cwd, ".freeflow/config.json"), JSON.stringify({ defaultMode: "workflow" }), "utf8");
|
|
3545
2580
|
|
|
3546
2581
|
const { handlers } = loadExtension();
|
|
3547
|
-
const notifications = [];
|
|
3548
2582
|
const ctx = context(cwd);
|
|
3549
|
-
|
|
3550
|
-
|
|
2583
|
+
const resources = await handlers.get("resources_discover")({ cwd }, ctx);
|
|
2584
|
+
assert.ok(resources.skillPaths.some((path) => path.endsWith("/skills/workflow/SKILL.md")));
|
|
3551
2585
|
|
|
3552
|
-
|
|
3553
|
-
assert.
|
|
2586
|
+
const result = await handlers.get("before_agent_start")({ prompt: "hi", systemPrompt: "base prompt" }, ctx);
|
|
2587
|
+
assert.equal(result.message, undefined);
|
|
2588
|
+
assert.match(result.systemPrompt, /## Workflow Cue/);
|
|
2589
|
+
assert.doesNotMatch(result.systemPrompt, /^# Workflow$/m);
|
|
2590
|
+
assert.doesNotMatch(result.systemPrompt, /freeflow-workflow-bootstrap|Freeflow Workflow Bootstrap/);
|
|
3554
2591
|
} finally {
|
|
3555
2592
|
await rm(cwd, { recursive: true, force: true });
|
|
3556
2593
|
}
|
|
3557
2594
|
});
|
|
3558
2595
|
|
|
3559
|
-
test("Pi
|
|
3560
|
-
const cwd = await mkdtemp(join(tmpdir(), "freeflow-pi-
|
|
2596
|
+
test("Pi suppresses persisted Workflow context while Skills are disabled", async () => {
|
|
2597
|
+
const cwd = await mkdtemp(join(tmpdir(), "freeflow-pi-workflow-bootstrap-disabled-"));
|
|
3561
2598
|
try {
|
|
3562
2599
|
await mkdir(join(cwd, ".freeflow"));
|
|
2600
|
+
const configPath = join(cwd, ".freeflow/config.json");
|
|
3563
2601
|
await writeFile(
|
|
3564
|
-
|
|
3565
|
-
JSON.stringify({
|
|
3566
|
-
defaultMode: "workflow",
|
|
3567
|
-
outputRouter: {
|
|
3568
|
-
enabled: true,
|
|
3569
|
-
vaultRoot: join(cwd, "vault"),
|
|
3570
|
-
observedRouting: {
|
|
3571
|
-
enabled: true,
|
|
3572
|
-
mcp: {
|
|
3573
|
-
servers: { github: { enabled: true, persistence: "exact" } },
|
|
3574
|
-
},
|
|
3575
|
-
},
|
|
3576
|
-
},
|
|
3577
|
-
}),
|
|
2602
|
+
configPath,
|
|
2603
|
+
JSON.stringify({ defaultMode: "workflow", skills: { enabled: false } }, null, 2),
|
|
3578
2604
|
"utf8",
|
|
3579
2605
|
);
|
|
3580
2606
|
|
|
3581
2607
|
const { handlers } = loadExtension();
|
|
3582
|
-
const
|
|
3583
|
-
|
|
3584
|
-
{
|
|
3585
|
-
type: "tool_result",
|
|
3586
|
-
toolName: "mcp",
|
|
3587
|
-
toolCallId: "mcp-1",
|
|
3588
|
-
input: { server: "github", tool: "search_issues" },
|
|
3589
|
-
content: [
|
|
3590
|
-
{
|
|
3591
|
-
type: "text",
|
|
3592
|
-
text: JSON.stringify({
|
|
3593
|
-
items: [
|
|
3594
|
-
{
|
|
3595
|
-
id: 1,
|
|
3596
|
-
title: "Alpha",
|
|
3597
|
-
html_url: "https://github.com/acme/repo/issues/1",
|
|
3598
|
-
body: "x".repeat(500),
|
|
3599
|
-
},
|
|
3600
|
-
{
|
|
3601
|
-
id: 2,
|
|
3602
|
-
title: "Beta",
|
|
3603
|
-
html_url: "https://github.com/acme/repo/issues/2",
|
|
3604
|
-
body: "y".repeat(500),
|
|
3605
|
-
},
|
|
3606
|
-
{
|
|
3607
|
-
id: 3,
|
|
3608
|
-
title: "Gamma",
|
|
3609
|
-
html_url: "https://github.com/acme/repo/issues/3",
|
|
3610
|
-
body: "z".repeat(500),
|
|
3611
|
-
},
|
|
3612
|
-
{
|
|
3613
|
-
id: 4,
|
|
3614
|
-
title: "Delta",
|
|
3615
|
-
html_url: "https://github.com/acme/repo/issues/4",
|
|
3616
|
-
body: "w".repeat(500),
|
|
3617
|
-
},
|
|
3618
|
-
],
|
|
3619
|
-
}),
|
|
3620
|
-
},
|
|
3621
|
-
],
|
|
3622
|
-
details: undefined,
|
|
3623
|
-
isError: false,
|
|
3624
|
-
},
|
|
3625
|
-
context(cwd),
|
|
3626
|
-
);
|
|
3627
|
-
|
|
3628
|
-
assert.ok(result);
|
|
3629
|
-
assert.match(result.content[0].text, /Freeflow routed this observed mcp result/);
|
|
3630
|
-
assert.match(result.content[0].text, /outputId=ffout_/);
|
|
3631
|
-
assert.match(result.content[0].text, /Alpha/);
|
|
3632
|
-
assert.match(result.content[0].text, /https:\/\/github\.com\/acme\/repo\/issues\/1/);
|
|
3633
|
-
assert.doesNotMatch(result.content[0].text, /xxxxxxxxxxxxxxxx/);
|
|
3634
|
-
assert.equal(result.details.freeflowObservedRouting.route, "observed");
|
|
3635
|
-
assert.equal(result.details.freeflowObservedRouting.producer.server, "github");
|
|
3636
|
-
} finally {
|
|
3637
|
-
await rm(cwd, { recursive: true, force: true });
|
|
3638
|
-
}
|
|
3639
|
-
});
|
|
2608
|
+
const contextHandler = handlers.get("context");
|
|
2609
|
+
assert.ok(contextHandler);
|
|
3640
2610
|
|
|
3641
|
-
|
|
3642
|
-
|
|
3643
|
-
|
|
3644
|
-
|
|
3645
|
-
|
|
3646
|
-
|
|
3647
|
-
|
|
3648
|
-
|
|
3649
|
-
|
|
3650
|
-
|
|
3651
|
-
|
|
3652
|
-
|
|
3653
|
-
|
|
3654
|
-
|
|
3655
|
-
|
|
3656
|
-
|
|
3657
|
-
"utf8",
|
|
2611
|
+
const workflowMessage = {
|
|
2612
|
+
role: "custom",
|
|
2613
|
+
customType: "freeflow-workflow-bootstrap",
|
|
2614
|
+
content: "# Freeflow Workflow Bootstrap",
|
|
2615
|
+
display: false,
|
|
2616
|
+
timestamp: Date.now(),
|
|
2617
|
+
};
|
|
2618
|
+
const userMessage = {
|
|
2619
|
+
role: "user",
|
|
2620
|
+
content: "hello",
|
|
2621
|
+
timestamp: Date.now(),
|
|
2622
|
+
};
|
|
2623
|
+
const disabled = await contextHandler({ messages: [workflowMessage, userMessage] }, context(cwd));
|
|
2624
|
+
assert.deepEqual(
|
|
2625
|
+
disabled.messages.filter((message) => message.customType !== "freeflow-runtime-state"),
|
|
2626
|
+
[userMessage],
|
|
3658
2627
|
);
|
|
2628
|
+
assert.equal(disabled.messages.at(-1).customType, "freeflow-runtime-state");
|
|
3659
2629
|
|
|
3660
|
-
|
|
3661
|
-
const
|
|
3662
|
-
|
|
3663
|
-
|
|
3664
|
-
|
|
3665
|
-
|
|
3666
|
-
input: { server: "github", tool: "search_issues" },
|
|
3667
|
-
content: [{ type: "text", text: "unchanged" }],
|
|
3668
|
-
details: undefined,
|
|
3669
|
-
isError: false,
|
|
3670
|
-
},
|
|
3671
|
-
context(cwd),
|
|
2630
|
+
await writeFile(configPath, JSON.stringify({ defaultMode: "workflow" }, null, 2), "utf8");
|
|
2631
|
+
const enabled = await contextHandler({ messages: [workflowMessage, userMessage] }, context(cwd));
|
|
2632
|
+
assert.ok(enabled);
|
|
2633
|
+
assert.deepEqual(
|
|
2634
|
+
enabled.messages.filter((message) => message.customType !== "freeflow-runtime-state"),
|
|
2635
|
+
[userMessage],
|
|
3672
2636
|
);
|
|
3673
|
-
|
|
3674
|
-
assert.equal(result, undefined);
|
|
2637
|
+
assert.equal(enabled.messages.at(-1).customType, "freeflow-runtime-state");
|
|
3675
2638
|
} finally {
|
|
3676
2639
|
await rm(cwd, { recursive: true, force: true });
|
|
3677
2640
|
}
|
|
3678
2641
|
});
|
|
3679
2642
|
|
|
3680
|
-
test("Pi
|
|
3681
|
-
const cwd = await mkdtemp(join(tmpdir(), "freeflow-pi-
|
|
2643
|
+
test("Pi suppresses persisted Cognitive Routing bootstrap context when routing is disabled", async () => {
|
|
2644
|
+
const cwd = await mkdtemp(join(tmpdir(), "freeflow-pi-cognitive-bootstrap-disabled-"));
|
|
3682
2645
|
try {
|
|
3683
2646
|
await mkdir(join(cwd, ".freeflow"));
|
|
3684
|
-
|
|
3685
|
-
await writeFile(fileVault, "not a directory", "utf8");
|
|
3686
|
-
await writeFile(
|
|
3687
|
-
join(cwd, ".freeflow/config.json"),
|
|
3688
|
-
JSON.stringify({
|
|
3689
|
-
defaultMode: "workflow",
|
|
3690
|
-
outputRouter: {
|
|
3691
|
-
enabled: true,
|
|
3692
|
-
vaultRoot: fileVault,
|
|
3693
|
-
observedRouting: {
|
|
3694
|
-
enabled: true,
|
|
3695
|
-
mcp: {
|
|
3696
|
-
servers: { github: { enabled: true, persistence: "exact" } },
|
|
3697
|
-
},
|
|
3698
|
-
},
|
|
3699
|
-
},
|
|
3700
|
-
}),
|
|
3701
|
-
"utf8",
|
|
3702
|
-
);
|
|
2647
|
+
await writeFile(join(cwd, ".freeflow/config.json"), JSON.stringify({ defaultMode: "workflow" }, null, 2), "utf8");
|
|
3703
2648
|
|
|
3704
2649
|
const { handlers } = loadExtension();
|
|
3705
|
-
const
|
|
3706
|
-
|
|
3707
|
-
type: "tool_result",
|
|
3708
|
-
toolName: "mcp",
|
|
3709
|
-
toolCallId: "mcp-fail-open",
|
|
3710
|
-
input: { server: "github", tool: "search_issues" },
|
|
3711
|
-
content: [{ type: "text", text: "original mcp output survives" }],
|
|
3712
|
-
details: undefined,
|
|
3713
|
-
isError: false,
|
|
3714
|
-
},
|
|
3715
|
-
context(cwd),
|
|
3716
|
-
);
|
|
2650
|
+
const contextHandler = handlers.get("context");
|
|
2651
|
+
assert.ok(contextHandler);
|
|
3717
2652
|
|
|
3718
|
-
|
|
3719
|
-
|
|
3720
|
-
|
|
3721
|
-
|
|
2653
|
+
const cognitiveMessage = {
|
|
2654
|
+
role: "custom",
|
|
2655
|
+
customType: "freeflow-cognitive-routing-bootstrap",
|
|
2656
|
+
content: "# Freeflow Cognitive Routing Bootstrap",
|
|
2657
|
+
display: false,
|
|
2658
|
+
timestamp: Date.now(),
|
|
2659
|
+
};
|
|
2660
|
+
const userMessage = {
|
|
2661
|
+
role: "user",
|
|
2662
|
+
content: "hello",
|
|
2663
|
+
timestamp: Date.now(),
|
|
2664
|
+
};
|
|
2665
|
+
const disabled = await contextHandler({ messages: [cognitiveMessage, userMessage] }, context(cwd));
|
|
2666
|
+
assert.deepEqual(
|
|
2667
|
+
disabled.messages.filter((message) => message.customType !== "freeflow-runtime-state"),
|
|
2668
|
+
[userMessage],
|
|
2669
|
+
);
|
|
2670
|
+
assert.equal(disabled.messages.at(-1).customType, "freeflow-runtime-state");
|
|
3722
2671
|
} finally {
|
|
3723
2672
|
await rm(cwd, { recursive: true, force: true });
|
|
3724
2673
|
}
|
|
3725
2674
|
});
|
|
3726
2675
|
|
|
3727
|
-
test("Pi
|
|
3728
|
-
const
|
|
3729
|
-
const toolResult = handlers.get("tool_result");
|
|
3730
|
-
assert.ok(toolResult);
|
|
3731
|
-
|
|
3732
|
-
const raw = Array.from({ length: 20 }, (_, index) => `line ${index + 1}`).join("\n");
|
|
3733
|
-
const result = await toolResult(
|
|
3734
|
-
{
|
|
3735
|
-
type: "tool_result",
|
|
3736
|
-
toolName: "read",
|
|
3737
|
-
toolCallId: "read-1",
|
|
3738
|
-
input: { path: "large.txt" },
|
|
3739
|
-
content: [{ type: "text", text: raw }],
|
|
3740
|
-
details: undefined,
|
|
3741
|
-
isError: false,
|
|
3742
|
-
},
|
|
3743
|
-
context(),
|
|
3744
|
-
);
|
|
3745
|
-
|
|
3746
|
-
assert.equal(result, undefined);
|
|
3747
|
-
});
|
|
3748
|
-
|
|
3749
|
-
test("Pi post-tool safety net vaults and labels large native bash output when enabled", async () => {
|
|
3750
|
-
const cwd = await mkdtemp(join(tmpdir(), "freeflow-pi-safety-net-"));
|
|
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-"));
|
|
3751
2678
|
try {
|
|
3752
2679
|
await mkdir(join(cwd, ".freeflow"));
|
|
3753
2680
|
await writeFile(
|
|
3754
2681
|
join(cwd, ".freeflow/config.json"),
|
|
3755
|
-
JSON.stringify({
|
|
3756
|
-
defaultMode: "workflow",
|
|
3757
|
-
outputRouter: {
|
|
3758
|
-
enabled: true,
|
|
3759
|
-
postToolRouting: "safety-net",
|
|
3760
|
-
largeOutputLines: 3,
|
|
3761
|
-
largeOutputBytes: 100_000,
|
|
3762
|
-
vaultRoot: join(cwd, "vault"),
|
|
3763
|
-
},
|
|
3764
|
-
}),
|
|
2682
|
+
JSON.stringify({ defaultMode: "workflow", contextVirtualization: true, conversationHistory: true }, null, 2),
|
|
3765
2683
|
"utf8",
|
|
3766
2684
|
);
|
|
3767
2685
|
|
|
3768
|
-
const { handlers
|
|
3769
|
-
const
|
|
3770
|
-
|
|
3771
|
-
const result = await toolResult(
|
|
3772
|
-
{
|
|
3773
|
-
type: "tool_result",
|
|
3774
|
-
toolName: "bash",
|
|
3775
|
-
toolCallId: "bash-1",
|
|
3776
|
-
input: { command: "npm test" },
|
|
3777
|
-
content: [{ type: "text", text: raw }],
|
|
3778
|
-
details: undefined,
|
|
3779
|
-
isError: false,
|
|
3780
|
-
},
|
|
3781
|
-
context(cwd),
|
|
3782
|
-
);
|
|
2686
|
+
const { handlers } = loadExtension();
|
|
2687
|
+
const beforeAgentStart = handlers.get("before_agent_start");
|
|
2688
|
+
assert.ok(beforeAgentStart);
|
|
3783
2689
|
|
|
3784
|
-
|
|
3785
|
-
const
|
|
3786
|
-
|
|
3787
|
-
|
|
3788
|
-
|
|
3789
|
-
|
|
3790
|
-
|
|
3791
|
-
|
|
3792
|
-
|
|
3793
|
-
|
|
3794
|
-
|
|
3795
|
-
|
|
3796
|
-
|
|
3797
|
-
|
|
3798
|
-
|
|
3799
|
-
|
|
3800
|
-
|
|
3801
|
-
|
|
3802
|
-
|
|
3803
|
-
);
|
|
3804
|
-
const payload = searched.details.result;
|
|
3805
|
-
assert.doesNotMatch(searched.content[0].text, /^\s*\{/);
|
|
3806
|
-
assert.equal(payload.evidence[0].excerpt, "line 18\nline 19\nline 20");
|
|
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
|
+
}
|
|
3807
2709
|
} finally {
|
|
3808
2710
|
await rm(cwd, { recursive: true, force: true });
|
|
3809
2711
|
}
|
|
3810
2712
|
});
|
|
3811
2713
|
|
|
3812
|
-
test("Pi
|
|
3813
|
-
const cwd = await mkdtemp(join(tmpdir(), "freeflow-pi-
|
|
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-"));
|
|
3814
2716
|
try {
|
|
3815
2717
|
await mkdir(join(cwd, ".freeflow"));
|
|
3816
2718
|
await writeFile(
|
|
3817
2719
|
join(cwd, ".freeflow/config.json"),
|
|
3818
|
-
JSON.stringify(
|
|
3819
|
-
|
|
3820
|
-
|
|
3821
|
-
|
|
3822
|
-
|
|
3823
|
-
largeOutputLines: 3,
|
|
3824
|
-
largeOutputBytes: 100_000,
|
|
3825
|
-
vaultRoot: join(cwd, "vault"),
|
|
2720
|
+
JSON.stringify(
|
|
2721
|
+
{
|
|
2722
|
+
defaultMode: "workflow",
|
|
2723
|
+
contextVirtualization: true,
|
|
2724
|
+
conversationHistory: true,
|
|
3826
2725
|
},
|
|
3827
|
-
|
|
2726
|
+
null,
|
|
2727
|
+
2,
|
|
2728
|
+
),
|
|
3828
2729
|
"utf8",
|
|
3829
2730
|
);
|
|
3830
2731
|
|
|
3831
2732
|
const { handlers } = loadExtension();
|
|
3832
|
-
const
|
|
3833
|
-
const
|
|
3834
|
-
const
|
|
3835
|
-
|
|
3836
|
-
|
|
3837
|
-
|
|
3838
|
-
|
|
3839
|
-
|
|
3840
|
-
|
|
3841
|
-
|
|
3842
|
-
|
|
3843
|
-
|
|
3844
|
-
|
|
3845
|
-
);
|
|
3846
|
-
|
|
3847
|
-
assert.
|
|
3848
|
-
|
|
3849
|
-
const
|
|
3850
|
-
|
|
3851
|
-
|
|
3852
|
-
|
|
3853
|
-
|
|
3854
|
-
|
|
3855
|
-
|
|
3856
|
-
|
|
3857
|
-
|
|
3858
|
-
|
|
3859
|
-
|
|
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"),
|
|
3860
2774
|
);
|
|
3861
2775
|
|
|
3862
|
-
|
|
3863
|
-
|
|
3864
|
-
|
|
3865
|
-
);
|
|
3866
|
-
assert.equal(second.details.freeflowOutputRouter.duplicateOfOutputId, firstOutputId);
|
|
3867
|
-
assert.match(second.content[0].text, /current raw output was vaulted as outputId=ffout_/);
|
|
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");
|
|
3868
2779
|
} finally {
|
|
3869
2780
|
await rm(cwd, { recursive: true, force: true });
|
|
3870
2781
|
}
|
|
3871
2782
|
});
|
|
3872
2783
|
|
|
3873
|
-
test("Pi
|
|
3874
|
-
const cwd = await mkdtemp(join(tmpdir(), "freeflow-pi-
|
|
2784
|
+
test("Pi session_start and session_compact keep the interaction contract on later turns", async () => {
|
|
2785
|
+
const cwd = await mkdtemp(join(tmpdir(), "freeflow-pi-session-cache-"));
|
|
3875
2786
|
try {
|
|
3876
2787
|
await mkdir(join(cwd, ".freeflow"));
|
|
3877
|
-
await writeFile(
|
|
3878
|
-
join(cwd, ".freeflow/config.json"),
|
|
3879
|
-
JSON.stringify({
|
|
3880
|
-
defaultMode: "workflow",
|
|
3881
|
-
outputRouter: {
|
|
3882
|
-
enabled: true,
|
|
3883
|
-
postToolRouting: "safety-net",
|
|
3884
|
-
largeOutputLines: 100,
|
|
3885
|
-
largeOutputBytes: 10_000,
|
|
3886
|
-
},
|
|
3887
|
-
}),
|
|
3888
|
-
"utf8",
|
|
3889
|
-
);
|
|
2788
|
+
await writeFile(join(cwd, ".freeflow/config.json"), JSON.stringify({ defaultMode: "workflow" }, null, 2), "utf8");
|
|
3890
2789
|
|
|
3891
2790
|
const { handlers } = loadExtension();
|
|
3892
|
-
const
|
|
3893
|
-
|
|
3894
|
-
|
|
3895
|
-
|
|
3896
|
-
|
|
3897
|
-
|
|
3898
|
-
|
|
3899
|
-
|
|
3900
|
-
|
|
3901
|
-
|
|
3902
|
-
|
|
3903
|
-
);
|
|
2791
|
+
const beforeAgentStart = handlers.get("before_agent_start");
|
|
2792
|
+
const sessionStart = handlers.get("session_start");
|
|
2793
|
+
const sessionCompact = handlers.get("session_compact");
|
|
2794
|
+
assert.ok(beforeAgentStart);
|
|
2795
|
+
assert.ok(sessionStart);
|
|
2796
|
+
assert.ok(sessionCompact);
|
|
2797
|
+
|
|
2798
|
+
await beforeAgentStart({ systemPrompt: "base prompt" }, context(cwd));
|
|
2799
|
+
const afterFirst = await beforeAgentStart({ systemPrompt: "base prompt" }, context(cwd));
|
|
2800
|
+
assert.match(afterFirst.systemPrompt, /# Freeflow Interaction Contract/);
|
|
2801
|
+
|
|
2802
|
+
await sessionCompact({ reason: "manual" }, context(cwd));
|
|
2803
|
+
const afterCompact = await beforeAgentStart({ systemPrompt: "base prompt" }, context(cwd));
|
|
2804
|
+
assert.match(afterCompact.systemPrompt, /# Freeflow Interaction Contract/);
|
|
2805
|
+
assert.doesNotMatch(afterCompact.systemPrompt, /## Loaded Workflow Skill/);
|
|
2806
|
+
assert.doesNotMatch(afterCompact.systemPrompt, /Output Router|freeflow_(search|run|batch)/i);
|
|
3904
2807
|
|
|
3905
|
-
|
|
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);
|
|
3906
2813
|
} finally {
|
|
3907
2814
|
await rm(cwd, { recursive: true, force: true });
|
|
3908
2815
|
}
|
|
3909
2816
|
});
|
|
3910
2817
|
|
|
3911
|
-
test("Pi
|
|
3912
|
-
const cwd = await mkdtemp(join(tmpdir(), "freeflow-pi-
|
|
2818
|
+
test("Pi ignores retired router config without affecting core activation", async () => {
|
|
2819
|
+
const cwd = await mkdtemp(join(tmpdir(), "freeflow-pi-retired-router-config-"));
|
|
3913
2820
|
try {
|
|
3914
2821
|
await mkdir(join(cwd, ".freeflow"));
|
|
3915
|
-
const blockedVaultPath = join(cwd, "vault-file");
|
|
3916
|
-
await writeFile(blockedVaultPath, "not a directory", "utf8");
|
|
3917
2822
|
await writeFile(
|
|
3918
2823
|
join(cwd, ".freeflow/config.json"),
|
|
3919
2824
|
JSON.stringify({
|
|
3920
2825
|
defaultMode: "workflow",
|
|
3921
|
-
outputRouter: {
|
|
3922
|
-
enabled: true,
|
|
3923
|
-
postToolRouting: "safety-net",
|
|
3924
|
-
largeOutputLines: 1,
|
|
3925
|
-
largeOutputBytes: 1,
|
|
3926
|
-
vaultRoot: blockedVaultPath,
|
|
3927
|
-
},
|
|
2826
|
+
outputRouter: { postToolRouting: "always" },
|
|
3928
2827
|
}),
|
|
3929
2828
|
"utf8",
|
|
3930
2829
|
);
|
|
3931
2830
|
|
|
3932
2831
|
const { handlers } = loadExtension();
|
|
3933
|
-
const
|
|
3934
|
-
const
|
|
3935
|
-
|
|
3936
|
-
|
|
3937
|
-
|
|
3938
|
-
toolCallId: "read-fail-open",
|
|
3939
|
-
input: { path: "large.txt" },
|
|
3940
|
-
content: [{ type: "text", text: raw }],
|
|
3941
|
-
details: undefined,
|
|
3942
|
-
isError: false,
|
|
3943
|
-
},
|
|
3944
|
-
context(cwd),
|
|
3945
|
-
);
|
|
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);
|
|
3946
2837
|
|
|
3947
|
-
assert.
|
|
3948
|
-
assert.
|
|
3949
|
-
assert.
|
|
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);
|
|
3950
2841
|
} finally {
|
|
3951
2842
|
await rm(cwd, { recursive: true, force: true });
|
|
3952
2843
|
}
|
|
@@ -3960,7 +2851,6 @@ test("Pi appends the interaction contract after existing project context", async
|
|
|
3960
2851
|
join(cwd, ".freeflow/config.json"),
|
|
3961
2852
|
JSON.stringify({
|
|
3962
2853
|
defaultMode: "workflow",
|
|
3963
|
-
outputRouter: { enabled: true },
|
|
3964
2854
|
}),
|
|
3965
2855
|
"utf8",
|
|
3966
2856
|
);
|
|
@@ -3975,18 +2865,13 @@ test("Pi appends the interaction contract after existing project context", async
|
|
|
3975
2865
|
assert.doesNotMatch(result.systemPrompt, /# Freeflow Runtime Kernel/);
|
|
3976
2866
|
assert.doesNotMatch(result.systemPrompt, /## Loaded Workflow Skill/);
|
|
3977
2867
|
assert.doesNotMatch(result.systemPrompt, /## Discovery-light/);
|
|
3978
|
-
assert.
|
|
3979
|
-
assert.match(result.systemPrompt, /freeflow_search/);
|
|
3980
|
-
assert.match(result.systemPrompt, /freeflow_run/);
|
|
3981
|
-
assert.match(result.systemPrompt, /freeflow_search action=transform/);
|
|
3982
|
-
assert.doesNotMatch(result.systemPrompt, /## Freeflow Output Router Reminder/);
|
|
3983
|
-
assert.doesNotMatch(result.systemPrompt, /## Loaded Output Router Safety Policy/);
|
|
2868
|
+
assert.doesNotMatch(result.systemPrompt, /Output Router|freeflow_(search|run|batch)/i);
|
|
3984
2869
|
} finally {
|
|
3985
2870
|
await rm(cwd, { recursive: true, force: true });
|
|
3986
2871
|
}
|
|
3987
2872
|
});
|
|
3988
2873
|
|
|
3989
|
-
test("Pi keeps
|
|
2874
|
+
test("Pi keeps core capability context alongside the interaction contract", async () => {
|
|
3990
2875
|
const cwd = await mkdtemp(join(tmpdir(), "freeflow-pi-already-full-"));
|
|
3991
2876
|
try {
|
|
3992
2877
|
await mkdir(join(cwd, ".freeflow"));
|
|
@@ -3994,7 +2879,6 @@ test("Pi keeps enabled capability context alongside the interaction contract", a
|
|
|
3994
2879
|
join(cwd, ".freeflow/config.json"),
|
|
3995
2880
|
JSON.stringify({
|
|
3996
2881
|
defaultMode: "workflow",
|
|
3997
|
-
outputRouter: { enabled: true },
|
|
3998
2882
|
}),
|
|
3999
2883
|
"utf8",
|
|
4000
2884
|
);
|
|
@@ -4007,10 +2891,7 @@ test("Pi keeps enabled capability context alongside the interaction contract", a
|
|
|
4007
2891
|
assert.doesNotMatch(result.systemPrompt, /# Freeflow Runtime Kernel/);
|
|
4008
2892
|
assert.doesNotMatch(result.systemPrompt, /## Loaded Workflow Skill/);
|
|
4009
2893
|
assert.doesNotMatch(result.systemPrompt, /## Discovery-light/);
|
|
4010
|
-
assert.
|
|
4011
|
-
assert.match(result.systemPrompt, /name: output-router/);
|
|
4012
|
-
assert.doesNotMatch(result.systemPrompt, /## Freeflow Output Router Reminder/);
|
|
4013
|
-
assert.doesNotMatch(result.systemPrompt, /## Loaded Output Router Safety Policy/);
|
|
2894
|
+
assert.doesNotMatch(result.systemPrompt, /Output Router|freeflow_(search|run|batch)/i);
|
|
4014
2895
|
assert.doesNotMatch(result.systemPrompt, /Capture raw evidence before transformation/);
|
|
4015
2896
|
} finally {
|
|
4016
2897
|
await rm(cwd, { recursive: true, force: true });
|