@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
@@ -6,11 +6,12 @@ import { tmpdir } from "node:os";
6
6
  import test from "node:test";
7
7
 
8
8
  import freeflowExtension from "../../dist/index.js";
9
- import { PIFLOW_HOST } from "../cognitive-routing/host-fixture.js";
9
+ import { PIFLOW_HOST } from "../fixtures/pi-host.js";
10
10
 
11
- function context(cwd) {
11
+ function context(cwd, systemPrompt = "") {
12
12
  return {
13
13
  cwd,
14
+ getSystemPrompt: () => systemPrompt,
14
15
  sessionManager: {
15
16
  getEntries: () => [],
16
17
  getBranch: () => [],
@@ -65,20 +66,55 @@ function lastRuntimeState(messages) {
65
66
  }
66
67
 
67
68
  test("re-entry recovery is stable and capability-neutral", async () => {
68
- const [core, cognitiveRouting, conversationHistory] = await Promise.all([
69
+ const [core, cognitiveRouting, conversationHistory, routingSkill] = await Promise.all([
69
70
  readFile(join(process.cwd(), "runtime", "prompts", "core.md"), "utf8"),
70
71
  readFile(join(process.cwd(), "runtime", "prompts", "cognitive-routing.md"), "utf8"),
71
72
  readFile(join(process.cwd(), "runtime", "prompts", "conversation-history.md"), "utf8"),
73
+ readFile(join(process.cwd(), "capabilities", "cognitive-routing", "SKILL.md"), "utf8"),
72
74
  ]);
73
75
 
76
+ assert.match(core, /## Load The Selected Method/);
77
+ assert.match(core, /Before applying a selected skill, read its current body/);
74
78
  assert.match(core, /## Recover After Context Loss/);
75
79
  assert.match(core, /latest Freeflow Runtime State/);
76
- assert.doesNotMatch(cognitiveRouting, /When Cognitive Routing is active and its skill is absent/);
77
- assert.match(cognitiveRouting, /A Runtime State refresh is host context, not a user interruption/);
80
+ assert.match(core, /recovery is not a bypass/);
81
+ assert.match(core, /evidence of prior approval; check that the approval still applies/);
82
+ assert.match(core, /choose or retain the current owner/);
83
+ assert.match(core, /apply its method and gather or produce evidence/);
84
+ assert.match(core, /read the complete `full` record/);
85
+ assert.match(core, /Make material gaps, contradictions, deferrals, and user-owned decisions explicit/);
86
+ assert.match(cognitiveRouting, /Before relying on Automatic routing, read the complete cognitive-routing skill/);
87
+ assert.match(cognitiveRouting, /This bootstrap read is the only environment call/);
88
+ assert.match(cognitiveRouting, /If unavailable, stop and report the missing method/);
89
+ // The approved compact cue keeps bootstrap/control boundaries; the loaded
90
+ // method owns detailed role and handoff policy. Keep both obligations checked.
78
91
  assert.match(
79
92
  cognitiveRouting,
80
- /`Control` and `Profile` in the latest Runtime State report current compute state, not Yield, Delegate, `ACT_BOUNDED`, or boundary state/,
93
+ /Under Manual control, the held Coordinator, Helper, or Executor follows ordinary unsplit Workflow/,
81
94
  );
95
+ assert.match(cognitiveRouting, /When routing is inactive, stop applying this split/);
96
+ assert.match(
97
+ cognitiveRouting,
98
+ /Use the latest Runtime State for control, profile, delegation mode, and current responsibility/,
99
+ );
100
+ assert.match(cognitiveRouting, /configured profiles in one agent\/session/);
101
+ assert.match(routingSkill, /Automatic role restrictions, delegation, and projection do not apply/);
102
+ assert.match(routingSkill, /Put the actual contract in `freeflow_delegate/);
103
+ assert.match(routingSkill, /A worker return ends its ordinary task work/);
104
+ assert.match(cognitiveRouting, /project every completed skill and instructional-reference read/);
105
+ assert.match(
106
+ routingSkill,
107
+ /For the current returned assessment, Coordinator uses `freeflow_unit\(operation: "recover"\)`/,
108
+ );
109
+ assert.match(routingSkill, /freeflow_unit\(operation: "recover"\)/);
110
+ assert.match(
111
+ routingSkill,
112
+ /Preserve the unit, assignment, original report, outcome, revision, selections, and assessment/,
113
+ );
114
+ assert.match(routingSkill, /read only exact admitted task paths/);
115
+ assert.match(routingSkill, /freeflow_return\(operation: "supplement"\)/);
116
+ assert.match(routingSkill, /Deliver the supplement or cancel recovery before `assess`/);
117
+ assert.match(routingSkill, /Do not simulate recovery by replacing an assignment\/report or opening a new unit/);
82
118
  assert.doesNotMatch(
83
119
  conversationHistory,
84
120
  /Current user direction, live source truth, and present runtime state remain authoritative/,
@@ -98,8 +134,10 @@ test("composes the mandatory core fragments, optional capabilities, discovery, a
98
134
  const order = [
99
135
  "# Freeflow Stable Guidance",
100
136
  "## Shared Terms",
137
+ "## Load The Selected Method",
101
138
  "## Recover After Context Loss",
102
139
  "## Three Nested Loops",
140
+ "## Evidence And Judgment",
103
141
  "## Workflow Cue",
104
142
  "## Action Selection Cue",
105
143
  "## Supported Exit",
@@ -112,6 +150,18 @@ test("composes the mandatory core fragments, optional capabilities, discovery, a
112
150
  order,
113
151
  [...order].sort((a, b) => a - b),
114
152
  );
153
+ assert.match(
154
+ prompt,
155
+ /Before applying a selected skill, read its current body when its exact method is absent from context/,
156
+ );
157
+ assert.match(prompt, /Verification establishes what direct evidence proves at the observed boundary/);
158
+ assert.match(
159
+ prompt,
160
+ /Review judges whether work or an artifact is aligned, correct, suitable, and sufficiently evidenced/,
161
+ );
162
+ assert.match(prompt, /Interpret requested intent, not sentence form/);
163
+ assert.match(prompt, /answer the question before any action that depends on it/);
164
+ assert.match(prompt, /An unresolved question does not automatically suspend independent, clearly authorized work/);
115
165
  assert.doesNotMatch(prompt, /## Mode\b|strict-workflow|conversation mode|workflow mode/);
116
166
  assert.doesNotMatch(prompt, /Skills prompt/);
117
167
  assert.doesNotMatch(prompt, /# Workflow\n/);
@@ -124,6 +174,8 @@ test("composes the mandatory core fragments, optional capabilities, discovery, a
124
174
  assert.match(runtimeState.content, /Context Virtualization: active/);
125
175
  assert.match(runtimeState.content, /Conversation History: active/);
126
176
  assert.match(runtimeState.content, /Cognitive Routing: inactive/);
177
+ assert.match(runtimeState.content, /Control: `unavailable`/);
178
+ assert.match(runtimeState.content, /Projection: `disabled`/);
127
179
  assert.doesNotMatch(runtimeState.content, /Default mode|Active mode|Interaction Contract|Skills/);
128
180
 
129
181
  const resources = await handlers.get("resources_discover")({ cwd }, ctx);
@@ -137,6 +189,46 @@ test("composes the mandatory core fragments, optional capabilities, discovery, a
137
189
  }
138
190
  });
139
191
 
192
+ test("subagents retain reference definitions while optional capabilities stay inactive", async () => {
193
+ const cwd = await configuredRepo({
194
+ contextVirtualization: true,
195
+ conversationHistory: true,
196
+ cognitiveRouting: {
197
+ enabled: true,
198
+ profiles: {
199
+ coordinator: { provider: "test", model: "coordinator", thinking: "high" },
200
+ executor: { provider: "test", model: "executor", thinking: "max" },
201
+ },
202
+ },
203
+ });
204
+ try {
205
+ const { handlers, activeToolNames } = loadExtension(freeflowExtension, null);
206
+ const ctx = context(cwd, "<!-- freeflow-subagent-capabilities: disabled -->");
207
+ await handlers.get("session_start")({ type: "session_start" }, ctx);
208
+
209
+ const before = await handlers.get("before_agent_start")({ systemPrompt: "base prompt" }, ctx);
210
+ assert.match(before.systemPrompt, /# Freeflow Stable Guidance/);
211
+ assert.match(before.systemPrompt, /# Freeflow Interaction Contract/);
212
+ assert.match(before.systemPrompt, /## Cognitive Routing Cue/);
213
+ assert.match(before.systemPrompt, /## Context Virtualization Cue/);
214
+ assert.match(before.systemPrompt, /## Conversation History Cue/);
215
+
216
+ const resources = await handlers.get("resources_discover")({ cwd }, ctx);
217
+ assert.ok(resources.skillPaths.some((path) => path.endsWith("/skills/action-selection/SKILL.md")));
218
+ assert.equal(resources.skillPaths.filter((path) => path.includes("/capabilities/")).length, 3);
219
+
220
+ const providerContext = await handlers.get("context")({ messages: [] }, ctx);
221
+ const runtimeState = lastRuntimeState(providerContext.messages);
222
+ assert.match(runtimeState.content, /Freeflow: active/);
223
+ assert.match(runtimeState.content, /Context Virtualization: inactive/);
224
+ assert.match(runtimeState.content, /Conversation History: inactive/);
225
+ assert.match(runtimeState.content, /Cognitive Routing: inactive/);
226
+ assert.ok(activeToolNames().includes("freeflow_context"));
227
+ } finally {
228
+ await rm(cwd, { recursive: true, force: true });
229
+ }
230
+ });
231
+
140
232
  test("provider context reuses the before-agent surface until the next provider turn", async () => {
141
233
  const cwd = await configuredRepo({ contextVirtualization: true });
142
234
  try {
@@ -152,7 +244,7 @@ test("provider context reuses the before-agent surface until the next provider t
152
244
  assert.match(lastRuntimeState(sameTurn.messages).content, /Context Virtualization: active/);
153
245
 
154
246
  const next = await handlers.get("before_agent_start")({ systemPrompt: "base prompt" }, ctx);
155
- assert.doesNotMatch(next.systemPrompt, /## Context Virtualization Cue/);
247
+ assert.equal(next.systemPrompt, before.systemPrompt, "settings preserve the complete reference surface");
156
248
  assert.match(next.systemPrompt, /# Freeflow Interaction Contract/);
157
249
  assert.match(next.systemPrompt, /## Shared Terms/);
158
250
  } finally {
@@ -197,7 +289,7 @@ test("missing optional prompt fragments preserve the mandatory core surface", as
197
289
  }
198
290
  });
199
291
 
200
- test("missing child prompt removes only that capability from every model-facing surface", async () => {
292
+ test("missing child prompt marks that capability unavailable while retaining its definitions", async () => {
201
293
  const root = await mkdtemp(join(tmpdir(), "freeflow-child-prompt-failure-"));
202
294
  const cwd = await configuredRepo({ contextVirtualization: true });
203
295
  try {
@@ -217,13 +309,13 @@ test("missing child prompt removes only that capability from every model-facing
217
309
  assert.doesNotMatch(before.systemPrompt, /## Context Virtualization Cue/);
218
310
  assert.match(before.systemPrompt, /# Freeflow Interaction Contract/);
219
311
  const resources = await handlers.get("resources_discover")({ cwd }, ctx);
220
- assert.ok(!resources.skillPaths.some((path) => path.endsWith("/capabilities/context-virtualization/SKILL.md")));
221
- assert.ok(!activeToolNames().includes("freeflow_context"));
312
+ assert.ok(resources.skillPaths.some((path) => path.endsWith("/capabilities/context-virtualization/SKILL.md")));
313
+ assert.ok(activeToolNames().includes("freeflow_context"));
222
314
 
223
315
  await handlers.get("session_tree")({}, ctx);
224
- assert.ok(!activeToolNames().includes("freeflow_context"));
316
+ assert.ok(activeToolNames().includes("freeflow_context"));
225
317
  await handlers.get("session_compact")({}, ctx);
226
- assert.ok(!activeToolNames().includes("freeflow_context"));
318
+ assert.ok(activeToolNames().includes("freeflow_context"));
227
319
 
228
320
  const providerContext = await handlers.get("context")({ messages: [] }, ctx);
229
321
  assert.match(providerContext.messages.at(-1).content, /Context Virtualization: unavailable/);
@@ -233,9 +325,9 @@ test("missing child prompt removes only that capability from every model-facing
233
325
  assert.doesNotMatch(whitespaceBefore.systemPrompt, /## Context Virtualization Cue/);
234
326
  const whitespaceResources = await handlers.get("resources_discover")({ cwd }, ctx);
235
327
  assert.ok(
236
- !whitespaceResources.skillPaths.some((path) => path.endsWith("/capabilities/context-virtualization/SKILL.md")),
328
+ whitespaceResources.skillPaths.some((path) => path.endsWith("/capabilities/context-virtualization/SKILL.md")),
237
329
  );
238
- assert.ok(!activeToolNames().includes("freeflow_context"));
330
+ assert.ok(activeToolNames().includes("freeflow_context"));
239
331
  const whitespaceContext = await handlers.get("context")({ messages: [] }, ctx);
240
332
  assert.match(whitespaceContext.messages.at(-1).content, /Context Virtualization: unavailable/);
241
333
  } finally {
@@ -244,7 +336,7 @@ test("missing child prompt removes only that capability from every model-facing
244
336
  }
245
337
  });
246
338
 
247
- test("missing mandatory Interaction Contract preserves the host prompt and hides base skills", async () => {
339
+ test("missing mandatory Interaction Contract retains a dormant surface with unavailable state", async () => {
248
340
  const root = await mkdtemp(join(tmpdir(), "freeflow-interaction-contract-failure-"));
249
341
  const cwd = await configuredRepo();
250
342
  try {
@@ -261,19 +353,21 @@ test("missing mandatory Interaction Contract preserves the host prompt and hides
261
353
  const { handlers, activeToolNames } = loadExtension(extension);
262
354
  const ctx = context(cwd);
263
355
  await handlers.get("session_start")({}, ctx);
264
- assert.ok(!activeToolNames().includes("freeflow_context"));
356
+ assert.ok(activeToolNames().includes("freeflow_context"));
265
357
  const before = await handlers.get("before_agent_start")({ systemPrompt: "base prompt" }, ctx);
266
- assert.equal(before.systemPrompt, "base prompt");
358
+ assert.ok(before.systemPrompt.startsWith("base prompt\n\n"));
359
+ assert.match(before.systemPrompt, /guidance is dormant/);
267
360
  const resources = await handlers.get("resources_discover")({ cwd }, ctx);
268
- assert.deepEqual(resources.skillPaths, []);
361
+ assert.equal(resources.skillPaths.length, 27);
269
362
  await handlers.get("session_tree")({}, ctx);
270
- assert.ok(!activeToolNames().includes("freeflow_context"));
363
+ assert.ok(activeToolNames().includes("freeflow_context"));
271
364
 
272
365
  await writeFile(join(root, "runtime", "prompts", "interaction-contract.md"), " \n\t", "utf8");
273
366
  const whitespaceBefore = await handlers.get("before_agent_start")({ systemPrompt: "base prompt" }, ctx);
274
- assert.equal(whitespaceBefore.systemPrompt, "base prompt");
367
+ assert.ok(whitespaceBefore.systemPrompt.startsWith("base prompt\n\n"));
368
+ assert.match(whitespaceBefore.systemPrompt, /guidance is dormant/);
275
369
  const whitespaceResources = await handlers.get("resources_discover")({ cwd }, ctx);
276
- assert.deepEqual(whitespaceResources.skillPaths, []);
370
+ assert.equal(whitespaceResources.skillPaths.length, 27);
277
371
  const whitespaceContext = await handlers.get("context")({ messages: [] }, ctx);
278
372
  assert.match(whitespaceContext.messages.at(-1).content, /Freeflow: unavailable/);
279
373
  } finally {
@@ -282,7 +376,7 @@ test("missing mandatory Interaction Contract preserves the host prompt and hides
282
376
  }
283
377
  });
284
378
 
285
- test("missing mandatory core prompt preserves the host prompt and hides base skills", async () => {
379
+ test("missing mandatory core prompt retains a dormant surface with unavailable state", async () => {
286
380
  const root = await mkdtemp(join(tmpdir(), "freeflow-core-prompt-failure-"));
287
381
  const cwd = await configuredRepo({ contextVirtualization: true });
288
382
  try {
@@ -299,9 +393,10 @@ test("missing mandatory core prompt preserves the host prompt and hides base ski
299
393
  await handlers.get("session_start")({ type: "session_start" }, ctx);
300
394
 
301
395
  const before = await handlers.get("before_agent_start")({ systemPrompt: "base prompt" }, ctx);
302
- assert.equal(before.systemPrompt, "base prompt");
396
+ assert.ok(before.systemPrompt.startsWith("base prompt\n\n"));
397
+ assert.match(before.systemPrompt, /guidance is dormant/);
303
398
  const resources = await handlers.get("resources_discover")({ cwd }, ctx);
304
- assert.deepEqual(resources.skillPaths, []);
399
+ assert.equal(resources.skillPaths.length, 27);
305
400
  const providerContext = await handlers.get("context")({ messages: [] }, ctx);
306
401
  assert.match(providerContext.messages.at(-1).content, /Freeflow: unavailable/);
307
402
  assert.doesNotMatch(
@@ -310,15 +405,16 @@ test("missing mandatory core prompt preserves the host prompt and hides base ski
310
405
  );
311
406
 
312
407
  await handlers.get("session_tree")({}, ctx);
313
- assert.ok(!activeToolNames().includes("freeflow_context"));
408
+ assert.ok(activeToolNames().includes("freeflow_context"));
314
409
  await handlers.get("session_compact")({}, ctx);
315
- assert.ok(!activeToolNames().includes("freeflow_context"));
410
+ assert.ok(activeToolNames().includes("freeflow_context"));
316
411
 
317
412
  await writeFile(join(root, "runtime", "prompts", "core.md"), " \n\t", "utf8");
318
413
  const whitespaceBefore = await handlers.get("before_agent_start")({ systemPrompt: "base prompt" }, ctx);
319
- assert.equal(whitespaceBefore.systemPrompt, "base prompt");
414
+ assert.ok(whitespaceBefore.systemPrompt.startsWith("base prompt\n\n"));
415
+ assert.match(whitespaceBefore.systemPrompt, /guidance is dormant/);
320
416
  const whitespaceResources = await handlers.get("resources_discover")({ cwd }, ctx);
321
- assert.deepEqual(whitespaceResources.skillPaths, []);
417
+ assert.equal(whitespaceResources.skillPaths.length, 27);
322
418
  const whitespaceContext = await handlers.get("context")({ messages: [] }, ctx);
323
419
  assert.match(whitespaceContext.messages.at(-1).content, /Freeflow: unavailable/);
324
420
  } finally {
@@ -333,7 +429,8 @@ test("Runtime State remains present while Freeflow is disabled without optional
333
429
  const { handlers } = loadExtension();
334
430
  const ctx = context(cwd);
335
431
  const before = await handlers.get("before_agent_start")({ systemPrompt: "base prompt" }, ctx);
336
- assert.equal(before.systemPrompt, "base prompt");
432
+ assert.ok(before.systemPrompt.startsWith("base prompt\n\n"));
433
+ assert.match(before.systemPrompt, /guidance is dormant/);
337
434
  assert.equal(before.message, undefined);
338
435
 
339
436
  const providerContext = await handlers.get("context")({ messages: [] }, ctx);
@@ -343,7 +440,7 @@ test("Runtime State remains present while Freeflow is disabled without optional
343
440
  assert.doesNotMatch(runtimeState.content, /Default mode|Active mode|Interaction Contract|Skills/);
344
441
 
345
442
  const resources = await handlers.get("resources_discover")({ cwd }, ctx);
346
- assert.deepEqual(resources.skillPaths, []);
443
+ assert.equal(resources.skillPaths.length, 27);
347
444
  } finally {
348
445
  await rm(cwd, { recursive: true, force: true });
349
446
  }
@@ -8,7 +8,7 @@ import { promisify } from "node:util";
8
8
 
9
9
  import freeflowExtension from "../../dist/index.js";
10
10
  import { resetSessionOverrides, setSessionCoreOverride } from "../../dist/runtime/runtime-context.js";
11
- import { PIFLOW_HOST } from "../cognitive-routing/host-fixture.js";
11
+ import { PIFLOW_HOST } from "../fixtures/pi-host.js";
12
12
 
13
13
  const execFileAsync = promisify(execFile);
14
14
 
@@ -51,12 +51,12 @@ function context(cwd, options = {}) {
51
51
  };
52
52
  }
53
53
 
54
- function loadExtension() {
54
+ function loadExtension(host = PIFLOW_HOST, runtimeApi = {}) {
55
55
  const handlers = new Map();
56
56
  const commands = [];
57
57
  const tools = [];
58
58
  const pi = {
59
- host: PIFLOW_HOST,
59
+ host,
60
60
  registerTool(tool) {
61
61
  tools.push(tool);
62
62
  },
@@ -77,6 +77,7 @@ function loadExtension() {
77
77
  },
78
78
  setActiveTools() {},
79
79
  };
80
+ Object.assign(pi, runtimeApi);
80
81
  freeflowExtension(pi);
81
82
  return { handlers, commands, pi };
82
83
  }
@@ -263,14 +264,22 @@ test("Cognitive Routing preset cancellation preserves the previous repository va
263
264
  cognitiveRouting: {
264
265
  enabled: true,
265
266
  profiles: {
266
- standard: { provider: "test", model: "model-a", thinkingLevel: "low" },
267
- reasoning: { provider: "test", model: "model-b", thinkingLevel: "high" },
267
+ coordinator: { provider: "test", model: "model-a", thinking: "low" },
268
+ executor: { provider: "test", model: "model-b", thinking: "high" },
268
269
  },
269
270
  },
270
271
  });
271
272
  const original = await readFile(join(cwd, ".freeflow/config.json"), "utf8");
272
273
  try {
273
- const { commands } = loadExtension();
274
+ const { commands } = loadExtension(
275
+ {},
276
+ {
277
+ setModel() {
278
+ return true;
279
+ },
280
+ setThinkingLevel() {},
281
+ },
282
+ );
274
283
  const command = freeflowCommand(commands);
275
284
  const settings = context(cwd, { isIdle: () => true });
276
285
  settings.modelRegistry = cognitiveRoutingModelRegistry();
@@ -281,7 +290,7 @@ test("Cognitive Routing preset cancellation preserves the previous repository va
281
290
  });
282
291
  component.handleInput("\u001b[B");
283
292
  component.handleInput("\r");
284
- component.handleInput("\u001b[B");
293
+ for (let index = 0; index < 4; index += 1) component.handleInput("\u001b[B");
285
294
  component.handleInput("\r");
286
295
  component.handleInput("\u001b");
287
296
  component.handleInput("\u001b");
@@ -300,13 +309,21 @@ test("Cognitive Routing settings refresh after enabling the capability", async (
300
309
  cognitiveRouting: {
301
310
  enabled: false,
302
311
  profiles: {
303
- standard: { provider: "test", model: "model-a", thinkingLevel: "low" },
304
- reasoning: { provider: "test", model: "model-b", thinkingLevel: "high" },
312
+ coordinator: { provider: "test", model: "model-a", thinking: "low" },
313
+ executor: { provider: "test", model: "model-b", thinking: "high" },
305
314
  },
306
315
  },
307
316
  });
308
317
  try {
309
- const { commands } = loadExtension();
318
+ const { commands } = loadExtension(
319
+ {},
320
+ {
321
+ setModel() {
322
+ return true;
323
+ },
324
+ setThinkingLevel() {},
325
+ },
326
+ );
310
327
  const command = freeflowCommand(commands);
311
328
  const settings = context(cwd, { isIdle: () => true });
312
329
  settings.modelRegistry = cognitiveRoutingModelRegistry();
@@ -322,13 +339,63 @@ test("Cognitive Routing settings refresh after enabling the capability", async (
322
339
  component.handleInput("\r");
323
340
  await component.waitForWrites();
324
341
  component.handleInput("\u001b");
325
- assert.match(component.render(120).join("\n"), /Cognitive Routing\s+enabled \(5\) configured/);
342
+ assert.match(component.render(120).join("\n"), /Cognitive Routing\s+enabled \(6\) configured/);
343
+ component.handleInput("\u001b");
344
+ return result;
345
+ };
346
+ await command.definition.handler("settings repo", settings);
347
+ const saved = JSON.parse(await readFile(join(cwd, ".freeflow/config.json"), "utf8"));
348
+ assert.equal(saved.cognitiveRouting.enabled, true);
349
+ assert.equal(settings.reloads.length, 1);
350
+ } finally {
351
+ await rm(cwd, { recursive: true, force: true });
352
+ }
353
+ });
354
+
355
+ test("Cognitive Routing settings can disable projection without disabling routing", async () => {
356
+ const cwd = await configuredRepo({
357
+ cognitiveRouting: {
358
+ enabled: true,
359
+ projection: true,
360
+ profiles: {
361
+ coordinator: { provider: "test", model: "model-a", thinking: "low" },
362
+ executor: { provider: "test", model: "model-b", thinking: "high" },
363
+ },
364
+ },
365
+ });
366
+ try {
367
+ const { commands } = loadExtension(
368
+ {},
369
+ {
370
+ setModel() {
371
+ return true;
372
+ },
373
+ setThinkingLevel() {},
374
+ },
375
+ );
376
+ const command = freeflowCommand(commands);
377
+ const settings = context(cwd, { isIdle: () => true });
378
+ settings.modelRegistry = cognitiveRoutingModelRegistry();
379
+ settings.ui.custom = async (factory) => {
380
+ let result;
381
+ const component = factory({ requestRender() {} }, theme, {}, (value) => {
382
+ result = value;
383
+ });
384
+ component.handleInput("\u001b[B");
385
+ component.handleInput("\r");
386
+ for (let index = 0; index < 5; index += 1) component.handleInput("\u001b[B");
387
+ component.handleInput("\r");
388
+ component.handleInput("\u001b[B");
389
+ component.handleInput("\r");
390
+ await component.waitForWrites();
391
+ component.handleInput("\u001b");
326
392
  component.handleInput("\u001b");
327
393
  return result;
328
394
  };
329
395
  await command.definition.handler("settings repo", settings);
330
396
  const saved = JSON.parse(await readFile(join(cwd, ".freeflow/config.json"), "utf8"));
331
397
  assert.equal(saved.cognitiveRouting.enabled, true);
398
+ assert.equal(saved.cognitiveRouting.projection ?? false, false);
332
399
  assert.equal(settings.reloads.length, 1);
333
400
  } finally {
334
401
  await rm(cwd, { recursive: true, force: true });
@@ -0,0 +1,75 @@
1
+ import assert from "node:assert/strict";
2
+ import test from "node:test";
3
+ import { SessionState } from "../../dist/provider-support/astra/session-state.js";
4
+ import { assemble, ENTRY_TYPE } from "../../dist/provider-support/astra/history.js";
5
+
6
+ function fixture() {
7
+ const branch = [];
8
+ const append = (type, fields = {}) => {
9
+ const entry = { id: `entry-${branch.length}`, parentId: branch.at(-1)?.id ?? null, type, ...fields };
10
+ branch.push(entry);
11
+ return entry.id;
12
+ };
13
+ const reader = {
14
+ getBranch: () => branch,
15
+ getLeafId: () => branch.at(-1)?.id ?? null,
16
+ getSessionFile: () => undefined,
17
+ };
18
+ const store = new SessionState({ appendEntry: (customType, data) => append("custom", { customType, data }) }, reader);
19
+ return { branch, append, reader, store };
20
+ }
21
+ const payload = (effort = "high") => ({
22
+ model: "astra",
23
+ instructions: "same",
24
+ tools: [],
25
+ reasoning: { effort },
26
+ input: [{ role: "user", content: "same" }],
27
+ });
28
+
29
+ for (const boundary of ["compaction", "branch_summary"]) {
30
+ test(`${boundary} excludes pre-boundary history even with identical input`, async () => {
31
+ const f = fixture();
32
+ const old = assemble(payload(), "astra", f.store.generation(), f.reader.getLeafId(), []);
33
+ f.store.append(old.record);
34
+ const epoch = f.append(boundary);
35
+ f.append("model_change", { provider: "p", modelId: "luna" });
36
+ f.append("model_change", { provider: "p", modelId: "astra" });
37
+ assert.equal(f.store.generation(), epoch);
38
+ const current = assemble(payload("low"), "astra", epoch, f.reader.getLeafId(), await f.store.records());
39
+ assert.equal(current.baseline, "low");
40
+ assert.equal(current.record.parent, null);
41
+ // A persisted cross-boundary parent must not become valid through normalization.
42
+ f.store.append({ ...current.record, parent: old.record.id, baseline: "high", effective: "high" });
43
+ await assert.rejects(f.store.records(), /Invalid Astra history parent/);
44
+ });
45
+ }
46
+
47
+ test("unknown generation and invalid native ancestry are rejected, not normalized into eligibility", async () => {
48
+ const f = fixture();
49
+ const old = assemble(payload(), "astra", "unrelated-model-entry", null, []);
50
+ f.store.append(old.record);
51
+ await assert.rejects(f.store.records(), /generation unavailable/);
52
+ f.branch[0].parentId = "missing";
53
+ await assert.rejects(f.store.records(), /Invalid Astra session ancestry/);
54
+ });
55
+
56
+ test("changed envelope or earlier projected content cannot replay an old effort update", async () => {
57
+ const high = payload();
58
+ const first = assemble(high, "astra", "root", null, []);
59
+ const low = { ...payload("low"), input: [...high.input, { role: "user", content: "later" }] };
60
+ const second = assemble(low, "astra", "root", null, [first.record]);
61
+ for (const changed of [
62
+ { ...low, instructions: "different" },
63
+ { ...low, input: [{ role: "user", content: "structural omission" }] },
64
+ ]) {
65
+ const result = assemble(changed, "astra", "root", null, [first.record, second.record]);
66
+ assert.equal(result.record.parent, null);
67
+ assert.deepEqual(
68
+ result.payload.input.filter((x) => x.type !== "configuration_update"),
69
+ changed.input,
70
+ );
71
+ }
72
+ const differentModel = assemble(low, "other-model", "root", null, [first.record, second.record]);
73
+ assert.equal(differentModel.record.parent, null);
74
+ assert.equal(differentModel.baseline, "low");
75
+ });