@hassangameryt/freeflow 0.7.3 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (276) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/.codex-plugin/plugin.json +1 -1
  4. package/.cursor-plugin/plugin.json +1 -1
  5. package/NOTICE +14 -0
  6. package/README.md +26 -31
  7. package/capabilities/cognitive-routing/SKILL.md +106 -161
  8. package/capabilities/cognitive-routing/references/both-mode.md +36 -0
  9. package/capabilities/cognitive-routing/references/executor-mode.md +48 -0
  10. package/capabilities/cognitive-routing/references/helper-mode.md +47 -0
  11. package/capabilities/compaction/SKILL.md +49 -0
  12. package/capabilities/compaction/references/summary-format.md +239 -0
  13. package/command-surface.json +4 -0
  14. package/gemini-extension.json +1 -1
  15. package/hooks/shared/runtime-context.mjs +28 -30
  16. package/licenses/Apache-2.0.txt +55 -0
  17. package/package.json +22 -9
  18. package/pi-extension/README.md +41 -0
  19. package/pi-extension/dist/cognitive-routing/assembler.js +393 -0
  20. package/pi-extension/dist/{cognitive-routing-v2 → cognitive-routing}/config.js +5 -2
  21. package/pi-extension/dist/cognitive-routing/economics.js +45 -0
  22. package/pi-extension/dist/cognitive-routing/event-schema.js +388 -0
  23. package/pi-extension/dist/{session-sources/events.js → cognitive-routing/event-store.js} +103 -40
  24. package/pi-extension/dist/cognitive-routing/gate.js +241 -0
  25. package/pi-extension/dist/cognitive-routing/handlers.js +899 -0
  26. package/pi-extension/dist/cognitive-routing/handoffs.js +350 -0
  27. package/pi-extension/dist/cognitive-routing/model-control.js +467 -0
  28. package/pi-extension/dist/{cognitive-routing-v2 → cognitive-routing}/projection.js +229 -56
  29. package/pi-extension/dist/cognitive-routing/provenance.js +81 -0
  30. package/pi-extension/dist/cognitive-routing/runtime.js +396 -0
  31. package/pi-extension/dist/{cognitive-routing-v2 → cognitive-routing}/schemas.js +43 -7
  32. package/pi-extension/dist/cognitive-routing/session.js +221 -0
  33. package/pi-extension/dist/{session-sources → cognitive-routing}/sources.js +99 -85
  34. package/pi-extension/dist/{cognitive-routing-v2 → cognitive-routing}/state.js +37 -378
  35. package/pi-extension/dist/cognitive-routing/status.js +80 -0
  36. package/pi-extension/dist/{cognitive-routing-v2 → cognitive-routing}/tools.js +12 -12
  37. package/pi-extension/dist/{cognitive-routing-v2 → cognitive-routing}/types.js +5 -11
  38. package/pi-extension/dist/compaction/carry.js +191 -0
  39. package/pi-extension/dist/compaction/config.js +22 -0
  40. package/pi-extension/dist/compaction/controller.js +471 -0
  41. package/pi-extension/dist/compaction/harness.js +77 -0
  42. package/pi-extension/dist/compaction/results.js +158 -0
  43. package/pi-extension/dist/compaction/thresholds.js +35 -0
  44. package/pi-extension/dist/compaction/tool.js +57 -0
  45. package/pi-extension/dist/host/branch.js +13 -0
  46. package/pi-extension/dist/host/canonical.js +12 -0
  47. package/pi-extension/dist/host/catalog.js +77 -0
  48. package/pi-extension/dist/host/config.js +374 -0
  49. package/pi-extension/dist/host/projection-tags.js +60 -0
  50. package/pi-extension/dist/host/prompts.js +82 -0
  51. package/pi-extension/dist/{session-sources → host}/read-only-session.js +112 -1
  52. package/pi-extension/dist/{runtime → host}/request-history.js +121 -39
  53. package/pi-extension/dist/host/runtime-state.js +189 -0
  54. package/pi-extension/dist/{settings/settings-ui.js → host/settings/freeflow-command.js} +620 -410
  55. package/pi-extension/dist/{settings/settings-tui.js → host/settings/settings-view.js} +186 -73
  56. package/pi-extension/dist/host/staging.js +28 -0
  57. package/pi-extension/dist/host/status.js +101 -0
  58. package/pi-extension/dist/index.js +423 -173
  59. package/pi-extension/dist/provider-support/cache/anchor.js +21 -0
  60. package/pi-extension/dist/provider-support/cache/anthropic.js +102 -0
  61. package/pi-extension/dist/provider-support/cache/health.js +73 -0
  62. package/pi-extension/dist/provider-support/cache/index.js +58 -0
  63. package/pi-extension/dist/provider-support/cache/keepalive.js +210 -0
  64. package/pi-extension/dist/provider-support/cache/monitor.js +17 -0
  65. package/pi-extension/dist/provider-support/cache/openai-responses.js +48 -0
  66. package/pi-extension/dist/provider-support/effort.js +13 -0
  67. package/pi-extension/dist/provider-support/index.js +44 -3
  68. package/pi-extension/dist/provider-support/{astra → openai}/adapter.js +46 -29
  69. package/pi-extension/dist/provider-support/{astra → openai}/history.js +10 -8
  70. package/pi-extension/dist/provider-support/{astra → openai}/index.js +8 -4
  71. package/pi-extension/dist/provider-support/{astra → openai}/session-state.js +29 -38
  72. package/pi-extension/dist/provider-support/routes.js +17 -0
  73. package/pi-extension/dist/tool-execution/apply-patch/grammar.js +23 -0
  74. package/pi-extension/dist/tool-execution/apply-patch/match.js +38 -0
  75. package/pi-extension/dist/tool-execution/apply-patch/parser.js +177 -0
  76. package/pi-extension/dist/tool-execution/apply-patch/plan.js +201 -0
  77. package/pi-extension/dist/tool-execution/apply-patch/tool.js +84 -0
  78. package/pi-extension/dist/tool-execution/background.js +238 -0
  79. package/pi-extension/dist/tool-execution/bash-guard.js +23 -0
  80. package/pi-extension/dist/tool-execution/config.js +22 -0
  81. package/pi-extension/dist/tool-execution/edit-messages.js +120 -0
  82. package/pi-extension/dist/tool-execution/file-state.js +138 -0
  83. package/pi-extension/dist/tool-execution/file-tracking.js +143 -0
  84. package/pi-extension/dist/tool-execution/prompt.js +54 -0
  85. package/pi-extension/dist/tool-execution/tools.js +16 -0
  86. package/pi-extension/freeflow/README.md +9 -0
  87. package/plugin.json +1 -1
  88. package/runtime/prompts/cognitive-routing.md +4 -4
  89. package/runtime/prompts/core.md +22 -75
  90. package/runtime/prompts/interaction-contract.md +15 -8
  91. package/runtime/prompts/tool-execution.md +38 -0
  92. package/runtime/prompts/working-method.md +108 -0
  93. package/skills/action-selection/SKILL.md +39 -47
  94. package/skills/action-selection/references/trajectory-stalls.md +2 -0
  95. package/skills/bypass/SKILL.md +2 -2
  96. package/skills/commit-work/SKILL.md +31 -83
  97. package/skills/commit-work/references/git-state-edges.md +1 -1
  98. package/skills/commit-work/references/simple-push.md +2 -0
  99. package/skills/commit-work/references/staging-decisions.md +1 -1
  100. package/skills/decision-gate/SKILL.md +31 -50
  101. package/skills/design-for-depth/SKILL.md +44 -53
  102. package/skills/design-for-depth/references/design-pressure-signals.md +46 -144
  103. package/skills/design-for-depth/references/implementation-decisions.md +1 -1
  104. package/skills/design-for-depth/references/interface-design-loop.md +5 -36
  105. package/skills/design-for-depth/references/module-and-dependency-design.md +2 -32
  106. package/skills/design-for-depth/references/state-and-failure-boundaries.md +2 -13
  107. package/skills/diagnose-failure/SKILL.md +39 -24
  108. package/skills/diagnose-failure/references/diagnostic-loop-catalog.md +2 -0
  109. package/skills/diagnose-failure/references/flaky-and-performance-diagnosis.md +2 -2
  110. package/skills/discuss/SKILL.md +35 -11
  111. package/skills/discuss/references/discussion-continuity.md +7 -27
  112. package/skills/evaluate-skill/SKILL.md +49 -80
  113. package/skills/evaluate-skill/references/evaluation-definition-schema.md +35 -5
  114. package/skills/evaluate-skill/references/evaluation-design.md +14 -14
  115. package/skills/evaluate-skill/references/execution-and-evidence.md +26 -5
  116. package/skills/evaluate-skill/references/review-and-revision.md +3 -1
  117. package/skills/evaluate-skill/scripts/lib/definitions.mjs +49 -8
  118. package/skills/evaluate-skill/scripts/lib/pi.mjs +14 -7
  119. package/skills/evaluate-skill/scripts/lib/review.mjs +196 -0
  120. package/skills/evaluate-skill/scripts/lib/runner.mjs +45 -8
  121. package/skills/evaluate-skill/scripts/lib/sandbox.mjs +2 -0
  122. package/skills/evaluate-skill/scripts/lib/trials.mjs +103 -0
  123. package/skills/evaluate-skill/scripts/lib/view.mjs +67 -1
  124. package/skills/evaluate-skill/scripts/pi-command-tool.mjs +144 -0
  125. package/skills/evaluate-skill/scripts/pi-guard.mjs +30 -13
  126. package/skills/evaluate-skill/scripts/skill-eval.mjs +114 -34
  127. package/skills/execute-work/SKILL.md +41 -9
  128. package/skills/execute-work/references/code-practices.md +2 -0
  129. package/skills/execute-work/references/execute-work-edges.md +11 -1
  130. package/skills/handoff/SKILL.md +1 -1
  131. package/skills/handoff/references/handoff-templates.md +1 -1
  132. package/skills/review-artifact/SKILL.md +42 -41
  133. package/skills/review-artifact/references/adjudicate-artifact-review.md +1 -1
  134. package/skills/review-artifact/references/independent-artifact-reviewer-contract.md +1 -1
  135. package/skills/review-work/SKILL.md +38 -32
  136. package/skills/review-work/references/adjudicate-work-review.md +1 -1
  137. package/skills/review-work/references/independent-work-reviewer-contract.md +1 -1
  138. package/skills/review-work/references/security-risk-lens.md +2 -4
  139. package/skills/setup-freeflow/SKILL.md +3 -1
  140. package/skills/setup-freeflow/references/activation-contract.md +7 -13
  141. package/skills/setup-freeflow/references/host-setup.md +3 -3
  142. package/skills/track-work/SKILL.md +113 -161
  143. package/skills/track-work/references/working-record-format.md +189 -350
  144. package/skills/track-work/scripts/lib/document.mjs +62 -6
  145. package/skills/track-work/scripts/lib/format.mjs +16 -1
  146. package/skills/track-work/scripts/lib/store.mjs +9 -8
  147. package/skills/track-work/scripts/lib/transitions.mjs +79 -4
  148. package/skills/track-work/scripts/working-record.mjs +34 -12
  149. package/skills/verify-work/SKILL.md +29 -20
  150. package/skills/verify-work/references/browser-runtime-evidence.md +1 -5
  151. package/skills/verify-work/references/integration-evidence.md +2 -0
  152. package/skills/verify-work/references/performance-evidence.md +3 -1
  153. package/skills/verify-work/references/test-design.md +3 -1
  154. package/skills/workflow/SKILL.md +65 -21
  155. package/skills/workflow/references/domain-skill-composition.md +5 -3
  156. package/skills/write-docs/SKILL.md +83 -0
  157. package/skills/write-docs/references/documentation-shapes.md +119 -0
  158. package/skills/write-docs/scripts/docs-check.mjs +139 -0
  159. package/skills/write-plan/SKILL.md +40 -40
  160. package/skills/write-plan/references/plan-shapes.md +1 -1
  161. package/skills/write-skill/SKILL.md +107 -147
  162. package/skills/write-skill/references/activation-boundaries.md +2 -0
  163. package/skills/write-skill/references/agent-first-instructions.md +22 -10
  164. package/skills/write-skill/references/progressive-disclosure.md +4 -2
  165. package/skills/write-skill/references/skill-dependency-graphs.md +2 -0
  166. package/skills/write-skill/scripts/lib/similarity.mjs +56 -0
  167. package/skills/write-skill/scripts/skill-author.mjs +18 -2
  168. package/skills/write-spec/SKILL.md +34 -41
  169. package/skills/write-spec/references/artifact-standards.md +1 -1
  170. package/skills/write-spec/references/decision-records.md +2 -0
  171. package/skills/write-spec/references/spec-shapes.md +1 -1
  172. package/capabilities/context-virtualization/SKILL.md +0 -103
  173. package/capabilities/conversation-history/SKILL.md +0 -53
  174. package/hooks/tests/check-host-adapters.test.mjs +0 -82
  175. package/hooks/tests/check-runtime-context-hook.sh +0 -212
  176. package/pi-extension/dist/cognitive-routing-v2/runtime.js +0 -2181
  177. package/pi-extension/dist/context-virtualization/commands.js +0 -89
  178. package/pi-extension/dist/context-virtualization/projector.js +0 -42
  179. package/pi-extension/dist/context-virtualization/runtime.js +0 -378
  180. package/pi-extension/dist/context-virtualization/state.js +0 -57
  181. package/pi-extension/dist/context-virtualization/types.js +0 -10
  182. package/pi-extension/dist/conversation-history/passages.js +0 -66
  183. package/pi-extension/dist/conversation-history/ranking.js +0 -105
  184. package/pi-extension/dist/conversation-history/runtime.js +0 -226
  185. package/pi-extension/dist/conversation-history/search.js +0 -56
  186. package/pi-extension/dist/conversation-history/types.js +0 -1
  187. package/pi-extension/dist/freeflow-context/resolver.js +0 -120
  188. package/pi-extension/dist/freeflow-context/runtime.js +0 -80
  189. package/pi-extension/dist/freeflow-context/source-projector.js +0 -95
  190. package/pi-extension/dist/freeflow-context/tool.js +0 -314
  191. package/pi-extension/dist/freeflow-context/types.js +0 -12
  192. package/pi-extension/dist/freeflow-context/visibility.js +0 -28
  193. package/pi-extension/dist/integrations/mcp-config.js +0 -29
  194. package/pi-extension/dist/runtime/runtime-context.js +0 -754
  195. package/pi-extension/dist/runtime/runtime-identity.js +0 -5
  196. package/pi-extension/dist/session-sources/provenance.js +0 -32
  197. package/pi-extension/dist/ui/text-component.js +0 -57
  198. package/pi-extension/src/cognitive-routing-v2/budget.ts +0 -42
  199. package/pi-extension/src/cognitive-routing-v2/config.ts +0 -173
  200. package/pi-extension/src/cognitive-routing-v2/projection.ts +0 -375
  201. package/pi-extension/src/cognitive-routing-v2/render.ts +0 -277
  202. package/pi-extension/src/cognitive-routing-v2/runtime.ts +0 -2241
  203. package/pi-extension/src/cognitive-routing-v2/schemas.ts +0 -180
  204. package/pi-extension/src/cognitive-routing-v2/state.ts +0 -839
  205. package/pi-extension/src/cognitive-routing-v2/tools.ts +0 -62
  206. package/pi-extension/src/cognitive-routing-v2/types.ts +0 -254
  207. package/pi-extension/src/context-virtualization/commands.ts +0 -111
  208. package/pi-extension/src/context-virtualization/projector.ts +0 -62
  209. package/pi-extension/src/context-virtualization/runtime.ts +0 -428
  210. package/pi-extension/src/context-virtualization/state.ts +0 -79
  211. package/pi-extension/src/context-virtualization/types.ts +0 -92
  212. package/pi-extension/src/conversation-history/passages.ts +0 -81
  213. package/pi-extension/src/conversation-history/ranking.ts +0 -155
  214. package/pi-extension/src/conversation-history/runtime.ts +0 -250
  215. package/pi-extension/src/conversation-history/search.ts +0 -79
  216. package/pi-extension/src/conversation-history/types.ts +0 -31
  217. package/pi-extension/src/freeflow-context/resolver.ts +0 -139
  218. package/pi-extension/src/freeflow-context/runtime.ts +0 -100
  219. package/pi-extension/src/freeflow-context/source-projector.ts +0 -106
  220. package/pi-extension/src/freeflow-context/tool.ts +0 -366
  221. package/pi-extension/src/freeflow-context/types.ts +0 -70
  222. package/pi-extension/src/freeflow-context/visibility.ts +0 -53
  223. package/pi-extension/src/index.ts +0 -390
  224. package/pi-extension/src/integrations/mcp-config.ts +0 -32
  225. package/pi-extension/src/provider-support/astra/adapter.ts +0 -108
  226. package/pi-extension/src/provider-support/astra/history.ts +0 -145
  227. package/pi-extension/src/provider-support/astra/index.ts +0 -12
  228. package/pi-extension/src/provider-support/astra/session-state.ts +0 -113
  229. package/pi-extension/src/provider-support/index.ts +0 -5
  230. package/pi-extension/src/runtime/request-history.ts +0 -243
  231. package/pi-extension/src/runtime/runtime-context.ts +0 -860
  232. package/pi-extension/src/runtime/runtime-identity.ts +0 -8
  233. package/pi-extension/src/session-sources/events.ts +0 -230
  234. package/pi-extension/src/session-sources/provenance.ts +0 -39
  235. package/pi-extension/src/session-sources/read-only-session.ts +0 -170
  236. package/pi-extension/src/session-sources/sources.ts +0 -334
  237. package/pi-extension/src/settings/settings-tui.ts +0 -501
  238. package/pi-extension/src/settings/settings-ui.ts +0 -1652
  239. package/pi-extension/src/ui/text-component.ts +0 -66
  240. package/pi-extension/tests/cache-reuse/history.test.js +0 -117
  241. package/pi-extension/tests/cache-reuse/native.test.js +0 -91
  242. package/pi-extension/tests/cache-reuse/settings.test.js +0 -211
  243. package/pi-extension/tests/cognitive-routing-v2/communication.test.js +0 -577
  244. package/pi-extension/tests/cognitive-routing-v2/compaction-input.test.js +0 -183
  245. package/pi-extension/tests/cognitive-routing-v2/delegation.test.js +0 -557
  246. package/pi-extension/tests/cognitive-routing-v2/native.test.js +0 -474
  247. package/pi-extension/tests/cognitive-routing-v2/performance.test.js +0 -87
  248. package/pi-extension/tests/cognitive-routing-v2/persistence.test.js +0 -300
  249. package/pi-extension/tests/cognitive-routing-v2/projection.test.js +0 -241
  250. package/pi-extension/tests/cognitive-routing-v2/recovery.test.js +0 -1034
  251. package/pi-extension/tests/cognitive-routing-v2/render.test.js +0 -244
  252. package/pi-extension/tests/cognitive-routing-v2/runtime.test.js +0 -423
  253. package/pi-extension/tests/cognitive-routing-v2/stabilization.test.js +0 -521
  254. package/pi-extension/tests/cognitive-routing-v2/state.test.js +0 -586
  255. package/pi-extension/tests/context-virtualization/extension.test.js +0 -775
  256. package/pi-extension/tests/context-virtualization/projector.test.js +0 -110
  257. package/pi-extension/tests/context-virtualization/runtime.test.js +0 -230
  258. package/pi-extension/tests/conversation-history/runtime.test.js +0 -98
  259. package/pi-extension/tests/conversation-history/search.test.js +0 -97
  260. package/pi-extension/tests/fixtures/pi-host.js +0 -4
  261. package/pi-extension/tests/fixtures/routing-native.js +0 -191
  262. package/pi-extension/tests/integration/mode-free-core.test.js +0 -220
  263. package/pi-extension/tests/integration/pi-extension.test.js +0 -1447
  264. package/pi-extension/tests/integration/prompt-architecture.test.js +0 -447
  265. package/pi-extension/tests/integration/unaffected-regressions.test.js +0 -478
  266. package/pi-extension/tests/provider-support/astra-boundaries.test.js +0 -75
  267. package/pi-extension/tests/provider-support/astra.test.js +0 -306
  268. package/pi-extension/tests/provider-support/cli.test.js +0 -92
  269. package/pi-extension/tests/provider-support/native.test.js +0 -183
  270. package/pi-extension/tests/settings/settings-tui.test.js +0 -243
  271. package/pi-extension/tsconfig.json +0 -16
  272. package/runtime/prompts/context-virtualization.md +0 -3
  273. package/runtime/prompts/conversation-history.md +0 -3
  274. package/skills/design-for-depth/references/software-design-philosophy.md +0 -202
  275. /package/pi-extension/dist/{cognitive-routing-v2 → cognitive-routing}/budget.js +0 -0
  276. /package/pi-extension/dist/{cognitive-routing-v2 → cognitive-routing}/render.js +0 -0
@@ -0,0 +1,374 @@
1
+ import { readFile } from "node:fs/promises";
2
+ import { join } from "node:path";
3
+ import { resolveCognitiveRoutingState, supportsCognitiveRoutingModelRegistry } from "../cognitive-routing/config.js";
4
+ import { sessionStage, stagedFor } from "./staging.js";
5
+ import { resolveToolExecutionConfig, validateToolExecutionConfig } from "../tool-execution/config.js";
6
+ import { resolveCompactionConfig, validateCompactionConfig } from "../compaction/config.js";
7
+ import { cachedBranch } from "./branch.js";
8
+ /**
9
+ * Freeflow configuration: the repository and personal config files, their layering, per-session core overrides, and
10
+ * the resulting capability state, with optional capabilities disabled inside subagents.
11
+ */
12
+ /**
13
+ * Settings a session may override, as dotted config paths. "enabled" is Freeflow's master switch; the others switch a
14
+ * capability and are applied as a layer above the personal config.
15
+ */
16
+ export const SESSION_OVERRIDE_KEYS = [
17
+ "enabled",
18
+ "cognitiveRouting.enabled",
19
+ "cognitiveRouting.projection",
20
+ "toolExecution.enabled",
21
+ "compaction.enabled",
22
+ "compaction.carry",
23
+ ];
24
+ const SESSION_OVERRIDES_ENTRY = "freeflow-session-overrides";
25
+ const SESSION_CORE_KEYS = new Set(SESSION_OVERRIDE_KEYS);
26
+ let currentSessionOverrides = {};
27
+ // pi-subagents stamps child system prompts with this tag before binding extensions.
28
+ // Keeping detection at the prompt boundary avoids a process-global child flag.
29
+ const SUBAGENT_AGENT_TAG = /<active_agent\s+name="[^"]+"\s*\/>/;
30
+ const FREEFLOW_SUBAGENT_CAPABILITIES_DISABLED_MARKER = "<!-- freeflow-subagent-capabilities: disabled -->";
31
+ const SUBAGENT_OPTIONAL_CAPABILITIES_MESSAGE = "Optional Freeflow capabilities are disabled for subagents.";
32
+ function isSubagentContext(context) {
33
+ try {
34
+ const systemPrompt = context?.getSystemPrompt?.();
35
+ return (
36
+ typeof systemPrompt === "string" &&
37
+ (SUBAGENT_AGENT_TAG.test(systemPrompt) || systemPrompt.includes(FREEFLOW_SUBAGENT_CAPABILITIES_DISABLED_MARKER))
38
+ );
39
+ } catch {
40
+ return false;
41
+ }
42
+ }
43
+ function disableSubagentCapability(capability) {
44
+ return {
45
+ ...capability,
46
+ enabled: false,
47
+ effective: false,
48
+ blockingReason: { code: "disabled", message: SUBAGENT_OPTIONAL_CAPABILITIES_MESSAGE },
49
+ };
50
+ }
51
+ function isRecord(value) {
52
+ return typeof value === "object" && value !== null && !Array.isArray(value);
53
+ }
54
+ function removedContextConfigError(value, filePath) {
55
+ const removedKeys = ["contextVirtualization", "conversationHistory"].filter((key) =>
56
+ Object.prototype.hasOwnProperty.call(value, key),
57
+ );
58
+ return removedKeys.length > 0
59
+ ? `Removed Freeflow context setting(s) in ${filePath}: ${removedKeys.join(", ")}. Delete those keys before using Freeflow.`
60
+ : null;
61
+ }
62
+ function validateCoreConfigFields(value) {
63
+ if (value.enabled !== undefined && typeof value.enabled !== "boolean") {
64
+ return "enabled must be a boolean";
65
+ }
66
+ return null;
67
+ }
68
+ function validateFreeflowConfigShape(value) {
69
+ if (!isRecord(value)) {
70
+ return "config must be a JSON object";
71
+ }
72
+ const removedContextError = removedContextConfigError(value, ".freeflow/config.json");
73
+ if (removedContextError) return removedContextError;
74
+ // outputRouter, observedRouting, and scriptTransform belong to retired features; they are accepted and ignored so
75
+ // existing configs keep loading.
76
+ const allowedKeys = new Set([
77
+ "enabled",
78
+ "outputRouter",
79
+ "observedRouting",
80
+ "scriptTransform",
81
+ "cognitiveRouting",
82
+ "toolExecution",
83
+ "compaction",
84
+ ]);
85
+ for (const key of Object.keys(value)) {
86
+ if (!allowedKeys.has(key)) {
87
+ return `unsupported top-level config key: ${key}`;
88
+ }
89
+ }
90
+ const coreError = validateCoreConfigFields(value);
91
+ if (coreError) return coreError;
92
+ for (const key of ["outputRouter", "observedRouting", "scriptTransform"]) {
93
+ if (value[key] !== undefined && !isRecord(value[key])) {
94
+ return `${key} must be an object`;
95
+ }
96
+ }
97
+ if (value.toolExecution !== undefined) {
98
+ const error = validateToolExecutionConfig(value.toolExecution);
99
+ if (error) return error;
100
+ }
101
+ if (value.compaction !== undefined) {
102
+ const error = validateCompactionConfig(value.compaction);
103
+ if (error) return error;
104
+ }
105
+ return null;
106
+ }
107
+ function validateFreeflowLocalConfigShape(value) {
108
+ if (!isRecord(value)) {
109
+ return "local config must be a JSON object";
110
+ }
111
+ const removedContextError = removedContextConfigError(value, ".freeflow/local.json");
112
+ if (removedContextError) return removedContextError;
113
+ // processing belongs to a retired feature; it is accepted and ignored so existing local configs keep loading.
114
+ const allowedKeys = new Set(["enabled", "processing", "cognitiveRouting", "toolExecution", "compaction"]);
115
+ for (const key of Object.keys(value)) {
116
+ if (!allowedKeys.has(key)) {
117
+ return `unsupported top-level local config key: ${key}`;
118
+ }
119
+ }
120
+ const coreError = validateCoreConfigFields(value);
121
+ if (coreError) return coreError;
122
+ if (value.toolExecution !== undefined) {
123
+ const error = validateToolExecutionConfig(value.toolExecution);
124
+ if (error) return error;
125
+ }
126
+ if (value.compaction !== undefined) return validateCompactionConfig(value.compaction);
127
+ return null;
128
+ }
129
+ async function readConfigFileState(path, validate) {
130
+ try {
131
+ const raw = await readFile(path, "utf8");
132
+ const parsed = JSON.parse(raw);
133
+ const validationError = validate(parsed);
134
+ if (validationError) {
135
+ return {
136
+ path,
137
+ exists: true,
138
+ valid: false,
139
+ parsed: {},
140
+ parseError: validationError,
141
+ };
142
+ }
143
+ return { path, exists: true, valid: true, parsed, parseError: null };
144
+ } catch (error) {
145
+ const code = error && typeof error === "object" ? error.code : undefined;
146
+ if (code === "ENOENT") {
147
+ return {
148
+ path,
149
+ exists: false,
150
+ valid: false,
151
+ parsed: {},
152
+ parseError: null,
153
+ };
154
+ }
155
+ const message = error instanceof Error ? error.message : String(error);
156
+ return {
157
+ path,
158
+ exists: true,
159
+ valid: false,
160
+ parsed: {},
161
+ parseError: message,
162
+ };
163
+ }
164
+ }
165
+ function readFreeflowConfigState(cwd) {
166
+ return readConfigFileState(join(cwd, ".freeflow/config.json"), validateFreeflowConfigShape);
167
+ }
168
+ function readFreeflowLocalConfigState(cwd) {
169
+ return readConfigFileState(join(cwd, ".freeflow/local.json"), validateFreeflowLocalConfigShape);
170
+ }
171
+ export async function readFreeflowConfig(cwd) {
172
+ const state = await readFreeflowConfigState(cwd);
173
+ return state.valid ? state.parsed : {};
174
+ }
175
+ export async function readFreeflowLocalConfig(cwd) {
176
+ const state = await readFreeflowLocalConfigState(cwd);
177
+ return state.valid ? state.parsed : {};
178
+ }
179
+ function resolveLayeredValue(repository, local, key, fallback) {
180
+ if (Object.hasOwn(local, key)) {
181
+ return { value: local[key], source: "local" };
182
+ }
183
+ if (Object.hasOwn(repository, key)) {
184
+ return { value: repository[key], source: "repository" };
185
+ }
186
+ return { value: fallback, source: "builtin" };
187
+ }
188
+ function resolveCoreConfig(repository, local) {
189
+ const enabled = resolveLayeredValue(repository, local, "enabled", true);
190
+ return {
191
+ config: { enabled: enabled.value },
192
+ sources: { enabled: enabled.source },
193
+ };
194
+ }
195
+ function normalizeSessionOverrides(value) {
196
+ if (!isRecord(value)) return {};
197
+ const overrides = {};
198
+ for (const key of SESSION_CORE_KEYS) {
199
+ if (typeof value[key] === "boolean") {
200
+ overrides[key] = value[key];
201
+ }
202
+ }
203
+ return overrides;
204
+ }
205
+ /** The capability overrides of this session as a config layer, e.g. { compaction: { carry: false } }. */
206
+ function sessionConfigLayer(overrides) {
207
+ const layer = {};
208
+ for (const [key, value] of Object.entries(overrides)) {
209
+ const [section, field] = key.split(".");
210
+ if (!field || typeof value !== "boolean") continue;
211
+ layer[section] = { ...(layer[section] ?? {}), [field]: value };
212
+ }
213
+ return layer;
214
+ }
215
+ function withSessionLayer(local, session) {
216
+ const merged = { ...local };
217
+ for (const [section, fields] of Object.entries(session))
218
+ merged[section] = { ...(isRecord(local[section]) ? local[section] : {}), ...fields };
219
+ return merged;
220
+ }
221
+ function resolveSessionCoreConfig(layers) {
222
+ const configured = layers.coreConfig;
223
+ const sources = layers.sources;
224
+ const enabled = currentSessionOverrides.enabled;
225
+ return {
226
+ config: { enabled: typeof enabled === "boolean" ? enabled : configured.enabled },
227
+ sources: { enabled: typeof enabled === "boolean" ? "session" : sources.enabled },
228
+ };
229
+ }
230
+ export async function readFreeflowConfigLayers(cwd) {
231
+ const [repository, local] = await Promise.all([readFreeflowConfigState(cwd), readFreeflowLocalConfigState(cwd)]);
232
+ const repositoryConfig = repository.valid ? repository.parsed : {};
233
+ const localConfig = local.valid ? local.parsed : {};
234
+ const core = resolveCoreConfig(repositoryConfig, localConfig);
235
+ const localValid = !local.exists || local.valid;
236
+ const configured = repository.valid && localValid;
237
+ let blockingState = null;
238
+ if (!repository.valid) {
239
+ blockingState = repository;
240
+ } else if (local.exists && !local.valid) {
241
+ blockingState = local;
242
+ }
243
+ return {
244
+ configured,
245
+ repositoryConfigured: repository.valid,
246
+ repository,
247
+ local,
248
+ coreConfig: core.config,
249
+ sources: core.sources,
250
+ blockingConfigPath: blockingState?.path ?? null,
251
+ parseError: blockingState?.parseError ?? null,
252
+ };
253
+ }
254
+ export async function readCapabilityState(cwd, host = undefined) {
255
+ const layers = await readFreeflowConfigLayers(cwd);
256
+ const effectiveCore = resolveSessionCoreConfig(layers);
257
+ const enabled = layers.configured && effectiveCore.config.enabled;
258
+ const subagentContext = isSubagentContext(host);
259
+ const hostSupportsCognitiveRouting = !subagentContext && supportsCognitiveRoutingModelRegistry(host);
260
+ const sessionLayer = sessionConfigLayer(currentSessionOverrides);
261
+ const configuredCognitiveRouting = await resolveCognitiveRoutingState(
262
+ layers.repository.parsed,
263
+ layers.local.parsed,
264
+ hostSupportsCognitiveRouting ? host : undefined,
265
+ sessionLayer,
266
+ );
267
+ const disabledReason = enabled ? undefined : { code: "disabled", message: "Freeflow is disabled" };
268
+ const cognitiveRouting = enabled
269
+ ? configuredCognitiveRouting
270
+ : {
271
+ ...configuredCognitiveRouting,
272
+ enabled: false,
273
+ effective: false,
274
+ blockingReason: disabledReason,
275
+ };
276
+ const repositoryConfig = layers.repository.valid ? layers.repository.parsed : {};
277
+ const localConfig = layers.local.valid ? layers.local.parsed : {};
278
+ const sessionLocalConfig = withSessionLayer(localConfig, sessionLayer);
279
+ const toolExecution = resolveToolExecutionConfig(repositoryConfig, sessionLocalConfig, enabled);
280
+ const compaction = resolveCompactionConfig(repositoryConfig, sessionLocalConfig, enabled);
281
+ const capabilityState = {
282
+ configured: layers.configured,
283
+ repositoryConfigured: layers.repositoryConfigured,
284
+ configExists: layers.repository.exists,
285
+ configValid: layers.configured,
286
+ configPath: layers.blockingConfigPath ?? layers.repository.path,
287
+ parseError: layers.parseError,
288
+ localConfigExists: layers.local.exists,
289
+ localConfigValid: !layers.local.exists || layers.local.valid,
290
+ localConfigPath: layers.local.path,
291
+ localConfigParseError: layers.local.parseError,
292
+ configuredCoreConfig: layers.coreConfig,
293
+ configuredSources: layers.sources,
294
+ sessionOverrides: { ...currentSessionOverrides },
295
+ configSources: effectiveCore.sources,
296
+ enabled,
297
+ hostSupportsCognitiveRouting,
298
+ cognitiveRouting,
299
+ toolExecution,
300
+ compaction,
301
+ };
302
+ if (!subagentContext) return capabilityState;
303
+ return {
304
+ ...capabilityState,
305
+ cognitiveRouting: disableSubagentCapability(capabilityState.cognitiveRouting),
306
+ toolExecution: disableSubagentCapability(capabilityState.toolExecution),
307
+ compaction: disableSubagentCapability(capabilityState.compaction),
308
+ };
309
+ }
310
+ function recordedSessionOverrides(sessionManager) {
311
+ let recorded = {};
312
+ for (const entry of sessionManager?.getBranch ? cachedBranch(sessionManager) : (sessionManager?.getEntries?.() ?? []))
313
+ if (entry.type === "custom" && entry.customType === SESSION_OVERRIDES_ENTRY)
314
+ recorded = normalizeSessionOverrides(entry.data?.overrides);
315
+ return recorded;
316
+ }
317
+ export function restoreSessionOverrides(ctx) {
318
+ const staged = stagedFor(ctx.sessionManager)?.overrides;
319
+ currentSessionOverrides = normalizeSessionOverrides(staged ?? recordedSessionOverrides(ctx.sessionManager));
320
+ }
321
+ /** Session overrides change nothing until the next prompt, so they are staged and written then. */
322
+ function recordSessionOverrides(ctx, pi) {
323
+ const stage = sessionStage(ctx?.sessionManager);
324
+ if (stage) stage.overrides = { ...currentSessionOverrides };
325
+ else pi?.appendEntry?.(SESSION_OVERRIDES_ENTRY, { overrides: { ...currentSessionOverrides } });
326
+ }
327
+ /** Write staged session overrides at the start of a prompt, unless they equal what the session recorded. */
328
+ export function writeStagedSessionOverrides(overrides, pi, sessionManager) {
329
+ if (!overrides) return;
330
+ const next = normalizeSessionOverrides(overrides);
331
+ if (JSON.stringify(next) === JSON.stringify(recordedSessionOverrides(sessionManager))) return;
332
+ pi?.appendEntry?.(SESSION_OVERRIDES_ENTRY, { overrides: { ...next } });
333
+ }
334
+ export async function setSessionCoreOverride(key, value, ctx, pi) {
335
+ if (!SESSION_CORE_KEYS.has(key)) {
336
+ throw new Error(`Invalid Freeflow session override: ${String(key)}`);
337
+ }
338
+ if (value !== null && typeof value !== "boolean") {
339
+ throw new Error(`Invalid Freeflow session override value for ${key}: ${String(value)}`);
340
+ }
341
+ const hasOverride = Object.hasOwn(currentSessionOverrides, key);
342
+ if ((value === null && !hasOverride) || (value !== null && hasOverride && currentSessionOverrides[key] === value)) {
343
+ return { changed: false, sessionOverrides: { ...currentSessionOverrides } };
344
+ }
345
+ const next = { ...currentSessionOverrides };
346
+ if (value === null) {
347
+ delete next[key];
348
+ } else {
349
+ next[key] = value;
350
+ }
351
+ currentSessionOverrides = next;
352
+ recordSessionOverrides(ctx, pi);
353
+ return {
354
+ changed: true,
355
+ // Every session override switches Freeflow or a capability, which changes tools and guidance: reload.
356
+ reloadRequired: true,
357
+ sessionOverrides: { ...currentSessionOverrides },
358
+ capabilityState: await readCapabilityState(ctx.cwd, ctx),
359
+ };
360
+ }
361
+ export async function resetSessionOverrides(ctx, pi) {
362
+ const hadCoreOverrides = Object.keys(currentSessionOverrides).length > 0;
363
+ const reloadRequired = hadCoreOverrides;
364
+ if (hadCoreOverrides) {
365
+ currentSessionOverrides = {};
366
+ recordSessionOverrides(ctx, pi);
367
+ }
368
+ return {
369
+ changed: hadCoreOverrides,
370
+ reloadRequired,
371
+ sessionOverrides: { ...currentSessionOverrides },
372
+ capabilityState: await readCapabilityState(ctx.cwd, ctx),
373
+ };
374
+ }
@@ -0,0 +1,60 @@
1
+ const projectedEntryIds = new WeakMap();
2
+ // Messages that came from an aligned projection; anything else in the request was generated after it.
3
+ const projectedMessages = new WeakSet();
4
+ // Projected messages whose body a host edit replaced; only these still need content association.
5
+ const editedMessages = new WeakSet();
6
+ /** The unedited native entry behind a request message, when Pi's projection established it. */
7
+ export function projectedEntryId(message) {
8
+ return message && typeof message === "object" ? projectedEntryIds.get(message) : undefined;
9
+ }
10
+ export function tagProjectedMessages(messages, projection, branch) {
11
+ if (!projection) return;
12
+ let count = 0;
13
+ for (const entry of projection.entries) count += entry.messages.length;
14
+ if (count !== messages.length) return;
15
+ let index = 0;
16
+ for (const entry of projection.entries)
17
+ for (const projected of entry.messages) if (projected?.role !== messages[index++]?.role) return;
18
+ // Host edits change the visible body, so their targets keep exact content association.
19
+ const edited = new Set();
20
+ for (const entry of branch) if (entry.type === "context_edit") edited.add(entry.targetId);
21
+ index = 0;
22
+ for (const entry of projection.entries) {
23
+ const source = entry.sourceEntry;
24
+ for (const _projected of entry.messages) {
25
+ const message = messages[index++];
26
+ if (!message || typeof message !== "object") continue;
27
+ projectedMessages.add(message);
28
+ if (edited.has(source.id)) editedMessages.add(message);
29
+ if (
30
+ entry.messages.length === 1 &&
31
+ (source.type === "message" || source.type === "custom_message") &&
32
+ !edited.has(source.id)
33
+ )
34
+ projectedEntryIds.set(message, source.id);
35
+ }
36
+ }
37
+ }
38
+ // Messages Freeflow renders from an entry (routing's views): the entry and the rendering variant, so a consumer can
39
+ // fingerprint each rendering once. Kept apart from projectedEntryIds, which names only unedited native messages.
40
+ const derivedIdentities = new WeakMap();
41
+ /** Mark a message rendered from a native entry with a stable identity: `<entry id>#<variant>` or another unique key. */
42
+ export function tagDerived(message, identity) {
43
+ derivedIdentities.set(message, identity);
44
+ }
45
+ /**
46
+ * A stable identity for a request message whose content never changes under that identity: the unedited native entry
47
+ * behind it, or the identity of a rendering. Undefined for generated or host-edited messages.
48
+ */
49
+ export function occurrenceIdentity(message) {
50
+ if (!message || typeof message !== "object") return undefined;
51
+ return derivedIdentities.get(message) ?? projectedEntryIds.get(message);
52
+ }
53
+ /** Whether a request message came from an aligned Pi projection. */
54
+ export function isProjected(message) {
55
+ return !!message && typeof message === "object" && projectedMessages.has(message);
56
+ }
57
+ /** Whether a host edit replaced the body of a projected request message. */
58
+ export function isHostEdited(message) {
59
+ return !!message && typeof message === "object" && editedMessages.has(message);
60
+ }
@@ -0,0 +1,82 @@
1
+ import { readFile } from "node:fs/promises";
2
+ /**
3
+ * Freeflow's prompt files and the fixed system section built from them. The section never changes with feature state
4
+ * so it stays cached; the Runtime State tells the model what is active.
5
+ */
6
+ let runtimeContextCache = null;
7
+ export function isPromptAvailable(value) {
8
+ return typeof value === "string" && value.trim().length > 0;
9
+ }
10
+ export function hasUsableMandatoryPrompts(freeflowContext) {
11
+ return (
12
+ isPromptAvailable(freeflowContext?.corePrompt) && isPromptAvailable(freeflowContext?.interactionContractPrompt)
13
+ );
14
+ }
15
+ async function readPromptFile(url) {
16
+ try {
17
+ const prompt = await readFile(url, "utf8");
18
+ return isPromptAvailable(prompt) ? prompt.trim() : null;
19
+ } catch {
20
+ return null;
21
+ }
22
+ }
23
+ async function loadRuntimeContext(capabilityState = undefined) {
24
+ const freeflowEnabled = capabilityState?.enabled === true;
25
+ const cognitiveRoutingEnabled = capabilityState?.cognitiveRouting?.effective === true;
26
+ const [corePrompt, interactionContractPrompt, workingMethodPrompt, cognitiveRoutingPrompt, toolExecutionPrompt] =
27
+ await Promise.all([
28
+ freeflowEnabled
29
+ ? readPromptFile(new URL("../../../runtime/prompts/core.md", import.meta.url))
30
+ : Promise.resolve(null),
31
+ freeflowEnabled
32
+ ? readPromptFile(new URL("../../../runtime/prompts/interaction-contract.md", import.meta.url))
33
+ : Promise.resolve(null),
34
+ freeflowEnabled
35
+ ? readPromptFile(new URL("../../../runtime/prompts/working-method.md", import.meta.url))
36
+ : Promise.resolve(null),
37
+ cognitiveRoutingEnabled
38
+ ? readPromptFile(new URL("../../../runtime/prompts/cognitive-routing.md", import.meta.url))
39
+ : Promise.resolve(null),
40
+ capabilityState?.toolExecution?.effective === true
41
+ ? readPromptFile(new URL("../../../runtime/prompts/tool-execution.md", import.meta.url))
42
+ : Promise.resolve(null),
43
+ ]);
44
+ return { corePrompt, interactionContractPrompt, workingMethodPrompt, cognitiveRoutingPrompt, toolExecutionPrompt };
45
+ }
46
+ function runtimeContextCacheSatisfies(capabilityState) {
47
+ if (!runtimeContextCache) return false;
48
+ const expected = {
49
+ corePrompt: capabilityState?.enabled === true,
50
+ interactionContractPrompt: capabilityState?.enabled === true,
51
+ cognitiveRoutingPrompt: capabilityState?.cognitiveRouting?.effective === true,
52
+ };
53
+ return Object.entries(expected).every(([key, required]) => !required || isPromptAvailable(runtimeContextCache[key]));
54
+ }
55
+ export async function refreshRuntimeContext(capabilityState = undefined) {
56
+ runtimeContextCache = await loadRuntimeContext(capabilityState);
57
+ return runtimeContextCache;
58
+ }
59
+ export async function getRuntimeContext(capabilityState = undefined) {
60
+ if (runtimeContextCacheSatisfies(capabilityState)) {
61
+ return runtimeContextCache;
62
+ }
63
+ return refreshRuntimeContext(capabilityState);
64
+ }
65
+ export function stableRuntimeContext(context) {
66
+ const sections = [
67
+ ["Freeflow core", context?.corePrompt],
68
+ ["Freeflow core", context?.interactionContractPrompt],
69
+ ["Working Method", context?.workingMethodPrompt],
70
+ ["Cognitive Routing", context?.cognitiveRoutingPrompt],
71
+ ];
72
+ return [
73
+ "# Freeflow availability contract",
74
+ "Freeflow keeps a fixed reference catalog of its instructions, skills, and tools to preserve prompt caching. Presence in this catalog does not mean a feature is active or an operation is permitted.",
75
+ "Apply the following guidance and Freeflow skills only when the latest extension-generated Freeflow Runtime State marks the corresponding feature active. When Freeflow is inactive, unavailable, or not configured, its core and capability guidance is dormant; do not bootstrap or follow it merely because it is listed. Explicit user instructions retain their normal authority.",
76
+ "For Cognitive Routing, follow the latest control/profile/responsibility snapshots on this branch. Earlier snapshots and notices are historical; they grant no current permission. Tool permissions are checked by the runtime, even though all definitions remain visible. Never call a disabled operation.",
77
+ "Messages that start with `[Freeflow notice, not from the user]` come from Freeflow, not the user. They report facts and harness steps; they are not the user's instructions.",
78
+ ...sections
79
+ .filter(([, text]) => isPromptAvailable(text))
80
+ .map(([feature, text]) => `## Reference guidance: ${feature}\n\n${text.trim()}`),
81
+ ].join("\n\n");
82
+ }
@@ -1,5 +1,6 @@
1
1
  import { createHash } from "node:crypto";
2
- import { open, stat } from "node:fs/promises";
2
+ import { statSync } from "node:fs";
3
+ import { lstat, open, stat } from "node:fs/promises";
3
4
  import { TextDecoder } from "node:util";
4
5
  export class ReadOnlySessionError extends Error {
5
6
  code;
@@ -50,7 +51,10 @@ export function parseReadOnlySessionText(text) {
50
51
  const seen = new Set();
51
52
  return lines.map((line, i) => decodeEntry(line, i + 1, seen));
52
53
  }
54
+ /** Whole-file snapshots read so far; tests check that a verified startup reads none. */
55
+ export const readOnlySessionCounters = { snapshots: 0 };
53
56
  export async function readOnlySessionSnapshot(path, options = {}) {
57
+ readOnlySessionCounters.snapshots++;
54
58
  const limits = { ...SESSION_READ_LIMITS, ...options };
55
59
  if (Object.values(limits).some((n) => !Number.isSafeInteger(n) || n <= 0))
56
60
  throw new ReadOnlySessionError("invalid_limit", "Invalid snapshot limit.");
@@ -146,3 +150,110 @@ export function activeReadOnlySessionBranch(snapshot, leafId) {
146
150
  }
147
151
  return path.reverse();
148
152
  }
153
+ // Byte offset and entry count up to which a session file is known to equal Pi's in-memory entries.
154
+ const verified = new Map();
155
+ /** Pi just parsed this file into memory, so its current bytes are the verified prefix. */
156
+ export function trustLoadedSession(reader) {
157
+ const path = reader.getSessionFile?.();
158
+ if (!path) return;
159
+ try {
160
+ verified.set(path, { size: statSync(path).size, count: reader.getEntries().length });
161
+ } catch {
162
+ verified.delete(path);
163
+ }
164
+ }
165
+ /**
166
+ * At `session_start`, trust the file only when Pi just parsed it. A reload keeps Pi's session in memory without
167
+ * re-reading the file, so it establishes nothing: after a failed write, memory could hold an entry the file lacks.
168
+ * The previous trust point, or a full check, still applies after a reload.
169
+ */
170
+ export function trustStartedSession(reason, reader) {
171
+ if (reason !== "reload") trustLoadedSession(reader);
172
+ }
173
+ async function tailMatches(path, reader) {
174
+ const point = verified.get(path);
175
+ if (!point) return false;
176
+ const entries = reader.getEntries();
177
+ let file;
178
+ try {
179
+ file = await open(path, "r");
180
+ const { size } = await file.stat();
181
+ if (size < point.size || entries.length < point.count) return false;
182
+ const buffer = Buffer.alloc(size - point.size);
183
+ const { bytesRead } = await file.read(buffer, 0, buffer.length, point.size);
184
+ if (bytesRead !== buffer.length) return false;
185
+ const lines = new TextDecoder("utf-8", { fatal: true }).decode(buffer).split("\n");
186
+ if (lines.pop() !== "") return false;
187
+ const appended = entries.slice(point.count);
188
+ // Pi appends each entry as its JSON serialization, in memory order.
189
+ if (lines.length !== appended.length || lines.some((line, i) => line !== JSON.stringify(appended[i]))) return false;
190
+ verified.set(path, { size, count: entries.length });
191
+ return true;
192
+ } catch {
193
+ return false;
194
+ } finally {
195
+ await file?.close();
196
+ }
197
+ }
198
+ /**
199
+ * Whether every entry Pi holds is persisted, judged only by the bytes appended since the trust point: true when the
200
+ * file grew by exactly those entries. False when there is no trust point or anything differs; callers then fall back
201
+ * to a full snapshot.
202
+ */
203
+ export async function persistedTailMatches(reader) {
204
+ const path = reader.getSessionFile?.();
205
+ return !!path && (await tailMatches(path, reader));
206
+ }
207
+ /** Verify one newly appended native anchor without re-reading the historical session. */
208
+ export async function persistedLastEntryMatches(path, entry) {
209
+ if (!path) return false;
210
+ let file;
211
+ try {
212
+ const expected = Buffer.from(`${JSON.stringify(entry)}\n`, "utf8");
213
+ if (!expected.length || expected.length > 1024 * 1024) return false;
214
+ const named = await lstat(path, { bigint: true });
215
+ if (!named.isFile() || named.isSymbolicLink() || named.size < BigInt(expected.length)) return false;
216
+ file = await open(path, "r");
217
+ const before = await file.stat({ bigint: true });
218
+ if (before.size > BigInt(Number.MAX_SAFE_INTEGER)) return false;
219
+ const actual = Buffer.alloc(expected.length);
220
+ let offset = 0;
221
+ while (offset < actual.length) {
222
+ const read = await file.read(
223
+ actual,
224
+ offset,
225
+ actual.length - offset,
226
+ Number(before.size) - actual.length + offset,
227
+ );
228
+ if (!read.bytesRead) return false;
229
+ offset += read.bytesRead;
230
+ }
231
+ const after = await file.stat({ bigint: true });
232
+ const current = await lstat(path, { bigint: true });
233
+ const signature = (value) => [value.dev, value.ino, value.size, value.mtimeNs, value.ctimeNs].join(":");
234
+ return (
235
+ before.isFile() &&
236
+ signature(named) === signature(before) &&
237
+ signature(before) === signature(after) &&
238
+ signature(after) === signature(current) &&
239
+ actual.equals(expected)
240
+ );
241
+ } catch {
242
+ return false;
243
+ } finally {
244
+ await file?.close();
245
+ }
246
+ }
247
+ /** Whether the persisted session holds exactly this branch: tail-only after a trust point, else a full comparison. */
248
+ export async function persistedBranchMatches(reader, leaf, branch) {
249
+ const path = reader.getSessionFile?.();
250
+ if (!path) return false;
251
+ if (await tailMatches(path, reader)) return true;
252
+ const snapshot = await readOnlySessionSnapshot(path);
253
+ const matches =
254
+ snapshot.sessionId === reader.getSessionId() &&
255
+ JSON.stringify(activeReadOnlySessionBranch(snapshot, leaf)) === JSON.stringify(branch);
256
+ if (matches) trustLoadedSession(reader);
257
+ else verified.delete(path);
258
+ return matches;
259
+ }