@hassangameryt/freeflow 0.7.1 → 0.7.3

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 (147) 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 +153 -128
  6. package/capabilities/cognitive-routing/SKILL.md +126 -274
  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 +136 -0
  12. package/pi-extension/dist/cognitive-routing-v2/projection.js +330 -0
  13. package/pi-extension/dist/cognitive-routing-v2/render.js +252 -0
  14. package/pi-extension/dist/cognitive-routing-v2/runtime.js +2181 -0
  15. package/pi-extension/dist/cognitive-routing-v2/schemas.js +173 -0
  16. package/pi-extension/dist/cognitive-routing-v2/state.js +828 -0
  17. package/pi-extension/dist/cognitive-routing-v2/tools.js +60 -0
  18. package/pi-extension/dist/cognitive-routing-v2/types.js +48 -0
  19. package/pi-extension/dist/context-virtualization/runtime.js +29 -7
  20. package/pi-extension/dist/freeflow-context/tool.js +12 -6
  21. package/pi-extension/dist/index.js +302 -626
  22. package/pi-extension/dist/provider-support/astra/adapter.js +110 -0
  23. package/pi-extension/dist/provider-support/astra/history.js +125 -0
  24. package/pi-extension/dist/provider-support/astra/index.js +11 -0
  25. package/pi-extension/dist/provider-support/astra/session-state.js +114 -0
  26. package/pi-extension/dist/provider-support/index.js +4 -0
  27. package/pi-extension/dist/runtime/request-history.js +216 -0
  28. package/pi-extension/dist/runtime/runtime-context.js +85 -16
  29. package/pi-extension/dist/session-sources/events.js +217 -0
  30. package/pi-extension/dist/session-sources/provenance.js +32 -0
  31. package/pi-extension/dist/session-sources/read-only-session.js +148 -0
  32. package/pi-extension/dist/session-sources/sources.js +277 -0
  33. package/pi-extension/dist/settings/settings-ui.js +274 -147
  34. package/pi-extension/src/cognitive-routing-v2/budget.ts +42 -0
  35. package/pi-extension/src/cognitive-routing-v2/config.ts +173 -0
  36. package/pi-extension/src/cognitive-routing-v2/projection.ts +375 -0
  37. package/pi-extension/src/cognitive-routing-v2/render.ts +277 -0
  38. package/pi-extension/src/cognitive-routing-v2/runtime.ts +2241 -0
  39. package/pi-extension/src/cognitive-routing-v2/schemas.ts +180 -0
  40. package/pi-extension/src/cognitive-routing-v2/state.ts +839 -0
  41. package/pi-extension/src/cognitive-routing-v2/tools.ts +62 -0
  42. package/pi-extension/src/cognitive-routing-v2/types.ts +254 -0
  43. package/pi-extension/src/context-virtualization/runtime.ts +30 -8
  44. package/pi-extension/src/freeflow-context/tool.ts +12 -6
  45. package/pi-extension/src/index.ts +308 -675
  46. package/pi-extension/src/provider-support/astra/adapter.ts +108 -0
  47. package/pi-extension/src/provider-support/astra/history.ts +145 -0
  48. package/pi-extension/src/provider-support/astra/index.ts +12 -0
  49. package/pi-extension/src/provider-support/astra/session-state.ts +113 -0
  50. package/pi-extension/src/provider-support/index.ts +5 -0
  51. package/pi-extension/src/runtime/request-history.ts +243 -0
  52. package/pi-extension/src/runtime/runtime-context.ts +100 -17
  53. package/pi-extension/src/session-sources/events.ts +230 -0
  54. package/pi-extension/src/session-sources/provenance.ts +39 -0
  55. package/pi-extension/src/session-sources/read-only-session.ts +170 -0
  56. package/pi-extension/src/session-sources/sources.ts +334 -0
  57. package/pi-extension/src/settings/settings-ui.ts +311 -158
  58. package/pi-extension/tests/cache-reuse/history.test.js +117 -0
  59. package/pi-extension/tests/cache-reuse/native.test.js +91 -0
  60. package/pi-extension/tests/cache-reuse/settings.test.js +211 -0
  61. package/pi-extension/tests/cognitive-routing-v2/communication.test.js +577 -0
  62. package/pi-extension/tests/cognitive-routing-v2/compaction-input.test.js +183 -0
  63. package/pi-extension/tests/cognitive-routing-v2/delegation.test.js +557 -0
  64. package/pi-extension/tests/cognitive-routing-v2/native.test.js +474 -0
  65. package/pi-extension/tests/cognitive-routing-v2/performance.test.js +87 -0
  66. package/pi-extension/tests/cognitive-routing-v2/persistence.test.js +300 -0
  67. package/pi-extension/tests/cognitive-routing-v2/projection.test.js +241 -0
  68. package/pi-extension/tests/cognitive-routing-v2/recovery.test.js +1034 -0
  69. package/pi-extension/tests/cognitive-routing-v2/render.test.js +244 -0
  70. package/pi-extension/tests/cognitive-routing-v2/runtime.test.js +423 -0
  71. package/pi-extension/tests/cognitive-routing-v2/stabilization.test.js +521 -0
  72. package/pi-extension/tests/cognitive-routing-v2/state.test.js +586 -0
  73. package/pi-extension/tests/context-virtualization/extension.test.js +33 -7
  74. package/pi-extension/tests/context-virtualization/runtime.test.js +17 -3
  75. package/pi-extension/tests/fixtures/routing-native.js +191 -0
  76. package/pi-extension/tests/integration/mode-free-core.test.js +6 -4
  77. package/pi-extension/tests/integration/pi-extension.test.js +655 -59
  78. package/pi-extension/tests/integration/prompt-architecture.test.js +127 -30
  79. package/pi-extension/tests/integration/unaffected-regressions.test.js +78 -11
  80. package/pi-extension/tests/provider-support/astra-boundaries.test.js +75 -0
  81. package/pi-extension/tests/provider-support/astra.test.js +306 -0
  82. package/pi-extension/tests/provider-support/cli.test.js +92 -0
  83. package/pi-extension/tests/provider-support/native.test.js +183 -0
  84. package/plugin.json +1 -1
  85. package/runtime/prompts/cognitive-routing.md +4 -5
  86. package/runtime/prompts/core.md +51 -45
  87. package/runtime/prompts/interaction-contract.md +5 -5
  88. package/skills/action-selection/SKILL.md +57 -97
  89. package/skills/design-for-depth/SKILL.md +58 -193
  90. package/skills/design-for-depth/references/implementation-decisions.md +76 -0
  91. package/skills/diagnose-failure/SKILL.md +64 -196
  92. package/skills/discuss/SKILL.md +64 -123
  93. package/skills/discuss/references/discussion-continuity.md +7 -3
  94. package/skills/execute-work/SKILL.md +69 -88
  95. package/skills/execute-work/references/execute-work-edges.md +9 -3
  96. package/skills/handoff/SKILL.md +44 -54
  97. package/skills/handoff/references/handoff-templates.md +15 -6
  98. package/skills/review-artifact/SKILL.md +80 -127
  99. package/skills/review-artifact/references/independent-artifact-reviewer-contract.md +1 -1
  100. package/skills/review-work/SKILL.md +65 -115
  101. package/skills/review-work/references/independent-work-reviewer-contract.md +1 -1
  102. package/skills/simplify-code/SKILL.md +47 -100
  103. package/skills/track-work/SKILL.md +128 -119
  104. package/skills/track-work/references/working-record-format.md +14 -2
  105. package/skills/verify-work/SKILL.md +58 -62
  106. package/skills/verify-work/references/integration-evidence.md +4 -2
  107. package/skills/verify-work/references/test-design.md +89 -0
  108. package/skills/workflow/SKILL.md +73 -119
  109. package/skills/write-plan/SKILL.md +46 -77
  110. package/skills/write-plan/references/plan-shapes.md +5 -5
  111. package/skills/write-spec/SKILL.md +51 -63
  112. package/skills/write-spec/references/artifact-standards.md +2 -2
  113. package/skills/write-spec/references/decision-records.md +3 -2
  114. package/skills/write-spec/references/spec-shapes.md +15 -0
  115. package/pi-extension/dist/cognitive-routing/commands.js +0 -96
  116. package/pi-extension/dist/cognitive-routing/config.js +0 -232
  117. package/pi-extension/dist/cognitive-routing/controller.js +0 -1292
  118. package/pi-extension/dist/cognitive-routing/history.js +0 -451
  119. package/pi-extension/dist/cognitive-routing/host.js +0 -188
  120. package/pi-extension/dist/cognitive-routing/pi-routing-session.js +0 -414
  121. package/pi-extension/dist/cognitive-routing/pi-session-control.js +0 -320
  122. package/pi-extension/dist/cognitive-routing/runtime.js +0 -43
  123. package/pi-extension/dist/cognitive-routing/tool.js +0 -212
  124. package/pi-extension/dist/cognitive-routing/types.js +0 -6
  125. package/pi-extension/src/cognitive-routing/commands.ts +0 -129
  126. package/pi-extension/src/cognitive-routing/config.ts +0 -303
  127. package/pi-extension/src/cognitive-routing/controller.ts +0 -1628
  128. package/pi-extension/src/cognitive-routing/history.ts +0 -650
  129. package/pi-extension/src/cognitive-routing/host.ts +0 -266
  130. package/pi-extension/src/cognitive-routing/pi-routing-session.ts +0 -538
  131. package/pi-extension/src/cognitive-routing/pi-session-control.ts +0 -444
  132. package/pi-extension/src/cognitive-routing/runtime.ts +0 -57
  133. package/pi-extension/src/cognitive-routing/tool.ts +0 -273
  134. package/pi-extension/src/cognitive-routing/types.ts +0 -89
  135. package/pi-extension/tests/cognitive-routing/commands.test.js +0 -134
  136. package/pi-extension/tests/cognitive-routing/controller.test.js +0 -1128
  137. package/pi-extension/tests/cognitive-routing/extension-lifecycle.test.js +0 -1142
  138. package/pi-extension/tests/cognitive-routing/history.test.js +0 -274
  139. package/pi-extension/tests/cognitive-routing/host-identity.test.js +0 -63
  140. package/pi-extension/tests/cognitive-routing/pi-normal-lifecycle.test.js +0 -434
  141. package/pi-extension/tests/cognitive-routing/pi-session-control.test.js +0 -174
  142. package/pi-extension/tests/cognitive-routing/pi-session-history.test.js +0 -91
  143. package/pi-extension/tests/cognitive-routing/profile-resolution.test.js +0 -279
  144. package/pi-extension/tests/cognitive-routing/tool.test.js +0 -230
  145. package/skills/tdd/SKILL.md +0 -143
  146. package/skills/tdd/references/test-design.md +0 -96
  147. /package/pi-extension/tests/{cognitive-routing/host-fixture.js → fixtures/pi-host.js} +0 -0
@@ -0,0 +1,557 @@
1
+ import assert from "node:assert/strict";
2
+ import { writeFile } from "node:fs/promises";
3
+ import { join } from "node:path";
4
+ import test from "node:test";
5
+ import { resolveCognitiveRoutingState } from "../../dist/cognitive-routing-v2/config.js";
6
+ import { RoutingRuntime } from "../../dist/cognitive-routing-v2/runtime.js";
7
+ import { replay } from "../../dist/cognitive-routing-v2/state.js";
8
+ import { fixture } from "../fixtures/routing-native.js";
9
+
10
+ const coordinator = { provider: "openai", model: "gpt-4o", thinking: "off" };
11
+ const worker = { provider: "openai", model: "gpt-4.1-mini", thinking: "off" };
12
+ const alternateExecutor = { provider: "openai", model: "gpt-4.1", thinking: "off" };
13
+ const bothConfig = {
14
+ enabled: true,
15
+ delegation: "both",
16
+ projection: true,
17
+ profiles: { coordinator, helper: worker, executor: worker },
18
+ };
19
+ const helperConfig = {
20
+ enabled: true,
21
+ delegation: "helper",
22
+ projection: true,
23
+ profiles: { coordinator, helper: worker },
24
+ };
25
+
26
+ function registry(available = ["coordinator", "helper", "executor"]) {
27
+ const models = {
28
+ coordinator: { provider: "test", id: "coordinator", reasoning: true },
29
+ helper: { provider: "test", id: "helper", reasoning: true },
30
+ executor: { provider: "test", id: "executor", reasoning: true },
31
+ };
32
+ return {
33
+ find(_provider, id) {
34
+ return available.includes(id) ? models[id] : undefined;
35
+ },
36
+ async getApiKeyAndHeaders() {
37
+ return { ok: true };
38
+ },
39
+ clampThinkingLevel(_model, level) {
40
+ return level;
41
+ },
42
+ };
43
+ }
44
+
45
+ const profile = (model) => ({ provider: "test", model, thinking: "off" });
46
+
47
+ test("delegation modes validate only enabled worker presets and preserve the executor default", async () => {
48
+ const legacy = await resolveCognitiveRoutingState(
49
+ {
50
+ cognitiveRouting: {
51
+ enabled: true,
52
+ profiles: {
53
+ coordinator: profile("coordinator"),
54
+ executor: profile("executor"),
55
+ helper: profile("missing-unused-helper"),
56
+ },
57
+ },
58
+ },
59
+ {},
60
+ { modelRegistry: registry() },
61
+ );
62
+ assert.equal(legacy.delegation, "executor");
63
+ assert.equal(legacy.delegationSource, "default");
64
+ assert.equal(legacy.effective, true, "an unavailable unused Helper preset does not block executor-only mode");
65
+
66
+ const helperOnly = await resolveCognitiveRoutingState(
67
+ {
68
+ cognitiveRouting: {
69
+ enabled: true,
70
+ delegation: "helper",
71
+ profiles: { coordinator: profile("coordinator"), helper: profile("helper") },
72
+ },
73
+ },
74
+ {},
75
+ { modelRegistry: registry() },
76
+ );
77
+ assert.equal(helperOnly.effective, true, "helper-only does not require Executor");
78
+
79
+ const missingBoth = await resolveCognitiveRoutingState(
80
+ {
81
+ cognitiveRouting: {
82
+ enabled: true,
83
+ delegation: "both",
84
+ profiles: { coordinator: profile("coordinator"), helper: profile("helper") },
85
+ },
86
+ },
87
+ {},
88
+ { modelRegistry: registry() },
89
+ );
90
+ assert.equal(missingBoth.effective, false);
91
+ assert.equal(missingBoth.blockingReason.code, "profile_missing");
92
+ assert.match(missingBoth.blockingReason.message, /executor/i);
93
+
94
+ const personalMode = await resolveCognitiveRoutingState(
95
+ {
96
+ cognitiveRouting: {
97
+ enabled: true,
98
+ delegation: "executor",
99
+ profiles: { coordinator: profile("coordinator"), executor: profile("executor") },
100
+ },
101
+ },
102
+ {
103
+ cognitiveRouting: {
104
+ delegation: "helper",
105
+ profiles: { helper: profile("helper") },
106
+ },
107
+ },
108
+ { modelRegistry: registry() },
109
+ );
110
+ assert.equal(personalMode.effective, true);
111
+ assert.equal(personalMode.delegation, "helper");
112
+ assert.equal(personalMode.delegationSource, "personal");
113
+
114
+ const malformedUnused = await resolveCognitiveRoutingState(
115
+ {
116
+ cognitiveRouting: {
117
+ enabled: true,
118
+ profiles: {
119
+ coordinator: profile("coordinator"),
120
+ executor: profile("executor"),
121
+ helper: { provider: "test", model: "helper" },
122
+ },
123
+ },
124
+ },
125
+ {},
126
+ { modelRegistry: registry() },
127
+ );
128
+ assert.equal(malformedUnused.configValid, false, "malformed unused presets remain invalid configuration");
129
+ });
130
+
131
+ test("a duplicate delegation operation cannot change its recorded worker", { timeout: 30000 }, async () => {
132
+ const originalInvoke = RoutingRuntime.prototype.invoke;
133
+ let checked = false;
134
+ RoutingRuntime.prototype.invoke = async function (name, id, input, signal, ctx) {
135
+ const result = await originalInvoke.call(this, name, id, input, signal, ctx);
136
+ if (!checked && name === "freeflow_delegate" && input.worker === "helper") {
137
+ checked = true;
138
+ this.receipts.clear();
139
+ const conflict = await originalInvoke.call(this, name, id, { ...input, worker: "executor" }, signal, ctx);
140
+ assert.equal(conflict.details.code, "operation_conflict");
141
+ }
142
+ return result;
143
+ };
144
+ try {
145
+ const result = await fixture(
146
+ (n) =>
147
+ n === 1
148
+ ? [
149
+ {
150
+ name: "freeflow_delegate",
151
+ args: { operation: "assign", worker: "helper", contract: "Keep the accepted Helper identity." },
152
+ },
153
+ ]
154
+ : n === 2
155
+ ? [
156
+ {
157
+ name: "freeflow_return",
158
+ args: { operation: "submit", report: "Helper identity retained.", outcome: "completed" },
159
+ },
160
+ ]
161
+ : n === 3
162
+ ? [
163
+ {
164
+ name: "freeflow_unit",
165
+ args: { operation: "close", outcome: "accepted", assessment: "Worker identity is stable." },
166
+ },
167
+ ]
168
+ : [],
169
+ true,
170
+ undefined,
171
+ true,
172
+ { cognitiveRouting: bothConfig, maxRequests: 5 },
173
+ );
174
+ assert.equal(checked, true);
175
+ assert.equal([...result.state.handoffs.values()].find((handoff) => handoff.kind === "delegate").to, "helper");
176
+ } finally {
177
+ RoutingRuntime.prototype.invoke = originalInvoke;
178
+ }
179
+ });
180
+
181
+ test("both mode requires an explicit worker and shares Helper history with Executor", { timeout: 30000 }, async () => {
182
+ let helperReadId;
183
+ const result = await fixture(
184
+ (n, body, manager) => {
185
+ const wire = JSON.stringify(body);
186
+ if (n === 1) return [{ name: "freeflow_delegate", args: { operation: "assign", contract: "Ambiguous worker." } }];
187
+ if (n === 2) {
188
+ assert.equal(replay(manager.getBranch()).assignments.size, 0, "ambiguous delegation appends no assignment");
189
+ return [
190
+ {
191
+ name: "freeflow_delegate",
192
+ args: { operation: "assign", worker: "helper", contract: "Read the marker for later implementation." },
193
+ },
194
+ ];
195
+ }
196
+ if (n === 3) {
197
+ assert.equal(body.model, "gpt-4.1-mini");
198
+ assert.equal(replay(manager.getBranch()).profile, "helper");
199
+ return [{ name: "read", args: { path: "unselected.txt" } }];
200
+ }
201
+ if (n === 4) {
202
+ const read = manager
203
+ .getBranch()
204
+ .filter((entry) => entry.message?.toolName === "read")
205
+ .at(-1);
206
+ helperReadId = read.id;
207
+ assert.equal(read.message.isError, false);
208
+ assert.match(wire, /UNSELECTED_PRIVATE_BODY_93/);
209
+ return [
210
+ {
211
+ name: "freeflow_return",
212
+ args: { operation: "submit", report: "Helper context gathered.", outcome: "completed" },
213
+ },
214
+ ];
215
+ }
216
+ if (n === 5) {
217
+ assert.equal(body.model, "gpt-4o");
218
+ assert.equal(replay(manager.getBranch()).profile, "coordinator");
219
+ assert.doesNotMatch(
220
+ wire,
221
+ /UNSELECTED_PRIVATE_BODY_93/,
222
+ "unselected Helper result stays out of Coordinator view",
223
+ );
224
+ return [
225
+ {
226
+ name: "freeflow_delegate",
227
+ args: { operation: "assign", worker: "executor", contract: "Use the gathered marker and report." },
228
+ },
229
+ ];
230
+ }
231
+ if (n === 6) {
232
+ assert.equal(body.model, "gpt-4.1-mini");
233
+ assert.equal(replay(manager.getBranch()).profile, "executor");
234
+ assert.match(wire, /UNSELECTED_PRIVATE_BODY_93/, "Executor inherits ordinary Helper history");
235
+ assert.equal(
236
+ manager.getBranch().filter((entry) => entry.message?.toolName === "read").length,
237
+ 1,
238
+ "Executor does not rerun Helper's read",
239
+ );
240
+ return [
241
+ {
242
+ name: "freeflow_project",
243
+ args: { operation: "add", refs: [`ctx:${helperReadId}`] },
244
+ },
245
+ {
246
+ name: "freeflow_return",
247
+ args: { operation: "submit", report: "Executor used shared context.", outcome: "completed" },
248
+ },
249
+ ];
250
+ }
251
+ if (n === 7) {
252
+ assert.equal(body.model, "gpt-4o");
253
+ assert.match(wire, /UNSELECTED_PRIVATE_BODY_93/);
254
+ assert.match(wire, /producer: helper/);
255
+ return [
256
+ {
257
+ name: "freeflow_unit",
258
+ args: { operation: "close", outcome: "accepted", assessment: "Shared worker context observed." },
259
+ },
260
+ ];
261
+ }
262
+ return [];
263
+ },
264
+ true,
265
+ undefined,
266
+ true,
267
+ { cognitiveRouting: bothConfig, maxRequests: 9 },
268
+ );
269
+ assert.equal(result.requests.length, 8);
270
+ assert.equal(result.state.units.values().next().value.state, "closed");
271
+ const delegates = [...result.state.handoffs.values()].filter((handoff) => handoff.kind === "delegate");
272
+ assert.deepEqual(
273
+ delegates.map((handoff) => handoff.to),
274
+ ["helper", "executor"],
275
+ );
276
+ });
277
+
278
+ test(
279
+ "helper-only rejects Executor delegation and recovers through the recorded Helper",
280
+ { timeout: 30000 },
281
+ async () => {
282
+ let assignmentId, baseReportId;
283
+ const result = await fixture(
284
+ (n, body, manager) => {
285
+ if (n === 1)
286
+ return [
287
+ {
288
+ name: "freeflow_delegate",
289
+ args: { operation: "assign", worker: "executor", contract: "Disabled target." },
290
+ },
291
+ ];
292
+ if (n === 2) {
293
+ assert.equal(replay(manager.getBranch()).assignments.size, 0);
294
+ return [{ name: "freeflow_delegate", args: { operation: "assign", contract: "Return then recover." } }];
295
+ }
296
+ if (n === 3) {
297
+ const state = replay(manager.getBranch());
298
+ assignmentId = state.assignmentId;
299
+ assert.equal(state.profile, "helper");
300
+ return [
301
+ {
302
+ name: "freeflow_return",
303
+ args: { operation: "submit", report: "Helper base report.", outcome: "completed" },
304
+ },
305
+ ];
306
+ }
307
+ if (n === 4) {
308
+ const state = replay(manager.getBranch());
309
+ baseReportId = state.assignments.get(assignmentId).returnHandoffId;
310
+ return [
311
+ {
312
+ name: "freeflow_unit",
313
+ args: { operation: "recover", request: "Read the approved recovery file.", paths: ["recovery.txt"] },
314
+ },
315
+ ];
316
+ }
317
+ if (n === 5) {
318
+ assert.equal(replay(manager.getBranch()).profile, "helper");
319
+ return [{ name: "read", args: { path: "recovery.txt" } }];
320
+ }
321
+ if (n === 6)
322
+ return [
323
+ {
324
+ name: "freeflow_return",
325
+ args: { operation: "supplement", report: "Helper recovery supplement.", outcome: "completed" },
326
+ },
327
+ ];
328
+ if (n === 7) {
329
+ const state = replay(manager.getBranch());
330
+ assert.equal(state.assignmentId, assignmentId);
331
+ assert.equal(state.assignments.get(assignmentId).returnHandoffId, baseReportId);
332
+ assert.equal(state.handoffs.get(baseReportId).text, "Helper base report.");
333
+ const completed = [...state.recoveries.values()].at(-1);
334
+ assert.equal(completed.state, "completed");
335
+ assert.equal(state.handoffs.get(completed.requestHandoffId).to, "helper");
336
+ assert.equal(state.handoffs.get(completed.supplementHandoffId).from, "helper");
337
+ return [
338
+ {
339
+ name: "freeflow_unit",
340
+ args: { operation: "close", outcome: "accepted", assessment: "Helper recovery retained lineage." },
341
+ },
342
+ ];
343
+ }
344
+ return [];
345
+ },
346
+ true,
347
+ undefined,
348
+ true,
349
+ { cognitiveRouting: helperConfig, maxRequests: 9 },
350
+ );
351
+ assert.equal(result.requests.length, 8);
352
+ },
353
+ );
354
+
355
+ test("new input routes through Coordinator before the same Helper assignment resumes", { timeout: 30000 }, async () => {
356
+ let assignmentId;
357
+ const result = await fixture(
358
+ (n, body, manager) => {
359
+ if (n === 1)
360
+ return [{ name: "freeflow_delegate", args: { operation: "assign", contract: "Keep the Helper task." } }];
361
+ if (n === 2) {
362
+ assignmentId = replay(manager.getBranch()).assignmentId;
363
+ assert.equal(replay(manager.getBranch()).profile, "helper");
364
+ return [];
365
+ }
366
+ if (n === 3) {
367
+ const state = replay(manager.getBranch());
368
+ assert.equal(body.model, "gpt-4o");
369
+ assert.equal(state.profile, "coordinator");
370
+ assert.equal(state.assignments.get(assignmentId).state, "outstanding");
371
+ return [];
372
+ }
373
+ if (n === 4) {
374
+ const state = replay(manager.getBranch());
375
+ assert.equal(state.profile, "helper");
376
+ assert.equal(state.assignmentId, assignmentId);
377
+ assert.doesNotMatch(JSON.stringify(body), /New delivered user input requires Coordinator attention/);
378
+ return [
379
+ {
380
+ name: "freeflow_return",
381
+ args: { operation: "submit", report: "Helper resumed after Coordinator attention.", outcome: "completed" },
382
+ },
383
+ ];
384
+ }
385
+ return [];
386
+ },
387
+ true,
388
+ async ({ session }) => {
389
+ await session.prompt("Consider this new direction before continuing.");
390
+ await session.waitForIdle();
391
+ await session.prompt("/freeflow resume");
392
+ await session.waitForIdle();
393
+ },
394
+ true,
395
+ { cognitiveRouting: helperConfig, maxRequests: 6 },
396
+ );
397
+ assert.equal(result.requests.length, 5);
398
+ assert.equal(result.state.assignments.get(assignmentId).state, "returned");
399
+ });
400
+
401
+ test("an accepted Helper assignment survives a later executor-only mode change", { timeout: 30000 }, async () => {
402
+ let assignmentId;
403
+ const result = await fixture(
404
+ (n, body, manager) => {
405
+ if (n === 1)
406
+ return [{ name: "freeflow_delegate", args: { operation: "assign", contract: "Keep this Helper assignment." } }];
407
+ if (n === 2) {
408
+ assignmentId = replay(manager.getBranch()).assignmentId;
409
+ assert.equal(replay(manager.getBranch()).profile, "helper");
410
+ return [];
411
+ }
412
+ if (n === 3) {
413
+ const state = replay(manager.getBranch());
414
+ assert.equal(state.assignmentId, assignmentId);
415
+ assert.equal(state.profile, "helper");
416
+ assert.equal(body.model, "gpt-4.1-mini");
417
+ assert.notEqual(body.model, alternateExecutor.model, "resume does not use the newly enabled Executor pair");
418
+ return [
419
+ {
420
+ name: "freeflow_return",
421
+ args: { operation: "submit", report: "Original Helper assignment resumed.", outcome: "completed" },
422
+ },
423
+ ];
424
+ }
425
+ if (n === 4)
426
+ return [
427
+ {
428
+ name: "freeflow_unit",
429
+ args: { operation: "close", outcome: "accepted", assessment: "Mode change did not retarget work." },
430
+ },
431
+ ];
432
+ return [];
433
+ },
434
+ true,
435
+ async ({ session, cwd }) => {
436
+ await writeFile(
437
+ join(cwd, ".freeflow/config.json"),
438
+ JSON.stringify({
439
+ cognitiveRouting: {
440
+ enabled: true,
441
+ delegation: "executor",
442
+ projection: true,
443
+ profiles: { coordinator, helper: worker, executor: alternateExecutor },
444
+ },
445
+ }),
446
+ );
447
+ await session.reload();
448
+ await session.prompt("/freeflow resume");
449
+ await session.waitForIdle();
450
+ },
451
+ true,
452
+ { cognitiveRouting: helperConfig, maxRequests: 6 },
453
+ );
454
+ assert.equal(result.requests.length, 5);
455
+ assert.equal(result.state.assignments.get(assignmentId).state, "returned");
456
+ });
457
+
458
+ test(
459
+ "an outstanding Helper assignment fails closed when its preset becomes unavailable",
460
+ { timeout: 30000 },
461
+ async () => {
462
+ let assignmentId, delegateHandoffId;
463
+ const result = await fixture(
464
+ (n, body, manager) => {
465
+ if (n === 1)
466
+ return [
467
+ { name: "freeflow_delegate", args: { operation: "assign", contract: "Retain unavailable Helper work." } },
468
+ ];
469
+ if (n === 2) {
470
+ const state = replay(manager.getBranch());
471
+ assignmentId = state.assignmentId;
472
+ delegateHandoffId = state.assignments.get(assignmentId).delegateHandoffId;
473
+ assert.equal(state.profile, "helper");
474
+ return [];
475
+ }
476
+ assert.fail(`blocked continuation unexpectedly reached the provider with ${body.model}`);
477
+ },
478
+ true,
479
+ async ({ session, cwd, requests, notices }) => {
480
+ await writeFile(
481
+ join(cwd, ".freeflow/config.json"),
482
+ JSON.stringify({
483
+ cognitiveRouting: {
484
+ enabled: true,
485
+ delegation: "executor",
486
+ projection: true,
487
+ profiles: { coordinator, executor: alternateExecutor },
488
+ },
489
+ }),
490
+ );
491
+ await session.reload();
492
+ const requestCount = requests.length;
493
+ await session.prompt("/freeflow resume");
494
+ await session.waitForIdle();
495
+ assert.equal(requests.length, requestCount, "blocked resume makes no provider request");
496
+ assert.equal(session.model.id, "gpt-4.1-mini", "no fallback setter applies the Executor pair");
497
+ assert.match(JSON.stringify(notices), /profile_missing|helper/i);
498
+ },
499
+ true,
500
+ { cognitiveRouting: helperConfig, maxRequests: 4 },
501
+ );
502
+ assert.equal(result.requests.length, 2);
503
+ assert.equal(result.state.assignmentId, assignmentId);
504
+ assert.equal(result.state.assignments.get(assignmentId).state, "outstanding");
505
+ assert.equal(result.state.assignments.get(assignmentId).delegateHandoffId, delegateHandoffId);
506
+ assert.equal(result.state.handoffs.get(delegateHandoffId).to, "helper");
507
+ assert.equal(
508
+ [...result.state.handoffs.values()].some((handoff) => handoff.kind === "return"),
509
+ false,
510
+ );
511
+ },
512
+ );
513
+
514
+ test("compaction and explicit resume preserve a Helper assignment", { timeout: 30000 }, async () => {
515
+ let assignmentId, basis;
516
+ const result = await fixture(
517
+ (n, body, manager) => {
518
+ if (n === 1)
519
+ return [{ name: "freeflow_delegate", args: { operation: "assign", contract: "Preserve Helper work." } }];
520
+ if (n === 2) {
521
+ const state = replay(manager.getBranch());
522
+ assignmentId = state.assignmentId;
523
+ basis = state.assignments.get(assignmentId).basisUserEntryId;
524
+ return [{ name: "read", args: { path: "evidence.txt" } }];
525
+ }
526
+ if (n === 4) {
527
+ const state = replay(manager.getBranch());
528
+ assert.equal(state.profile, "helper");
529
+ assert.equal(state.assignmentId, assignmentId);
530
+ assert.equal(state.resumeBasis.get(assignmentId), basis);
531
+ assert.doesNotMatch(JSON.stringify(body), /New delivered user input requires Coordinator attention/);
532
+ return [
533
+ {
534
+ name: "freeflow_return",
535
+ args: { operation: "submit", report: "Helper resumed after compaction.", outcome: "completed" },
536
+ },
537
+ ];
538
+ }
539
+ return [];
540
+ },
541
+ true,
542
+ async ({ session, manager }) => {
543
+ await session.compact();
544
+ assert.equal(
545
+ manager.buildSessionContext().messages.some((message) => message.role === "user"),
546
+ false,
547
+ );
548
+ await session.reload();
549
+ await session.prompt("/freeflow resume");
550
+ await session.waitForIdle();
551
+ },
552
+ true,
553
+ { cognitiveRouting: helperConfig, maxRequests: 6 },
554
+ );
555
+ assert.equal(result.requests.length, 5);
556
+ assert.equal(result.state.assignments.get(assignmentId).state, "returned");
557
+ });