@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,577 @@
1
+ import assert from "node:assert/strict";
2
+ import test from "node:test";
3
+ import { fixture } from "../fixtures/routing-native.js";
4
+ import { Sources } from "../../dist/session-sources/sources.js";
5
+ import { initialState } from "../../dist/cognitive-routing-v2/state.js";
6
+ import { prepareView, changeSelection } from "../../dist/cognitive-routing-v2/projection.js";
7
+ import { RoutingRuntime } from "../../dist/cognitive-routing-v2/runtime.js";
8
+
9
+ const call = (name, args) => [{ name, args }];
10
+ const receipt = (m, name) =>
11
+ m
12
+ .getBranch()
13
+ .filter((e) => e.message?.toolName === name)
14
+ .at(-1).message.details;
15
+ const model = {
16
+ id: "receiver",
17
+ provider: "fixture",
18
+ api: "fixture",
19
+ input: ["text"],
20
+ contextWindow: 100000,
21
+ maxTokens: 1000,
22
+ };
23
+ const assistant = (text, calls = []) => ({
24
+ role: "assistant",
25
+ content: [{ type: "text", text }, ...calls],
26
+ model: "receiver",
27
+ provider: "fixture",
28
+ api: "fixture",
29
+ timestamp: 1,
30
+ stopReason: calls.length ? "toolUse" : "stop",
31
+ });
32
+ const entry = (id, parentId, message) => ({ type: "message", id, parentId, message });
33
+ function prepare(state, entries, messages = entries.map((e) => e.message), view = "executor") {
34
+ return prepareView({
35
+ state,
36
+ sources: new Sources(entries, state),
37
+ messages,
38
+ view,
39
+ projection: true,
40
+ model,
41
+ pair: { provider: "fixture", modelId: "receiver", thinking: "off" },
42
+ systemPrompt: "",
43
+ tools: [],
44
+ runtimeMessage: RoutingRuntime.prototype.runtimeMessage.call(
45
+ { projectionEnabled: true, token: "test", evidenceFacts: () => ({ selected: [] }) },
46
+ state,
47
+ ),
48
+ instance: "test",
49
+ });
50
+ }
51
+
52
+ test("source locators retain native occurrence identity and accept optional adapter labels", () => {
53
+ const state = initialState();
54
+ const assistantWith = (id, name, args) =>
55
+ entry(id, null, assistant("Observed", [{ type: "toolCall", id, name, arguments: args }]));
56
+ const result = (id, toolName) =>
57
+ entry(`${id}-result`, id, {
58
+ role: "toolResult",
59
+ toolCallId: id,
60
+ toolName,
61
+ content: [{ type: "text", text: "RESULT_BODY" }],
62
+ });
63
+ const entries = [
64
+ assistantWith("read-1", "read", { path: "same.txt", offset: 1, limit: 20 }),
65
+ result("read-1", "read"),
66
+ assistantWith("read-2", "read", { path: "same.txt", offset: 1, limit: 20 }),
67
+ result("read-2", "read"),
68
+ assistantWith("bash-1", "bash", { command: `printf "bad${String.fromCharCode(0)}${"x".repeat(220)}"` }),
69
+ result("bash-1", "bash"),
70
+ assistantWith("unknown-1", "write", { path: "out.txt" }),
71
+ result("unknown-1", "write"),
72
+ entry(
73
+ "ambiguous",
74
+ null,
75
+ assistant("Ambiguous", [
76
+ { type: "toolCall", id: "dup", name: "read", arguments: { path: "one.txt" } },
77
+ { type: "toolCall", id: "dup", name: "read", arguments: { path: "two.txt" } },
78
+ ]),
79
+ ),
80
+ entry("ambiguous-result", "ambiguous", {
81
+ role: "toolResult",
82
+ toolCallId: "dup",
83
+ toolName: "read",
84
+ content: [{ type: "text", text: "RESULT_BODY" }],
85
+ }),
86
+ ];
87
+ for (const source of entries)
88
+ state.authors.set(source.id, { profile: "executor", executionId: "e", assignmentId: "a" });
89
+ const sources = new Sources(entries, state);
90
+ assert.deepEqual(sources.locator("ctx:read-1-result"), {
91
+ callRef: "ctx:read-1",
92
+ toolCallId: "read-1",
93
+ });
94
+ assert.notEqual(sources.locator("ctx:read-1-result").callRef, sources.locator("ctx:read-2-result").callRef);
95
+ assert.deepEqual(sources.locator("ctx:bash-1-result"), {
96
+ callRef: "ctx:bash-1",
97
+ toolCallId: "bash-1",
98
+ });
99
+ assert.deepEqual(sources.locator("ctx:unknown-1-result"), {
100
+ callRef: "ctx:unknown-1",
101
+ toolCallId: "unknown-1",
102
+ });
103
+ assert.equal(sources.locator("ctx:ambiguous-result"), undefined);
104
+
105
+ const adapted = new Sources(entries, state, undefined, () => ({
106
+ label: `adapter${String.fromCharCode(0)}${"x".repeat(220)}`,
107
+ }));
108
+ const adaptedLocator = adapted.locator("ctx:bash-1-result");
109
+ assert.equal(adaptedLocator.truncated, true);
110
+ assert.ok(adaptedLocator.label.length <= 160);
111
+ assert.doesNotMatch(adaptedLocator.label, /\u0000/);
112
+ });
113
+
114
+ test("registered projection accepts a known eligible ref without inspection first", async () => {
115
+ await fixture((n, _body, manager) => {
116
+ if (n === 1) return call("freeflow_delegate", { operation: "assign", contract: "Read evidence and return." });
117
+ if (n === 2) return call("read", { path: "evidence.txt" });
118
+ if (n === 3) {
119
+ const ref = `ctx:${manager.getBranch().find((e) => e.message?.toolName === "read").id}`;
120
+ return call("freeflow_project", { operation: "add", refs: [ref] });
121
+ }
122
+ if (n === 4) {
123
+ const projects = manager.getBranch().filter((e) => e.message?.toolName === "freeflow_project");
124
+ assert.equal(projects.length, 1);
125
+ assert.deepEqual(
126
+ projects[0].message.details.items.map((item) => item.status),
127
+ ["added"],
128
+ );
129
+ assert.equal(projects[0].message.details.selected.length, 1);
130
+ return [
131
+ { name: "freeflow_return", args: { operation: "submit", report: "Evidence ready.", outcome: "completed" } },
132
+ ];
133
+ }
134
+ return [];
135
+ });
136
+ });
137
+
138
+ test("provenance covers old sources and complete exchanges, with stable prefixes and unchanged originals", () => {
139
+ const state = initialState();
140
+ const entries = Array.from({ length: 40 }, (_, i) =>
141
+ entry(`m${i}`, i ? `m${i - 1}` : null, assistant(`Observation ${i}`)),
142
+ );
143
+ for (const [i, e] of entries.entries())
144
+ state.authors.set(e.id, {
145
+ profile: i % 2 ? "executor" : "coordinator",
146
+ assignmentId: i % 2 ? "earlier-assignment" : undefined,
147
+ });
148
+ entries.push(entry("unknown", "m39", assistant("Imported observation")));
149
+ entries.push(
150
+ entry(
151
+ "calls",
152
+ "unknown",
153
+ assistant("Read observations", [{ type: "toolCall", id: "c", name: "read", arguments: { path: "x" } }]),
154
+ ),
155
+ );
156
+ entries.push(
157
+ entry("result", "calls", {
158
+ role: "toolResult",
159
+ toolCallId: "c",
160
+ toolName: "read",
161
+ content: [{ type: "text", text: "FULL_RESULT" }],
162
+ }),
163
+ );
164
+ state.authors.set("calls", { profile: "executor", assignmentId: "current" });
165
+ state.authors.set("result", { profile: "executor", assignmentId: "current" });
166
+ const before = JSON.stringify(entries);
167
+ const prepared = prepare(state, entries);
168
+ const annotations = prepared.messages.filter((m) => m.customType === "freeflow-routing-v2-refs");
169
+ for (let i = 0; i < 40; i++)
170
+ assert.ok(
171
+ annotations.some((m) => m.content.includes(`ctx:m${i} | producer: ${i % 2 ? "executor" : "coordinator"}`)),
172
+ );
173
+ assert.ok(annotations.some((m) => m.content.includes("ctx:unknown | producer: unknown/common")));
174
+ const callIndex = prepared.messages.findIndex((m) => m.role === "assistant" && m.content.some((b) => b.id === "c"));
175
+ assert.equal(prepared.messages[callIndex + 1].toolCallId, "c", "no annotation splits a call/result exchange");
176
+ assert.match(prepared.messages[callIndex + 2].content, /ctx:result.*producer: executor/);
177
+ const extended = prepare(state, [...entries, entry("later", "result", assistant("Later observation"))]);
178
+ assert.deepEqual(
179
+ extended.messages.slice(0, prepared.messages.length - 1),
180
+ prepared.messages.slice(0, -1),
181
+ "appends leave old provenance stable",
182
+ );
183
+ assert.equal(JSON.stringify(entries), before);
184
+ });
185
+
186
+ test("new control selections are rejected while old selections and mixed assistant text remain usable", () => {
187
+ const state = initialState();
188
+ const entries = [
189
+ entry(
190
+ "control",
191
+ null,
192
+ assistant("Substantive executor finding", [
193
+ { type: "toolCall", id: "c", name: "freeflow_project", arguments: { operation: "inspect" } },
194
+ ]),
195
+ ),
196
+ entry("receipt", "control", {
197
+ role: "toolResult",
198
+ toolCallId: "c",
199
+ toolName: "freeflow_project",
200
+ content: [{ type: "text", text: '{"status":"unchanged"}' }],
201
+ }),
202
+ ];
203
+ for (const e of entries) state.authors.set(e.id, { profile: "executor", assignmentId: "a" });
204
+ const sources = new Sources(entries, state);
205
+ for (const s of sources.byRef.values()) state.exposure.set(s.ref, s.hash);
206
+ const empty = { revision: 0, selected: [], unresolved: [], withdrawals: [] };
207
+ const attempted = changeSelection(
208
+ empty,
209
+ { operation: "add", refs: ["ctx:control", "ctx:receipt", "ctx:control#text"] },
210
+ sources,
211
+ state,
212
+ );
213
+ assert.deepEqual(attempted.selected, ["ctx:control#text"]);
214
+ assert.deepEqual(
215
+ attempted.unresolved.map((p) => p.code),
216
+ ["routing_source", "routing_source"],
217
+ );
218
+ assert.deepEqual(
219
+ attempted.unresolved.map((p) => p.detail),
220
+ [
221
+ "Routing control messages are not task evidence. Use ctx:control#text if its visible assistant text is the intended evidence.",
222
+ "Routing control receipts are not task evidence; saved communication is delivered separately.",
223
+ ],
224
+ );
225
+ const prior = { ...empty, revision: 1, selected: ["ctx:receipt"] };
226
+ assert.equal(changeSelection(prior, { operation: "add", refs: ["ctx:receipt"] }, sources, state), prior);
227
+ state.assignmentId = "a";
228
+ state.selections.set("a", attempted);
229
+ const projected = prepare(
230
+ state,
231
+ entries,
232
+ entries.map((e) => e.message),
233
+ "coordinator",
234
+ );
235
+ assert.ok(
236
+ projected.messages.some(
237
+ (m) => m.customType === "freeflow-routing-v2-refs" && m.content.includes("ctx:control#text | producer: executor"),
238
+ ),
239
+ );
240
+ assert.ok(projected.messages.some((m) => m.customType === "freeflow-assistant-text"));
241
+ state.selections.set("a", prior);
242
+ assert.ok(
243
+ prepare(
244
+ state,
245
+ entries,
246
+ entries.map((e) => e.message),
247
+ "coordinator",
248
+ ).messages.some((m) => m.toolName === "freeflow_project"),
249
+ );
250
+ });
251
+
252
+ test("restoration preserves full report metadata and deduplicates by accepted occurrence", () => {
253
+ const state = initialState();
254
+ state.profile = "coordinator";
255
+ state.assignmentId = "a";
256
+ state.assignments.set("a", { id: "a", state: "returned", contract: "CONTRACT", delegateHandoffId: "d" });
257
+ state.handoffs.set("d", { id: "d", kind: "delegate", toolCallId: "dc" });
258
+ const h = {
259
+ id: "h",
260
+ assignmentId: "a",
261
+ kind: "return",
262
+ state: "configured",
263
+ toolCallId: "rc",
264
+ text: "SAME_REPORT",
265
+ outcome: "partial",
266
+ limitations: ["UNRUN_INTEGRATION"],
267
+ reportRevision: 2,
268
+ };
269
+ state.handoffs.set("h", h);
270
+ state.assessment = { handoffId: "h", view: "active", problems: [] };
271
+ const restored = JSON.stringify(prepare(state, [], [], "coordinator").messages);
272
+ for (const value of ["SAME_REPORT", "UNRUN_INTEGRATION", "partial", "report:h:2", "assignment:a"])
273
+ assert.ok(restored.includes(value));
274
+ const old = {
275
+ role: "toolResult",
276
+ toolName: "freeflow_return",
277
+ toolCallId: "other",
278
+ content: [{ type: "text", text: JSON.stringify({ report: h.text, outcome: "completed", limitations: [] }) }],
279
+ };
280
+ const oldResult = prepare(state, [], [old], "coordinator");
281
+ assert.ok(
282
+ oldResult.messages.some(
283
+ (m) => m.customType === "freeflow-routing-communication" && m.content.includes("SAME_REPORT"),
284
+ ),
285
+ "equal text from another call cannot satisfy current report",
286
+ );
287
+ const incomplete = { ...old, toolCallId: "rc" };
288
+ const corrected = prepare(state, [], [incomplete], "coordinator");
289
+ assert.equal(JSON.stringify(corrected.messages).split("SAME_REPORT").length - 1, 1);
290
+ assert.ok(JSON.stringify(corrected.messages).includes("UNRUN_INTEGRATION"));
291
+ });
292
+
293
+ for (const projection of [false, true])
294
+ test(`native report arrives complete without inspection and survives close, projection ${projection}`, async () => {
295
+ await fixture((n, body, m) => {
296
+ if (n === 1) return call("freeflow_delegate", { operation: "assign", contract: "Return a partial report." });
297
+ if (n === 2)
298
+ return call("freeflow_return", {
299
+ operation: "submit",
300
+ report: "REPORT_DIRECT_91",
301
+ outcome: "partial",
302
+ limitations: ["LIMIT_DIRECT_92"],
303
+ });
304
+ if (n === 3) {
305
+ const r = receipt(m, "freeflow_return");
306
+ assert.equal(r.outcome, "partial");
307
+ assert.deepEqual(r.limitations, ["LIMIT_DIRECT_92"]);
308
+ assert.match(r.assignmentRef, /^assignment:/);
309
+ assert.match(r.reportRef, /^report:.*:1$/);
310
+ for (const text of ["REPORT_DIRECT_91", "LIMIT_DIRECT_92", r.reportRef])
311
+ assert.ok(JSON.stringify(body).includes(text));
312
+ return call("freeflow_unit", {
313
+ operation: "close",
314
+ outcome: "deferred",
315
+ assessment: "Partial result acknowledged.",
316
+ });
317
+ }
318
+ if (n === 4)
319
+ for (const text of ["REPORT_DIRECT_91", "LIMIT_DIRECT_92"]) assert.ok(JSON.stringify(body).includes(text));
320
+ return [];
321
+ }, projection);
322
+ });
323
+
324
+ test("native inspection snapshots distinguish page/scope totals, exclude controls and explain invalid work refs", async () => {
325
+ let first;
326
+ await fixture((n, body, m) => {
327
+ if (n === 1) return call("freeflow_delegate", { operation: "assign", contract: "Read observations." });
328
+ if (n === 2) return Array.from({ length: 35 }, () => ({ name: "read", args: { path: "evidence.txt" } }));
329
+ if (n === 3) return call("freeflow_project", { operation: "inspect", scope: "history" });
330
+ if (n === 4) {
331
+ first = receipt(m, "freeflow_project");
332
+ assert.equal(first.pageCounts.candidates, 30);
333
+ assert.ok(first.scopeCounts.candidates > 30);
334
+ assert.equal(first.scopeCounts.eligible, first.scopeCounts.candidates);
335
+ assert.ok(first.candidates.every((c) => c.producer === "executor" && !c.toolName?.startsWith("freeflow_")));
336
+ return call("freeflow_project", { operation: "inspect", scope: "history", cursor: first.nextCursor });
337
+ }
338
+ if (n === 5) {
339
+ const second = receipt(m, "freeflow_project");
340
+ assert.deepEqual(second.scopeCounts, first.scopeCounts);
341
+ assert.equal(
342
+ new Set([...first.candidates, ...second.candidates].map((c) => c.ref)).size,
343
+ first.scopeCounts.candidates,
344
+ );
345
+ return call("freeflow_return", { operation: "submit", report: "Done", outcome: "completed" });
346
+ }
347
+ if (n === 6)
348
+ return call("freeflow_unit", {
349
+ operation: "inspect",
350
+ view: "detail",
351
+ ref: receipt(m, "freeflow_return").handoff,
352
+ });
353
+ if (n === 7) {
354
+ assert.equal(receipt(m, "freeflow_unit").code, "invalid_work_ref");
355
+ return call("freeflow_unit", {
356
+ operation: "inspect",
357
+ view: "detail",
358
+ ref: receipt(m, "freeflow_return").assignmentRef,
359
+ });
360
+ }
361
+ if (n === 8) {
362
+ assert.equal(receipt(m, "freeflow_unit").report, "Done");
363
+ return call("freeflow_unit", { operation: "close", outcome: "accepted", assessment: "Read work assessed." });
364
+ }
365
+ return [];
366
+ });
367
+ });
368
+
369
+ test("native compaction restores report limitations and revision into the same assessment", async () => {
370
+ let saved;
371
+ await fixture(
372
+ (n, body, m) => {
373
+ if (n === 1)
374
+ return call("freeflow_delegate", { operation: "assign", contract: "Return a report for assessment." });
375
+ if (n === 2)
376
+ return call("freeflow_return", {
377
+ operation: "submit",
378
+ report: "COMPACT_REPORT_61",
379
+ outcome: "partial",
380
+ limitations: ["COMPACT_LIMIT_62"],
381
+ });
382
+ if (n === 3) {
383
+ saved = receipt(m, "freeflow_return");
384
+ return [];
385
+ }
386
+ if (n === 4) return call("freeflow_unit", { operation: "assess" });
387
+ if (n === 5) {
388
+ const wire = JSON.stringify(body);
389
+ for (const value of ["COMPACT_REPORT_61", "COMPACT_LIMIT_62", "partial", saved.assignmentRef, saved.reportRef])
390
+ assert.ok(wire.includes(value), value);
391
+ }
392
+ return [];
393
+ },
394
+ true,
395
+ async ({ session }) => {
396
+ await session.compact();
397
+ await session.prompt("Resume assessment of the existing saved report.");
398
+ await session.waitForIdle();
399
+ },
400
+ );
401
+ });
402
+
403
+ test("native retry delivers the unchanged complete report without a follow-up inspect", async () => {
404
+ let saved;
405
+ await fixture((n, body, m) => {
406
+ if (n === 1) return call("freeflow_delegate", { operation: "assign", contract: "Return available findings." });
407
+ if (n === 2) return call("freeflow_project", { operation: "add", refs: ["ctx:unavailable"] });
408
+ if (n === 3)
409
+ return call("freeflow_return", {
410
+ operation: "submit",
411
+ report: "RETRY_REPORT_11",
412
+ outcome: "partial",
413
+ limitations: ["RETRY_LIMIT_12"],
414
+ });
415
+ if (n === 4) {
416
+ saved = receipt(m, "freeflow_return");
417
+ assert.equal(saved.ready, false);
418
+ return call("freeflow_project", {
419
+ operation: "remove",
420
+ refs: ["ctx:unavailable"],
421
+ reason: "Original capture unavailable; retain limitation.",
422
+ });
423
+ }
424
+ if (n === 5) return call("freeflow_return", { operation: "retry" });
425
+ if (n === 6) {
426
+ const retried = receipt(m, "freeflow_return");
427
+ for (const key of ["report", "reportRef", "reportRevision", "assignmentRef", "outcome", "limitations"])
428
+ assert.deepEqual(retried[key], saved[key]);
429
+ assert.equal(retried.reportUnchanged, true);
430
+ for (const value of ["RETRY_REPORT_11", "RETRY_LIMIT_12", saved.reportRef])
431
+ assert.ok(JSON.stringify(body).includes(value));
432
+ return call("freeflow_unit", { operation: "close", outcome: "deferred", assessment: "Partial report assessed." });
433
+ }
434
+ return [];
435
+ });
436
+ });
437
+
438
+ test("empty new assignment advertises earlier evidence and selected receipts retain its producer", async () => {
439
+ let ref, oldAssignment;
440
+ await fixture((n, body, m) => {
441
+ if (n === 1 || n === 5)
442
+ return call("freeflow_delegate", {
443
+ operation: "assign",
444
+ contract: n === 1 ? "Read an observation." : "Reuse the earlier observation.",
445
+ });
446
+ if (n === 2) return call("read", { path: "evidence.txt" });
447
+ if (n === 3) {
448
+ ref = `ctx:${m.getBranch().find((e) => e.message?.toolName === "read").id}`;
449
+ return call("freeflow_return", { operation: "submit", report: "Observation captured.", outcome: "completed" });
450
+ }
451
+ if (n === 4)
452
+ return call("freeflow_unit", { operation: "close", outcome: "accepted", assessment: "Supported result." });
453
+ if (n === 6) return call("freeflow_project", { operation: "inspect" });
454
+ if (n === 7) {
455
+ const r = receipt(m, "freeflow_project");
456
+ assert.equal(r.count, 0);
457
+ assert.ok(r.otherAssignments > 0);
458
+ assert.match(r.historyHint, /scope history/);
459
+ return call("freeflow_project", { operation: "inspect", scope: "history" });
460
+ }
461
+ if (n === 8) {
462
+ const candidate = receipt(m, "freeflow_project").candidates.find((c) => c.ref === ref);
463
+ assert.equal(candidate.producer, "executor");
464
+ assert.equal(candidate.eligible, true);
465
+ oldAssignment = candidate.assignment;
466
+ return call("freeflow_project", { operation: "add", refs: [ref] });
467
+ }
468
+ if (n === 9) {
469
+ const selected = receipt(m, "freeflow_project").evidence.selected[0];
470
+ assert.equal(selected.producer, "executor");
471
+ assert.equal(selected.assignment, oldAssignment);
472
+ return call("freeflow_return", {
473
+ operation: "submit",
474
+ report: "Earlier evidence selected.",
475
+ outcome: "completed",
476
+ });
477
+ }
478
+ if (n === 10) {
479
+ const wire = JSON.stringify(body);
480
+ assert.ok(wire.includes(`${ref} | producer: executor`));
481
+ assert.ok(wire.includes("EXACT_EVIDENCE_BODY_81"));
482
+ return call("freeflow_unit", { operation: "inspect", view: "detail", ref: "assignment:absent" });
483
+ }
484
+ if (n === 11) {
485
+ assert.equal(receipt(m, "freeflow_unit").code, "work_unavailable");
486
+ return call("freeflow_unit", { operation: "close", outcome: "accepted", assessment: "Supported result." });
487
+ }
488
+ return [];
489
+ });
490
+ });
491
+
492
+ test("normal inspection hides delivery gaps while selected inspection preserves their diagnostics", async () => {
493
+ let ref;
494
+ const result = await fixture(
495
+ (n, body, m) => {
496
+ if (n === 1) return call("freeflow_delegate", { operation: "assign", contract: "Read and inspect evidence." });
497
+ if (n === 2) return call("read", { path: "evidence.txt" });
498
+ if (n === 3) {
499
+ ref = `ctx:${m.getBranch().find((e) => e.message?.content?.some?.((b) => b.type === "toolCall" && b.name === "read")).id}`;
500
+ return call("freeflow_project", { operation: "inspect", scope: "assignment" });
501
+ }
502
+ if ([4, 5, 6].includes(n)) {
503
+ const r = receipt(m, "freeflow_project");
504
+ assert.ok(r.candidates.every((c) => c.eligible && c.targetReady));
505
+ assert.ok(r.candidates.every((c) => !Object.hasOwn(c, "preview")));
506
+ assert.ok(!JSON.stringify(r).includes("EXACT_EVIDENCE_BODY_81"), "inspection does not repeat source text");
507
+ assert.equal(
508
+ r.candidates.find((c) => c.ref === ref),
509
+ undefined,
510
+ "incompatible whole message is not a candidate",
511
+ );
512
+ assert.ok(
513
+ r.candidates.some((c) => c.ref === `${ref}#text`),
514
+ "usable visible text remains offered",
515
+ );
516
+ assert.equal(r.scopeCounts.candidates, r.scopeCounts.targetReady);
517
+ assert.equal(r.pageCounts.candidates, r.pageCounts.targetReady);
518
+ if (n === 4) return call("freeflow_project", { operation: "inspect", scope: "active" });
519
+ if (n === 5) return call("freeflow_project", { operation: "inspect", scope: "history" });
520
+ return call("freeflow_project", { operation: "add", refs: [ref] });
521
+ }
522
+ if (n === 7) {
523
+ const r = receipt(m, "freeflow_project");
524
+ assert.equal(r.ready, false);
525
+ assert.ok(r.problems.some((p) => p.code === "target_representation"));
526
+ return call("freeflow_project", { operation: "inspect", scope: "selected" });
527
+ }
528
+ if (n === 8) {
529
+ const r = receipt(m, "freeflow_project");
530
+ assert.deepEqual(r.selected, [ref]);
531
+ const selected = r.candidates.find((c) => c.ref === ref);
532
+ assert.equal(Object.hasOwn(selected, "preview"), false);
533
+ assert.equal(selected.targetReady, false);
534
+ assert.ok(selected.limitations.some((p) => p.code === "target_representation"));
535
+ assert.equal(r.scopeCounts.candidates, 1);
536
+ assert.equal(r.scopeCounts.targetReady, 0);
537
+ }
538
+ return [];
539
+ },
540
+ true,
541
+ undefined,
542
+ true,
543
+ {
544
+ extensions: [
545
+ (pi) =>
546
+ pi.on("message_end", (e) => {
547
+ if (
548
+ e.message.role === "assistant" &&
549
+ e.message.content.some((b) => b.type === "toolCall" && b.name === "read")
550
+ )
551
+ return {
552
+ message: {
553
+ ...e.message,
554
+ content: [
555
+ ...e.message.content,
556
+ {
557
+ type: "thinking",
558
+ thinking: "Fixture signed reasoning",
559
+ thinkingSignature: JSON.stringify({
560
+ type: "reasoning",
561
+ id: "rs_candidate",
562
+ summary: [{ type: "summary_text", text: "Fixture signed reasoning" }],
563
+ }),
564
+ },
565
+ ],
566
+ },
567
+ };
568
+ }),
569
+ ],
570
+ },
571
+ );
572
+ assert.deepEqual(
573
+ [...result.state.selections.values()].at(-1).selected,
574
+ [ref],
575
+ "inspection does not silently withdraw evidence",
576
+ );
577
+ });