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