@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,586 @@
1
+ import assert from "node:assert/strict";
2
+ import test from "node:test";
3
+ import { initialState, reduce, parseRoutingEvent } from "../../dist/cognitive-routing-v2/state.js";
4
+ const pair = { provider: "fixture", modelId: "model", thinking: "off" };
5
+ function machine() {
6
+ let s = initialState(),
7
+ n = 0;
8
+ return {
9
+ get state() {
10
+ return s;
11
+ },
12
+ event(data) {
13
+ return {
14
+ version: 2,
15
+ eventId: `event-${++n}`,
16
+ operationId: `op-${n}`,
17
+ stepId: data.type,
18
+ recordedSessionId: "session",
19
+ data,
20
+ };
21
+ },
22
+ apply(data) {
23
+ const e = this.event(data);
24
+ s = reduce(s, e);
25
+ return e;
26
+ },
27
+ set(e) {
28
+ s = reduce(s, e);
29
+ },
30
+ };
31
+ }
32
+ const control = (profile) => ({ type: "control", control: "automatic", profile, reason: "fixture control" });
33
+ const opened = (id, profile, assignmentId) => ({
34
+ type: "execution-opened",
35
+ execution: { id, profile, assignmentId, basisUserEntryId: "user", pair, resultEntryIds: [] },
36
+ });
37
+ const bound = (id) => ({
38
+ type: "execution-bound",
39
+ executionId: id,
40
+ assistantEntryId: `assistant-${id}`,
41
+ resultEntryIds: [],
42
+ outcome: "completed",
43
+ });
44
+ function delegated(id = "a1", unitId = "u1", assignmentIds = [id], executionId = "e1", replacement) {
45
+ const hid = `d-${id}`;
46
+ return {
47
+ type: "delegate-accepted",
48
+ unit: { id: unitId, objective: "fixture objective", state: "open", assignmentIds },
49
+ assignment: {
50
+ id,
51
+ unitId,
52
+ contract: "fixture contract",
53
+ basisUserEntryId: "user",
54
+ state: "outstanding",
55
+ delegateHandoffId: hid,
56
+ },
57
+ handoff: {
58
+ id: hid,
59
+ kind: "delegate",
60
+ assignmentId: id,
61
+ text: "fixture contract",
62
+ from: "coordinator",
63
+ to: "executor",
64
+ executionId,
65
+ toolCallId: `call-${executionId}`,
66
+ basisUserEntryId: "user",
67
+ state: "pending",
68
+ reportRevision: 0,
69
+ limitations: [],
70
+ },
71
+ ...(replacement ? { replacement } : {}),
72
+ };
73
+ }
74
+ function returned(report = "exact report", revision = 1) {
75
+ return {
76
+ type: "return-accepted",
77
+ handoff: {
78
+ id: "return",
79
+ kind: "return",
80
+ assignmentId: "a1",
81
+ text: report,
82
+ from: "executor",
83
+ to: "coordinator",
84
+ executionId: "e2",
85
+ toolCallId: "return-call",
86
+ basisUserEntryId: "user",
87
+ state: "pending",
88
+ reportRevision: revision,
89
+ outcome: "partial",
90
+ limitations: ["uncertainty retained"],
91
+ },
92
+ };
93
+ }
94
+ function started() {
95
+ const m = machine();
96
+ m.apply(control("coordinator"));
97
+ m.apply(opened("e1", "coordinator"));
98
+ m.apply(delegated());
99
+ m.apply(bound("e1"));
100
+ m.apply({ type: "handoff-state", handoffId: "d-a1", state: "configured", observedPair: pair });
101
+ return m;
102
+ }
103
+ const recoveryRequest = (assignmentId = "a1") => ({
104
+ type: "recovery-request-accepted",
105
+ recovery: {
106
+ id: "recovery",
107
+ assignmentId,
108
+ assessmentHandoffId: "return",
109
+ baseReportRevision: 1,
110
+ request: "recover evidence",
111
+ requestedPaths: ["recovery.txt"],
112
+ paths: ["/tmp/recovery.txt"],
113
+ state: "requested",
114
+ requestHandoffId: "recovery-request",
115
+ supplementRevision: 0,
116
+ },
117
+ handoff: {
118
+ id: "recovery-request",
119
+ kind: "recovery-request",
120
+ assignmentId,
121
+ text: "recover evidence",
122
+ from: "coordinator",
123
+ to: "executor",
124
+ executionId: "e3",
125
+ toolCallId: "recover-call",
126
+ basisUserEntryId: "user",
127
+ state: "pending",
128
+ reportRevision: 0,
129
+ limitations: [],
130
+ },
131
+ });
132
+ const recoverySupplement = (revision = 1) => ({
133
+ type: "recovery-supplement-accepted",
134
+ recoveryId: "recovery",
135
+ handoff: {
136
+ id: "recovery-return",
137
+ kind: "recovery-return",
138
+ assignmentId: "a1",
139
+ text: "recovered evidence",
140
+ from: "executor",
141
+ to: "coordinator",
142
+ executionId: "e4",
143
+ toolCallId: "supplement-call",
144
+ basisUserEntryId: "user",
145
+ state: "pending",
146
+ reportRevision: revision,
147
+ outcome: "completed",
148
+ limitations: [],
149
+ },
150
+ });
151
+ function recoveryReady() {
152
+ const m = started();
153
+ m.apply(opened("e2", "executor", "a1"));
154
+ m.apply(returned());
155
+ m.apply(bound("e2"));
156
+ m.apply({ type: "handoff-state", handoffId: "return", state: "configured", observedPair: pair });
157
+ m.apply(opened("e3", "coordinator", "a1"));
158
+ m.apply(recoveryRequest());
159
+ return m;
160
+ }
161
+ const reservation = (revision) => ({
162
+ id: "reservation",
163
+ selectionRevision: revision,
164
+ receiver: "coordinator",
165
+ target: pair,
166
+ qualification: "fixture",
167
+ sources: [],
168
+ maximumInputTokens: 10000,
169
+ outputReserve: 1000,
170
+ estimateMethod: "fixture",
171
+ });
172
+
173
+ test("event identity and operation idempotency reject changed payloads", () => {
174
+ const m = machine(),
175
+ e = m.apply(control("coordinator"));
176
+ const before = m.state;
177
+ m.set(e);
178
+ assert.equal(m.state, before);
179
+ assert.throws(
180
+ () => m.set({ ...e, data: control("executor") }),
181
+ (e) => e.code === "event_identity_conflict",
182
+ );
183
+ assert.throws(
184
+ () => m.set({ ...e, eventId: "other", data: control("executor") }),
185
+ (e) => e.code === "operation_conflict",
186
+ );
187
+ });
188
+ test("closed nested schemas reject malformed optional handoff and source fields", () => {
189
+ const m = machine();
190
+ for (const patch of [{ reason: {} }, { outcome: 123 }, { unexpected: "field" }]) {
191
+ const data = delegated();
192
+ Object.assign(data.handoff, patch);
193
+ assert.throws(
194
+ () => parseRoutingEvent(m.event(data)),
195
+ (e) => e.code === "invalid_assignment",
196
+ );
197
+ }
198
+ const data = opened("e", "executor", "a");
199
+ data.execution.pair = { ...data.execution.pair, extra: "invalid" };
200
+ assert.throws(
201
+ () => parseRoutingEvent(m.event(data)),
202
+ (e) => e.code === "invalid_execution",
203
+ );
204
+ });
205
+ test("prototype names are bounded malformed events, not raw decoder errors", () => {
206
+ for (const type of ["constructor", "__proto__", "toString", "unknown"])
207
+ assert.throws(
208
+ () =>
209
+ parseRoutingEvent({
210
+ version: 2,
211
+ eventId: "e",
212
+ operationId: "o",
213
+ stepId: "s",
214
+ recordedSessionId: "session",
215
+ data: { type },
216
+ }),
217
+ (e) => e.name === "RoutingError" && e.code === "invalid_event_type",
218
+ );
219
+ });
220
+ test("outstanding assignments cannot be implicitly replaced", () => {
221
+ const m = started();
222
+ assert.throws(
223
+ () => m.apply(delegated("a2", "u1", ["a1", "a2"])),
224
+ (e) => e.code === "wrong_control",
225
+ );
226
+ m.apply(control("coordinator"));
227
+ assert.throws(
228
+ () => m.apply(delegated("a2", "u1", ["a1", "a2"])),
229
+ (e) => e.code === "assignment_outstanding",
230
+ );
231
+ assert.equal(m.state.assignments.size, 1);
232
+ });
233
+ test("replacement retains old assignment and requires old execution to be resolved", () => {
234
+ const m = started();
235
+ m.apply(opened("e2", "executor", "a1"));
236
+ m.apply(control("coordinator"));
237
+ m.apply(opened("e3", "coordinator", "a1"));
238
+ const replacement = delegated("a2", "u1", ["a1", "a2"], "e3", {
239
+ assignmentId: "a1",
240
+ reason: "current direction changed",
241
+ });
242
+ assert.throws(
243
+ () => m.apply(replacement),
244
+ (e) => e.code === "unresolved_worker_execution",
245
+ );
246
+ m.apply(bound("e2"));
247
+ m.apply(replacement);
248
+ assert.equal(m.state.unitId, "u1");
249
+ assert.equal(m.state.assignments.get("a1").state, "superseded");
250
+ assert.equal(m.state.assignments.get("a1").contract, "fixture contract");
251
+ assert.equal(m.state.assignmentId, "a2");
252
+ });
253
+ test("worker identity rejects foreign returns and recovery requests", () => {
254
+ const m = machine();
255
+ m.apply(control("coordinator"));
256
+ m.apply(opened("e1", "coordinator"));
257
+ const helperDelegate = delegated();
258
+ helperDelegate.handoff.to = "helper";
259
+ m.apply(helperDelegate);
260
+ m.apply(bound("e1"));
261
+ m.apply({ type: "handoff-state", handoffId: "d-a1", state: "configured", observedPair: pair });
262
+ m.apply(opened("e2", "helper", "a1"));
263
+ const foreignReturn = returned();
264
+ assert.throws(
265
+ () => m.apply(foreignReturn),
266
+ (error) => error.code === "return_identity_mismatch",
267
+ );
268
+ const helperReturn = returned();
269
+ helperReturn.handoff.from = "helper";
270
+ m.apply(helperReturn);
271
+ m.apply(bound("e2"));
272
+ m.apply({ type: "handoff-state", handoffId: "return", state: "configured", observedPair: pair });
273
+ m.apply(opened("e3", "coordinator", "a1"));
274
+ const foreignRecovery = recoveryRequest();
275
+ assert.throws(
276
+ () => m.apply(foreignRecovery),
277
+ (error) => error.code === "recovery_not_available",
278
+ );
279
+ foreignRecovery.handoff.to = "helper";
280
+ m.apply(foreignRecovery);
281
+ assert.equal(m.state.recoveryId, "recovery");
282
+ assert.equal(m.state.handoffs.get("recovery-request").to, "helper");
283
+ });
284
+
285
+ test("wrong-worker return, selection, supplement, and retry events are symmetric and state-preserving", () => {
286
+ const startedFor = (worker) => {
287
+ const m = machine();
288
+ m.apply(control("coordinator"));
289
+ m.apply(opened("e1", "coordinator"));
290
+ const delegation = delegated();
291
+ delegation.handoff.to = worker;
292
+ m.apply(delegation);
293
+ m.apply(bound("e1"));
294
+ m.apply({ type: "handoff-state", handoffId: "d-a1", state: "configured", observedPair: pair });
295
+ return m;
296
+ };
297
+ const returnFrom = (worker) => {
298
+ const value = returned();
299
+ value.handoff.from = worker;
300
+ return value;
301
+ };
302
+ const requestTo = (worker) => {
303
+ const value = recoveryRequest();
304
+ value.handoff.to = worker;
305
+ return value;
306
+ };
307
+ const supplementFrom = (worker) => {
308
+ const value = recoverySupplement();
309
+ value.handoff.from = worker;
310
+ return value;
311
+ };
312
+
313
+ for (const [worker, foreign] of [
314
+ ["executor", "helper"],
315
+ ["helper", "executor"],
316
+ ]) {
317
+ const returning = startedFor(worker);
318
+ returning.apply(opened("e2", worker, "a1"));
319
+ const beforeReturn = returning.state;
320
+ assert.throws(
321
+ () => returning.apply(returnFrom(foreign)),
322
+ (error) => error.code === "return_identity_mismatch",
323
+ );
324
+ assert.equal(returning.state, beforeReturn);
325
+ assert.equal(returning.state.assignments.get("a1").state, "outstanding");
326
+
327
+ const selecting = startedFor(worker);
328
+ selecting.apply(control(foreign));
329
+ const beforeSelection = selecting.state;
330
+ assert.throws(
331
+ () =>
332
+ selecting.apply({
333
+ type: "selection-changed",
334
+ assignmentId: "a1",
335
+ selection: { revision: 1, selected: [], unresolved: [], withdrawals: [] },
336
+ }),
337
+ (error) => error.code === "selection_wrong_phase",
338
+ );
339
+ assert.equal(selecting.state, beforeSelection);
340
+ assert.equal(selecting.state.selections.has("a1"), false);
341
+
342
+ const retrying = startedFor(worker);
343
+ retrying.apply(opened("e2", worker, "a1"));
344
+ retrying.apply(returnFrom(worker));
345
+ retrying.apply({ type: "handoff-state", handoffId: "return", state: "blocked", reason: "fixture gap" });
346
+ retrying.apply(control(foreign));
347
+ const beforeRetry = retrying.state;
348
+ assert.throws(
349
+ () =>
350
+ retrying.apply({
351
+ type: "handoff-retry-requested",
352
+ handoffId: "return",
353
+ attemptId: "retry",
354
+ executionId: "e2",
355
+ toolCallId: "retry-call",
356
+ reportRevision: 1,
357
+ selectionRevision: 0,
358
+ }),
359
+ (error) => error.code === "invalid_return_retry",
360
+ );
361
+ assert.equal(retrying.state, beforeRetry);
362
+ assert.equal(retrying.state.handoffs.get("return").state, "blocked");
363
+ assert.equal(retrying.state.attempts.has("return"), false);
364
+
365
+ const supplementing = startedFor(worker);
366
+ supplementing.apply(opened("e2", worker, "a1"));
367
+ supplementing.apply(returnFrom(worker));
368
+ supplementing.apply(bound("e2"));
369
+ supplementing.apply({ type: "handoff-state", handoffId: "return", state: "configured", observedPair: pair });
370
+ supplementing.apply(opened("e3", "coordinator", "a1"));
371
+ supplementing.apply(requestTo(worker));
372
+ supplementing.apply(bound("e3"));
373
+ supplementing.apply({
374
+ type: "handoff-state",
375
+ handoffId: "recovery-request",
376
+ state: "configured",
377
+ observedPair: pair,
378
+ });
379
+ supplementing.apply(opened("e4", worker, "a1"));
380
+ const beforeSupplement = supplementing.state;
381
+ assert.throws(
382
+ () => supplementing.apply(supplementFrom(foreign)),
383
+ (error) => error.code === "recovery_not_returnable",
384
+ );
385
+ assert.equal(supplementing.state, beforeSupplement);
386
+ assert.equal(supplementing.state.recoveries.get("recovery").state, "reading");
387
+ assert.equal(supplementing.state.handoffs.has("recovery-return"), false);
388
+ }
389
+ });
390
+
391
+ test("saved report revisions and retries preserve exact earlier events", () => {
392
+ const m = started();
393
+ m.apply(opened("e2", "executor", "a1"));
394
+ const first = m.apply(returned());
395
+ m.apply({ type: "handoff-state", handoffId: "return", state: "blocked", reason: "fixture gap" });
396
+ m.apply({
397
+ type: "handoff-retry-requested",
398
+ handoffId: "return",
399
+ attemptId: "retry",
400
+ executionId: "e2",
401
+ toolCallId: "retry-call",
402
+ reportRevision: 1,
403
+ selectionRevision: 0,
404
+ });
405
+ assert.equal(m.state.handoffs.get("return").text, "exact report");
406
+ assert.equal(m.state.handoffs.get("return").reportRevision, 1);
407
+ m.apply(returned("revised exact report", 2));
408
+ assert.equal(m.state.handoffs.get("return").text, "revised exact report");
409
+ assert.equal([...m.state.events.values()].find((e) => e.eventId === first.eventId).data.handoff.text, "exact report");
410
+ assert.equal(m.state.assignments.size, 1);
411
+ });
412
+ test("recovery transitions preserve the original report and reject foreign identities", () => {
413
+ const ready = () => {
414
+ const m = started();
415
+ m.apply(opened("e2", "executor", "a1"));
416
+ m.apply(returned());
417
+ m.apply(bound("e2"));
418
+ m.apply({ type: "handoff-state", handoffId: "return", state: "configured", observedPair: pair });
419
+ m.apply(opened("e3", "coordinator", "a1"));
420
+ return m;
421
+ };
422
+ const foreign = ready();
423
+ assert.throws(
424
+ () => foreign.apply(recoveryRequest("foreign")),
425
+ (error) => error.code === "recovery_not_available",
426
+ );
427
+
428
+ const m = ready();
429
+ const base = structuredClone(m.state.handoffs.get("return"));
430
+ const request = m.apply(recoveryRequest());
431
+ m.set(request);
432
+ assert.equal(m.state.recoveries.size, 1, "exact event replay is idempotent");
433
+ m.apply(bound("e3"));
434
+ m.apply({ type: "handoff-state", handoffId: "recovery-request", state: "configured", observedPair: pair });
435
+ assert.equal(m.state.recoveries.get("recovery").state, "reading");
436
+ m.apply(opened("e4", "executor", "a1"));
437
+ m.apply(recoverySupplement());
438
+ assert.equal(m.state.recoveries.get("recovery").state, "returning");
439
+ assert.deepEqual(m.state.handoffs.get("return"), base, "the base report remains immutable");
440
+ assert.throws(
441
+ () => m.apply(recoverySupplement(3)),
442
+ (error) => error.code === "invalid_report_revision",
443
+ );
444
+ });
445
+
446
+ test("assessment cannot resume while attached recovery remains unsettled", () => {
447
+ const m = recoveryReady();
448
+ const before = m.state;
449
+ assert.throws(
450
+ () =>
451
+ m.apply({
452
+ type: "assessment-resumed",
453
+ handoffId: "return",
454
+ basisUserEntryId: "new-user",
455
+ }),
456
+ (error) => error.code === "recovery_outstanding",
457
+ );
458
+ assert.equal(m.state, before);
459
+ assert.equal(m.state.assessment.view, "suspended");
460
+ assert.equal(m.state.recoveryId, "recovery");
461
+ });
462
+
463
+ for (const outcome of ["cancelled", "deferred"]) {
464
+ test(`${outcome} unit closure atomically disposes attached recovery`, () => {
465
+ const m = recoveryReady();
466
+ m.apply({ type: "unit-closed", unitId: "u1", outcome, assessment: `${outcome} with recovery` });
467
+ assert.equal(m.state.unitId, undefined);
468
+ assert.equal(m.state.assignmentId, undefined);
469
+ assert.equal(m.state.pendingId, undefined);
470
+ assert.equal(m.state.recoveryId, undefined);
471
+ assert.equal(m.state.recoveries.get("recovery").state, "cancelled");
472
+ assert.equal(m.state.recoveries.get("recovery").cancellationReason, `${outcome} with recovery`);
473
+ assert.equal(m.state.handoffs.get("recovery-request").state, "superseded");
474
+ assert.equal(m.state.handoffs.get("return").text, "exact report");
475
+ m.apply(opened(`e-${outcome}`, "coordinator"));
476
+ m.apply(delegated(`a-${outcome}`, `u-${outcome}`, [`a-${outcome}`], `e-${outcome}`));
477
+ assert.equal(m.state.unitId, `u-${outcome}`);
478
+ });
479
+ }
480
+
481
+ test("accepted unit closure rejects attached recovery without changing state", () => {
482
+ const m = recoveryReady();
483
+ const before = m.state;
484
+ assert.throws(
485
+ () => m.apply({ type: "unit-closed", unitId: "u1", outcome: "accepted", assessment: "incorrect" }),
486
+ (error) => error.code === "unfinished_work",
487
+ );
488
+ assert.equal(m.state, before);
489
+ assert.equal(m.state.recoveryId, "recovery");
490
+ });
491
+
492
+ test("assessment resumption rejects stale selection and preserves suspension on failure", () => {
493
+ const m = started();
494
+ m.apply(opened("e2", "executor", "a1"));
495
+ m.apply(returned());
496
+ m.apply({
497
+ type: "selection-changed",
498
+ assignmentId: "a1",
499
+ selection: { revision: 1, selected: [], unresolved: [], withdrawals: [] },
500
+ });
501
+ m.apply({ type: "handoff-state", handoffId: "return", state: "configured", observedPair: pair });
502
+ m.apply({
503
+ type: "assessment-suspended",
504
+ handoffId: "return",
505
+ reason: "user-attention",
506
+ basisUserEntryId: "new-user",
507
+ problems: [],
508
+ });
509
+ assert.throws(
510
+ () =>
511
+ m.apply({
512
+ type: "assessment-resumed",
513
+ handoffId: "return",
514
+ basisUserEntryId: "new-user",
515
+ reservation: reservation(0),
516
+ }),
517
+ (e) => e.code === "stale_selection",
518
+ );
519
+ assert.equal(m.state.assessment.view, "suspended");
520
+ m.apply({
521
+ type: "assessment-resumed",
522
+ handoffId: "return",
523
+ basisUserEntryId: "new-user",
524
+ reservation: reservation(1),
525
+ });
526
+ assert.equal(m.state.assessment.view, "active");
527
+ });
528
+ test("accepted closure cannot conceal work; later work creates a new unit", () => {
529
+ const m = started();
530
+ m.apply(control("coordinator"));
531
+ assert.throws(
532
+ () => m.apply({ type: "unit-closed", unitId: "u1", outcome: "accepted", assessment: "incorrect" }),
533
+ (e) => e.code === "unfinished_work",
534
+ );
535
+ m.apply({
536
+ type: "unit-closed",
537
+ unitId: "u1",
538
+ outcome: "cancelled",
539
+ assessment: "user stopped work",
540
+ supersededAssignmentId: "a1",
541
+ });
542
+ assert.equal(m.state.units.get("u1").state, "closed");
543
+ m.apply(opened("e3", "coordinator"));
544
+ assert.throws(
545
+ () => m.apply(delegated("a2", "u1", ["a2"], "e3")),
546
+ (e) => e.code === "closed_unit_reused",
547
+ );
548
+ m.apply(delegated("a2", "u2", ["a2"], "e3"));
549
+ assert.equal(m.state.unitId, "u2");
550
+ assert.equal(m.state.units.size, 2);
551
+ });
552
+ test("profile override events are complete-pair, branch-replayable session state", () => {
553
+ const m = machine();
554
+ const coordinator = { provider: "fixture", modelId: "coordinator-fast", thinking: "high" };
555
+ const executor = { provider: "fixture", modelId: "executor-cheap", thinking: "low" };
556
+ m.apply({
557
+ type: "profile-overrides",
558
+ overrides: { coordinator },
559
+ reason: "fixture session override",
560
+ });
561
+ assert.deepEqual(m.state.profileOverrides.get("coordinator"), coordinator);
562
+ assert.equal(m.state.profileOverrides.has("executor"), false);
563
+ m.apply({
564
+ type: "profile-overrides",
565
+ overrides: { executor },
566
+ reason: "fixture session override",
567
+ });
568
+ assert.deepEqual(m.state.profileOverrides.get("executor"), executor);
569
+ m.apply({
570
+ type: "profile-overrides",
571
+ overrides: { coordinator: null, executor: null },
572
+ reason: "fixture reset",
573
+ });
574
+ assert.equal(m.state.profileOverrides.size, 0);
575
+ assert.throws(
576
+ () =>
577
+ parseRoutingEvent(
578
+ m.event({
579
+ type: "profile-overrides",
580
+ overrides: { coordinator: { provider: "fixture", modelId: "bad", thinking: "unknown" } },
581
+ reason: "invalid",
582
+ }),
583
+ ),
584
+ (e) => e.code === "invalid_profile_overrides",
585
+ );
586
+ });
@@ -169,6 +169,31 @@ function renderComponent(component) {
169
169
  return component.render(120).join("\\n");
170
170
  }
171
171
 
172
+ test("disabled Freeflow Context retains valid full operation schemas", async () => {
173
+ const cwd = await mkdtemp(join(tmpdir(), "freeflow-context-disabled-schema-"));
174
+ try {
175
+ await writeConfig(cwd, {});
176
+ const harness = createHarness(cwd, []);
177
+ await harness.handlers.get("session_start")({ reason: "startup" }, harness.ctx);
178
+
179
+ const contextTool = harness.tools.find((tool) => tool.name === "freeflow_context");
180
+ assert.ok(contextTool);
181
+ assert.equal(contextTool.parameters.type, "object");
182
+ assert.deepEqual(
183
+ contextTool.parameters.oneOf.map((v) => v.properties.operation.const),
184
+ ["archive", "restore", "search", "retrieve"],
185
+ );
186
+ const denied = await contextTool.execute(
187
+ "disabled",
188
+ { operation: "archive", targets: [{ ref: "ctx:tool-1" }] },
189
+ undefined,
190
+ );
191
+ assert.equal(denied.details.result.status, "rejected");
192
+ } finally {
193
+ await rm(cwd, { recursive: true, force: true });
194
+ }
195
+ });
196
+
172
197
  test("enabled Context Virtualization registers, projects, archives, restores, and reports through commands", async () => {
173
198
  const cwd = await mkdtemp(join(tmpdir(), "freeflow-context-extension-"));
174
199
  try {
@@ -189,7 +214,7 @@ test("enabled Context Virtualization registers, projects, archives, restores, an
189
214
 
190
215
  const contextTool = harness.tools.find((tool) => tool.name === "freeflow_context");
191
216
  assert.ok(contextTool);
192
- assert.doesNotMatch(contextTool.description, /conversation history/i);
217
+ assert.match(contextTool.description, /conversation history/i);
193
218
  const staleSearch = await contextTool.execute(
194
219
  "context-stale-search",
195
220
  { operation: "search", query: "not enabled" },
@@ -275,7 +300,7 @@ test("enabled Context Virtualization registers, projects, archives, restores, an
275
300
  }
276
301
  });
277
302
 
278
- test("enabled Conversation History exposes only search and retrieve operations", async () => {
303
+ test("enabled Conversation History permits search and retrieve within the stable catalog", async () => {
279
304
  const cwd = await mkdtemp(join(tmpdir(), "freeflow-conversation-history-extension-"));
280
305
  try {
281
306
  await writeConfig(cwd, { conversationHistory: true });
@@ -287,7 +312,7 @@ test("enabled Conversation History exposes only search and retrieve operations",
287
312
  assert.ok(contextTool);
288
313
  assert.deepEqual(
289
314
  contextTool.parameters.oneOf.map((variant) => variant.properties.operation.const),
290
- ["search", "retrieve"],
315
+ ["archive", "restore", "search", "retrieve"],
291
316
  );
292
317
  assert.equal(typeof contextTool.renderCall, "function");
293
318
  assert.equal(typeof contextTool.renderResult, "function");
@@ -349,7 +374,7 @@ test("enabled Conversation History exposes only search and retrieve operations",
349
374
  assert.match(renderComponent(expandedResult), /ctx:render-source/);
350
375
  assert.match(renderComponent(expandedResult), /database timeout was recovered/);
351
376
  assert.match(contextTool.description, /hidden conversation history/i);
352
- assert.doesNotMatch(contextTool.description, /future context projections/i);
377
+ assert.match(contextTool.description, /future context projections/i);
353
378
  const before = await harness.handlers.get("before_agent_start")({ systemPrompt: "base" }, harness.ctx);
354
379
  assert.match(before.systemPrompt, /## Conversation History Cue/);
355
380
  assert.doesNotMatch(before.systemPrompt, /# Conversation History$/m);
@@ -728,20 +753,21 @@ test("visible invalid sources do not make an empty hidden corpus partial", async
728
753
  }
729
754
  });
730
755
 
731
- test("disabled Context Virtualization hides the tool and leaves tool results unchanged", async () => {
756
+ test("disabled Context Virtualization blocks execution while preserving definitions and reference labels", async () => {
732
757
  const cwd = await mkdtemp(join(tmpdir(), "freeflow-context-disabled-"));
733
758
  try {
734
759
  await writeConfig(cwd, false);
735
760
  const harness = createHarness(cwd, [toolResultEntry()]);
736
761
  await harness.handlers.get("session_start")({ reason: "startup" }, harness.ctx);
737
762
 
738
- assert.ok(!harness.activeToolNames().includes("freeflow_context"));
763
+ assert.ok(harness.activeToolNames().includes("freeflow_context"));
739
764
  const contextResult = await harness.handlers.get("context")(
740
765
  { messages: [harness.entries[0].message] },
741
766
  harness.ctx,
742
767
  );
743
768
  assert.ok(contextResult);
744
- assert.deepEqual(contextResult.messages[0], harness.entries[0].message);
769
+ assert.deepEqual(contextResult.messages[0].content.slice(0, -1), harness.entries[0].message.content);
770
+ assert.match(contextResult.messages[0].content.at(-1).text, /context-ref:/);
745
771
  assert.equal(contextResult.messages.at(-1).customType, "freeflow-runtime-state");
746
772
  } finally {
747
773
  await rm(cwd, { recursive: true, force: true });