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