@hassangameryt/freeflow 0.7.0 → 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.
Files changed (136) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +2 -1
  3. package/.codex-plugin/plugin.json +2 -2
  4. package/.cursor-plugin/plugin.json +25 -0
  5. package/README.md +57 -26
  6. package/capabilities/cognitive-routing/SKILL.md +94 -311
  7. package/com.github.copilot/hooks/hooks.json +11 -0
  8. package/command-surface.json +7 -1
  9. package/gemini-extension.json +5 -0
  10. package/hooks/adapters/claude-session-start.mjs +16 -0
  11. package/hooks/adapters/codex-session-start.mjs +16 -0
  12. package/hooks/adapters/copilot-session-start.mjs +13 -0
  13. package/hooks/adapters/cursor-session-start.mjs +13 -0
  14. package/hooks/adapters/gemini-session-start.mjs +15 -0
  15. package/hooks/claude/hooks.json +18 -0
  16. package/hooks/codex/hooks.json +18 -0
  17. package/hooks/cursor/hooks.json +9 -0
  18. package/hooks/freeflow-runtime-context.mjs +3 -215
  19. package/hooks/hooks.json +4 -5
  20. package/hooks/shared/runtime-context.mjs +210 -0
  21. package/hooks/tests/check-host-adapters.test.mjs +82 -0
  22. package/hooks/tests/check-runtime-context-hook.sh +34 -3
  23. package/package.json +20 -6
  24. package/pi-extension/dist/cognitive-routing-v2/budget.js +42 -0
  25. package/pi-extension/dist/cognitive-routing-v2/config.js +120 -0
  26. package/pi-extension/dist/cognitive-routing-v2/projection.js +321 -0
  27. package/pi-extension/dist/cognitive-routing-v2/render.js +231 -0
  28. package/pi-extension/dist/cognitive-routing-v2/runtime.js +1585 -0
  29. package/pi-extension/dist/cognitive-routing-v2/schemas.js +107 -0
  30. package/pi-extension/dist/cognitive-routing-v2/state.js +612 -0
  31. package/pi-extension/dist/cognitive-routing-v2/tools.js +91 -0
  32. package/pi-extension/dist/cognitive-routing-v2/types.js +44 -0
  33. package/pi-extension/dist/freeflow-context/tool.js +5 -0
  34. package/pi-extension/dist/index.js +279 -624
  35. package/pi-extension/dist/runtime/runtime-context.js +63 -13
  36. package/pi-extension/dist/session-sources/events.js +195 -0
  37. package/pi-extension/dist/session-sources/read-only-session.js +148 -0
  38. package/pi-extension/dist/session-sources/sources.js +233 -0
  39. package/pi-extension/dist/settings/settings-ui.js +44 -117
  40. package/pi-extension/src/cognitive-routing-v2/budget.ts +42 -0
  41. package/pi-extension/src/cognitive-routing-v2/config.ts +143 -0
  42. package/pi-extension/src/cognitive-routing-v2/projection.ts +365 -0
  43. package/pi-extension/src/cognitive-routing-v2/render.ts +256 -0
  44. package/pi-extension/src/cognitive-routing-v2/runtime.ts +1634 -0
  45. package/pi-extension/src/cognitive-routing-v2/schemas.ts +109 -0
  46. package/pi-extension/src/cognitive-routing-v2/state.ts +622 -0
  47. package/pi-extension/src/cognitive-routing-v2/tools.ts +93 -0
  48. package/pi-extension/src/cognitive-routing-v2/types.ts +222 -0
  49. package/pi-extension/src/freeflow-context/tool.ts +5 -0
  50. package/pi-extension/src/index.ts +285 -674
  51. package/pi-extension/src/runtime/runtime-context.ts +75 -14
  52. package/pi-extension/src/session-sources/events.ts +208 -0
  53. package/pi-extension/src/session-sources/read-only-session.ts +170 -0
  54. package/pi-extension/src/session-sources/sources.ts +263 -0
  55. package/pi-extension/src/settings/settings-ui.ts +54 -131
  56. package/pi-extension/tests/cognitive-routing-v2/communication.test.js +484 -0
  57. package/pi-extension/tests/cognitive-routing-v2/compaction-input.test.js +183 -0
  58. package/pi-extension/tests/cognitive-routing-v2/native.test.js +441 -0
  59. package/pi-extension/tests/cognitive-routing-v2/performance.test.js +87 -0
  60. package/pi-extension/tests/cognitive-routing-v2/persistence.test.js +245 -0
  61. package/pi-extension/tests/cognitive-routing-v2/projection.test.js +241 -0
  62. package/pi-extension/tests/cognitive-routing-v2/recovery.test.js +236 -0
  63. package/pi-extension/tests/cognitive-routing-v2/render.test.js +230 -0
  64. package/pi-extension/tests/cognitive-routing-v2/runtime.test.js +153 -0
  65. package/pi-extension/tests/cognitive-routing-v2/stabilization.test.js +521 -0
  66. package/pi-extension/tests/cognitive-routing-v2/state.test.js +275 -0
  67. package/pi-extension/tests/context-virtualization/extension.test.js +16 -0
  68. package/pi-extension/tests/fixtures/routing-native.js +188 -0
  69. package/pi-extension/tests/integration/mode-free-core.test.js +3 -2
  70. package/pi-extension/tests/integration/pi-extension.test.js +154 -37
  71. package/pi-extension/tests/integration/prompt-architecture.test.js +87 -6
  72. package/pi-extension/tests/integration/unaffected-regressions.test.js +77 -10
  73. package/plugin.json +33 -0
  74. package/runtime/prompts/cognitive-routing.md +4 -5
  75. package/runtime/prompts/core.md +51 -45
  76. package/runtime/prompts/interaction-contract.md +5 -5
  77. package/skills/action-selection/SKILL.md +57 -97
  78. package/skills/design-for-depth/SKILL.md +58 -193
  79. package/skills/design-for-depth/references/implementation-decisions.md +76 -0
  80. package/skills/diagnose-failure/SKILL.md +64 -196
  81. package/skills/discuss/SKILL.md +64 -123
  82. package/skills/discuss/references/discussion-continuity.md +7 -3
  83. package/skills/execute-work/SKILL.md +69 -88
  84. package/skills/execute-work/references/execute-work-edges.md +9 -3
  85. package/skills/handoff/SKILL.md +44 -54
  86. package/skills/handoff/references/handoff-templates.md +15 -6
  87. package/skills/review-artifact/SKILL.md +80 -127
  88. package/skills/review-artifact/references/independent-artifact-reviewer-contract.md +1 -1
  89. package/skills/review-work/SKILL.md +65 -115
  90. package/skills/review-work/references/independent-work-reviewer-contract.md +1 -1
  91. package/skills/simplify-code/SKILL.md +47 -100
  92. package/skills/track-work/SKILL.md +128 -119
  93. package/skills/track-work/references/working-record-format.md +14 -2
  94. package/skills/verify-work/SKILL.md +58 -62
  95. package/skills/verify-work/references/integration-evidence.md +4 -2
  96. package/skills/verify-work/references/test-design.md +89 -0
  97. package/skills/workflow/SKILL.md +73 -119
  98. package/skills/write-plan/SKILL.md +46 -77
  99. package/skills/write-plan/references/plan-shapes.md +5 -5
  100. package/skills/write-spec/SKILL.md +51 -63
  101. package/skills/write-spec/references/artifact-standards.md +2 -2
  102. package/skills/write-spec/references/decision-records.md +3 -2
  103. package/skills/write-spec/references/spec-shapes.md +15 -0
  104. package/pi-extension/dist/cognitive-routing/commands.js +0 -96
  105. package/pi-extension/dist/cognitive-routing/config.js +0 -232
  106. package/pi-extension/dist/cognitive-routing/controller.js +0 -1292
  107. package/pi-extension/dist/cognitive-routing/history.js +0 -451
  108. package/pi-extension/dist/cognitive-routing/host.js +0 -188
  109. package/pi-extension/dist/cognitive-routing/pi-routing-session.js +0 -414
  110. package/pi-extension/dist/cognitive-routing/pi-session-control.js +0 -320
  111. package/pi-extension/dist/cognitive-routing/runtime.js +0 -43
  112. package/pi-extension/dist/cognitive-routing/tool.js +0 -212
  113. package/pi-extension/dist/cognitive-routing/types.js +0 -6
  114. package/pi-extension/src/cognitive-routing/commands.ts +0 -129
  115. package/pi-extension/src/cognitive-routing/config.ts +0 -303
  116. package/pi-extension/src/cognitive-routing/controller.ts +0 -1628
  117. package/pi-extension/src/cognitive-routing/history.ts +0 -650
  118. package/pi-extension/src/cognitive-routing/host.ts +0 -266
  119. package/pi-extension/src/cognitive-routing/pi-routing-session.ts +0 -538
  120. package/pi-extension/src/cognitive-routing/pi-session-control.ts +0 -444
  121. package/pi-extension/src/cognitive-routing/runtime.ts +0 -57
  122. package/pi-extension/src/cognitive-routing/tool.ts +0 -273
  123. package/pi-extension/src/cognitive-routing/types.ts +0 -89
  124. package/pi-extension/tests/cognitive-routing/commands.test.js +0 -134
  125. package/pi-extension/tests/cognitive-routing/controller.test.js +0 -1128
  126. package/pi-extension/tests/cognitive-routing/extension-lifecycle.test.js +0 -1142
  127. package/pi-extension/tests/cognitive-routing/history.test.js +0 -274
  128. package/pi-extension/tests/cognitive-routing/host-identity.test.js +0 -63
  129. package/pi-extension/tests/cognitive-routing/pi-normal-lifecycle.test.js +0 -434
  130. package/pi-extension/tests/cognitive-routing/pi-session-control.test.js +0 -174
  131. package/pi-extension/tests/cognitive-routing/pi-session-history.test.js +0 -91
  132. package/pi-extension/tests/cognitive-routing/profile-resolution.test.js +0 -279
  133. package/pi-extension/tests/cognitive-routing/tool.test.js +0 -230
  134. package/skills/tdd/SKILL.md +0 -143
  135. package/skills/tdd/references/test-design.md +0 -96
  136. /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 "../cognitive-routing/host-fixture.js";
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, 25);
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");