@hassangameryt/freeflow 0.7.3 → 0.8.0

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 (276) 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/NOTICE +14 -0
  6. package/README.md +26 -31
  7. package/capabilities/cognitive-routing/SKILL.md +106 -161
  8. package/capabilities/cognitive-routing/references/both-mode.md +36 -0
  9. package/capabilities/cognitive-routing/references/executor-mode.md +48 -0
  10. package/capabilities/cognitive-routing/references/helper-mode.md +47 -0
  11. package/capabilities/compaction/SKILL.md +49 -0
  12. package/capabilities/compaction/references/summary-format.md +239 -0
  13. package/command-surface.json +4 -0
  14. package/gemini-extension.json +1 -1
  15. package/hooks/shared/runtime-context.mjs +28 -30
  16. package/licenses/Apache-2.0.txt +55 -0
  17. package/package.json +22 -9
  18. package/pi-extension/README.md +41 -0
  19. package/pi-extension/dist/cognitive-routing/assembler.js +393 -0
  20. package/pi-extension/dist/{cognitive-routing-v2 → cognitive-routing}/config.js +5 -2
  21. package/pi-extension/dist/cognitive-routing/economics.js +45 -0
  22. package/pi-extension/dist/cognitive-routing/event-schema.js +388 -0
  23. package/pi-extension/dist/{session-sources/events.js → cognitive-routing/event-store.js} +103 -40
  24. package/pi-extension/dist/cognitive-routing/gate.js +241 -0
  25. package/pi-extension/dist/cognitive-routing/handlers.js +899 -0
  26. package/pi-extension/dist/cognitive-routing/handoffs.js +350 -0
  27. package/pi-extension/dist/cognitive-routing/model-control.js +467 -0
  28. package/pi-extension/dist/{cognitive-routing-v2 → cognitive-routing}/projection.js +229 -56
  29. package/pi-extension/dist/cognitive-routing/provenance.js +81 -0
  30. package/pi-extension/dist/cognitive-routing/runtime.js +396 -0
  31. package/pi-extension/dist/{cognitive-routing-v2 → cognitive-routing}/schemas.js +43 -7
  32. package/pi-extension/dist/cognitive-routing/session.js +221 -0
  33. package/pi-extension/dist/{session-sources → cognitive-routing}/sources.js +99 -85
  34. package/pi-extension/dist/{cognitive-routing-v2 → cognitive-routing}/state.js +37 -378
  35. package/pi-extension/dist/cognitive-routing/status.js +80 -0
  36. package/pi-extension/dist/{cognitive-routing-v2 → cognitive-routing}/tools.js +12 -12
  37. package/pi-extension/dist/{cognitive-routing-v2 → cognitive-routing}/types.js +5 -11
  38. package/pi-extension/dist/compaction/carry.js +191 -0
  39. package/pi-extension/dist/compaction/config.js +22 -0
  40. package/pi-extension/dist/compaction/controller.js +471 -0
  41. package/pi-extension/dist/compaction/harness.js +77 -0
  42. package/pi-extension/dist/compaction/results.js +158 -0
  43. package/pi-extension/dist/compaction/thresholds.js +35 -0
  44. package/pi-extension/dist/compaction/tool.js +57 -0
  45. package/pi-extension/dist/host/branch.js +13 -0
  46. package/pi-extension/dist/host/canonical.js +12 -0
  47. package/pi-extension/dist/host/catalog.js +77 -0
  48. package/pi-extension/dist/host/config.js +374 -0
  49. package/pi-extension/dist/host/projection-tags.js +60 -0
  50. package/pi-extension/dist/host/prompts.js +82 -0
  51. package/pi-extension/dist/{session-sources → host}/read-only-session.js +112 -1
  52. package/pi-extension/dist/{runtime → host}/request-history.js +121 -39
  53. package/pi-extension/dist/host/runtime-state.js +189 -0
  54. package/pi-extension/dist/{settings/settings-ui.js → host/settings/freeflow-command.js} +620 -410
  55. package/pi-extension/dist/{settings/settings-tui.js → host/settings/settings-view.js} +186 -73
  56. package/pi-extension/dist/host/staging.js +28 -0
  57. package/pi-extension/dist/host/status.js +101 -0
  58. package/pi-extension/dist/index.js +423 -173
  59. package/pi-extension/dist/provider-support/cache/anchor.js +21 -0
  60. package/pi-extension/dist/provider-support/cache/anthropic.js +102 -0
  61. package/pi-extension/dist/provider-support/cache/health.js +73 -0
  62. package/pi-extension/dist/provider-support/cache/index.js +58 -0
  63. package/pi-extension/dist/provider-support/cache/keepalive.js +210 -0
  64. package/pi-extension/dist/provider-support/cache/monitor.js +17 -0
  65. package/pi-extension/dist/provider-support/cache/openai-responses.js +48 -0
  66. package/pi-extension/dist/provider-support/effort.js +13 -0
  67. package/pi-extension/dist/provider-support/index.js +44 -3
  68. package/pi-extension/dist/provider-support/{astra → openai}/adapter.js +46 -29
  69. package/pi-extension/dist/provider-support/{astra → openai}/history.js +10 -8
  70. package/pi-extension/dist/provider-support/{astra → openai}/index.js +8 -4
  71. package/pi-extension/dist/provider-support/{astra → openai}/session-state.js +29 -38
  72. package/pi-extension/dist/provider-support/routes.js +17 -0
  73. package/pi-extension/dist/tool-execution/apply-patch/grammar.js +23 -0
  74. package/pi-extension/dist/tool-execution/apply-patch/match.js +38 -0
  75. package/pi-extension/dist/tool-execution/apply-patch/parser.js +177 -0
  76. package/pi-extension/dist/tool-execution/apply-patch/plan.js +201 -0
  77. package/pi-extension/dist/tool-execution/apply-patch/tool.js +84 -0
  78. package/pi-extension/dist/tool-execution/background.js +238 -0
  79. package/pi-extension/dist/tool-execution/bash-guard.js +23 -0
  80. package/pi-extension/dist/tool-execution/config.js +22 -0
  81. package/pi-extension/dist/tool-execution/edit-messages.js +120 -0
  82. package/pi-extension/dist/tool-execution/file-state.js +138 -0
  83. package/pi-extension/dist/tool-execution/file-tracking.js +143 -0
  84. package/pi-extension/dist/tool-execution/prompt.js +54 -0
  85. package/pi-extension/dist/tool-execution/tools.js +16 -0
  86. package/pi-extension/freeflow/README.md +9 -0
  87. package/plugin.json +1 -1
  88. package/runtime/prompts/cognitive-routing.md +4 -4
  89. package/runtime/prompts/core.md +22 -75
  90. package/runtime/prompts/interaction-contract.md +15 -8
  91. package/runtime/prompts/tool-execution.md +38 -0
  92. package/runtime/prompts/working-method.md +108 -0
  93. package/skills/action-selection/SKILL.md +39 -47
  94. package/skills/action-selection/references/trajectory-stalls.md +2 -0
  95. package/skills/bypass/SKILL.md +2 -2
  96. package/skills/commit-work/SKILL.md +31 -83
  97. package/skills/commit-work/references/git-state-edges.md +1 -1
  98. package/skills/commit-work/references/simple-push.md +2 -0
  99. package/skills/commit-work/references/staging-decisions.md +1 -1
  100. package/skills/decision-gate/SKILL.md +31 -50
  101. package/skills/design-for-depth/SKILL.md +44 -53
  102. package/skills/design-for-depth/references/design-pressure-signals.md +46 -144
  103. package/skills/design-for-depth/references/implementation-decisions.md +1 -1
  104. package/skills/design-for-depth/references/interface-design-loop.md +5 -36
  105. package/skills/design-for-depth/references/module-and-dependency-design.md +2 -32
  106. package/skills/design-for-depth/references/state-and-failure-boundaries.md +2 -13
  107. package/skills/diagnose-failure/SKILL.md +39 -24
  108. package/skills/diagnose-failure/references/diagnostic-loop-catalog.md +2 -0
  109. package/skills/diagnose-failure/references/flaky-and-performance-diagnosis.md +2 -2
  110. package/skills/discuss/SKILL.md +35 -11
  111. package/skills/discuss/references/discussion-continuity.md +7 -27
  112. package/skills/evaluate-skill/SKILL.md +49 -80
  113. package/skills/evaluate-skill/references/evaluation-definition-schema.md +35 -5
  114. package/skills/evaluate-skill/references/evaluation-design.md +14 -14
  115. package/skills/evaluate-skill/references/execution-and-evidence.md +26 -5
  116. package/skills/evaluate-skill/references/review-and-revision.md +3 -1
  117. package/skills/evaluate-skill/scripts/lib/definitions.mjs +49 -8
  118. package/skills/evaluate-skill/scripts/lib/pi.mjs +14 -7
  119. package/skills/evaluate-skill/scripts/lib/review.mjs +196 -0
  120. package/skills/evaluate-skill/scripts/lib/runner.mjs +45 -8
  121. package/skills/evaluate-skill/scripts/lib/sandbox.mjs +2 -0
  122. package/skills/evaluate-skill/scripts/lib/trials.mjs +103 -0
  123. package/skills/evaluate-skill/scripts/lib/view.mjs +67 -1
  124. package/skills/evaluate-skill/scripts/pi-command-tool.mjs +144 -0
  125. package/skills/evaluate-skill/scripts/pi-guard.mjs +30 -13
  126. package/skills/evaluate-skill/scripts/skill-eval.mjs +114 -34
  127. package/skills/execute-work/SKILL.md +41 -9
  128. package/skills/execute-work/references/code-practices.md +2 -0
  129. package/skills/execute-work/references/execute-work-edges.md +11 -1
  130. package/skills/handoff/SKILL.md +1 -1
  131. package/skills/handoff/references/handoff-templates.md +1 -1
  132. package/skills/review-artifact/SKILL.md +42 -41
  133. package/skills/review-artifact/references/adjudicate-artifact-review.md +1 -1
  134. package/skills/review-artifact/references/independent-artifact-reviewer-contract.md +1 -1
  135. package/skills/review-work/SKILL.md +38 -32
  136. package/skills/review-work/references/adjudicate-work-review.md +1 -1
  137. package/skills/review-work/references/independent-work-reviewer-contract.md +1 -1
  138. package/skills/review-work/references/security-risk-lens.md +2 -4
  139. package/skills/setup-freeflow/SKILL.md +3 -1
  140. package/skills/setup-freeflow/references/activation-contract.md +7 -13
  141. package/skills/setup-freeflow/references/host-setup.md +3 -3
  142. package/skills/track-work/SKILL.md +113 -161
  143. package/skills/track-work/references/working-record-format.md +189 -350
  144. package/skills/track-work/scripts/lib/document.mjs +62 -6
  145. package/skills/track-work/scripts/lib/format.mjs +16 -1
  146. package/skills/track-work/scripts/lib/store.mjs +9 -8
  147. package/skills/track-work/scripts/lib/transitions.mjs +79 -4
  148. package/skills/track-work/scripts/working-record.mjs +34 -12
  149. package/skills/verify-work/SKILL.md +29 -20
  150. package/skills/verify-work/references/browser-runtime-evidence.md +1 -5
  151. package/skills/verify-work/references/integration-evidence.md +2 -0
  152. package/skills/verify-work/references/performance-evidence.md +3 -1
  153. package/skills/verify-work/references/test-design.md +3 -1
  154. package/skills/workflow/SKILL.md +65 -21
  155. package/skills/workflow/references/domain-skill-composition.md +5 -3
  156. package/skills/write-docs/SKILL.md +83 -0
  157. package/skills/write-docs/references/documentation-shapes.md +119 -0
  158. package/skills/write-docs/scripts/docs-check.mjs +139 -0
  159. package/skills/write-plan/SKILL.md +40 -40
  160. package/skills/write-plan/references/plan-shapes.md +1 -1
  161. package/skills/write-skill/SKILL.md +107 -147
  162. package/skills/write-skill/references/activation-boundaries.md +2 -0
  163. package/skills/write-skill/references/agent-first-instructions.md +22 -10
  164. package/skills/write-skill/references/progressive-disclosure.md +4 -2
  165. package/skills/write-skill/references/skill-dependency-graphs.md +2 -0
  166. package/skills/write-skill/scripts/lib/similarity.mjs +56 -0
  167. package/skills/write-skill/scripts/skill-author.mjs +18 -2
  168. package/skills/write-spec/SKILL.md +34 -41
  169. package/skills/write-spec/references/artifact-standards.md +1 -1
  170. package/skills/write-spec/references/decision-records.md +2 -0
  171. package/skills/write-spec/references/spec-shapes.md +1 -1
  172. package/capabilities/context-virtualization/SKILL.md +0 -103
  173. package/capabilities/conversation-history/SKILL.md +0 -53
  174. package/hooks/tests/check-host-adapters.test.mjs +0 -82
  175. package/hooks/tests/check-runtime-context-hook.sh +0 -212
  176. package/pi-extension/dist/cognitive-routing-v2/runtime.js +0 -2181
  177. package/pi-extension/dist/context-virtualization/commands.js +0 -89
  178. package/pi-extension/dist/context-virtualization/projector.js +0 -42
  179. package/pi-extension/dist/context-virtualization/runtime.js +0 -378
  180. package/pi-extension/dist/context-virtualization/state.js +0 -57
  181. package/pi-extension/dist/context-virtualization/types.js +0 -10
  182. package/pi-extension/dist/conversation-history/passages.js +0 -66
  183. package/pi-extension/dist/conversation-history/ranking.js +0 -105
  184. package/pi-extension/dist/conversation-history/runtime.js +0 -226
  185. package/pi-extension/dist/conversation-history/search.js +0 -56
  186. package/pi-extension/dist/conversation-history/types.js +0 -1
  187. package/pi-extension/dist/freeflow-context/resolver.js +0 -120
  188. package/pi-extension/dist/freeflow-context/runtime.js +0 -80
  189. package/pi-extension/dist/freeflow-context/source-projector.js +0 -95
  190. package/pi-extension/dist/freeflow-context/tool.js +0 -314
  191. package/pi-extension/dist/freeflow-context/types.js +0 -12
  192. package/pi-extension/dist/freeflow-context/visibility.js +0 -28
  193. package/pi-extension/dist/integrations/mcp-config.js +0 -29
  194. package/pi-extension/dist/runtime/runtime-context.js +0 -754
  195. package/pi-extension/dist/runtime/runtime-identity.js +0 -5
  196. package/pi-extension/dist/session-sources/provenance.js +0 -32
  197. package/pi-extension/dist/ui/text-component.js +0 -57
  198. package/pi-extension/src/cognitive-routing-v2/budget.ts +0 -42
  199. package/pi-extension/src/cognitive-routing-v2/config.ts +0 -173
  200. package/pi-extension/src/cognitive-routing-v2/projection.ts +0 -375
  201. package/pi-extension/src/cognitive-routing-v2/render.ts +0 -277
  202. package/pi-extension/src/cognitive-routing-v2/runtime.ts +0 -2241
  203. package/pi-extension/src/cognitive-routing-v2/schemas.ts +0 -180
  204. package/pi-extension/src/cognitive-routing-v2/state.ts +0 -839
  205. package/pi-extension/src/cognitive-routing-v2/tools.ts +0 -62
  206. package/pi-extension/src/cognitive-routing-v2/types.ts +0 -254
  207. package/pi-extension/src/context-virtualization/commands.ts +0 -111
  208. package/pi-extension/src/context-virtualization/projector.ts +0 -62
  209. package/pi-extension/src/context-virtualization/runtime.ts +0 -428
  210. package/pi-extension/src/context-virtualization/state.ts +0 -79
  211. package/pi-extension/src/context-virtualization/types.ts +0 -92
  212. package/pi-extension/src/conversation-history/passages.ts +0 -81
  213. package/pi-extension/src/conversation-history/ranking.ts +0 -155
  214. package/pi-extension/src/conversation-history/runtime.ts +0 -250
  215. package/pi-extension/src/conversation-history/search.ts +0 -79
  216. package/pi-extension/src/conversation-history/types.ts +0 -31
  217. package/pi-extension/src/freeflow-context/resolver.ts +0 -139
  218. package/pi-extension/src/freeflow-context/runtime.ts +0 -100
  219. package/pi-extension/src/freeflow-context/source-projector.ts +0 -106
  220. package/pi-extension/src/freeflow-context/tool.ts +0 -366
  221. package/pi-extension/src/freeflow-context/types.ts +0 -70
  222. package/pi-extension/src/freeflow-context/visibility.ts +0 -53
  223. package/pi-extension/src/index.ts +0 -390
  224. package/pi-extension/src/integrations/mcp-config.ts +0 -32
  225. package/pi-extension/src/provider-support/astra/adapter.ts +0 -108
  226. package/pi-extension/src/provider-support/astra/history.ts +0 -145
  227. package/pi-extension/src/provider-support/astra/index.ts +0 -12
  228. package/pi-extension/src/provider-support/astra/session-state.ts +0 -113
  229. package/pi-extension/src/provider-support/index.ts +0 -5
  230. package/pi-extension/src/runtime/request-history.ts +0 -243
  231. package/pi-extension/src/runtime/runtime-context.ts +0 -860
  232. package/pi-extension/src/runtime/runtime-identity.ts +0 -8
  233. package/pi-extension/src/session-sources/events.ts +0 -230
  234. package/pi-extension/src/session-sources/provenance.ts +0 -39
  235. package/pi-extension/src/session-sources/read-only-session.ts +0 -170
  236. package/pi-extension/src/session-sources/sources.ts +0 -334
  237. package/pi-extension/src/settings/settings-tui.ts +0 -501
  238. package/pi-extension/src/settings/settings-ui.ts +0 -1652
  239. package/pi-extension/src/ui/text-component.ts +0 -66
  240. package/pi-extension/tests/cache-reuse/history.test.js +0 -117
  241. package/pi-extension/tests/cache-reuse/native.test.js +0 -91
  242. package/pi-extension/tests/cache-reuse/settings.test.js +0 -211
  243. package/pi-extension/tests/cognitive-routing-v2/communication.test.js +0 -577
  244. package/pi-extension/tests/cognitive-routing-v2/compaction-input.test.js +0 -183
  245. package/pi-extension/tests/cognitive-routing-v2/delegation.test.js +0 -557
  246. package/pi-extension/tests/cognitive-routing-v2/native.test.js +0 -474
  247. package/pi-extension/tests/cognitive-routing-v2/performance.test.js +0 -87
  248. package/pi-extension/tests/cognitive-routing-v2/persistence.test.js +0 -300
  249. package/pi-extension/tests/cognitive-routing-v2/projection.test.js +0 -241
  250. package/pi-extension/tests/cognitive-routing-v2/recovery.test.js +0 -1034
  251. package/pi-extension/tests/cognitive-routing-v2/render.test.js +0 -244
  252. package/pi-extension/tests/cognitive-routing-v2/runtime.test.js +0 -423
  253. package/pi-extension/tests/cognitive-routing-v2/stabilization.test.js +0 -521
  254. package/pi-extension/tests/cognitive-routing-v2/state.test.js +0 -586
  255. package/pi-extension/tests/context-virtualization/extension.test.js +0 -775
  256. package/pi-extension/tests/context-virtualization/projector.test.js +0 -110
  257. package/pi-extension/tests/context-virtualization/runtime.test.js +0 -230
  258. package/pi-extension/tests/conversation-history/runtime.test.js +0 -98
  259. package/pi-extension/tests/conversation-history/search.test.js +0 -97
  260. package/pi-extension/tests/fixtures/pi-host.js +0 -4
  261. package/pi-extension/tests/fixtures/routing-native.js +0 -191
  262. package/pi-extension/tests/integration/mode-free-core.test.js +0 -220
  263. package/pi-extension/tests/integration/pi-extension.test.js +0 -1447
  264. package/pi-extension/tests/integration/prompt-architecture.test.js +0 -447
  265. package/pi-extension/tests/integration/unaffected-regressions.test.js +0 -478
  266. package/pi-extension/tests/provider-support/astra-boundaries.test.js +0 -75
  267. package/pi-extension/tests/provider-support/astra.test.js +0 -306
  268. package/pi-extension/tests/provider-support/cli.test.js +0 -92
  269. package/pi-extension/tests/provider-support/native.test.js +0 -183
  270. package/pi-extension/tests/settings/settings-tui.test.js +0 -243
  271. package/pi-extension/tsconfig.json +0 -16
  272. package/runtime/prompts/context-virtualization.md +0 -3
  273. package/runtime/prompts/conversation-history.md +0 -3
  274. package/skills/design-for-depth/references/software-design-philosophy.md +0 -202
  275. /package/pi-extension/dist/{cognitive-routing-v2 → cognitive-routing}/budget.js +0 -0
  276. /package/pi-extension/dist/{cognitive-routing-v2 → cognitive-routing}/render.js +0 -0
@@ -1,1034 +0,0 @@
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
- );