@hassangameryt/freeflow 0.6.0 → 0.7.1
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 +3 -2
- package/.codex-plugin/plugin.json +5 -6
- package/.cursor-plugin/plugin.json +25 -0
- package/README.md +66 -56
- package/capabilities/cognitive-routing/SKILL.md +288 -130
- package/com.github.copilot/hooks/hooks.json +11 -0
- package/command-surface.json +0 -24
- package/gemini-extension.json +5 -0
- package/hooks/adapters/claude-session-start.mjs +16 -0
- package/hooks/adapters/codex-session-start.mjs +16 -0
- package/hooks/adapters/copilot-session-start.mjs +13 -0
- package/hooks/adapters/cursor-session-start.mjs +13 -0
- package/hooks/adapters/gemini-session-start.mjs +15 -0
- package/hooks/claude/hooks.json +18 -0
- package/hooks/codex/hooks.json +18 -0
- package/hooks/cursor/hooks.json +9 -0
- package/hooks/freeflow-runtime-context.mjs +3 -650
- package/hooks/hooks.json +4 -30
- package/hooks/shared/runtime-context.mjs +210 -0
- package/hooks/tests/check-host-adapters.test.mjs +82 -0
- package/hooks/tests/check-runtime-context-hook.sh +140 -320
- package/package.json +20 -4
- package/pi-extension/dist/cognitive-routing/commands.js +3 -3
- package/pi-extension/dist/cognitive-routing/config.js +16 -20
- package/pi-extension/dist/cognitive-routing/controller.js +478 -48
- package/pi-extension/dist/cognitive-routing/history.js +12 -0
- package/pi-extension/dist/cognitive-routing/host.js +74 -12
- package/pi-extension/dist/cognitive-routing/pi-routing-session.js +414 -0
- package/pi-extension/dist/cognitive-routing/pi-session-control.js +320 -0
- package/pi-extension/dist/cognitive-routing/tool.js +1 -0
- package/pi-extension/dist/cognitive-routing/types.js +1 -1
- package/pi-extension/dist/index.js +236 -219
- package/pi-extension/dist/runtime/runtime-context.js +126 -286
- package/pi-extension/dist/settings/settings-ui.js +52 -336
- package/pi-extension/src/cognitive-routing/commands.ts +15 -5
- package/pi-extension/src/cognitive-routing/config.ts +16 -20
- package/pi-extension/src/cognitive-routing/controller.ts +561 -51
- package/pi-extension/src/cognitive-routing/history.ts +12 -0
- package/pi-extension/src/cognitive-routing/host.ts +110 -16
- package/pi-extension/src/cognitive-routing/pi-routing-session.ts +538 -0
- package/pi-extension/src/cognitive-routing/pi-session-control.ts +444 -0
- package/pi-extension/src/cognitive-routing/tool.ts +1 -0
- package/pi-extension/src/cognitive-routing/types.ts +1 -1
- package/pi-extension/src/index.ts +249 -239
- package/pi-extension/src/runtime/runtime-context.ts +129 -305
- package/pi-extension/src/settings/settings-ui.ts +53 -371
- package/pi-extension/tests/cognitive-routing/commands.test.js +1 -1
- package/pi-extension/tests/cognitive-routing/controller.test.js +69 -40
- package/pi-extension/tests/cognitive-routing/extension-lifecycle.test.js +128 -57
- package/pi-extension/tests/cognitive-routing/pi-normal-lifecycle.test.js +434 -0
- package/pi-extension/tests/cognitive-routing/pi-session-control.test.js +174 -0
- package/pi-extension/tests/cognitive-routing/pi-session-history.test.js +91 -0
- package/pi-extension/tests/cognitive-routing/profile-resolution.test.js +47 -4
- package/pi-extension/tests/integration/mode-free-core.test.js +218 -0
- package/pi-extension/tests/integration/pi-extension.test.js +430 -2478
- package/pi-extension/tests/integration/prompt-architecture.test.js +124 -79
- package/pi-extension/tests/integration/unaffected-regressions.test.js +411 -0
- package/plugin.json +33 -0
- package/runtime/prompts/cognitive-routing.md +5 -4
- package/runtime/prompts/conversation-history.md +1 -1
- package/runtime/prompts/core.md +77 -6
- package/runtime/prompts/interaction-contract.md +6 -2
- package/skills/action-selection/SKILL.md +93 -44
- package/skills/action-selection/references/trajectory-stalls.md +71 -0
- package/skills/bypass/SKILL.md +109 -39
- package/skills/commit-work/SKILL.md +94 -55
- package/skills/commit-work/references/git-state-edges.md +94 -0
- package/skills/commit-work/references/simple-push.md +92 -0
- package/skills/commit-work/references/staging-decisions.md +79 -46
- package/skills/decision-gate/SKILL.md +80 -22
- package/skills/design-for-depth/SKILL.md +208 -43
- package/skills/design-for-depth/references/design-pressure-signals.md +108 -240
- package/skills/design-for-depth/references/interface-design-loop.md +77 -97
- package/skills/design-for-depth/references/module-and-dependency-design.md +147 -0
- package/skills/design-for-depth/references/software-design-philosophy.md +127 -173
- package/skills/design-for-depth/references/state-and-failure-boundaries.md +136 -0
- package/skills/diagnose-failure/SKILL.md +198 -52
- package/skills/diagnose-failure/references/diagnostic-loop-catalog.md +19 -5
- package/skills/diagnose-failure/references/flaky-and-performance-diagnosis.md +13 -5
- package/skills/discuss/SKILL.md +118 -76
- package/skills/discuss/references/discussion-continuity.md +84 -32
- package/skills/evaluate-skill/SKILL.md +166 -52
- package/skills/evaluate-skill/references/evaluation-definition-schema.md +2 -2
- package/skills/evaluate-skill/references/evaluation-design.md +56 -30
- package/skills/evaluate-skill/references/execution-and-evidence.md +1 -1
- package/skills/evaluate-skill/references/review-and-revision.md +62 -21
- package/skills/execute-work/SKILL.md +98 -59
- package/skills/execute-work/references/code-practices.md +4 -4
- package/skills/execute-work/references/execute-work-edges.md +42 -22
- package/skills/review-artifact/SKILL.md +132 -71
- package/skills/review-artifact/references/adjudicate-artifact-review.md +101 -0
- package/skills/review-artifact/references/independent-artifact-reviewer-contract.md +52 -41
- package/skills/review-work/SKILL.md +115 -72
- package/skills/review-work/references/adjudicate-work-review.md +97 -0
- package/skills/review-work/references/independent-work-reviewer-contract.md +40 -35
- package/skills/review-work/references/security-risk-lens.md +17 -15
- package/skills/setup-freeflow/SKILL.md +14 -17
- package/skills/setup-freeflow/references/activation-contract.md +24 -30
- package/skills/setup-freeflow/references/host-setup.md +6 -7
- package/skills/simplify-code/SKILL.md +97 -39
- package/skills/simplify-code/references/simplification-patterns.md +1 -1
- package/skills/tdd/SKILL.md +108 -41
- package/skills/tdd/references/test-design.md +1 -1
- package/skills/track-work/SKILL.md +133 -300
- package/skills/track-work/references/working-record-format.md +515 -0
- package/skills/track-work/scripts/lib/document.mjs +713 -0
- package/skills/track-work/scripts/lib/format.mjs +166 -0
- package/skills/track-work/scripts/lib/store.mjs +233 -0
- package/skills/track-work/scripts/lib/transitions.mjs +790 -0
- package/skills/track-work/scripts/lib/views.mjs +27 -430
- package/skills/track-work/scripts/working-record.mjs +292 -98
- package/skills/verify-work/SKILL.md +46 -20
- package/skills/verify-work/references/browser-runtime-evidence.md +1 -1
- package/skills/verify-work/references/integration-evidence.md +1 -7
- package/skills/verify-work/references/performance-evidence.md +1 -1
- package/skills/workflow/SKILL.md +110 -80
- package/skills/workflow/references/domain-skill-composition.md +24 -18
- package/skills/write-skill/SKILL.md +218 -62
- package/skills/write-skill/references/activation-boundaries.md +63 -21
- package/skills/write-skill/references/agent-first-instructions.md +61 -27
- package/skills/write-skill/references/progressive-disclosure.md +91 -41
- package/skills/write-skill/references/skill-dependency-graphs.md +190 -0
- package/runtime/prompts/skills.md +0 -65
- package/skills/mode-contract/SKILL.md +0 -104
- package/skills/track-work/scripts/lib/codec.mjs +0 -570
- package/skills/track-work/scripts/lib/commands.mjs +0 -568
- package/skills/track-work/scripts/lib/contract.mjs +0 -303
- package/skills/track-work/scripts/lib/model.mjs +0 -506
- package/skills/track-work/scripts/lib/operations.mjs +0 -965
- package/skills/track-work/scripts/lib/record-store.mjs +0 -264
- package/skills/track-work/scripts/lib/result.mjs +0 -37
- package/skills/track-work/scripts/lib/rewrites.mjs +0 -740
- package/skills/track-work/scripts/lib/schema.mjs +0 -277
- package/skills/track-work/scripts/lib/source-inventory.mjs +0 -162
- package/skills/track-work/scripts/lib/workspace.mjs +0 -134
|
@@ -0,0 +1,411 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { execFile } from "node:child_process";
|
|
3
|
+
import { mkdir, mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { tmpdir } from "node:os";
|
|
6
|
+
import test from "node:test";
|
|
7
|
+
import { promisify } from "node:util";
|
|
8
|
+
|
|
9
|
+
import freeflowExtension from "../../dist/index.js";
|
|
10
|
+
import { resetSessionOverrides, setSessionCoreOverride } from "../../dist/runtime/runtime-context.js";
|
|
11
|
+
import { PIFLOW_HOST } from "../cognitive-routing/host-fixture.js";
|
|
12
|
+
|
|
13
|
+
const execFileAsync = promisify(execFile);
|
|
14
|
+
|
|
15
|
+
const theme = {
|
|
16
|
+
fg(_color, text) {
|
|
17
|
+
return text;
|
|
18
|
+
},
|
|
19
|
+
bold(text) {
|
|
20
|
+
return text;
|
|
21
|
+
},
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
function context(cwd, options = {}) {
|
|
25
|
+
const notifications = [];
|
|
26
|
+
const reloads = [];
|
|
27
|
+
const statuses = [];
|
|
28
|
+
return {
|
|
29
|
+
cwd,
|
|
30
|
+
notifications,
|
|
31
|
+
reloads,
|
|
32
|
+
statuses,
|
|
33
|
+
mode: options.mode,
|
|
34
|
+
hasUI: options.hasUI,
|
|
35
|
+
isIdle: options.isIdle,
|
|
36
|
+
async reload() {
|
|
37
|
+
reloads.push(true);
|
|
38
|
+
},
|
|
39
|
+
sessionManager: {
|
|
40
|
+
getBranch: () => options.entries ?? [],
|
|
41
|
+
getEntries: () => options.entries ?? [],
|
|
42
|
+
},
|
|
43
|
+
ui: {
|
|
44
|
+
setStatus(name, value) {
|
|
45
|
+
statuses.push({ name, value });
|
|
46
|
+
},
|
|
47
|
+
notify(message, level) {
|
|
48
|
+
notifications.push({ message, level });
|
|
49
|
+
},
|
|
50
|
+
},
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function loadExtension() {
|
|
55
|
+
const handlers = new Map();
|
|
56
|
+
const commands = [];
|
|
57
|
+
const tools = [];
|
|
58
|
+
const pi = {
|
|
59
|
+
host: PIFLOW_HOST,
|
|
60
|
+
registerTool(tool) {
|
|
61
|
+
tools.push(tool);
|
|
62
|
+
},
|
|
63
|
+
registerCommand(name, definition) {
|
|
64
|
+
commands.push({ name, definition });
|
|
65
|
+
},
|
|
66
|
+
registerShortcut() {},
|
|
67
|
+
on(name, handler) {
|
|
68
|
+
handlers.set(name, handler);
|
|
69
|
+
},
|
|
70
|
+
appendEntry() {},
|
|
71
|
+
sendUserMessage() {},
|
|
72
|
+
getAllTools() {
|
|
73
|
+
return tools.map((tool) => ({ name: tool.name }));
|
|
74
|
+
},
|
|
75
|
+
getActiveTools() {
|
|
76
|
+
return tools.map((tool) => tool.name);
|
|
77
|
+
},
|
|
78
|
+
setActiveTools() {},
|
|
79
|
+
};
|
|
80
|
+
freeflowExtension(pi);
|
|
81
|
+
return { handlers, commands, pi };
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
async function configuredRepo(config = {}) {
|
|
85
|
+
const cwd = await mkdtemp(join(tmpdir(), "freeflow-unaffected-regressions-"));
|
|
86
|
+
await mkdir(join(cwd, ".freeflow"));
|
|
87
|
+
await writeFile(join(cwd, ".freeflow/config.json"), JSON.stringify(config, null, 2), "utf8");
|
|
88
|
+
return cwd;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function freeflowCommand(commands) {
|
|
92
|
+
const command = commands.find((candidate) => candidate.name === "freeflow");
|
|
93
|
+
assert.ok(command);
|
|
94
|
+
return command;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
test("non-TUI settings selectors provide guidance without mutation", async () => {
|
|
98
|
+
const cwd = await configuredRepo();
|
|
99
|
+
const configPath = join(cwd, ".freeflow/config.json");
|
|
100
|
+
const original = await readFile(configPath, "utf8");
|
|
101
|
+
try {
|
|
102
|
+
const { commands } = loadExtension();
|
|
103
|
+
const command = freeflowCommand(commands);
|
|
104
|
+
const rpc = context(cwd, { mode: "rpc" });
|
|
105
|
+
rpc.ui.custom = async () => assert.fail("RPC settings must not open the TUI");
|
|
106
|
+
await command.definition.handler("settings", rpc);
|
|
107
|
+
assert.match(rpc.notifications.at(-1).message, /require Pi TUI/);
|
|
108
|
+
assert.equal(await readFile(configPath, "utf8"), original);
|
|
109
|
+
|
|
110
|
+
const print = context(cwd, { mode: "json", hasUI: false });
|
|
111
|
+
await assert.rejects(() => command.definition.handler("settings", print), /require Pi TUI/);
|
|
112
|
+
assert.equal(await readFile(configPath, "utf8"), original);
|
|
113
|
+
} finally {
|
|
114
|
+
await rm(cwd, { recursive: true, force: true });
|
|
115
|
+
}
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
test("settings refuse a mid-run mutation before opening the TUI", async () => {
|
|
119
|
+
const cwd = await configuredRepo();
|
|
120
|
+
const configPath = join(cwd, ".freeflow/config.json");
|
|
121
|
+
const original = await readFile(configPath, "utf8");
|
|
122
|
+
try {
|
|
123
|
+
const { commands } = loadExtension();
|
|
124
|
+
const command = freeflowCommand(commands);
|
|
125
|
+
const settings = context(cwd, { isIdle: () => false });
|
|
126
|
+
settings.ui.custom = async () => assert.fail("settings UI must not open while Pi is running");
|
|
127
|
+
await command.definition.handler("settings repo", settings);
|
|
128
|
+
assert.match(settings.notifications.at(-1).message, /only while Pi is idle/);
|
|
129
|
+
assert.equal(await readFile(configPath, "utf8"), original);
|
|
130
|
+
} finally {
|
|
131
|
+
await rm(cwd, { recursive: true, force: true });
|
|
132
|
+
}
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
test("settings recheck idle state before committing a master-switch selection", async () => {
|
|
136
|
+
const cwd = await configuredRepo({ enabled: false });
|
|
137
|
+
const configPath = join(cwd, ".freeflow/config.json");
|
|
138
|
+
const original = await readFile(configPath, "utf8");
|
|
139
|
+
let idle = true;
|
|
140
|
+
try {
|
|
141
|
+
const { commands } = loadExtension();
|
|
142
|
+
const command = freeflowCommand(commands);
|
|
143
|
+
const settings = context(cwd, { isIdle: () => idle });
|
|
144
|
+
settings.ui.custom = async (factory) => {
|
|
145
|
+
let result;
|
|
146
|
+
const component = factory({ requestRender() {} }, theme, {}, (value) => {
|
|
147
|
+
result = value;
|
|
148
|
+
});
|
|
149
|
+
idle = false;
|
|
150
|
+
component.handleInput("\r");
|
|
151
|
+
component.handleInput("\u001b[A");
|
|
152
|
+
component.handleInput("\r");
|
|
153
|
+
await component.waitForWrites();
|
|
154
|
+
component.handleInput("\u001b");
|
|
155
|
+
return result;
|
|
156
|
+
};
|
|
157
|
+
await command.definition.handler("settings repo", settings);
|
|
158
|
+
assert.match(settings.notifications.at(-1).message, /only while Pi is idle/);
|
|
159
|
+
assert.equal(await readFile(configPath, "utf8"), original);
|
|
160
|
+
} finally {
|
|
161
|
+
await rm(cwd, { recursive: true, force: true });
|
|
162
|
+
}
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
test("personal settings keep repository state unchanged while editing context", async () => {
|
|
166
|
+
const cwd = await configuredRepo();
|
|
167
|
+
const configPath = join(cwd, ".freeflow/config.json");
|
|
168
|
+
const original = await readFile(configPath, "utf8");
|
|
169
|
+
try {
|
|
170
|
+
const { commands } = loadExtension();
|
|
171
|
+
const command = freeflowCommand(commands);
|
|
172
|
+
const settings = context(cwd);
|
|
173
|
+
settings.ui.custom = async (factory) => {
|
|
174
|
+
let result;
|
|
175
|
+
const component = factory({ requestRender() {} }, theme, {}, (value) => {
|
|
176
|
+
result = value;
|
|
177
|
+
});
|
|
178
|
+
component.handleInput("\u001b[B");
|
|
179
|
+
component.handleInput("\u001b[B");
|
|
180
|
+
component.handleInput("\r");
|
|
181
|
+
component.handleInput("\r");
|
|
182
|
+
component.handleInput("\u001b[B");
|
|
183
|
+
component.handleInput("\r");
|
|
184
|
+
await component.waitForWrites();
|
|
185
|
+
component.handleInput("\u001b");
|
|
186
|
+
component.handleInput("\u001b");
|
|
187
|
+
return result;
|
|
188
|
+
};
|
|
189
|
+
await command.definition.handler("settings", settings);
|
|
190
|
+
assert.equal(await readFile(configPath, "utf8"), original);
|
|
191
|
+
assert.equal(JSON.parse(await readFile(join(cwd, ".freeflow/local.json"), "utf8")).contextVirtualization, true);
|
|
192
|
+
} finally {
|
|
193
|
+
await rm(cwd, { recursive: true, force: true });
|
|
194
|
+
}
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
test("context settings refresh their parent summary after a child changes", async () => {
|
|
198
|
+
const cwd = await configuredRepo({ contextVirtualization: true });
|
|
199
|
+
try {
|
|
200
|
+
const { commands } = loadExtension();
|
|
201
|
+
const command = freeflowCommand(commands);
|
|
202
|
+
const settings = context(cwd);
|
|
203
|
+
settings.ui.custom = async (factory) => {
|
|
204
|
+
let result;
|
|
205
|
+
const component = factory({ requestRender() {} }, theme, {}, (value) => {
|
|
206
|
+
result = value;
|
|
207
|
+
});
|
|
208
|
+
assert.match(component.render(120).join("\n"), /Freeflow Context\s+enabled \(2\) 1\/2 enabled/);
|
|
209
|
+
component.handleInput("\u001b[B");
|
|
210
|
+
component.handleInput("\u001b[B");
|
|
211
|
+
component.handleInput("\r");
|
|
212
|
+
component.handleInput("\r");
|
|
213
|
+
component.handleInput("\u001b[B");
|
|
214
|
+
component.handleInput("\u001b[B");
|
|
215
|
+
component.handleInput("\r");
|
|
216
|
+
await component.waitForWrites();
|
|
217
|
+
component.handleInput("\u001b");
|
|
218
|
+
assert.match(component.render(120).join("\n"), /Freeflow Context\s+disabled \(2\) 0\/2 enabled/);
|
|
219
|
+
component.handleInput("\u001b");
|
|
220
|
+
return result;
|
|
221
|
+
};
|
|
222
|
+
await command.definition.handler("settings", settings);
|
|
223
|
+
const local = JSON.parse(await readFile(join(cwd, ".freeflow/local.json"), "utf8"));
|
|
224
|
+
assert.equal(local.contextVirtualization, false);
|
|
225
|
+
assert.equal(settings.reloads.length, 1);
|
|
226
|
+
} finally {
|
|
227
|
+
await rm(cwd, { recursive: true, force: true });
|
|
228
|
+
}
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
test("repository settings show personal effective sources without changing shared state", async () => {
|
|
232
|
+
const cwd = await configuredRepo({ contextVirtualization: false });
|
|
233
|
+
const repositoryPath = join(cwd, ".freeflow/config.json");
|
|
234
|
+
const localPath = join(cwd, ".freeflow/local.json");
|
|
235
|
+
try {
|
|
236
|
+
await writeFile(localPath, JSON.stringify({ contextVirtualization: true }, null, 2), "utf8");
|
|
237
|
+
const { commands } = loadExtension();
|
|
238
|
+
const command = freeflowCommand(commands);
|
|
239
|
+
const settings = context(cwd, { isIdle: () => true });
|
|
240
|
+
settings.ui.custom = async (factory) => {
|
|
241
|
+
let result;
|
|
242
|
+
const component = factory({ requestRender() {} }, theme, {}, (value) => {
|
|
243
|
+
result = value;
|
|
244
|
+
});
|
|
245
|
+
component.handleInput("\u001b[B");
|
|
246
|
+
component.handleInput("\u001b[B");
|
|
247
|
+
component.handleInput("\r");
|
|
248
|
+
assert.match(component.render(120).join("\n"), /Context Virtualization\s+disabled \(effective enabled · local\)/);
|
|
249
|
+
component.handleInput("\u001b");
|
|
250
|
+
return result;
|
|
251
|
+
};
|
|
252
|
+
await command.definition.handler("settings repo", settings);
|
|
253
|
+
assert.deepEqual(JSON.parse(await readFile(repositoryPath, "utf8")), { contextVirtualization: false });
|
|
254
|
+
assert.deepEqual(JSON.parse(await readFile(localPath, "utf8")), { contextVirtualization: true });
|
|
255
|
+
assert.equal(settings.reloads.length, 0);
|
|
256
|
+
} finally {
|
|
257
|
+
await rm(cwd, { recursive: true, force: true });
|
|
258
|
+
}
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
test("Cognitive Routing preset cancellation preserves the previous repository value", async () => {
|
|
262
|
+
const cwd = await configuredRepo({
|
|
263
|
+
cognitiveRouting: {
|
|
264
|
+
enabled: true,
|
|
265
|
+
profiles: {
|
|
266
|
+
standard: { provider: "test", model: "model-a", thinkingLevel: "low" },
|
|
267
|
+
reasoning: { provider: "test", model: "model-b", thinkingLevel: "high" },
|
|
268
|
+
},
|
|
269
|
+
},
|
|
270
|
+
});
|
|
271
|
+
const original = await readFile(join(cwd, ".freeflow/config.json"), "utf8");
|
|
272
|
+
try {
|
|
273
|
+
const { commands } = loadExtension();
|
|
274
|
+
const command = freeflowCommand(commands);
|
|
275
|
+
const settings = context(cwd, { isIdle: () => true });
|
|
276
|
+
settings.modelRegistry = cognitiveRoutingModelRegistry();
|
|
277
|
+
settings.ui.custom = async (factory) => {
|
|
278
|
+
let result;
|
|
279
|
+
const component = factory({ requestRender() {} }, theme, {}, (value) => {
|
|
280
|
+
result = value;
|
|
281
|
+
});
|
|
282
|
+
component.handleInput("\u001b[B");
|
|
283
|
+
component.handleInput("\r");
|
|
284
|
+
component.handleInput("\u001b[B");
|
|
285
|
+
component.handleInput("\r");
|
|
286
|
+
component.handleInput("\u001b");
|
|
287
|
+
component.handleInput("\u001b");
|
|
288
|
+
return result;
|
|
289
|
+
};
|
|
290
|
+
await command.definition.handler("settings repo", settings);
|
|
291
|
+
assert.equal(await readFile(join(cwd, ".freeflow/config.json"), "utf8"), original);
|
|
292
|
+
assert.equal(settings.reloads.length, 0);
|
|
293
|
+
} finally {
|
|
294
|
+
await rm(cwd, { recursive: true, force: true });
|
|
295
|
+
}
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
test("Cognitive Routing settings refresh after enabling the capability", async () => {
|
|
299
|
+
const cwd = await configuredRepo({
|
|
300
|
+
cognitiveRouting: {
|
|
301
|
+
enabled: false,
|
|
302
|
+
profiles: {
|
|
303
|
+
standard: { provider: "test", model: "model-a", thinkingLevel: "low" },
|
|
304
|
+
reasoning: { provider: "test", model: "model-b", thinkingLevel: "high" },
|
|
305
|
+
},
|
|
306
|
+
},
|
|
307
|
+
});
|
|
308
|
+
try {
|
|
309
|
+
const { commands } = loadExtension();
|
|
310
|
+
const command = freeflowCommand(commands);
|
|
311
|
+
const settings = context(cwd, { isIdle: () => true });
|
|
312
|
+
settings.modelRegistry = cognitiveRoutingModelRegistry();
|
|
313
|
+
settings.ui.custom = async (factory) => {
|
|
314
|
+
let result;
|
|
315
|
+
const component = factory({ requestRender() {} }, theme, {}, (value) => {
|
|
316
|
+
result = value;
|
|
317
|
+
});
|
|
318
|
+
component.handleInput("\u001b[B");
|
|
319
|
+
component.handleInput("\r");
|
|
320
|
+
component.handleInput("\r");
|
|
321
|
+
component.handleInput("\u001b[A");
|
|
322
|
+
component.handleInput("\r");
|
|
323
|
+
await component.waitForWrites();
|
|
324
|
+
component.handleInput("\u001b");
|
|
325
|
+
assert.match(component.render(120).join("\n"), /Cognitive Routing\s+enabled \(5\) configured/);
|
|
326
|
+
component.handleInput("\u001b");
|
|
327
|
+
return result;
|
|
328
|
+
};
|
|
329
|
+
await command.definition.handler("settings repo", settings);
|
|
330
|
+
const saved = JSON.parse(await readFile(join(cwd, ".freeflow/config.json"), "utf8"));
|
|
331
|
+
assert.equal(saved.cognitiveRouting.enabled, true);
|
|
332
|
+
assert.equal(settings.reloads.length, 1);
|
|
333
|
+
} finally {
|
|
334
|
+
await rm(cwd, { recursive: true, force: true });
|
|
335
|
+
}
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
test("session settings preserve mandatory prompts through compaction and resume", async () => {
|
|
339
|
+
const cwd = await configuredRepo();
|
|
340
|
+
try {
|
|
341
|
+
const { handlers } = loadExtension();
|
|
342
|
+
const ctx = context(cwd);
|
|
343
|
+
const before = await handlers.get("before_agent_start")({ systemPrompt: "base prompt" }, ctx);
|
|
344
|
+
assert.match(before.systemPrompt, /# Freeflow Interaction Contract/);
|
|
345
|
+
await handlers.get("session_compact")({ reason: "manual" }, ctx);
|
|
346
|
+
const afterCompact = await handlers.get("before_agent_start")({ systemPrompt: "base prompt" }, ctx);
|
|
347
|
+
assert.match(afterCompact.systemPrompt, /# Freeflow Interaction Contract/);
|
|
348
|
+
await handlers.get("session_start")({ reason: "resume" }, ctx);
|
|
349
|
+
const afterResume = await handlers.get("before_agent_start")({ systemPrompt: "base prompt" }, ctx);
|
|
350
|
+
assert.match(afterResume.systemPrompt, /# Freeflow Interaction Contract/);
|
|
351
|
+
} finally {
|
|
352
|
+
await rm(cwd, { recursive: true, force: true });
|
|
353
|
+
}
|
|
354
|
+
});
|
|
355
|
+
|
|
356
|
+
test("retired router-shaped configuration remains inert while core activation works", async () => {
|
|
357
|
+
const cwd = await configuredRepo({ outputRouter: { postToolRouting: "always" } });
|
|
358
|
+
try {
|
|
359
|
+
const { handlers } = loadExtension();
|
|
360
|
+
const ctx = context(cwd);
|
|
361
|
+
const before = await handlers.get("before_agent_start")({ systemPrompt: "base prompt" }, ctx);
|
|
362
|
+
assert.match(before.systemPrompt, /# Freeflow Interaction Contract/);
|
|
363
|
+
assert.doesNotMatch(before.systemPrompt, /Output Router|freeflow_(search|run|batch)/i);
|
|
364
|
+
} finally {
|
|
365
|
+
await rm(cwd, { recursive: true, force: true });
|
|
366
|
+
}
|
|
367
|
+
});
|
|
368
|
+
|
|
369
|
+
test("remaining session overrides apply without changing repository configuration", async () => {
|
|
370
|
+
const cwd = await configuredRepo();
|
|
371
|
+
try {
|
|
372
|
+
const configPath = join(cwd, ".freeflow/config.json");
|
|
373
|
+
const original = await readFile(configPath, "utf8");
|
|
374
|
+
const { pi } = loadExtension();
|
|
375
|
+
const ctx = context(cwd);
|
|
376
|
+
await setSessionCoreOverride("contextVirtualization", true, ctx, pi);
|
|
377
|
+
const state = await import("../../dist/runtime/runtime-context.js").then(({ readCapabilityState }) =>
|
|
378
|
+
readCapabilityState(cwd, undefined, PIFLOW_HOST),
|
|
379
|
+
);
|
|
380
|
+
assert.equal(state.contextVirtualization.effective, true);
|
|
381
|
+
assert.equal(await readFile(configPath, "utf8"), original);
|
|
382
|
+
await resetSessionOverrides(ctx, pi);
|
|
383
|
+
} finally {
|
|
384
|
+
await rm(cwd, { recursive: true, force: true });
|
|
385
|
+
}
|
|
386
|
+
});
|
|
387
|
+
|
|
388
|
+
function cognitiveRoutingModelRegistry() {
|
|
389
|
+
const models = [
|
|
390
|
+
{ provider: "test", id: "model-a", name: "Model A", reasoning: true },
|
|
391
|
+
{ provider: "test", id: "model-b", name: "Model B", reasoning: true },
|
|
392
|
+
];
|
|
393
|
+
const supported = new Map([
|
|
394
|
+
["model-a", new Set(["off", "low", "medium", "high"])],
|
|
395
|
+
["model-b", new Set(["off", "low", "medium", "high", "max"])],
|
|
396
|
+
]);
|
|
397
|
+
return {
|
|
398
|
+
getAvailable() {
|
|
399
|
+
return models;
|
|
400
|
+
},
|
|
401
|
+
find(provider, id) {
|
|
402
|
+
return models.find((model) => model.provider === provider && model.id === id);
|
|
403
|
+
},
|
|
404
|
+
async getApiKeyAndHeaders() {
|
|
405
|
+
return { ok: true };
|
|
406
|
+
},
|
|
407
|
+
clampThinkingLevel(model, level) {
|
|
408
|
+
return supported.get(model.id)?.has(level) ? level : "off";
|
|
409
|
+
},
|
|
410
|
+
};
|
|
411
|
+
}
|
package/plugin.json
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://agent-plugins.org/schemas/1.0.0/plugin.schema.json",
|
|
3
|
+
"name": "freeflow",
|
|
4
|
+
"version": "0.7.1",
|
|
5
|
+
"description": "Feedback-based control system for coding agents.",
|
|
6
|
+
"author": {
|
|
7
|
+
"name": "Hassan Mohiddin"
|
|
8
|
+
},
|
|
9
|
+
"homepage": "https://github.com/hassan-mohiddin/freeflow",
|
|
10
|
+
"repository": "https://github.com/hassan-mohiddin/freeflow",
|
|
11
|
+
"license": "MIT",
|
|
12
|
+
"keywords": [
|
|
13
|
+
"workflow",
|
|
14
|
+
"skills",
|
|
15
|
+
"agents",
|
|
16
|
+
"feedback-loop",
|
|
17
|
+
"task-memory",
|
|
18
|
+
"planning",
|
|
19
|
+
"verification",
|
|
20
|
+
"review",
|
|
21
|
+
"agent-plugins",
|
|
22
|
+
"gemini",
|
|
23
|
+
"copilot",
|
|
24
|
+
"vscode",
|
|
25
|
+
"cursor",
|
|
26
|
+
"kiro",
|
|
27
|
+
"opencode",
|
|
28
|
+
"hermes"
|
|
29
|
+
],
|
|
30
|
+
"extensions": {
|
|
31
|
+
"com.github.copilot": {}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
## Cognitive Routing Cue
|
|
2
2
|
|
|
3
|
-
Cognitive Routing changes compute only
|
|
3
|
+
Cognitive Routing changes compute only; it never changes Workflow ownership or authority. `Control` and `Profile` in the latest Runtime State report current compute state, not Yield, Delegate, `ACT_BOUNDED`, or boundary state. A Runtime State refresh is host context, not a user interruption, and does not end an active route or `ACT_BOUNDED` scope unless contradicted. Recover routing state from visible records; never infer `Boundary state: NONE`.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
- **Manual:** the held profile runs the ordinary unsplit Workflow; do not request switching.
|
|
6
|
+
- **Automatic:** each user interaction begins in Reasoning, which owns conversational and user-facing analysis, discussion, decisions, questions, assessment, and reporting. This work needs no route marker.
|
|
6
7
|
|
|
7
|
-
|
|
8
|
+
If the full Cognitive Routing skill is not visible, read it before interpreting or acting on the request. Make that read the only environment call; it is not a route transition. If the read fails or is unavailable, stop and report missing context.
|
|
8
9
|
|
|
9
|
-
|
|
10
|
+
Once visible, follow the skill's boundary-first execution model. Automatic Standard only executes active Yield or Delegate contracts; at every return condition it writes `YIELD HANDOFF` or `RETURN` and switches to Reasoning. Standard never handles substantive user-facing interaction.
|
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
## Conversation History Cue
|
|
2
2
|
|
|
3
|
-
When the next decision requires exact prior conversation evidence absent from active context, use Conversation History instead of reconstructing it from memory.
|
|
3
|
+
When the next decision requires exact prior conversation evidence absent from active context, use Conversation History instead of reconstructing it from memory.
|
package/runtime/prompts/core.md
CHANGED
|
@@ -2,12 +2,83 @@
|
|
|
2
2
|
|
|
3
3
|
Freeflow is a workflow layer for one active coding agent. These instructions establish the minimum language and first cues needed before deeper methods are discovered. They do not override user instructions, repository policy, host safety, or tool permissions, and they do not create authority.
|
|
4
4
|
|
|
5
|
-
Use the latest
|
|
5
|
+
Use the latest Freeflow Runtime State supplied by the host. It refreshes at session start, after context loss, or when displayed state changes; otherwise the existing snapshot may remain visible without re-emission. Treat earlier state as history and use only the Freeflow guidance, skills, and tools exposed for this request.
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
Freeflow's core guidance and the separate Interaction Contract are always delivered together whenever Freeflow is enabled. The Interaction Contract remains a separately editable prompt fragment; it is not an optional capability.
|
|
8
8
|
|
|
9
|
-
|
|
10
|
-
- `workflow`: use the adaptive Workflow for consequential or mutating work.
|
|
11
|
-
- `strict-workflow`: use the same Workflow with stronger decision, evidence, verification, and checkpoint pressure at high-risk or hard-to-reverse boundaries.
|
|
9
|
+
## Shared Terms
|
|
12
10
|
|
|
13
|
-
|
|
11
|
+
- **Authority envelope:** the requested outcome, permitted effects, covered active evidence generation, and stop condition established by a direct request or still-valid approval.
|
|
12
|
+
- **Passive observation:** inspecting existing evidence or sources without exercising target behavior or intentionally changing task state.
|
|
13
|
+
- **Active evidence generation:** exercising target behavior to produce new evidence, including tests, reproductions, benchmarks, prototypes, instrumentation, and runtime probes.
|
|
14
|
+
- **Mutation or delivery:** changing the repository, durable task or session, or external state.
|
|
15
|
+
- **Bounded activity:** one coherent unit of discussion, preservation, execution, evidence, or judgment that ends in one assessable result.
|
|
16
|
+
- **Current owner:** the one Freeflow activity responsible for carrying the current bounded activity to an assessable result. Methods, lenses, references, domain guidance, tools, and reviewers may support it without taking ownership automatically.
|
|
17
|
+
- **Slice:** one coherent outcome that may span several bounded activities and methods. Track Work gives it durable identity when task memory is needed.
|
|
18
|
+
- **Evidence boundary:** the strongest claim current direct evidence can support. It is established by the observing mechanism, not by intent, authority, or confidence.
|
|
19
|
+
- **Self-review:** after fresh evidence initially supports a bounded result, the producing agent silently checks that supported state once for alignment, correctness, suitability, and unnecessary complexity before accepting or reusing it. A corrected state receives only the affected recheck. Self-review is not independent review.
|
|
20
|
+
- **Independent review:** separately selected judgment from a context that did not produce the reviewed state. It reports without editing and does not authorize correction.
|
|
21
|
+
- **Checkpoint:** a deliberately selected boundary that dependent work must not cross unresolved. It may require a decision, review, preservation action, or delivery result. An activity ending or work pausing does not create one automatically.
|
|
22
|
+
- **Re-entry:** return only to the narrowest current owner whose responsibility changed, preserving valid work, decisions, and evidence.
|
|
23
|
+
- **Supported Exit:** the justified end of the current Interaction Lifecycle: an evidence-supported answer, wait, pause, handoff, deferment, controlled boundary, stop, or completion, with material limits and unresolved state made explicit.
|
|
24
|
+
|
|
25
|
+
One Interaction Lifecycle may contain several bounded activities. A Slice may span several bounded activities and methods. Each bounded activity has one current owner and may contain several environment interactions.
|
|
26
|
+
|
|
27
|
+
## Recover After Context Loss
|
|
28
|
+
|
|
29
|
+
After compaction, summarization, clear, resume, session navigation, handoff, or uncertain continuity, pause task-directed work. Until recovery is complete, use the environment only for bounded reads needed to recover current state.
|
|
30
|
+
|
|
31
|
+
Treat summaries, records, prior skill reads, transition results, and historical messages as memory. They may preserve still-valid context and authority, but they do not create or widen authority, prove live state, keep a method active, or settle current ownership. Current user direction, live source truth, and the latest Freeflow Runtime State take precedence when they conflict.
|
|
32
|
+
|
|
33
|
+
Before continuing:
|
|
34
|
+
|
|
35
|
+
- Require the latest Freeflow Runtime State. If it is absent, stale, conflicting, or inconsistent with the exposed skills and tools, do not infer it.
|
|
36
|
+
- Read each relevant active capability skill before relying on or changing that capability.
|
|
37
|
+
- When ongoing work has or may have a Working Record, read Track Work and recover its `resume` view before task work.
|
|
38
|
+
- Identify the current owner. Read Workflow when authority, ownership, route, or continuity is unclear, then read the owner’s skill when its exact method is absent.
|
|
39
|
+
- Recover the current Slice, route or execution boundary, blockers, checkpoints, evidence boundary, partial effects, and stop condition without inventing missing state.
|
|
40
|
+
|
|
41
|
+
Resume only when authority, ownership, current work, route, evidence requirements, and the stop condition are coherent. Otherwise inspect the missing source, ask, defer, or stop.
|
|
42
|
+
|
|
43
|
+
## Three Nested Loops
|
|
44
|
+
|
|
45
|
+
One user-facing **Interaction Lifecycle** runs from Entry to a Feedback Loop when needed to a Supported Exit. A later user turn or new evidence begins another lifecycle.
|
|
46
|
+
|
|
47
|
+
For each bounded activity, the **Feedback Loop** is:
|
|
48
|
+
|
|
49
|
+
```text
|
|
50
|
+
Orient
|
|
51
|
+
-> choose one current owner
|
|
52
|
+
-> use the current owner
|
|
53
|
+
-> gather or produce evidence
|
|
54
|
+
-> determine what the result proves
|
|
55
|
+
-> self-review the supported result
|
|
56
|
+
-> continue, correct, re-enter, ask, defer, stop, or exit
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
When the current owner must touch the environment, the **Environment Interaction Loop** is:
|
|
60
|
+
|
|
61
|
+
```text
|
|
62
|
+
Need evidence or a covered effect
|
|
63
|
+
-> select and bound one environment action
|
|
64
|
+
-> choose the tool
|
|
65
|
+
-> execute once
|
|
66
|
+
-> observe
|
|
67
|
+
-> identify what changed
|
|
68
|
+
-> reconcile observation residency when available
|
|
69
|
+
-> return to the current owner
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
The Environment Interaction Loop may run zero or more times inside one Feedback Loop. It never changes the current owner or authority by itself.
|
|
73
|
+
|
|
74
|
+
## Workflow Cue
|
|
75
|
+
|
|
76
|
+
Workflow owns authority interpretation and enforcement, one current owner for the current bounded activity, evidence-driven re-entry, and Supported Exit. A direct request covers only its bounded outcome and entailed tools, checks, and reversible local choices. Before uncovered active evidence generation, mutation, delivery, or another separately controlled action, state the purpose, action, expected result, and stop condition; ask once and wait. Freeflow guidance, memory, reviews, and new evidence do not grant authority.
|
|
77
|
+
|
|
78
|
+
## Action Selection Cue
|
|
79
|
+
|
|
80
|
+
Before an uncertain, broad, or repeated environment interaction, use Action Selection to choose and bound one useful action. Return its observation and state change to the current owner. An obvious mechanical and directly verifiable action takes the fast path without manufactured alternatives.
|
|
81
|
+
|
|
82
|
+
## Supported Exit
|
|
83
|
+
|
|
84
|
+
Reach a Supported Exit when current evidence and authority support the reported answer, wait, pause, deferment, controlled boundary, stop, or completion; required self-review and selected checkpoints are resolved where applicable; task memory and required artifacts are accurate; and no material contradiction, source conflict, or user-owned decision remains hidden. Report the outcome, evidence, limits, and current route.
|
|
@@ -2,6 +2,10 @@
|
|
|
2
2
|
|
|
3
3
|
Work as a collaborative senior engineer. Read the whole user turn before responding. Treat the user’s goals and choices as theirs, and judge factual and technical claims from evidence: agree when supported, disagree when warranted, state uncertainty plainly, and revise when evidence changes.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
Questions, criticism, examples, hypotheses, and tentative ideas do not authorize action by themselves.
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
When a turn combines a direct question with a requested action, answer the question first. If the answer leaves the action clear, supported, and explicitly authorized, proceed in the same response. If the answer could change the user’s choice, expose a source conflict, or leave the action conditional, answer and wait.
|
|
8
|
+
|
|
9
|
+
With a clear action request, recommend brief discussion only when user input could materially change the outcome, scope, tradeoffs, approach, or acceptance. Otherwise stay within the request and current authority envelope, choose reversible local details, and proceed. Ask before going beyond the request.
|
|
10
|
+
|
|
11
|
+
Use Freeflow terminology in user-facing responses only when it materially improves clarity. Otherwise speak in ordinary engineering language.
|