@hassangameryt/freeflow 0.7.1 → 0.7.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +1 -1
- package/.cursor-plugin/plugin.json +1 -1
- package/README.md +28 -23
- package/capabilities/cognitive-routing/SKILL.md +94 -311
- package/command-surface.json +7 -1
- package/gemini-extension.json +1 -1
- package/package.json +4 -4
- package/pi-extension/dist/cognitive-routing-v2/budget.js +42 -0
- package/pi-extension/dist/cognitive-routing-v2/config.js +120 -0
- package/pi-extension/dist/cognitive-routing-v2/projection.js +321 -0
- package/pi-extension/dist/cognitive-routing-v2/render.js +231 -0
- package/pi-extension/dist/cognitive-routing-v2/runtime.js +1585 -0
- package/pi-extension/dist/cognitive-routing-v2/schemas.js +107 -0
- package/pi-extension/dist/cognitive-routing-v2/state.js +612 -0
- package/pi-extension/dist/cognitive-routing-v2/tools.js +91 -0
- package/pi-extension/dist/cognitive-routing-v2/types.js +44 -0
- package/pi-extension/dist/freeflow-context/tool.js +5 -0
- package/pi-extension/dist/index.js +279 -624
- package/pi-extension/dist/runtime/runtime-context.js +63 -13
- package/pi-extension/dist/session-sources/events.js +195 -0
- package/pi-extension/dist/session-sources/read-only-session.js +148 -0
- package/pi-extension/dist/session-sources/sources.js +233 -0
- package/pi-extension/dist/settings/settings-ui.js +44 -117
- package/pi-extension/src/cognitive-routing-v2/budget.ts +42 -0
- package/pi-extension/src/cognitive-routing-v2/config.ts +143 -0
- package/pi-extension/src/cognitive-routing-v2/projection.ts +365 -0
- package/pi-extension/src/cognitive-routing-v2/render.ts +256 -0
- package/pi-extension/src/cognitive-routing-v2/runtime.ts +1634 -0
- package/pi-extension/src/cognitive-routing-v2/schemas.ts +109 -0
- package/pi-extension/src/cognitive-routing-v2/state.ts +622 -0
- package/pi-extension/src/cognitive-routing-v2/tools.ts +93 -0
- package/pi-extension/src/cognitive-routing-v2/types.ts +222 -0
- package/pi-extension/src/freeflow-context/tool.ts +5 -0
- package/pi-extension/src/index.ts +285 -674
- package/pi-extension/src/runtime/runtime-context.ts +75 -14
- package/pi-extension/src/session-sources/events.ts +208 -0
- package/pi-extension/src/session-sources/read-only-session.ts +170 -0
- package/pi-extension/src/session-sources/sources.ts +263 -0
- package/pi-extension/src/settings/settings-ui.ts +54 -131
- package/pi-extension/tests/cognitive-routing-v2/communication.test.js +484 -0
- package/pi-extension/tests/cognitive-routing-v2/compaction-input.test.js +183 -0
- package/pi-extension/tests/cognitive-routing-v2/native.test.js +441 -0
- package/pi-extension/tests/cognitive-routing-v2/performance.test.js +87 -0
- package/pi-extension/tests/cognitive-routing-v2/persistence.test.js +245 -0
- package/pi-extension/tests/cognitive-routing-v2/projection.test.js +241 -0
- package/pi-extension/tests/cognitive-routing-v2/recovery.test.js +236 -0
- package/pi-extension/tests/cognitive-routing-v2/render.test.js +230 -0
- package/pi-extension/tests/cognitive-routing-v2/runtime.test.js +153 -0
- package/pi-extension/tests/cognitive-routing-v2/stabilization.test.js +521 -0
- package/pi-extension/tests/cognitive-routing-v2/state.test.js +275 -0
- package/pi-extension/tests/context-virtualization/extension.test.js +16 -0
- package/pi-extension/tests/fixtures/routing-native.js +188 -0
- package/pi-extension/tests/integration/mode-free-core.test.js +3 -2
- package/pi-extension/tests/integration/pi-extension.test.js +154 -37
- package/pi-extension/tests/integration/prompt-architecture.test.js +87 -6
- package/pi-extension/tests/integration/unaffected-regressions.test.js +77 -10
- package/plugin.json +1 -1
- package/runtime/prompts/cognitive-routing.md +4 -5
- package/runtime/prompts/core.md +51 -45
- package/runtime/prompts/interaction-contract.md +5 -5
- package/skills/action-selection/SKILL.md +57 -97
- package/skills/design-for-depth/SKILL.md +58 -193
- package/skills/design-for-depth/references/implementation-decisions.md +76 -0
- package/skills/diagnose-failure/SKILL.md +64 -196
- package/skills/discuss/SKILL.md +64 -123
- package/skills/discuss/references/discussion-continuity.md +7 -3
- package/skills/execute-work/SKILL.md +69 -88
- package/skills/execute-work/references/execute-work-edges.md +9 -3
- package/skills/handoff/SKILL.md +44 -54
- package/skills/handoff/references/handoff-templates.md +15 -6
- package/skills/review-artifact/SKILL.md +80 -127
- package/skills/review-artifact/references/independent-artifact-reviewer-contract.md +1 -1
- package/skills/review-work/SKILL.md +65 -115
- package/skills/review-work/references/independent-work-reviewer-contract.md +1 -1
- package/skills/simplify-code/SKILL.md +47 -100
- package/skills/track-work/SKILL.md +128 -119
- package/skills/track-work/references/working-record-format.md +14 -2
- package/skills/verify-work/SKILL.md +58 -62
- package/skills/verify-work/references/integration-evidence.md +4 -2
- package/skills/verify-work/references/test-design.md +89 -0
- package/skills/workflow/SKILL.md +73 -119
- package/skills/write-plan/SKILL.md +46 -77
- package/skills/write-plan/references/plan-shapes.md +5 -5
- package/skills/write-spec/SKILL.md +51 -63
- package/skills/write-spec/references/artifact-standards.md +2 -2
- package/skills/write-spec/references/decision-records.md +3 -2
- package/skills/write-spec/references/spec-shapes.md +15 -0
- package/pi-extension/dist/cognitive-routing/commands.js +0 -96
- package/pi-extension/dist/cognitive-routing/config.js +0 -232
- package/pi-extension/dist/cognitive-routing/controller.js +0 -1292
- package/pi-extension/dist/cognitive-routing/history.js +0 -451
- package/pi-extension/dist/cognitive-routing/host.js +0 -188
- package/pi-extension/dist/cognitive-routing/pi-routing-session.js +0 -414
- package/pi-extension/dist/cognitive-routing/pi-session-control.js +0 -320
- package/pi-extension/dist/cognitive-routing/runtime.js +0 -43
- package/pi-extension/dist/cognitive-routing/tool.js +0 -212
- package/pi-extension/dist/cognitive-routing/types.js +0 -6
- package/pi-extension/src/cognitive-routing/commands.ts +0 -129
- package/pi-extension/src/cognitive-routing/config.ts +0 -303
- package/pi-extension/src/cognitive-routing/controller.ts +0 -1628
- package/pi-extension/src/cognitive-routing/history.ts +0 -650
- package/pi-extension/src/cognitive-routing/host.ts +0 -266
- package/pi-extension/src/cognitive-routing/pi-routing-session.ts +0 -538
- package/pi-extension/src/cognitive-routing/pi-session-control.ts +0 -444
- package/pi-extension/src/cognitive-routing/runtime.ts +0 -57
- package/pi-extension/src/cognitive-routing/tool.ts +0 -273
- package/pi-extension/src/cognitive-routing/types.ts +0 -89
- package/pi-extension/tests/cognitive-routing/commands.test.js +0 -134
- package/pi-extension/tests/cognitive-routing/controller.test.js +0 -1128
- package/pi-extension/tests/cognitive-routing/extension-lifecycle.test.js +0 -1142
- package/pi-extension/tests/cognitive-routing/history.test.js +0 -274
- package/pi-extension/tests/cognitive-routing/host-identity.test.js +0 -63
- package/pi-extension/tests/cognitive-routing/pi-normal-lifecycle.test.js +0 -434
- package/pi-extension/tests/cognitive-routing/pi-session-control.test.js +0 -174
- package/pi-extension/tests/cognitive-routing/pi-session-history.test.js +0 -91
- package/pi-extension/tests/cognitive-routing/profile-resolution.test.js +0 -279
- package/pi-extension/tests/cognitive-routing/tool.test.js +0 -230
- package/skills/tdd/SKILL.md +0 -143
- package/skills/tdd/references/test-design.md +0 -96
- /package/pi-extension/tests/{cognitive-routing/host-fixture.js → fixtures/pi-host.js} +0 -0
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import test from "node:test";
|
|
3
|
+
import { initialState, reduce, parseRoutingEvent } from "../../dist/cognitive-routing-v2/state.js";
|
|
4
|
+
const pair = { provider: "fixture", modelId: "model", thinking: "off" };
|
|
5
|
+
function machine() {
|
|
6
|
+
let s = initialState(),
|
|
7
|
+
n = 0;
|
|
8
|
+
return {
|
|
9
|
+
get state() {
|
|
10
|
+
return s;
|
|
11
|
+
},
|
|
12
|
+
event(data) {
|
|
13
|
+
return {
|
|
14
|
+
version: 2,
|
|
15
|
+
eventId: `event-${++n}`,
|
|
16
|
+
operationId: `op-${n}`,
|
|
17
|
+
stepId: data.type,
|
|
18
|
+
recordedSessionId: "session",
|
|
19
|
+
data,
|
|
20
|
+
};
|
|
21
|
+
},
|
|
22
|
+
apply(data) {
|
|
23
|
+
const e = this.event(data);
|
|
24
|
+
s = reduce(s, e);
|
|
25
|
+
return e;
|
|
26
|
+
},
|
|
27
|
+
set(e) {
|
|
28
|
+
s = reduce(s, e);
|
|
29
|
+
},
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
const control = (profile) => ({ type: "control", control: "automatic", profile, reason: "fixture control" });
|
|
33
|
+
const opened = (id, profile, assignmentId) => ({
|
|
34
|
+
type: "execution-opened",
|
|
35
|
+
execution: { id, profile, assignmentId, basisUserEntryId: "user", pair, resultEntryIds: [] },
|
|
36
|
+
});
|
|
37
|
+
const bound = (id) => ({
|
|
38
|
+
type: "execution-bound",
|
|
39
|
+
executionId: id,
|
|
40
|
+
assistantEntryId: `assistant-${id}`,
|
|
41
|
+
resultEntryIds: [],
|
|
42
|
+
outcome: "completed",
|
|
43
|
+
});
|
|
44
|
+
function delegated(id = "a1", unitId = "u1", assignmentIds = [id], executionId = "e1", replacement) {
|
|
45
|
+
const hid = `d-${id}`;
|
|
46
|
+
return {
|
|
47
|
+
type: "delegate-accepted",
|
|
48
|
+
unit: { id: unitId, objective: "fixture objective", state: "open", assignmentIds },
|
|
49
|
+
assignment: {
|
|
50
|
+
id,
|
|
51
|
+
unitId,
|
|
52
|
+
contract: "fixture contract",
|
|
53
|
+
basisUserEntryId: "user",
|
|
54
|
+
state: "outstanding",
|
|
55
|
+
delegateHandoffId: hid,
|
|
56
|
+
},
|
|
57
|
+
handoff: {
|
|
58
|
+
id: hid,
|
|
59
|
+
kind: "delegate",
|
|
60
|
+
assignmentId: id,
|
|
61
|
+
text: "fixture contract",
|
|
62
|
+
from: "coordinator",
|
|
63
|
+
to: "executor",
|
|
64
|
+
executionId,
|
|
65
|
+
toolCallId: `call-${executionId}`,
|
|
66
|
+
basisUserEntryId: "user",
|
|
67
|
+
state: "pending",
|
|
68
|
+
reportRevision: 0,
|
|
69
|
+
limitations: [],
|
|
70
|
+
},
|
|
71
|
+
...(replacement ? { replacement } : {}),
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
function returned(report = "exact report", revision = 1) {
|
|
75
|
+
return {
|
|
76
|
+
type: "return-accepted",
|
|
77
|
+
handoff: {
|
|
78
|
+
id: "return",
|
|
79
|
+
kind: "return",
|
|
80
|
+
assignmentId: "a1",
|
|
81
|
+
text: report,
|
|
82
|
+
from: "executor",
|
|
83
|
+
to: "coordinator",
|
|
84
|
+
executionId: "e2",
|
|
85
|
+
toolCallId: "return-call",
|
|
86
|
+
basisUserEntryId: "user",
|
|
87
|
+
state: "pending",
|
|
88
|
+
reportRevision: revision,
|
|
89
|
+
outcome: "partial",
|
|
90
|
+
limitations: ["uncertainty retained"],
|
|
91
|
+
},
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
function started() {
|
|
95
|
+
const m = machine();
|
|
96
|
+
m.apply(control("coordinator"));
|
|
97
|
+
m.apply(opened("e1", "coordinator"));
|
|
98
|
+
m.apply(delegated());
|
|
99
|
+
m.apply(bound("e1"));
|
|
100
|
+
m.apply({ type: "handoff-state", handoffId: "d-a1", state: "configured", observedPair: pair });
|
|
101
|
+
return m;
|
|
102
|
+
}
|
|
103
|
+
const reservation = (revision) => ({
|
|
104
|
+
id: "reservation",
|
|
105
|
+
selectionRevision: revision,
|
|
106
|
+
receiver: "coordinator",
|
|
107
|
+
target: pair,
|
|
108
|
+
qualification: "fixture",
|
|
109
|
+
sources: [],
|
|
110
|
+
maximumInputTokens: 10000,
|
|
111
|
+
outputReserve: 1000,
|
|
112
|
+
estimateMethod: "fixture",
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
test("event identity and operation idempotency reject changed payloads", () => {
|
|
116
|
+
const m = machine(),
|
|
117
|
+
e = m.apply(control("coordinator"));
|
|
118
|
+
const before = m.state;
|
|
119
|
+
m.set(e);
|
|
120
|
+
assert.equal(m.state, before);
|
|
121
|
+
assert.throws(
|
|
122
|
+
() => m.set({ ...e, data: control("executor") }),
|
|
123
|
+
(e) => e.code === "event_identity_conflict",
|
|
124
|
+
);
|
|
125
|
+
assert.throws(
|
|
126
|
+
() => m.set({ ...e, eventId: "other", data: control("executor") }),
|
|
127
|
+
(e) => e.code === "operation_conflict",
|
|
128
|
+
);
|
|
129
|
+
});
|
|
130
|
+
test("closed nested schemas reject malformed optional handoff and source fields", () => {
|
|
131
|
+
const m = machine();
|
|
132
|
+
for (const patch of [{ reason: {} }, { outcome: 123 }, { unexpected: "field" }]) {
|
|
133
|
+
const data = delegated();
|
|
134
|
+
Object.assign(data.handoff, patch);
|
|
135
|
+
assert.throws(
|
|
136
|
+
() => parseRoutingEvent(m.event(data)),
|
|
137
|
+
(e) => e.code === "invalid_assignment",
|
|
138
|
+
);
|
|
139
|
+
}
|
|
140
|
+
const data = opened("e", "executor", "a");
|
|
141
|
+
data.execution.pair = { ...data.execution.pair, extra: "invalid" };
|
|
142
|
+
assert.throws(
|
|
143
|
+
() => parseRoutingEvent(m.event(data)),
|
|
144
|
+
(e) => e.code === "invalid_execution",
|
|
145
|
+
);
|
|
146
|
+
});
|
|
147
|
+
test("prototype names are bounded malformed events, not raw decoder errors", () => {
|
|
148
|
+
for (const type of ["constructor", "__proto__", "toString", "unknown"])
|
|
149
|
+
assert.throws(
|
|
150
|
+
() =>
|
|
151
|
+
parseRoutingEvent({
|
|
152
|
+
version: 2,
|
|
153
|
+
eventId: "e",
|
|
154
|
+
operationId: "o",
|
|
155
|
+
stepId: "s",
|
|
156
|
+
recordedSessionId: "session",
|
|
157
|
+
data: { type },
|
|
158
|
+
}),
|
|
159
|
+
(e) => e.name === "RoutingError" && e.code === "invalid_event_type",
|
|
160
|
+
);
|
|
161
|
+
});
|
|
162
|
+
test("outstanding assignments cannot be implicitly replaced", () => {
|
|
163
|
+
const m = started();
|
|
164
|
+
assert.throws(
|
|
165
|
+
() => m.apply(delegated("a2", "u1", ["a1", "a2"])),
|
|
166
|
+
(e) => e.code === "wrong_control",
|
|
167
|
+
);
|
|
168
|
+
m.apply(control("coordinator"));
|
|
169
|
+
assert.throws(
|
|
170
|
+
() => m.apply(delegated("a2", "u1", ["a1", "a2"])),
|
|
171
|
+
(e) => e.code === "assignment_outstanding",
|
|
172
|
+
);
|
|
173
|
+
assert.equal(m.state.assignments.size, 1);
|
|
174
|
+
});
|
|
175
|
+
test("replacement retains old assignment and requires old execution to be resolved", () => {
|
|
176
|
+
const m = started();
|
|
177
|
+
m.apply(opened("e2", "executor", "a1"));
|
|
178
|
+
m.apply(control("coordinator"));
|
|
179
|
+
m.apply(opened("e3", "coordinator", "a1"));
|
|
180
|
+
const replacement = delegated("a2", "u1", ["a1", "a2"], "e3", {
|
|
181
|
+
assignmentId: "a1",
|
|
182
|
+
reason: "current direction changed",
|
|
183
|
+
});
|
|
184
|
+
assert.throws(
|
|
185
|
+
() => m.apply(replacement),
|
|
186
|
+
(e) => e.code === "unresolved_executor_execution",
|
|
187
|
+
);
|
|
188
|
+
m.apply(bound("e2"));
|
|
189
|
+
m.apply(replacement);
|
|
190
|
+
assert.equal(m.state.unitId, "u1");
|
|
191
|
+
assert.equal(m.state.assignments.get("a1").state, "superseded");
|
|
192
|
+
assert.equal(m.state.assignments.get("a1").contract, "fixture contract");
|
|
193
|
+
assert.equal(m.state.assignmentId, "a2");
|
|
194
|
+
});
|
|
195
|
+
test("saved report revisions and retries preserve exact earlier events", () => {
|
|
196
|
+
const m = started();
|
|
197
|
+
m.apply(opened("e2", "executor", "a1"));
|
|
198
|
+
const first = m.apply(returned());
|
|
199
|
+
m.apply({ type: "handoff-state", handoffId: "return", state: "blocked", reason: "fixture gap" });
|
|
200
|
+
m.apply({
|
|
201
|
+
type: "handoff-retry-requested",
|
|
202
|
+
handoffId: "return",
|
|
203
|
+
attemptId: "retry",
|
|
204
|
+
executionId: "e2",
|
|
205
|
+
toolCallId: "retry-call",
|
|
206
|
+
reportRevision: 1,
|
|
207
|
+
selectionRevision: 0,
|
|
208
|
+
});
|
|
209
|
+
assert.equal(m.state.handoffs.get("return").text, "exact report");
|
|
210
|
+
assert.equal(m.state.handoffs.get("return").reportRevision, 1);
|
|
211
|
+
m.apply(returned("revised exact report", 2));
|
|
212
|
+
assert.equal(m.state.handoffs.get("return").text, "revised exact report");
|
|
213
|
+
assert.equal([...m.state.events.values()].find((e) => e.eventId === first.eventId).data.handoff.text, "exact report");
|
|
214
|
+
assert.equal(m.state.assignments.size, 1);
|
|
215
|
+
});
|
|
216
|
+
test("assessment resumption rejects stale selection and preserves suspension on failure", () => {
|
|
217
|
+
const m = started();
|
|
218
|
+
m.apply(opened("e2", "executor", "a1"));
|
|
219
|
+
m.apply(returned());
|
|
220
|
+
m.apply({
|
|
221
|
+
type: "selection-changed",
|
|
222
|
+
assignmentId: "a1",
|
|
223
|
+
selection: { revision: 1, selected: [], unresolved: [], withdrawals: [] },
|
|
224
|
+
});
|
|
225
|
+
m.apply({ type: "handoff-state", handoffId: "return", state: "configured", observedPair: pair });
|
|
226
|
+
m.apply({
|
|
227
|
+
type: "assessment-suspended",
|
|
228
|
+
handoffId: "return",
|
|
229
|
+
reason: "user-attention",
|
|
230
|
+
basisUserEntryId: "new-user",
|
|
231
|
+
problems: [],
|
|
232
|
+
});
|
|
233
|
+
assert.throws(
|
|
234
|
+
() =>
|
|
235
|
+
m.apply({
|
|
236
|
+
type: "assessment-resumed",
|
|
237
|
+
handoffId: "return",
|
|
238
|
+
basisUserEntryId: "new-user",
|
|
239
|
+
reservation: reservation(0),
|
|
240
|
+
}),
|
|
241
|
+
(e) => e.code === "stale_selection",
|
|
242
|
+
);
|
|
243
|
+
assert.equal(m.state.assessment.view, "suspended");
|
|
244
|
+
m.apply({
|
|
245
|
+
type: "assessment-resumed",
|
|
246
|
+
handoffId: "return",
|
|
247
|
+
basisUserEntryId: "new-user",
|
|
248
|
+
reservation: reservation(1),
|
|
249
|
+
});
|
|
250
|
+
assert.equal(m.state.assessment.view, "active");
|
|
251
|
+
});
|
|
252
|
+
test("accepted closure cannot conceal work; later work creates a new unit", () => {
|
|
253
|
+
const m = started();
|
|
254
|
+
m.apply(control("coordinator"));
|
|
255
|
+
assert.throws(
|
|
256
|
+
() => m.apply({ type: "unit-closed", unitId: "u1", outcome: "accepted", assessment: "incorrect" }),
|
|
257
|
+
(e) => e.code === "unfinished_work",
|
|
258
|
+
);
|
|
259
|
+
m.apply({
|
|
260
|
+
type: "unit-closed",
|
|
261
|
+
unitId: "u1",
|
|
262
|
+
outcome: "cancelled",
|
|
263
|
+
assessment: "user stopped work",
|
|
264
|
+
supersededAssignmentId: "a1",
|
|
265
|
+
});
|
|
266
|
+
assert.equal(m.state.units.get("u1").state, "closed");
|
|
267
|
+
m.apply(opened("e3", "coordinator"));
|
|
268
|
+
assert.throws(
|
|
269
|
+
() => m.apply(delegated("a2", "u1", ["a2"], "e3")),
|
|
270
|
+
(e) => e.code === "closed_unit_reused",
|
|
271
|
+
);
|
|
272
|
+
m.apply(delegated("a2", "u2", ["a2"], "e3"));
|
|
273
|
+
assert.equal(m.state.unitId, "u2");
|
|
274
|
+
assert.equal(m.state.units.size, 2);
|
|
275
|
+
});
|
|
@@ -169,6 +169,22 @@ function renderComponent(component) {
|
|
|
169
169
|
return component.render(120).join("\\n");
|
|
170
170
|
}
|
|
171
171
|
|
|
172
|
+
test("disabled Freeflow Context does not publish an empty oneOf schema", async () => {
|
|
173
|
+
const cwd = await mkdtemp(join(tmpdir(), "freeflow-context-disabled-schema-"));
|
|
174
|
+
try {
|
|
175
|
+
await writeConfig(cwd, {});
|
|
176
|
+
const harness = createHarness(cwd, []);
|
|
177
|
+
await harness.handlers.get("session_start")({ reason: "startup" }, harness.ctx);
|
|
178
|
+
|
|
179
|
+
const contextTool = harness.tools.find((tool) => tool.name === "freeflow_context");
|
|
180
|
+
assert.ok(contextTool);
|
|
181
|
+
assert.deepEqual(contextTool.parameters.properties, {});
|
|
182
|
+
assert.equal(Object.hasOwn(contextTool.parameters, "oneOf"), false);
|
|
183
|
+
} finally {
|
|
184
|
+
await rm(cwd, { recursive: true, force: true });
|
|
185
|
+
}
|
|
186
|
+
});
|
|
187
|
+
|
|
172
188
|
test("enabled Context Virtualization registers, projects, archives, restores, and reports through commands", async () => {
|
|
173
189
|
const cwd = await mkdtemp(join(tmpdir(), "freeflow-context-extension-"));
|
|
174
190
|
try {
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { mkdtemp, mkdir, writeFile, rm } from "node:fs/promises";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import {
|
|
6
|
+
createAgentSession,
|
|
7
|
+
DefaultResourceLoader,
|
|
8
|
+
ModelRuntime,
|
|
9
|
+
SessionManager,
|
|
10
|
+
SettingsManager,
|
|
11
|
+
} from "@earendil-works/pi-coding-agent";
|
|
12
|
+
import freeflow from "../../dist/index.js";
|
|
13
|
+
import { replay } from "../../dist/cognitive-routing-v2/state.js";
|
|
14
|
+
|
|
15
|
+
export function response(n, calls = [], text = "fixture response") {
|
|
16
|
+
const output = [
|
|
17
|
+
{
|
|
18
|
+
type: "message",
|
|
19
|
+
id: `msg-${n}`,
|
|
20
|
+
role: "assistant",
|
|
21
|
+
status: "completed",
|
|
22
|
+
content: [{ type: "output_text", text, annotations: [] }],
|
|
23
|
+
},
|
|
24
|
+
...calls.map((c, i) => ({
|
|
25
|
+
type: "function_call",
|
|
26
|
+
id: `fc_${n}_${i}`,
|
|
27
|
+
call_id: `call-${n}-${i}`,
|
|
28
|
+
name: c.name,
|
|
29
|
+
arguments: JSON.stringify(c.args),
|
|
30
|
+
})),
|
|
31
|
+
];
|
|
32
|
+
const events = [{ type: "response.created", response: { id: `r-${n}`, status: "in_progress", output: [] } }];
|
|
33
|
+
output.forEach((item, i) => {
|
|
34
|
+
events.push({
|
|
35
|
+
type: "response.output_item.added",
|
|
36
|
+
output_index: i,
|
|
37
|
+
item: item.type === "message" ? { ...item, content: [] } : { ...item, arguments: "" },
|
|
38
|
+
});
|
|
39
|
+
if (item.type === "message")
|
|
40
|
+
events.push({ type: "response.output_text.delta", output_index: i, content_index: 0, delta: text });
|
|
41
|
+
else
|
|
42
|
+
events.push(
|
|
43
|
+
{ type: "response.function_call_arguments.delta", output_index: i, delta: item.arguments },
|
|
44
|
+
{ type: "response.function_call_arguments.done", output_index: i, arguments: item.arguments },
|
|
45
|
+
);
|
|
46
|
+
events.push({ type: "response.output_item.done", output_index: i, item });
|
|
47
|
+
});
|
|
48
|
+
events.push({
|
|
49
|
+
type: "response.completed",
|
|
50
|
+
response: {
|
|
51
|
+
id: `r-${n}`,
|
|
52
|
+
status: "completed",
|
|
53
|
+
output,
|
|
54
|
+
usage: { input_tokens: 10, output_tokens: 10, total_tokens: 20 },
|
|
55
|
+
},
|
|
56
|
+
});
|
|
57
|
+
return new Response(events.map((e) => `data: ${JSON.stringify(e)}\n\n`).join("") + "data: [DONE]\n\n", {
|
|
58
|
+
status: 200,
|
|
59
|
+
headers: { "content-type": "text/event-stream" },
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export async function fixture(script, projection = true, after, withUI = true, options = {}) {
|
|
64
|
+
const root = await mkdtemp(join(tmpdir(), "freeflow-v2-native-"));
|
|
65
|
+
const cwd = join(root, "cwd"),
|
|
66
|
+
agentDir = join(root, "agent");
|
|
67
|
+
await mkdir(join(cwd, ".freeflow"), { recursive: true });
|
|
68
|
+
await mkdir(agentDir);
|
|
69
|
+
await writeFile(
|
|
70
|
+
join(cwd, ".freeflow/config.json"),
|
|
71
|
+
JSON.stringify({
|
|
72
|
+
cognitiveRouting: {
|
|
73
|
+
enabled: true,
|
|
74
|
+
projection,
|
|
75
|
+
profiles: {
|
|
76
|
+
coordinator: { provider: "openai", model: "gpt-4o", thinking: "off" },
|
|
77
|
+
executor: { provider: "openai", model: "gpt-4.1-mini", thinking: "off" },
|
|
78
|
+
},
|
|
79
|
+
},
|
|
80
|
+
}),
|
|
81
|
+
);
|
|
82
|
+
await writeFile(join(cwd, "evidence.txt"), "EXACT_EVIDENCE_BODY_81");
|
|
83
|
+
await writeFile(join(cwd, "unselected.txt"), "UNSELECTED_PRIVATE_BODY_93");
|
|
84
|
+
await writeFile(
|
|
85
|
+
join(agentDir, "models.json"),
|
|
86
|
+
JSON.stringify({ providers: { openai: { baseUrl: "https://fixture.invalid/v1" } } }),
|
|
87
|
+
);
|
|
88
|
+
const priorFetch = globalThis.fetch,
|
|
89
|
+
offline = process.env.PI_OFFLINE;
|
|
90
|
+
process.env.PI_OFFLINE = "1";
|
|
91
|
+
const requests = [],
|
|
92
|
+
errors = [],
|
|
93
|
+
contexts = [],
|
|
94
|
+
transportFailures = [],
|
|
95
|
+
notices = [];
|
|
96
|
+
let session;
|
|
97
|
+
const manager = SessionManager.create(cwd, join(root, "sessions"));
|
|
98
|
+
globalThis.fetch = async (url, init) => {
|
|
99
|
+
assert.match(String(url), /^https:\/\/fixture\.invalid\//, "no unexpected network destination");
|
|
100
|
+
const body = JSON.parse(String(init.body));
|
|
101
|
+
requests.push(body);
|
|
102
|
+
assert.ok(requests.length <= (options.maxRequests ?? 15), "bounded fixture request count");
|
|
103
|
+
try {
|
|
104
|
+
const calls = await script(requests.length, body, manager, requests);
|
|
105
|
+
return options.response ? options.response(requests.length, calls) : response(requests.length, calls);
|
|
106
|
+
} catch (error) {
|
|
107
|
+
transportFailures.push(error);
|
|
108
|
+
throw error;
|
|
109
|
+
}
|
|
110
|
+
};
|
|
111
|
+
try {
|
|
112
|
+
const modelRuntime = await ModelRuntime.create({
|
|
113
|
+
authPath: join(agentDir, "auth.json"),
|
|
114
|
+
modelsPath: join(agentDir, "models.json"),
|
|
115
|
+
modelsStorePath: join(agentDir, "models-store.json"),
|
|
116
|
+
allowModelNetwork: false,
|
|
117
|
+
});
|
|
118
|
+
await modelRuntime.setRuntimeApiKey("openai", "fixture-not-a-real-key");
|
|
119
|
+
const settingsManager = SettingsManager.inMemory({
|
|
120
|
+
compaction: { enabled: false, keepRecentTokens: 1, reserveTokens: 1 },
|
|
121
|
+
retry: { enabled: false, maxRetries: 0, provider: { maxRetries: 0 } },
|
|
122
|
+
...options.settings,
|
|
123
|
+
});
|
|
124
|
+
const loader = new DefaultResourceLoader({
|
|
125
|
+
cwd,
|
|
126
|
+
agentDir,
|
|
127
|
+
settingsManager,
|
|
128
|
+
noExtensions: true,
|
|
129
|
+
noSkills: true,
|
|
130
|
+
noPromptTemplates: true,
|
|
131
|
+
noThemes: true,
|
|
132
|
+
noContextFiles: true,
|
|
133
|
+
extensionFactories: [
|
|
134
|
+
freeflow,
|
|
135
|
+
...(options.extensions ?? []),
|
|
136
|
+
(pi) => {
|
|
137
|
+
pi.on("context", (event) => {
|
|
138
|
+
contexts.push(structuredClone(event.messages));
|
|
139
|
+
});
|
|
140
|
+
pi.on("session_before_compact", () => ({
|
|
141
|
+
compaction: {
|
|
142
|
+
summary: "Fixture compacted prior work; evidence must come from canonical sources.",
|
|
143
|
+
firstKeptEntryId: manager.getLeafId(),
|
|
144
|
+
tokensBefore: 100,
|
|
145
|
+
},
|
|
146
|
+
}));
|
|
147
|
+
},
|
|
148
|
+
],
|
|
149
|
+
});
|
|
150
|
+
await loader.reload();
|
|
151
|
+
assert.deepEqual(loader.getExtensions().errors, []);
|
|
152
|
+
({ session } = await createAgentSession({
|
|
153
|
+
cwd,
|
|
154
|
+
agentDir,
|
|
155
|
+
modelRuntime,
|
|
156
|
+
model: modelRuntime.getModel("openai", "gpt-4o"),
|
|
157
|
+
thinkingLevel: "off",
|
|
158
|
+
settingsManager,
|
|
159
|
+
sessionManager: manager,
|
|
160
|
+
resourceLoader: loader,
|
|
161
|
+
}));
|
|
162
|
+
// Public retained bindings make Pi emit session_start again on SDK reload, including headless mode.
|
|
163
|
+
await session.bindExtensions({
|
|
164
|
+
mode: "print",
|
|
165
|
+
onError: (e) => errors.push(e),
|
|
166
|
+
...(withUI ? { uiContext: { notify: (...args) => notices.push(args), setStatus: () => {} } } : {}),
|
|
167
|
+
});
|
|
168
|
+
await options.onSession?.(session);
|
|
169
|
+
await session.prompt("Complete the fixture assignment.");
|
|
170
|
+
await session.waitForIdle();
|
|
171
|
+
assert.deepEqual(errors, [], "extension lifecycle errors");
|
|
172
|
+
if (after) await after({ session, manager, requests, contexts, cwd, notices });
|
|
173
|
+
assert.deepEqual(errors, [], "post-lifecycle errors");
|
|
174
|
+
assert.deepEqual(transportFailures, [], "scripted provider assertions must not be swallowed as provider errors");
|
|
175
|
+
assert.notEqual(
|
|
176
|
+
session.messages.at(-1)?.stopReason,
|
|
177
|
+
"error",
|
|
178
|
+
`fixture completes without provider failure: ${session.messages.at(-1)?.errorMessage ?? ""}`,
|
|
179
|
+
);
|
|
180
|
+
return { requests, state: replay(manager.getBranch()), entries: manager.getEntries(), contexts };
|
|
181
|
+
} finally {
|
|
182
|
+
session?.dispose();
|
|
183
|
+
globalThis.fetch = priorFetch;
|
|
184
|
+
if (offline === undefined) delete process.env.PI_OFFLINE;
|
|
185
|
+
else process.env.PI_OFFLINE = offline;
|
|
186
|
+
await rm(root, { recursive: true, force: true });
|
|
187
|
+
}
|
|
188
|
+
}
|
|
@@ -14,7 +14,7 @@ import {
|
|
|
14
14
|
runtimeContext,
|
|
15
15
|
setSessionCoreOverride,
|
|
16
16
|
} from "../../dist/runtime/runtime-context.js";
|
|
17
|
-
import { PIFLOW_HOST } from "../
|
|
17
|
+
import { PIFLOW_HOST } from "../fixtures/pi-host.js";
|
|
18
18
|
|
|
19
19
|
function context(cwd) {
|
|
20
20
|
const notifications = [];
|
|
@@ -138,8 +138,9 @@ test("Pi exposes base skills without mode or Skills controls", async () => {
|
|
|
138
138
|
const { handlers, commands } = loadExtension();
|
|
139
139
|
const ctx = context(cwd);
|
|
140
140
|
const resources = await handlers.get("resources_discover")({ cwd }, ctx);
|
|
141
|
-
assert.equal(resources.skillPaths.length,
|
|
141
|
+
assert.equal(resources.skillPaths.length, 24);
|
|
142
142
|
assert.ok(resources.skillPaths.some((path) => path.endsWith("/skills/workflow/SKILL.md")));
|
|
143
|
+
assert.ok(!resources.skillPaths.some((path) => path.endsWith("/skills/tdd/SKILL.md")));
|
|
143
144
|
assert.ok(!resources.skillPaths.some((path) => path.endsWith("/skills/mode-contract/SKILL.md")));
|
|
144
145
|
|
|
145
146
|
const freeflowCommand = commands.find((command) => command.name === "freeflow");
|