@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,1034 @@
1
+ import assert from "node:assert/strict";
2
+ import test from "node:test";
3
+ import { access, readFile } from "node:fs/promises";
4
+ import { join } from "node:path";
5
+ import { fixture } from "../fixtures/routing-native.js";
6
+ import { replay } from "../../dist/cognitive-routing-v2/state.js";
7
+ import { RoutingRuntime } from "../../dist/cognitive-routing-v2/runtime.js";
8
+
9
+ for (const recoveryWorker of ["executor", "helper"]) {
10
+ const cognitiveRouting =
11
+ recoveryWorker === "helper"
12
+ ? {
13
+ enabled: true,
14
+ delegation: "helper",
15
+ projection: true,
16
+ profiles: {
17
+ coordinator: { provider: "openai", model: "gpt-4o", thinking: "off" },
18
+ helper: { provider: "openai", model: "gpt-4.1-mini", thinking: "off" },
19
+ },
20
+ }
21
+ : undefined;
22
+ test(
23
+ `attached ${recoveryWorker} recovery reuses omitted evidence, gates exact native reads, and blocks returned work`,
24
+ { timeout: 30000 },
25
+ async () => {
26
+ let assignmentId, baseReportId, baseReportRevision, omittedRef, supplementRef, recoveryRef, cwd;
27
+ const originalInvoke = RoutingRuntime.prototype.invoke;
28
+ const replayed = new Set();
29
+ RoutingRuntime.prototype.invoke = async function (name, id, input, signal, ctx) {
30
+ const intercepted =
31
+ name === "freeflow_unit" && input.operation === "recover"
32
+ ? { ...input, request: `${input.request} [accepted]` }
33
+ : name === "freeflow_return" && input.operation === "supplement"
34
+ ? { ...input, report: `${input.report} [accepted]` }
35
+ : input;
36
+ const first = await originalInvoke.call(this, name, id, intercepted, signal, ctx);
37
+ const key = `${name}:${input.operation}`;
38
+ if (
39
+ ((name === "freeflow_unit" && input.operation === "recover") ||
40
+ (name === "freeflow_return" && input.operation === "supplement")) &&
41
+ !replayed.has(key)
42
+ ) {
43
+ replayed.add(key);
44
+ this.receipts.clear();
45
+ const repeated = await originalInvoke.call(this, name, id, intercepted, signal, ctx);
46
+ assert.deepEqual(repeated, first, "same accepted operation returns the recorded receipt");
47
+ this.receipts.clear();
48
+ const changed = await originalInvoke.call(
49
+ this,
50
+ name,
51
+ id,
52
+ { ...intercepted, [input.operation === "recover" ? "request" : "report"]: "CHANGED_PAYLOAD" },
53
+ signal,
54
+ ctx,
55
+ );
56
+ assert.equal(changed.details.code, "operation_conflict");
57
+ }
58
+ return first;
59
+ };
60
+ try {
61
+ await fixture(
62
+ async (n, body, manager) => {
63
+ if (n === 1)
64
+ return [
65
+ { name: "freeflow_delegate", args: { operation: "assign", contract: "Read two files and report." } },
66
+ ];
67
+ if (n === 2) {
68
+ assert.equal(replay(manager.getBranch()).profile, recoveryWorker);
69
+ return [
70
+ { name: "read", args: { path: "evidence.txt" } },
71
+ { name: "read", args: { path: "unselected.txt" } },
72
+ ];
73
+ }
74
+ if (n === 3) {
75
+ const reads = manager.getBranch().filter((e) => e.message?.toolName === "read" && !e.message.isError);
76
+ omittedRef = `ctx:${reads[1].id}`;
77
+ return [
78
+ { name: "freeflow_project", args: { operation: "add", refs: [`ctx:${reads[0].id}`] } },
79
+ {
80
+ name: "freeflow_return",
81
+ args: { operation: "submit", report: "BASE_REPORT_UNCHANGED_41", outcome: "completed" },
82
+ },
83
+ ];
84
+ }
85
+ if (n === 4) {
86
+ const state = replay(manager.getBranch());
87
+ assignmentId = state.assignmentId;
88
+ baseReportId = state.assignments.get(assignmentId).returnHandoffId;
89
+ baseReportRevision = state.handoffs.get(baseReportId).reportRevision;
90
+ const wire = JSON.stringify(body);
91
+ assert.match(wire, /BASE_REPORT_UNCHANGED_41/);
92
+ assert.match(wire, /EXACT_EVIDENCE_BODY_81/);
93
+ assert.doesNotMatch(wire, /UNSELECTED_PRIVATE_BODY_93/);
94
+ return [
95
+ {
96
+ name: "freeflow_unit",
97
+ args: {
98
+ operation: "recover",
99
+ request: "Select the omitted result and read the approved third file only.",
100
+ paths: ["recovery.txt"],
101
+ },
102
+ },
103
+ ];
104
+ }
105
+ if (n === 5) {
106
+ assert.equal(body.model, "gpt-4.1-mini");
107
+ assert.equal(replay(manager.getBranch()).profile, recoveryWorker);
108
+ assert.match(
109
+ JSON.stringify(body),
110
+ /Select the omitted result and read the approved third file only\. \[accepted\]/,
111
+ );
112
+ return [
113
+ { name: "read", args: { path: "recovery.txt" } },
114
+ { name: "read", args: { path: "@recovery.txt" } },
115
+ { name: "read", args: { path: "evidence.txt" } },
116
+ { name: "write", args: { path: "recovery-write.txt", content: "forbidden" } },
117
+ { name: "bash", args: { command: "touch recovery-bash.txt" } },
118
+ ];
119
+ }
120
+ if (n === 6) {
121
+ const recent = manager
122
+ .getBranch()
123
+ .filter((e) => ["read", "write", "bash"].includes(e.message?.toolName))
124
+ .slice(-5);
125
+ const allowed = recent.find(
126
+ (e) =>
127
+ e.message.toolName === "read" &&
128
+ !e.message.isError &&
129
+ JSON.stringify(e.message).includes("FRESH_RECOVERY_BODY_57"),
130
+ );
131
+ assert.ok(allowed, "the exact approved native read executes");
132
+ assert.equal(
133
+ recent.filter((e) => e !== allowed).every((e) => e.message.isError),
134
+ true,
135
+ );
136
+ await assert.rejects(access(join(cwd, "recovery-write.txt")));
137
+ await assert.rejects(access(join(cwd, "recovery-bash.txt")));
138
+ assert.equal(await readFile(join(cwd, "@recovery.txt"), "utf8"), "LITERAL_AT_FILE_BODY_68");
139
+ assert.equal(
140
+ recent.some((e) => JSON.stringify(e.message).includes("LITERAL_AT_FILE_BODY_68")),
141
+ false,
142
+ "Pi never reaches either @-transformed or literal resource for a rejected alias",
143
+ );
144
+ return [
145
+ {
146
+ name: "freeflow_project",
147
+ args: { operation: "add", refs: [omittedRef, `ctx:${allowed.id}`, "ctx:missing"] },
148
+ },
149
+ {
150
+ name: "freeflow_return",
151
+ args: { operation: "supplement", report: "RECOVERY_SUPPLEMENT_73", outcome: "completed" },
152
+ },
153
+ ];
154
+ }
155
+ if (n === 7) return [{ name: "read", args: { path: "recovery.txt" } }];
156
+ if (n === 8) {
157
+ const postSupplement = manager
158
+ .getBranch()
159
+ .filter((e) => e.message?.toolName === "read")
160
+ .at(-1);
161
+ assert.equal(postSupplement.message.isError, true, "a saved supplement ends recovery read permission");
162
+ assert.equal(postSupplement.message.content[0].text, "worker_task_phase_ended");
163
+ return [
164
+ {
165
+ name: "freeflow_project",
166
+ args: {
167
+ operation: "remove",
168
+ refs: ["ctx:missing"],
169
+ reason: "Fixture removes the unresolved request.",
170
+ },
171
+ },
172
+ { name: "freeflow_return", args: { operation: "retry" } },
173
+ ];
174
+ }
175
+ assert.equal(body.model, "gpt-4o");
176
+ const wire = JSON.stringify(body);
177
+ const state = replay(manager.getBranch());
178
+ if (n === 14) {
179
+ assert.equal(state.assessment.view, "suspended");
180
+ assert.equal(state.assessment.suspensionReason, "user-attention");
181
+ assert.doesNotMatch(wire, /EXACT_EVIDENCE_BODY_81/);
182
+ assert.match(wire, /NEW_USER_ATTENTION_29/);
183
+ return [{ name: "freeflow_unit", args: { operation: "assess" } }];
184
+ }
185
+ for (const value of [
186
+ "BASE_REPORT_UNCHANGED_41",
187
+ "RECOVERY_SUPPLEMENT_73 [accepted]",
188
+ "EXACT_EVIDENCE_BODY_81",
189
+ "UNSELECTED_PRIVATE_BODY_93",
190
+ "FRESH_RECOVERY_BODY_57",
191
+ ])
192
+ assert.ok(wire.includes(value), `missing ${value}`);
193
+ assert.match(wire, new RegExp(`producer: ${recoveryWorker}`));
194
+ if (n === 15) {
195
+ assert.equal(state.assessment.view, "active");
196
+ return [];
197
+ }
198
+ assert.equal(state.assignmentId, assignmentId);
199
+ assert.equal(state.assignments.get(assignmentId).state, "returned");
200
+ assert.equal(state.assignments.get(assignmentId).returnHandoffId, baseReportId);
201
+ assert.equal(state.handoffs.get(baseReportId).text, "BASE_REPORT_UNCHANGED_41");
202
+ assert.equal(state.handoffs.get(baseReportId).outcome, "completed");
203
+ assert.equal(state.handoffs.get(baseReportId).reportRevision, baseReportRevision);
204
+ assert.equal(state.assessment.handoffId, baseReportId);
205
+ assert.equal(state.assessment.view, "active");
206
+ assert.equal(state.recoveryId, undefined);
207
+ const completedRecovery = [...state.recoveries.values()].at(-1);
208
+ assert.equal(completedRecovery.state, "completed");
209
+ supplementRef = `supplement:${completedRecovery.id}:${completedRecovery.supplementRevision}`;
210
+ recoveryRef = `recovery:${completedRecovery.id}`;
211
+ assert.equal(
212
+ state.selections.get(assignmentId).selected.includes(omittedRef),
213
+ true,
214
+ "the originally omitted result is selected",
215
+ );
216
+ for (const marker of ["EXACT_EVIDENCE_BODY_81", "UNSELECTED_PRIVATE_BODY_93"])
217
+ assert.equal(
218
+ manager
219
+ .getBranch()
220
+ .filter(
221
+ (e) =>
222
+ e.message?.toolName === "read" &&
223
+ !e.message.isError &&
224
+ JSON.stringify(e.message).includes(marker),
225
+ ).length,
226
+ 1,
227
+ `${marker} was not replayed`,
228
+ );
229
+ if (n === 9)
230
+ return [{ name: "freeflow_unit", args: { operation: "inspect", view: "detail", ref: supplementRef } }];
231
+ if (n === 10) {
232
+ const detail = manager
233
+ .getBranch()
234
+ .filter((e) => e.message?.toolName === "freeflow_unit")
235
+ .at(-1).message.details;
236
+ assert.equal(detail.supplement, "RECOVERY_SUPPLEMENT_73 [accepted]");
237
+ assert.equal(detail.report, "BASE_REPORT_UNCHANGED_41");
238
+ assert.equal(detail.reportRef, `report:${baseReportId}:${baseReportRevision}`);
239
+ return [{ name: "freeflow_unit", args: { operation: "inspect", view: "detail", ref: recoveryRef } }];
240
+ }
241
+ const detail = manager
242
+ .getBranch()
243
+ .filter((e) => e.message?.toolName === "freeflow_unit")
244
+ .at(-1).message.details;
245
+ assert.equal(detail.recovery.state, "completed");
246
+ assert.equal(detail.supplementRef, supplementRef);
247
+ return [];
248
+ },
249
+ true,
250
+ async ({ session, manager, requests }) => {
251
+ await session.prompt("/freeflow resume");
252
+ await session.waitForIdle();
253
+ assert.equal(requests.length, 12);
254
+ await session.compact();
255
+ assert.equal(
256
+ manager.buildSessionContext().messages.some((message) => message.role === "user"),
257
+ false,
258
+ );
259
+ await session.reload();
260
+ await session.prompt("/freeflow resume");
261
+ await session.waitForIdle();
262
+ await session.prompt("NEW_USER_ATTENTION_29");
263
+ await session.waitForIdle();
264
+ assert.equal(requests.length, 15);
265
+ },
266
+ true,
267
+ { beforePrompt: (fixtureState) => (cwd = fixtureState.cwd), cognitiveRouting },
268
+ );
269
+ } finally {
270
+ RoutingRuntime.prototype.invoke = originalInvoke;
271
+ }
272
+ },
273
+ );
274
+ }
275
+
276
+ for (const returnedWorker of ["executor", "helper"]) {
277
+ const cognitiveRouting =
278
+ returnedWorker === "helper"
279
+ ? {
280
+ enabled: true,
281
+ delegation: "helper",
282
+ projection: true,
283
+ profiles: {
284
+ coordinator: { provider: "openai", model: "gpt-4o", thinking: "off" },
285
+ helper: { provider: "openai", model: "gpt-4.1-mini", thinking: "off" },
286
+ },
287
+ }
288
+ : undefined;
289
+ test(`a blocked ${returnedWorker} return denies ordinary work before retry`, { timeout: 30000 }, async () => {
290
+ let assignmentId;
291
+ const result = await fixture(
292
+ (n, body, manager) => {
293
+ if (n === 1)
294
+ return [
295
+ { name: "freeflow_delegate", args: { operation: "assign", contract: "Return with a delivery gap." } },
296
+ ];
297
+ if (n === 2) {
298
+ const state = replay(manager.getBranch());
299
+ assignmentId = state.assignmentId;
300
+ assert.equal(state.profile, returnedWorker);
301
+ return [
302
+ { name: "freeflow_project", args: { operation: "add", refs: ["ctx:missing"] } },
303
+ {
304
+ name: "freeflow_return",
305
+ args: { operation: "submit", report: "RETURNED_WORKER_REPORT_67", outcome: "completed" },
306
+ },
307
+ ];
308
+ }
309
+ if (n === 3) {
310
+ const state = replay(manager.getBranch());
311
+ assert.equal(state.profile, returnedWorker);
312
+ assert.equal(state.assignments.get(assignmentId).state, "returned");
313
+ assert.equal(state.handoffs.get(state.assignments.get(assignmentId).returnHandoffId).state, "blocked");
314
+ return [{ name: "read", args: { path: "evidence.txt" } }];
315
+ }
316
+ if (n === 4) {
317
+ const denied = manager
318
+ .getBranch()
319
+ .filter((entry) => entry.message?.toolName === "read")
320
+ .at(-1);
321
+ assert.equal(denied.message.isError, true);
322
+ assert.equal(denied.message.content[0].text, "worker_task_phase_ended");
323
+ return [
324
+ {
325
+ name: "freeflow_project",
326
+ args: { operation: "remove", refs: ["ctx:missing"], reason: "Resolve the fixture delivery gap." },
327
+ },
328
+ { name: "freeflow_return", args: { operation: "retry" } },
329
+ ];
330
+ }
331
+ assert.equal(body.model, "gpt-4o");
332
+ const state = replay(manager.getBranch());
333
+ assert.equal(state.assignmentId, assignmentId);
334
+ assert.equal(state.assignments.get(assignmentId).state, "returned");
335
+ assert.equal(state.handoffs.get(state.assignments.get(assignmentId).returnHandoffId).state, "configured");
336
+ assert.match(JSON.stringify(body), /RETURNED_WORKER_REPORT_67/);
337
+ return [];
338
+ },
339
+ true,
340
+ undefined,
341
+ true,
342
+ { cognitiveRouting, maxRequests: 7 },
343
+ );
344
+ assert.equal(result.requests.length, 5);
345
+ });
346
+ }
347
+
348
+ test(
349
+ "partial recovery supplement delivers communication while evidence remains suspended",
350
+ { timeout: 30000 },
351
+ async () => {
352
+ let baseReportId;
353
+ await fixture((n, body, manager) => {
354
+ if (n === 1)
355
+ return [{ name: "freeflow_delegate", args: { operation: "assign", contract: "Return a base report." } }];
356
+ if (n === 2)
357
+ return [
358
+ {
359
+ name: "freeflow_return",
360
+ args: { operation: "submit", report: "FALLBACK_BASE_REPORT_19", outcome: "completed" },
361
+ },
362
+ ];
363
+ if (n === 3) {
364
+ const state = replay(manager.getBranch());
365
+ baseReportId = state.assignments.get(state.assignmentId).returnHandoffId;
366
+ return [
367
+ {
368
+ name: "freeflow_unit",
369
+ args: { operation: "recover", request: "Report unavailable evidence honestly." },
370
+ },
371
+ ];
372
+ }
373
+ if (n === 4)
374
+ return [
375
+ { name: "freeflow_project", args: { operation: "add", refs: ["ctx:missing"] } },
376
+ {
377
+ name: "freeflow_return",
378
+ args: { operation: "supplement", report: "FALLBACK_SUPPLEMENT_23", outcome: "partial" },
379
+ },
380
+ ];
381
+ assert.equal(body.model, "gpt-4o");
382
+ const wire = JSON.stringify(body);
383
+ assert.match(wire, /FALLBACK_BASE_REPORT_19/);
384
+ assert.match(wire, /FALLBACK_SUPPLEMENT_23/);
385
+ assert.match(wire, /source_unavailable|Exact source is unavailable/);
386
+ const state = replay(manager.getBranch());
387
+ assert.equal(state.assessment.handoffId, baseReportId);
388
+ assert.equal(state.assessment.view, "suspended");
389
+ assert.equal(state.assessment.suspensionReason, "delivery-gap");
390
+ assert.equal(state.recoveryId, undefined);
391
+ assert.equal([...state.recoveries.values()].at(-1).state, "completed");
392
+ return [];
393
+ });
394
+ },
395
+ );
396
+
397
+ test(
398
+ "recovery cancellation preserves the report and restores the same assessment explicitly",
399
+ { timeout: 30000 },
400
+ async () => {
401
+ let baseReportId;
402
+ await fixture(
403
+ (n, _body, manager) => {
404
+ if (n === 1)
405
+ return [{ name: "freeflow_delegate", args: { operation: "assign", contract: "Return a base report." } }];
406
+ if (n === 2)
407
+ return [
408
+ { name: "freeflow_return", args: { operation: "submit", report: "CANCEL_BASE_31", outcome: "completed" } },
409
+ ];
410
+ if (n === 3) {
411
+ const state = replay(manager.getBranch());
412
+ baseReportId = state.assignments.get(state.assignmentId).returnHandoffId;
413
+ return [{ name: "freeflow_unit", args: { operation: "recover", request: "Wait for cancellation." } }];
414
+ }
415
+ if (n === 4) return [];
416
+ if (n === 5)
417
+ return [{ name: "freeflow_unit", args: { operation: "cancel-recovery", reason: "No longer needed." } }];
418
+ if (n === 6) {
419
+ const state = replay(manager.getBranch());
420
+ assert.equal(state.recoveryId, undefined);
421
+ assert.equal([...state.recoveries.values()].at(-1).state, "cancelled");
422
+ assert.equal(state.handoffs.get(baseReportId).text, "CANCEL_BASE_31");
423
+ assert.equal(state.assessment.handoffId, baseReportId);
424
+ assert.equal(state.assessment.view, "suspended");
425
+ return [{ name: "freeflow_unit", args: { operation: "assess" } }];
426
+ }
427
+ assert.equal(replay(manager.getBranch()).assessment.view, "active");
428
+ return [];
429
+ },
430
+ true,
431
+ async ({ session }) => {
432
+ await session.prompt("Cancel the evidence recovery without changing the report.");
433
+ await session.waitForIdle();
434
+ },
435
+ );
436
+ },
437
+ );
438
+
439
+ test(
440
+ "fresh user recovery uses its own input basis without a changed-direction warning",
441
+ { timeout: 30000 },
442
+ async () => {
443
+ await fixture(
444
+ (n, body) => {
445
+ if (n === 1) return [{ name: "freeflow_delegate", args: { operation: "assign", contract: "Return once." } }];
446
+ if (n === 2)
447
+ return [{ name: "freeflow_return", args: { operation: "submit", report: "base", outcome: "completed" } }];
448
+ if (n === 3) return [];
449
+ if (n === 4)
450
+ return [
451
+ {
452
+ name: "freeflow_unit",
453
+ args: { operation: "recover", request: "Recover evidence for this assessment." },
454
+ },
455
+ ];
456
+ if (n === 5) {
457
+ const wire = JSON.stringify(body);
458
+ assert.equal(body.model, "gpt-4.1-mini");
459
+ assert.match(wire, /Recovery request: Recover evidence for this assessment\./);
460
+ assert.doesNotMatch(wire, /Current input differs from the active Executor responsibility/);
461
+ return [
462
+ {
463
+ name: "freeflow_return",
464
+ args: { operation: "supplement", report: "fresh-basis supplement", outcome: "completed" },
465
+ },
466
+ ];
467
+ }
468
+ return [];
469
+ },
470
+ true,
471
+ async ({ session }) => {
472
+ await session.prompt("Please recover omitted evidence now.");
473
+ await session.waitForIdle();
474
+ },
475
+ );
476
+ },
477
+ );
478
+
479
+ test("assessment waits for recovery cancellation before resuming", { timeout: 30000 }, async () => {
480
+ let selection;
481
+ await fixture(
482
+ (n, body, manager) => {
483
+ if (n === 1) return [{ name: "freeflow_delegate", args: { operation: "assign", contract: "Return once." } }];
484
+ if (n === 2)
485
+ return [{ name: "freeflow_return", args: { operation: "submit", report: "base", outcome: "completed" } }];
486
+ if (n === 3) return [{ name: "freeflow_unit", args: { operation: "recover", request: "Recover later." } }];
487
+ if (n === 4) {
488
+ const state = replay(manager.getBranch());
489
+ selection = structuredClone(state.selections.get(state.assignmentId));
490
+ return [];
491
+ }
492
+ if (n === 5) {
493
+ const wire = JSON.stringify(body);
494
+ assert.match(
495
+ wire,
496
+ /Finish and deliver the recovery supplement or cancel recovery before using freeflow_unit assess/,
497
+ );
498
+ assert.doesNotMatch(wire, /Use freeflow_unit assess to restore the assessment/);
499
+ return [{ name: "freeflow_unit", args: { operation: "assess" } }];
500
+ }
501
+ if (n === 6) {
502
+ const state = replay(manager.getBranch());
503
+ const result = manager
504
+ .getBranch()
505
+ .filter((entry) => entry.message?.toolName === "freeflow_unit")
506
+ .at(-1).message;
507
+ assert.equal(result.details.code, "recovery_outstanding");
508
+ assert.equal(state.assessment.view, "suspended");
509
+ assert.equal(state.assessment.suspensionReason, "user-attention");
510
+ assert.equal(state.recoveries.get(state.recoveryId).state, "reading");
511
+ assert.deepEqual(state.selections.get(state.assignmentId), selection);
512
+ return [{ name: "freeflow_unit", args: { operation: "cancel-recovery", reason: "No longer needed." } }];
513
+ }
514
+ if (n === 7) {
515
+ const state = replay(manager.getBranch());
516
+ const wire = JSON.stringify(body);
517
+ assert.equal(state.recoveryId, undefined);
518
+ assert.equal(state.assessment.view, "suspended");
519
+ assert.match(wire, /Use freeflow_unit assess to restore the assessment/);
520
+ assert.doesNotMatch(
521
+ JSON.stringify(
522
+ body.input.filter((item) => JSON.stringify(item).includes("# Cognitive Routing Runtime State")).at(-1),
523
+ ),
524
+ /Finish and deliver the recovery supplement or cancel recovery/,
525
+ );
526
+ return [{ name: "freeflow_unit", args: { operation: "assess" } }];
527
+ }
528
+ const state = replay(manager.getBranch());
529
+ assert.equal(state.assessment.view, "active");
530
+ assert.equal(state.recoveryId, undefined);
531
+ return [];
532
+ },
533
+ true,
534
+ async ({ session }) => {
535
+ await session.prompt("Assess before the open recovery is settled.");
536
+ await session.waitForIdle();
537
+ },
538
+ );
539
+ });
540
+
541
+ test("cancelled unit closure atomically abandons attached recovery", { timeout: 30000 }, async () => {
542
+ let baseReportId, selectionSnapshot;
543
+ await fixture(
544
+ (n, _body, manager) => {
545
+ if (n === 1) return [{ name: "freeflow_delegate", args: { operation: "assign", contract: "Return once." } }];
546
+ if (n === 2)
547
+ return [{ name: "freeflow_return", args: { operation: "submit", report: "close-base", outcome: "completed" } }];
548
+ if (n === 3) {
549
+ const state = replay(manager.getBranch());
550
+ baseReportId = state.assignments.get(state.assignmentId).returnHandoffId;
551
+ return [{ name: "freeflow_unit", args: { operation: "recover", request: "Recover later." } }];
552
+ }
553
+ if (n === 4) return [];
554
+ if (n === 5) {
555
+ selectionSnapshot = structuredClone([...replay(manager.getBranch()).selections]);
556
+ return [
557
+ {
558
+ name: "freeflow_unit",
559
+ args: { operation: "close", outcome: "cancelled", assessment: "Stop unit and recovery." },
560
+ },
561
+ ];
562
+ }
563
+ const state = replay(manager.getBranch());
564
+ const recovery = [...state.recoveries.values()].at(-1);
565
+ assert.equal(state.unitId, undefined);
566
+ assert.equal(state.assignmentId, undefined);
567
+ assert.equal(state.pendingId, undefined);
568
+ assert.equal(state.recoveryId, undefined);
569
+ assert.equal(recovery.state, "cancelled");
570
+ assert.equal(recovery.cancellationReason, "Stop unit and recovery.");
571
+ assert.equal(state.handoffs.get(baseReportId).text, "close-base");
572
+ assert.deepEqual([...state.selections], selectionSnapshot);
573
+ return [];
574
+ },
575
+ true,
576
+ async ({ session }) => {
577
+ await session.prompt("Cancel the unit and its recovery.");
578
+ await session.waitForIdle();
579
+ },
580
+ );
581
+ });
582
+
583
+ test("projection-off recovery can read packaged instructions and resume assessment", { timeout: 30000 }, async () => {
584
+ const packageRoot = process.cwd();
585
+ await fixture(
586
+ (n, body, manager) => {
587
+ if (n === 1)
588
+ return [{ name: "freeflow_delegate", args: { operation: "assign", contract: "Return a base report." } }];
589
+ if (n === 2)
590
+ return [
591
+ {
592
+ name: "freeflow_return",
593
+ args: { operation: "submit", report: "NO_PROJECTION_BASE_43", outcome: "completed" },
594
+ },
595
+ ];
596
+ if (n === 3)
597
+ return [
598
+ { name: "freeflow_unit", args: { operation: "recover", request: "Read the packaged Workflow method." } },
599
+ ];
600
+ if (n === 4)
601
+ return [{ name: "read", args: { path: join(packageRoot, "skills/workflow/SKILL.md"), offset: 1, limit: 8 } }];
602
+ if (n === 5) {
603
+ const read = manager
604
+ .getBranch()
605
+ .filter((e) => e.message?.toolName === "read")
606
+ .at(-1);
607
+ assert.equal(read.message.isError, false);
608
+ assert.match(JSON.stringify(read.message), /name: workflow|# Workflow/);
609
+ return [
610
+ {
611
+ name: "freeflow_return",
612
+ args: { operation: "supplement", report: "PACKAGED_METHOD_READ_47", outcome: "completed" },
613
+ },
614
+ ];
615
+ }
616
+ assert.equal(body.model, "gpt-4o");
617
+ const state = replay(manager.getBranch());
618
+ assert.equal(state.assessment.view, "active");
619
+ assert.equal(state.recoveryId, undefined);
620
+ assert.match(JSON.stringify(body), /NO_PROJECTION_BASE_43/);
621
+ assert.match(JSON.stringify(body), /PACKAGED_METHOD_READ_47/);
622
+ return [];
623
+ },
624
+ false,
625
+ undefined,
626
+ true,
627
+ );
628
+ });
629
+
630
+ test(
631
+ "new input during recovery blocks further reads and a supplement preserves user attention",
632
+ { timeout: 30000 },
633
+ async () => {
634
+ let active, assignmentId, baseReportId;
635
+ await fixture(
636
+ async (n, body, manager) => {
637
+ if (n === 1)
638
+ return [{ name: "freeflow_delegate", args: { operation: "assign", contract: "Return a base report." } }];
639
+ if (n === 2)
640
+ return [
641
+ {
642
+ name: "freeflow_return",
643
+ args: { operation: "submit", report: "INTERRUPTED_RECOVERY_BASE_59", outcome: "completed" },
644
+ },
645
+ ];
646
+ if (n === 3) {
647
+ const state = replay(manager.getBranch());
648
+ assignmentId = state.assignmentId;
649
+ baseReportId = state.assignments.get(assignmentId).returnHandoffId;
650
+ return [
651
+ {
652
+ name: "freeflow_unit",
653
+ args: { operation: "recover", request: "Read only recovery.txt.", paths: ["recovery.txt"] },
654
+ },
655
+ ];
656
+ }
657
+ if (n === 4) {
658
+ assert.equal(body.model, "gpt-4.1-mini");
659
+ await active.steer("NEW_RECOVERY_ATTENTION_61");
660
+ return [{ name: "read", args: { path: "recovery.txt" } }];
661
+ }
662
+ if (n === 5) {
663
+ const state = replay(manager.getBranch());
664
+ assert.equal(state.assessment.view, "suspended");
665
+ assert.equal(state.assessment.suspensionReason, "user-attention");
666
+ assert.match(JSON.stringify(body), /New delivered user input requires Coordinator attention/);
667
+ return [{ name: "read", args: { path: "recovery.txt" } }];
668
+ }
669
+ if (n === 6) {
670
+ const lastRead = manager
671
+ .getBranch()
672
+ .filter((entry) => entry.message?.toolName === "read")
673
+ .at(-1);
674
+ assert.equal(lastRead.message.isError, true, "new input revokes recovery-read permission");
675
+ return [
676
+ {
677
+ name: "freeflow_return",
678
+ args: { operation: "supplement", report: "INTERRUPTED_RECOVERY_SUPPLEMENT_67", outcome: "partial" },
679
+ },
680
+ ];
681
+ }
682
+ assert.equal(body.model, "gpt-4o");
683
+ assert.match(JSON.stringify(body), /NEW_RECOVERY_ATTENTION_61/);
684
+ assert.match(JSON.stringify(body), /INTERRUPTED_RECOVERY_SUPPLEMENT_67/);
685
+ const state = replay(manager.getBranch());
686
+ assert.equal(state.assignmentId, assignmentId);
687
+ assert.equal(state.assignments.get(assignmentId).state, "returned");
688
+ assert.equal(state.assignments.get(assignmentId).returnHandoffId, baseReportId);
689
+ assert.equal(state.handoffs.get(baseReportId).text, "INTERRUPTED_RECOVERY_BASE_59");
690
+ assert.equal(state.assessment.handoffId, baseReportId);
691
+ assert.equal(state.assessment.view, "suspended");
692
+ assert.equal(state.assessment.suspensionReason, "user-attention");
693
+ assert.equal(state.recoveryId, undefined);
694
+ assert.equal([...state.recoveries.values()].at(-1).state, "completed");
695
+ return [];
696
+ },
697
+ true,
698
+ undefined,
699
+ true,
700
+ { onSession: (session) => (active = session) },
701
+ );
702
+ },
703
+ );
704
+
705
+ test("manual hold and navigation preserve an unchanged recovery for explicit resume", { timeout: 30000 }, async () => {
706
+ let assignmentId, baseReportId, recoveryId, requestHandoffId;
707
+ await fixture(
708
+ (n, body, manager) => {
709
+ if (n === 1)
710
+ return [{ name: "freeflow_delegate", args: { operation: "assign", contract: "Return a base report." } }];
711
+ if (n === 2)
712
+ return [
713
+ {
714
+ name: "freeflow_return",
715
+ args: { operation: "submit", report: "MANUAL_RECOVERY_BASE_71", outcome: "completed" },
716
+ },
717
+ ];
718
+ if (n === 3) {
719
+ const state = replay(manager.getBranch());
720
+ assignmentId = state.assignmentId;
721
+ baseReportId = state.assignments.get(assignmentId).returnHandoffId;
722
+ return [{ name: "freeflow_unit", args: { operation: "recover", request: "Resume this lookup unchanged." } }];
723
+ }
724
+ if (n === 4) {
725
+ const state = replay(manager.getBranch());
726
+ recoveryId = state.recoveryId;
727
+ requestHandoffId = state.recoveries.get(recoveryId).requestHandoffId;
728
+ assert.equal(state.recoveries.get(recoveryId).state, "reading");
729
+ return [];
730
+ }
731
+ if (n === 5) {
732
+ const state = replay(manager.getBranch());
733
+ assert.equal(body.model, "gpt-4.1-mini");
734
+ assert.equal(state.recoveryId, recoveryId);
735
+ assert.equal(state.recoveries.get(recoveryId).state, "reading");
736
+ assert.equal(state.assignmentId, assignmentId);
737
+ assert.equal(state.assignments.get(assignmentId).state, "returned");
738
+ return [
739
+ {
740
+ name: "freeflow_return",
741
+ args: { operation: "supplement", report: "MANUAL_RECOVERY_SUPPLEMENT_73", outcome: "completed" },
742
+ },
743
+ ];
744
+ }
745
+ const state = replay(manager.getBranch());
746
+ assert.equal(body.model, "gpt-4o");
747
+ assert.equal(state.control, "automatic");
748
+ assert.equal(state.assignmentId, assignmentId);
749
+ assert.equal(state.assignments.get(assignmentId).returnHandoffId, baseReportId);
750
+ assert.equal(state.handoffs.get(baseReportId).text, "MANUAL_RECOVERY_BASE_71");
751
+ assert.equal(state.assessment.handoffId, baseReportId);
752
+ assert.equal(state.assessment.view, "active");
753
+ assert.equal(state.recoveries.get(recoveryId).state, "completed");
754
+ assert.equal(state.recoveries.size, 1);
755
+ assert.equal(state.assignments.size, 1);
756
+ return [];
757
+ },
758
+ true,
759
+ async ({ session, manager, requests }) => {
760
+ const reads = manager.getEntries().filter((entry) => entry.message?.toolName === "read").length;
761
+ await session.prompt("/freeflow profile executor");
762
+ const configured = manager
763
+ .getEntries()
764
+ .find(
765
+ (entry) =>
766
+ entry.data?.data?.type === "handoff-state" &&
767
+ entry.data.data.handoffId === requestHandoffId &&
768
+ entry.data.data.state === "configured",
769
+ );
770
+ await session.navigateTree(configured.id, { summarize: false });
771
+ const before = requests.length;
772
+ await session.prompt("/freeflow resume");
773
+ assert.equal(requests.length, before, "resume cannot release a manual hold");
774
+ let state = replay(manager.getBranch());
775
+ assert.equal(state.control, "manual");
776
+ assert.equal(state.profile, "executor");
777
+ assert.equal(state.recoveryId, recoveryId);
778
+ assert.equal(state.recoveries.get(recoveryId).state, "reading");
779
+ await session.prompt("/freeflow profile auto");
780
+ state = replay(manager.getBranch());
781
+ assert.equal(state.control, "automatic");
782
+ assert.equal(state.profile, "coordinator");
783
+ await session.prompt("/freeflow resume");
784
+ await session.waitForIdle();
785
+ assert.equal(requests.length, before + 2);
786
+ assert.equal(
787
+ manager.getEntries().filter((entry) => entry.message?.toolName === "read").length,
788
+ reads,
789
+ "navigation and resume do not replay task reads",
790
+ );
791
+ },
792
+ );
793
+ });
794
+
795
+ test(
796
+ "resume after attention restores saved-return correction without reopening task work",
797
+ { timeout: 30000 },
798
+ async () => {
799
+ let reportId;
800
+ await fixture(
801
+ (n, body, manager) => {
802
+ if (n === 1)
803
+ return [{ name: "freeflow_delegate", args: { operation: "assign", contract: "Read and report." } }];
804
+ if (n === 2) return [{ name: "read", args: { path: "evidence.txt" } }];
805
+ if (n === 3)
806
+ return [
807
+ { name: "freeflow_project", args: { operation: "add", refs: ["ctx:missing"] } },
808
+ {
809
+ name: "freeflow_return",
810
+ args: { operation: "submit", report: "ORIGINAL_SAVED_REPORT", outcome: "partial" },
811
+ },
812
+ ];
813
+ if (n === 4) {
814
+ reportId = replay(manager.getBranch()).pendingId;
815
+ return [];
816
+ }
817
+ if (n === 5) {
818
+ assert.equal(body.model, "gpt-4o");
819
+ return [];
820
+ }
821
+ if (n === 6) {
822
+ assert.equal(body.model, "gpt-4.1-mini");
823
+ return [{ name: "read", args: { path: "unselected.txt" } }];
824
+ }
825
+ if (n === 7) {
826
+ assert.equal(
827
+ manager
828
+ .getBranch()
829
+ .filter((e) => e.message?.toolName === "read")
830
+ .at(-1).message.isError,
831
+ true,
832
+ );
833
+ return [
834
+ {
835
+ name: "freeflow_project",
836
+ args: {
837
+ operation: "remove",
838
+ refs: ["ctx:missing"],
839
+ reason: "Invalid reference; evidence gap remains disclosed.",
840
+ },
841
+ },
842
+ { name: "freeflow_return", args: { operation: "retry" } },
843
+ ];
844
+ }
845
+ assert.equal(body.model, "gpt-4o");
846
+ assert.match(JSON.stringify(body), /ORIGINAL_SAVED_REPORT/);
847
+ assert.match(JSON.stringify(body), /evidence gap remains disclosed/);
848
+ const state = replay(manager.getBranch());
849
+ assert.equal(state.assessment.handoffId, reportId);
850
+ assert.equal(state.handoffs.get(reportId).reportRevision, 1);
851
+ assert.equal(state.assessment.view, "active");
852
+ return [];
853
+ },
854
+ true,
855
+ async ({ session, requests }) => {
856
+ await session.prompt("Explain the saved return's problem.");
857
+ await session.waitForIdle();
858
+ await session.prompt("/freeflow resume");
859
+ await session.waitForIdle();
860
+ assert.equal(requests.length, 8);
861
+ },
862
+ );
863
+ },
864
+ );
865
+
866
+ const normal = (n) =>
867
+ n === 1
868
+ ? [{ name: "freeflow_delegate", args: { operation: "assign", contract: "Read evidence.txt and report." } }]
869
+ : n === 2
870
+ ? [{ name: "read", args: { path: "evidence.txt" } }]
871
+ : n === 3
872
+ ? [{ name: "freeflow_return", args: { operation: "submit", report: "Read completed.", outcome: "completed" } }]
873
+ : [];
874
+
875
+ for (const cut of ["delegate-accepted", "executor-configured", "executor-call", "read-result", "return-accepted"]) {
876
+ test(`native navigation reconciles valid ${cut} without replaying work`, { timeout: 30000 }, async () => {
877
+ await fixture(normal, false, async ({ session, manager, requests }) => {
878
+ const chosen = manager
879
+ .getEntries()
880
+ .find((e) =>
881
+ cut === "executor-configured"
882
+ ? e.data?.data?.type === "handoff-state" && e.data.data.state === "configured"
883
+ : cut === "executor-call"
884
+ ? e.message?.role === "assistant" && e.message.content?.some((b) => b.name === "read")
885
+ : cut === "read-result"
886
+ ? e.message?.toolName === "read"
887
+ : e.data?.data?.type === cut,
888
+ );
889
+ const reads = manager.getEntries().filter((e) => e.message?.toolName === "read").length;
890
+ await session.navigateTree(chosen.id, { summarize: false });
891
+ const before = requests.length;
892
+ await session.prompt("Explain the saved state at this point.");
893
+ await session.waitForIdle();
894
+ assert.equal(requests.length, before + 1);
895
+ assert.equal(requests.at(-1).model, "gpt-4o");
896
+ assert.equal(manager.getEntries().filter((e) => e.message?.toolName === "read").length, reads);
897
+ assert.equal(replay(manager.getBranch()).profile, "coordinator");
898
+ });
899
+ });
900
+ }
901
+
902
+ test(
903
+ "explicit resume continues the same assignment after Coordinator handles new input",
904
+ { timeout: 30000 },
905
+ async () => {
906
+ let assignment, unit;
907
+ await fixture(
908
+ (n, body, manager) => {
909
+ if (n <= 2) return normal(n);
910
+ if (n === 3) {
911
+ const s = replay(manager.getBranch());
912
+ assignment = s.assignmentId;
913
+ unit = s.unitId;
914
+ return [];
915
+ }
916
+ if (n === 4) {
917
+ assert.equal(body.model, "gpt-4o");
918
+ return [];
919
+ }
920
+ if (n === 5) {
921
+ assert.equal(body.model, "gpt-4.1-mini");
922
+ const s = replay(manager.getBranch());
923
+ assert.equal(s.assignmentId, assignment);
924
+ assert.equal(s.unitId, unit);
925
+ assert.equal(s.assignments.size, 1);
926
+ assert.match(JSON.stringify(body), /Do not edit files/);
927
+ assert.doesNotMatch(JSON.stringify(body), /New delivered user input requires Coordinator attention/);
928
+ return [{ name: "read", args: { path: "unselected.txt" } }];
929
+ }
930
+ if (n === 6) {
931
+ const read = manager
932
+ .getBranch()
933
+ .filter((e) => e.message?.toolName === "read")
934
+ .at(-1);
935
+ assert.equal(read.message.isError, false);
936
+ return [
937
+ {
938
+ name: "freeflow_return",
939
+ args: { operation: "submit", report: "Resumed read completed; no edits.", outcome: "completed" },
940
+ },
941
+ ];
942
+ }
943
+ assert.equal(body.model, "gpt-4o");
944
+ return [];
945
+ },
946
+ false,
947
+ async ({ session, manager, requests }) => {
948
+ await session.prompt("Do not edit files. Explain the current partial result.");
949
+ await session.waitForIdle();
950
+ await session.prompt("/freeflow resume");
951
+ await session.waitForIdle();
952
+ assert.equal(requests.length, 7);
953
+ assert.equal(replay(manager.getBranch()).assignments.get(assignment).state, "returned");
954
+ },
955
+ );
956
+ },
957
+ );
958
+
959
+ test("current manual hold survives navigation and resume cannot release it", { timeout: 30000 }, async () => {
960
+ await fixture(normal, false, async ({ session, manager, requests }) => {
961
+ const old = manager.getEntries().find((e) => e.data?.data?.type === "delegate-accepted");
962
+ await session.prompt("/freeflow profile executor");
963
+ await session.navigateTree(old.id, { summarize: false });
964
+ const before = requests.length;
965
+ await session.prompt("/freeflow resume");
966
+ assert.equal(requests.length, before);
967
+ const s = replay(manager.getBranch());
968
+ assert.equal(s.control, "manual");
969
+ assert.equal(s.profile, "executor");
970
+ assert.equal(session.model.id, "gpt-4.1-mini");
971
+ });
972
+ });
973
+
974
+ test(
975
+ "late steering keeps the captured Executor request coherent and blocks dependent task tools",
976
+ { timeout: 30000 },
977
+ async () => {
978
+ let active;
979
+ await fixture(
980
+ async (n, body, manager) => {
981
+ if (n === 1) return normal(n);
982
+ if (n === 2) {
983
+ await active.steer("Stop further reads and explain the current result.");
984
+ return normal(n);
985
+ }
986
+ if (n === 3) {
987
+ assert.equal(body.model, "gpt-4.1-mini");
988
+ assert.match(JSON.stringify(body), /New delivered user input requires Coordinator attention/);
989
+ assert.ok(
990
+ body.tools.some((t) => t.name === "freeflow_delegate"),
991
+ "stable catalog retains definitions; runtime gates still deny wrong-role effects",
992
+ );
993
+ assert.deepEqual(body.tools.find((t) => t.name === "freeflow_unit").parameters.properties.operation.enum, [
994
+ "inspect",
995
+ "assess",
996
+ "recover",
997
+ "cancel-recovery",
998
+ "close",
999
+ ]);
1000
+ return [{ name: "read", args: { path: "unselected.txt" } }];
1001
+ }
1002
+ if (n === 4) {
1003
+ assert.equal(
1004
+ manager
1005
+ .getBranch()
1006
+ .filter((e) => e.message?.toolName === "read")
1007
+ .at(-1).message.isError,
1008
+ true,
1009
+ );
1010
+ return [
1011
+ {
1012
+ name: "freeflow_return",
1013
+ args: { operation: "submit", report: "Stopped for the newer instruction.", outcome: "partial" },
1014
+ },
1015
+ ];
1016
+ }
1017
+ assert.equal(body.model, "gpt-4o");
1018
+ assert.ok(
1019
+ body.tools.some((t) => t.name === "freeflow_return") && body.tools.some((t) => t.name === "freeflow_project"),
1020
+ );
1021
+ assert.match(JSON.stringify(body), /Stop further reads/);
1022
+ return [];
1023
+ },
1024
+ false,
1025
+ undefined,
1026
+ true,
1027
+ {
1028
+ onSession: (session) => {
1029
+ active = session;
1030
+ },
1031
+ },
1032
+ );
1033
+ },
1034
+ );