@hassangameryt/freeflow 0.7.3 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (276) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/.codex-plugin/plugin.json +1 -1
  4. package/.cursor-plugin/plugin.json +1 -1
  5. package/NOTICE +14 -0
  6. package/README.md +26 -31
  7. package/capabilities/cognitive-routing/SKILL.md +106 -161
  8. package/capabilities/cognitive-routing/references/both-mode.md +36 -0
  9. package/capabilities/cognitive-routing/references/executor-mode.md +48 -0
  10. package/capabilities/cognitive-routing/references/helper-mode.md +47 -0
  11. package/capabilities/compaction/SKILL.md +49 -0
  12. package/capabilities/compaction/references/summary-format.md +239 -0
  13. package/command-surface.json +4 -0
  14. package/gemini-extension.json +1 -1
  15. package/hooks/shared/runtime-context.mjs +28 -30
  16. package/licenses/Apache-2.0.txt +55 -0
  17. package/package.json +22 -9
  18. package/pi-extension/README.md +41 -0
  19. package/pi-extension/dist/cognitive-routing/assembler.js +393 -0
  20. package/pi-extension/dist/{cognitive-routing-v2 → cognitive-routing}/config.js +5 -2
  21. package/pi-extension/dist/cognitive-routing/economics.js +45 -0
  22. package/pi-extension/dist/cognitive-routing/event-schema.js +388 -0
  23. package/pi-extension/dist/{session-sources/events.js → cognitive-routing/event-store.js} +103 -40
  24. package/pi-extension/dist/cognitive-routing/gate.js +241 -0
  25. package/pi-extension/dist/cognitive-routing/handlers.js +899 -0
  26. package/pi-extension/dist/cognitive-routing/handoffs.js +350 -0
  27. package/pi-extension/dist/cognitive-routing/model-control.js +467 -0
  28. package/pi-extension/dist/{cognitive-routing-v2 → cognitive-routing}/projection.js +229 -56
  29. package/pi-extension/dist/cognitive-routing/provenance.js +81 -0
  30. package/pi-extension/dist/cognitive-routing/runtime.js +396 -0
  31. package/pi-extension/dist/{cognitive-routing-v2 → cognitive-routing}/schemas.js +43 -7
  32. package/pi-extension/dist/cognitive-routing/session.js +221 -0
  33. package/pi-extension/dist/{session-sources → cognitive-routing}/sources.js +99 -85
  34. package/pi-extension/dist/{cognitive-routing-v2 → cognitive-routing}/state.js +37 -378
  35. package/pi-extension/dist/cognitive-routing/status.js +80 -0
  36. package/pi-extension/dist/{cognitive-routing-v2 → cognitive-routing}/tools.js +12 -12
  37. package/pi-extension/dist/{cognitive-routing-v2 → cognitive-routing}/types.js +5 -11
  38. package/pi-extension/dist/compaction/carry.js +191 -0
  39. package/pi-extension/dist/compaction/config.js +22 -0
  40. package/pi-extension/dist/compaction/controller.js +471 -0
  41. package/pi-extension/dist/compaction/harness.js +77 -0
  42. package/pi-extension/dist/compaction/results.js +158 -0
  43. package/pi-extension/dist/compaction/thresholds.js +35 -0
  44. package/pi-extension/dist/compaction/tool.js +57 -0
  45. package/pi-extension/dist/host/branch.js +13 -0
  46. package/pi-extension/dist/host/canonical.js +12 -0
  47. package/pi-extension/dist/host/catalog.js +77 -0
  48. package/pi-extension/dist/host/config.js +374 -0
  49. package/pi-extension/dist/host/projection-tags.js +60 -0
  50. package/pi-extension/dist/host/prompts.js +82 -0
  51. package/pi-extension/dist/{session-sources → host}/read-only-session.js +112 -1
  52. package/pi-extension/dist/{runtime → host}/request-history.js +121 -39
  53. package/pi-extension/dist/host/runtime-state.js +189 -0
  54. package/pi-extension/dist/{settings/settings-ui.js → host/settings/freeflow-command.js} +620 -410
  55. package/pi-extension/dist/{settings/settings-tui.js → host/settings/settings-view.js} +186 -73
  56. package/pi-extension/dist/host/staging.js +28 -0
  57. package/pi-extension/dist/host/status.js +101 -0
  58. package/pi-extension/dist/index.js +423 -173
  59. package/pi-extension/dist/provider-support/cache/anchor.js +21 -0
  60. package/pi-extension/dist/provider-support/cache/anthropic.js +102 -0
  61. package/pi-extension/dist/provider-support/cache/health.js +73 -0
  62. package/pi-extension/dist/provider-support/cache/index.js +58 -0
  63. package/pi-extension/dist/provider-support/cache/keepalive.js +210 -0
  64. package/pi-extension/dist/provider-support/cache/monitor.js +17 -0
  65. package/pi-extension/dist/provider-support/cache/openai-responses.js +48 -0
  66. package/pi-extension/dist/provider-support/effort.js +13 -0
  67. package/pi-extension/dist/provider-support/index.js +44 -3
  68. package/pi-extension/dist/provider-support/{astra → openai}/adapter.js +46 -29
  69. package/pi-extension/dist/provider-support/{astra → openai}/history.js +10 -8
  70. package/pi-extension/dist/provider-support/{astra → openai}/index.js +8 -4
  71. package/pi-extension/dist/provider-support/{astra → openai}/session-state.js +29 -38
  72. package/pi-extension/dist/provider-support/routes.js +17 -0
  73. package/pi-extension/dist/tool-execution/apply-patch/grammar.js +23 -0
  74. package/pi-extension/dist/tool-execution/apply-patch/match.js +38 -0
  75. package/pi-extension/dist/tool-execution/apply-patch/parser.js +177 -0
  76. package/pi-extension/dist/tool-execution/apply-patch/plan.js +201 -0
  77. package/pi-extension/dist/tool-execution/apply-patch/tool.js +84 -0
  78. package/pi-extension/dist/tool-execution/background.js +238 -0
  79. package/pi-extension/dist/tool-execution/bash-guard.js +23 -0
  80. package/pi-extension/dist/tool-execution/config.js +22 -0
  81. package/pi-extension/dist/tool-execution/edit-messages.js +120 -0
  82. package/pi-extension/dist/tool-execution/file-state.js +138 -0
  83. package/pi-extension/dist/tool-execution/file-tracking.js +143 -0
  84. package/pi-extension/dist/tool-execution/prompt.js +54 -0
  85. package/pi-extension/dist/tool-execution/tools.js +16 -0
  86. package/pi-extension/freeflow/README.md +9 -0
  87. package/plugin.json +1 -1
  88. package/runtime/prompts/cognitive-routing.md +4 -4
  89. package/runtime/prompts/core.md +22 -75
  90. package/runtime/prompts/interaction-contract.md +15 -8
  91. package/runtime/prompts/tool-execution.md +38 -0
  92. package/runtime/prompts/working-method.md +108 -0
  93. package/skills/action-selection/SKILL.md +39 -47
  94. package/skills/action-selection/references/trajectory-stalls.md +2 -0
  95. package/skills/bypass/SKILL.md +2 -2
  96. package/skills/commit-work/SKILL.md +31 -83
  97. package/skills/commit-work/references/git-state-edges.md +1 -1
  98. package/skills/commit-work/references/simple-push.md +2 -0
  99. package/skills/commit-work/references/staging-decisions.md +1 -1
  100. package/skills/decision-gate/SKILL.md +31 -50
  101. package/skills/design-for-depth/SKILL.md +44 -53
  102. package/skills/design-for-depth/references/design-pressure-signals.md +46 -144
  103. package/skills/design-for-depth/references/implementation-decisions.md +1 -1
  104. package/skills/design-for-depth/references/interface-design-loop.md +5 -36
  105. package/skills/design-for-depth/references/module-and-dependency-design.md +2 -32
  106. package/skills/design-for-depth/references/state-and-failure-boundaries.md +2 -13
  107. package/skills/diagnose-failure/SKILL.md +39 -24
  108. package/skills/diagnose-failure/references/diagnostic-loop-catalog.md +2 -0
  109. package/skills/diagnose-failure/references/flaky-and-performance-diagnosis.md +2 -2
  110. package/skills/discuss/SKILL.md +35 -11
  111. package/skills/discuss/references/discussion-continuity.md +7 -27
  112. package/skills/evaluate-skill/SKILL.md +49 -80
  113. package/skills/evaluate-skill/references/evaluation-definition-schema.md +35 -5
  114. package/skills/evaluate-skill/references/evaluation-design.md +14 -14
  115. package/skills/evaluate-skill/references/execution-and-evidence.md +26 -5
  116. package/skills/evaluate-skill/references/review-and-revision.md +3 -1
  117. package/skills/evaluate-skill/scripts/lib/definitions.mjs +49 -8
  118. package/skills/evaluate-skill/scripts/lib/pi.mjs +14 -7
  119. package/skills/evaluate-skill/scripts/lib/review.mjs +196 -0
  120. package/skills/evaluate-skill/scripts/lib/runner.mjs +45 -8
  121. package/skills/evaluate-skill/scripts/lib/sandbox.mjs +2 -0
  122. package/skills/evaluate-skill/scripts/lib/trials.mjs +103 -0
  123. package/skills/evaluate-skill/scripts/lib/view.mjs +67 -1
  124. package/skills/evaluate-skill/scripts/pi-command-tool.mjs +144 -0
  125. package/skills/evaluate-skill/scripts/pi-guard.mjs +30 -13
  126. package/skills/evaluate-skill/scripts/skill-eval.mjs +114 -34
  127. package/skills/execute-work/SKILL.md +41 -9
  128. package/skills/execute-work/references/code-practices.md +2 -0
  129. package/skills/execute-work/references/execute-work-edges.md +11 -1
  130. package/skills/handoff/SKILL.md +1 -1
  131. package/skills/handoff/references/handoff-templates.md +1 -1
  132. package/skills/review-artifact/SKILL.md +42 -41
  133. package/skills/review-artifact/references/adjudicate-artifact-review.md +1 -1
  134. package/skills/review-artifact/references/independent-artifact-reviewer-contract.md +1 -1
  135. package/skills/review-work/SKILL.md +38 -32
  136. package/skills/review-work/references/adjudicate-work-review.md +1 -1
  137. package/skills/review-work/references/independent-work-reviewer-contract.md +1 -1
  138. package/skills/review-work/references/security-risk-lens.md +2 -4
  139. package/skills/setup-freeflow/SKILL.md +3 -1
  140. package/skills/setup-freeflow/references/activation-contract.md +7 -13
  141. package/skills/setup-freeflow/references/host-setup.md +3 -3
  142. package/skills/track-work/SKILL.md +113 -161
  143. package/skills/track-work/references/working-record-format.md +189 -350
  144. package/skills/track-work/scripts/lib/document.mjs +62 -6
  145. package/skills/track-work/scripts/lib/format.mjs +16 -1
  146. package/skills/track-work/scripts/lib/store.mjs +9 -8
  147. package/skills/track-work/scripts/lib/transitions.mjs +79 -4
  148. package/skills/track-work/scripts/working-record.mjs +34 -12
  149. package/skills/verify-work/SKILL.md +29 -20
  150. package/skills/verify-work/references/browser-runtime-evidence.md +1 -5
  151. package/skills/verify-work/references/integration-evidence.md +2 -0
  152. package/skills/verify-work/references/performance-evidence.md +3 -1
  153. package/skills/verify-work/references/test-design.md +3 -1
  154. package/skills/workflow/SKILL.md +65 -21
  155. package/skills/workflow/references/domain-skill-composition.md +5 -3
  156. package/skills/write-docs/SKILL.md +83 -0
  157. package/skills/write-docs/references/documentation-shapes.md +119 -0
  158. package/skills/write-docs/scripts/docs-check.mjs +139 -0
  159. package/skills/write-plan/SKILL.md +40 -40
  160. package/skills/write-plan/references/plan-shapes.md +1 -1
  161. package/skills/write-skill/SKILL.md +107 -147
  162. package/skills/write-skill/references/activation-boundaries.md +2 -0
  163. package/skills/write-skill/references/agent-first-instructions.md +22 -10
  164. package/skills/write-skill/references/progressive-disclosure.md +4 -2
  165. package/skills/write-skill/references/skill-dependency-graphs.md +2 -0
  166. package/skills/write-skill/scripts/lib/similarity.mjs +56 -0
  167. package/skills/write-skill/scripts/skill-author.mjs +18 -2
  168. package/skills/write-spec/SKILL.md +34 -41
  169. package/skills/write-spec/references/artifact-standards.md +1 -1
  170. package/skills/write-spec/references/decision-records.md +2 -0
  171. package/skills/write-spec/references/spec-shapes.md +1 -1
  172. package/capabilities/context-virtualization/SKILL.md +0 -103
  173. package/capabilities/conversation-history/SKILL.md +0 -53
  174. package/hooks/tests/check-host-adapters.test.mjs +0 -82
  175. package/hooks/tests/check-runtime-context-hook.sh +0 -212
  176. package/pi-extension/dist/cognitive-routing-v2/runtime.js +0 -2181
  177. package/pi-extension/dist/context-virtualization/commands.js +0 -89
  178. package/pi-extension/dist/context-virtualization/projector.js +0 -42
  179. package/pi-extension/dist/context-virtualization/runtime.js +0 -378
  180. package/pi-extension/dist/context-virtualization/state.js +0 -57
  181. package/pi-extension/dist/context-virtualization/types.js +0 -10
  182. package/pi-extension/dist/conversation-history/passages.js +0 -66
  183. package/pi-extension/dist/conversation-history/ranking.js +0 -105
  184. package/pi-extension/dist/conversation-history/runtime.js +0 -226
  185. package/pi-extension/dist/conversation-history/search.js +0 -56
  186. package/pi-extension/dist/conversation-history/types.js +0 -1
  187. package/pi-extension/dist/freeflow-context/resolver.js +0 -120
  188. package/pi-extension/dist/freeflow-context/runtime.js +0 -80
  189. package/pi-extension/dist/freeflow-context/source-projector.js +0 -95
  190. package/pi-extension/dist/freeflow-context/tool.js +0 -314
  191. package/pi-extension/dist/freeflow-context/types.js +0 -12
  192. package/pi-extension/dist/freeflow-context/visibility.js +0 -28
  193. package/pi-extension/dist/integrations/mcp-config.js +0 -29
  194. package/pi-extension/dist/runtime/runtime-context.js +0 -754
  195. package/pi-extension/dist/runtime/runtime-identity.js +0 -5
  196. package/pi-extension/dist/session-sources/provenance.js +0 -32
  197. package/pi-extension/dist/ui/text-component.js +0 -57
  198. package/pi-extension/src/cognitive-routing-v2/budget.ts +0 -42
  199. package/pi-extension/src/cognitive-routing-v2/config.ts +0 -173
  200. package/pi-extension/src/cognitive-routing-v2/projection.ts +0 -375
  201. package/pi-extension/src/cognitive-routing-v2/render.ts +0 -277
  202. package/pi-extension/src/cognitive-routing-v2/runtime.ts +0 -2241
  203. package/pi-extension/src/cognitive-routing-v2/schemas.ts +0 -180
  204. package/pi-extension/src/cognitive-routing-v2/state.ts +0 -839
  205. package/pi-extension/src/cognitive-routing-v2/tools.ts +0 -62
  206. package/pi-extension/src/cognitive-routing-v2/types.ts +0 -254
  207. package/pi-extension/src/context-virtualization/commands.ts +0 -111
  208. package/pi-extension/src/context-virtualization/projector.ts +0 -62
  209. package/pi-extension/src/context-virtualization/runtime.ts +0 -428
  210. package/pi-extension/src/context-virtualization/state.ts +0 -79
  211. package/pi-extension/src/context-virtualization/types.ts +0 -92
  212. package/pi-extension/src/conversation-history/passages.ts +0 -81
  213. package/pi-extension/src/conversation-history/ranking.ts +0 -155
  214. package/pi-extension/src/conversation-history/runtime.ts +0 -250
  215. package/pi-extension/src/conversation-history/search.ts +0 -79
  216. package/pi-extension/src/conversation-history/types.ts +0 -31
  217. package/pi-extension/src/freeflow-context/resolver.ts +0 -139
  218. package/pi-extension/src/freeflow-context/runtime.ts +0 -100
  219. package/pi-extension/src/freeflow-context/source-projector.ts +0 -106
  220. package/pi-extension/src/freeflow-context/tool.ts +0 -366
  221. package/pi-extension/src/freeflow-context/types.ts +0 -70
  222. package/pi-extension/src/freeflow-context/visibility.ts +0 -53
  223. package/pi-extension/src/index.ts +0 -390
  224. package/pi-extension/src/integrations/mcp-config.ts +0 -32
  225. package/pi-extension/src/provider-support/astra/adapter.ts +0 -108
  226. package/pi-extension/src/provider-support/astra/history.ts +0 -145
  227. package/pi-extension/src/provider-support/astra/index.ts +0 -12
  228. package/pi-extension/src/provider-support/astra/session-state.ts +0 -113
  229. package/pi-extension/src/provider-support/index.ts +0 -5
  230. package/pi-extension/src/runtime/request-history.ts +0 -243
  231. package/pi-extension/src/runtime/runtime-context.ts +0 -860
  232. package/pi-extension/src/runtime/runtime-identity.ts +0 -8
  233. package/pi-extension/src/session-sources/events.ts +0 -230
  234. package/pi-extension/src/session-sources/provenance.ts +0 -39
  235. package/pi-extension/src/session-sources/read-only-session.ts +0 -170
  236. package/pi-extension/src/session-sources/sources.ts +0 -334
  237. package/pi-extension/src/settings/settings-tui.ts +0 -501
  238. package/pi-extension/src/settings/settings-ui.ts +0 -1652
  239. package/pi-extension/src/ui/text-component.ts +0 -66
  240. package/pi-extension/tests/cache-reuse/history.test.js +0 -117
  241. package/pi-extension/tests/cache-reuse/native.test.js +0 -91
  242. package/pi-extension/tests/cache-reuse/settings.test.js +0 -211
  243. package/pi-extension/tests/cognitive-routing-v2/communication.test.js +0 -577
  244. package/pi-extension/tests/cognitive-routing-v2/compaction-input.test.js +0 -183
  245. package/pi-extension/tests/cognitive-routing-v2/delegation.test.js +0 -557
  246. package/pi-extension/tests/cognitive-routing-v2/native.test.js +0 -474
  247. package/pi-extension/tests/cognitive-routing-v2/performance.test.js +0 -87
  248. package/pi-extension/tests/cognitive-routing-v2/persistence.test.js +0 -300
  249. package/pi-extension/tests/cognitive-routing-v2/projection.test.js +0 -241
  250. package/pi-extension/tests/cognitive-routing-v2/recovery.test.js +0 -1034
  251. package/pi-extension/tests/cognitive-routing-v2/render.test.js +0 -244
  252. package/pi-extension/tests/cognitive-routing-v2/runtime.test.js +0 -423
  253. package/pi-extension/tests/cognitive-routing-v2/stabilization.test.js +0 -521
  254. package/pi-extension/tests/cognitive-routing-v2/state.test.js +0 -586
  255. package/pi-extension/tests/context-virtualization/extension.test.js +0 -775
  256. package/pi-extension/tests/context-virtualization/projector.test.js +0 -110
  257. package/pi-extension/tests/context-virtualization/runtime.test.js +0 -230
  258. package/pi-extension/tests/conversation-history/runtime.test.js +0 -98
  259. package/pi-extension/tests/conversation-history/search.test.js +0 -97
  260. package/pi-extension/tests/fixtures/pi-host.js +0 -4
  261. package/pi-extension/tests/fixtures/routing-native.js +0 -191
  262. package/pi-extension/tests/integration/mode-free-core.test.js +0 -220
  263. package/pi-extension/tests/integration/pi-extension.test.js +0 -1447
  264. package/pi-extension/tests/integration/prompt-architecture.test.js +0 -447
  265. package/pi-extension/tests/integration/unaffected-regressions.test.js +0 -478
  266. package/pi-extension/tests/provider-support/astra-boundaries.test.js +0 -75
  267. package/pi-extension/tests/provider-support/astra.test.js +0 -306
  268. package/pi-extension/tests/provider-support/cli.test.js +0 -92
  269. package/pi-extension/tests/provider-support/native.test.js +0 -183
  270. package/pi-extension/tests/settings/settings-tui.test.js +0 -243
  271. package/pi-extension/tsconfig.json +0 -16
  272. package/runtime/prompts/context-virtualization.md +0 -3
  273. package/runtime/prompts/conversation-history.md +0 -3
  274. package/skills/design-for-depth/references/software-design-philosophy.md +0 -202
  275. /package/pi-extension/dist/{cognitive-routing-v2 → cognitive-routing}/budget.js +0 -0
  276. /package/pi-extension/dist/{cognitive-routing-v2 → cognitive-routing}/render.js +0 -0
@@ -1,22 +1,11 @@
1
1
  import { assemble, EFFORTS } from "./history.js";
2
2
  import { SessionState } from "./session-state.js";
3
- export function requestKey(payload, model) {
4
- if (
5
- !payload ||
6
- payload.model !== "gpt-6-astra" ||
7
- model?.id !== payload.model ||
8
- payload.store !== false ||
9
- !Array.isArray(payload.input) ||
10
- !EFFORTS.includes(payload.reasoning?.effort) ||
11
- payload.previous_response_id ||
12
- payload.conversation ||
13
- payload.context_management ||
14
- (payload.truncation && payload.truncation !== "disabled") ||
15
- (payload.reasoning.mode && payload.reasoning.mode !== "standard") ||
16
- payload.agents ||
17
- payload.input.some((x) => x?.type === "configuration_update" || x?.type === "compaction_trigger")
18
- )
19
- return;
3
+ import { isChatGPTSignIn } from "../routes.js";
4
+ // Only these qualified official routes receive effort-history adaptation; keys isolate models.
5
+ const SUPPORTED_MODELS = new Set(["gpt-6-astra", "gpt-6-luna", "gpt-6-sol"]);
6
+ /** The effort-history key for a qualified official GPT-6 route, or undefined when the route is not supported. */
7
+ export function effortHistoryRoute(model) {
8
+ if (!model || !SUPPORTED_MODELS.has(model.id)) return;
20
9
  const codex = model.provider === "openai-codex" && model.api === "openai-codex-responses";
21
10
  const api = model.provider === "openai" && model.api === "openai-responses";
22
11
  if (!codex && !api) return;
@@ -35,15 +24,43 @@ export function requestKey(payload, model) {
35
24
  if (!allowed.includes(base)) return;
36
25
  return `${model.provider}/${model.api}/${model.id}`;
37
26
  }
38
- export class AstraAdapter {
27
+ export function requestKey(payload, model) {
28
+ if (
29
+ !payload ||
30
+ !SUPPORTED_MODELS.has(payload.model) ||
31
+ model?.id !== payload.model ||
32
+ payload.store !== false ||
33
+ !Array.isArray(payload.input) ||
34
+ !EFFORTS.includes(payload.reasoning?.effort) ||
35
+ payload.previous_response_id ||
36
+ payload.conversation ||
37
+ payload.context_management ||
38
+ (payload.truncation && payload.truncation !== "disabled") ||
39
+ (payload.reasoning.mode && payload.reasoning.mode !== "standard") ||
40
+ payload.agents ||
41
+ payload.input.some((x) => x?.type === "configuration_update" || x?.type === "compaction_trigger")
42
+ )
43
+ return;
44
+ return effortHistoryRoute(model);
45
+ }
46
+ export class OpenAIEffortAdapter {
39
47
  pi;
48
+ enabled;
49
+ monitor;
40
50
  session;
41
51
  generation = 0;
42
52
  compacting = false;
43
53
  queue = Promise.resolve();
44
54
  statusSignature;
45
- constructor(pi) {
55
+ constructor(
56
+ pi,
57
+ // Global Freeflow disablement leaves provider requests and session history untouched.
58
+ enabled = () => true,
59
+ monitor,
60
+ ) {
46
61
  this.pi = pi;
62
+ this.enabled = enabled;
63
+ this.monitor = monitor;
47
64
  }
48
65
  reset(ctx) {
49
66
  this.generation++;
@@ -54,22 +71,22 @@ export class AstraAdapter {
54
71
  setCompacting(value) {
55
72
  this.compacting = value;
56
73
  }
57
- status(ctx, label) {
74
+ status(_ctx, label) {
58
75
  if (this.statusSignature === label) return;
59
76
  this.statusSignature = label;
60
- // Diagnostics must never turn a completed payload transformation into an exception.
61
- try {
62
- ctx.ui?.setStatus?.("freeflow-astra-effort", label);
63
- } catch {}
77
+ // A cache diagnostic, reported to /freeflow status rather than the settings footer.
78
+ this.monitor?.set("openai-effort", label);
64
79
  }
65
80
  async adapt(payload, ctx) {
66
81
  const generation = this.generation;
67
82
  const operation = this.queue.then(async () => {
68
- const key = requestKey(payload, ctx.model);
69
- if (generation !== this.generation || this.compacting || !key) {
83
+ // Sign in with ChatGPT rejects configuration_update items (live, 400), so effort history cannot apply.
84
+ const key = isChatGPTSignIn(ctx.model, ctx) ? undefined : requestKey(payload, ctx.model);
85
+ if (generation !== this.generation || this.compacting || !key || !this.enabled()) {
70
86
  this.status(ctx, undefined);
71
87
  return payload;
72
88
  }
89
+ const label = ctx.model.id === "gpt-6-astra" ? "Astra" : ctx.model.id;
73
90
  try {
74
91
  const reader = ctx.sessionManager;
75
92
  if (
@@ -92,15 +109,15 @@ export class AstraAdapter {
92
109
  this.session?.store !== store ||
93
110
  key !== requestKey(payload, ctx.model)
94
111
  )
95
- throw new Error("Astra request changed during preparation");
112
+ throw new Error("OpenAI effort request changed during preparation");
96
113
  const result = assemble(payload, key, store.generation(), basis, records);
97
114
  if (result.record) store.append(result.record);
98
- this.status(ctx, `Astra ${result.effective} · cache baseline ${result.baseline}`);
115
+ this.status(ctx, `${label} ${result.effective} · cache baseline ${result.baseline}`);
99
116
  return result.payload;
100
117
  } catch {
101
118
  // Pi swallows hook exceptions. Return the untouched full-history request,
102
119
  // including its requested effort, instead of a partially pinned request.
103
- this.status(ctx, "Astra cache adaptation unavailable · native effort");
120
+ this.status(ctx, `${label} cache adaptation unavailable · native effort`);
104
121
  return payload;
105
122
  }
106
123
  });
@@ -1,5 +1,6 @@
1
1
  import { createHash, randomUUID } from "node:crypto";
2
- export const ENTRY_TYPE = "freeflow-astra-effort-v1";
2
+ // This namespace starts a fresh effort chain; old Astra entries are deliberately ignored.
3
+ export const ENTRY_TYPE = "freeflow-openai-effort-v1";
3
4
  export const EFFORTS = ["low", "medium", "high", "xhigh", "max"];
4
5
  export const hash = (value) => createHash("sha256").update(JSON.stringify(value)).digest("hex");
5
6
  const digest = (x) => typeof x === "string" && /^[a-f0-9]{64}$/.test(x);
@@ -45,7 +46,7 @@ export function parseAttempt(value) {
45
46
  ].includes(k),
46
47
  )
47
48
  )
48
- throw new Error("Invalid Astra effort history");
49
+ throw new Error("Invalid OpenAI effort history");
49
50
  return value;
50
51
  }
51
52
  // Hash complete serialized items, including opaque reasoning; never persist their bodies.
@@ -72,16 +73,16 @@ export function assemble(payload, key, generation, basis, records) {
72
73
  const seen = new Set();
73
74
  let cursor = prior;
74
75
  while (cursor) {
75
- if (seen.has(cursor.id)) throw new Error("Cyclic Astra history");
76
+ if (seen.has(cursor.id)) throw new Error("Cyclic OpenAI effort history");
76
77
  seen.add(cursor.id);
77
78
  if (cursor.update) {
78
- if (hashes[cursor.update.at] !== cursor.update.prefixHash) throw new Error("Astra anchor changed");
79
+ if (hashes[cursor.update.at] !== cursor.update.prefixHash) throw new Error("OpenAI effort anchor changed");
79
80
  updates.unshift(cursor.update);
80
81
  }
81
82
  if (cursor.parent === null) break;
82
83
  const parent = byId.get(cursor.parent);
83
84
  if (!parent || parent.key !== key || parent.generation !== generation || parent.baseline !== baseline)
84
- throw new Error("Astra history parent unavailable");
85
+ throw new Error("OpenAI effort history parent unavailable");
85
86
  cursor = parent;
86
87
  }
87
88
  let update;
@@ -90,10 +91,10 @@ export function assemble(payload, key, generation, basis, records) {
90
91
  const last = payload.input.length - 1;
91
92
  const at = payload.input[last]?.role === "user" && last >= (prior?.length ?? 0) ? last : payload.input.length;
92
93
  update = { at, prefixHash: hashes[at], effort: desired };
93
- if (updates.at(-1)?.at === at) throw new Error("Adjacent Astra effort updates");
94
+ if (updates.at(-1)?.at === at) throw new Error("Adjacent OpenAI effort updates");
94
95
  updates.push(update);
95
96
  }
96
- if (updates.some((u, i) => i > 0 && u.at <= updates[i - 1].at)) throw new Error("Invalid Astra update order");
97
+ if (updates.some((u, i) => i > 0 && u.at <= updates[i - 1].at)) throw new Error("Invalid OpenAI effort update order");
97
98
  const output = structuredClone(payload);
98
99
  output.input = [];
99
100
  let nextUpdate = 0;
@@ -104,7 +105,8 @@ export function assemble(payload, key, generation, basis, records) {
104
105
  if (i < payload.input.length) output.input.push(structuredClone(payload.input[i]));
105
106
  }
106
107
  output.reasoning.effort = baseline;
107
- const identical = prior?.length === payload.input.length && prior.effective === desired;
108
+ // Records mark baselines and effort changes; a longer input under the same effort needs none.
109
+ const identical = !!prior && !update;
108
110
  const record = identical
109
111
  ? undefined
110
112
  : {
@@ -1,7 +1,11 @@
1
- import { AstraAdapter } from "./adapter.js";
2
- export function registerAstraSupport(pi) {
3
- const adapter = new AstraAdapter(pi);
4
- pi.on("session_start", (_event, ctx) => adapter.reset(ctx));
1
+ import { OpenAIEffortAdapter } from "./adapter.js";
2
+ import { trustStartedSession } from "../../host/read-only-session.js";
3
+ export function registerOpenAIEffortSupport(pi, enabled, monitor) {
4
+ const adapter = new OpenAIEffortAdapter(pi, enabled, monitor);
5
+ pi.on("session_start", (event, ctx) => {
6
+ trustStartedSession(event?.reason, ctx.sessionManager);
7
+ adapter.reset(ctx);
8
+ });
5
9
  pi.on("session_shutdown", (_event, ctx) => adapter.reset(ctx));
6
10
  pi.on("session_tree", (_event, ctx) => adapter.reset(ctx));
7
11
  pi.on("session_before_compact", () => adapter.setCompacting(true));
@@ -1,5 +1,6 @@
1
- import { activeReadOnlySessionBranch, readOnlySessionSnapshot } from "../../session-sources/read-only-session.js";
1
+ import { persistedBranchMatches } from "../../host/read-only-session.js";
2
2
  import { ENTRY_TYPE, parseAttempt } from "./history.js";
3
+ import { cachedBranch } from "../../host/branch.js";
3
4
  export class SessionState {
4
5
  pi;
5
6
  reader;
@@ -10,35 +11,33 @@ export class SessionState {
10
11
  this.reader = reader;
11
12
  }
12
13
  branch() {
13
- const entries = this.reader.getBranch();
14
+ const entries = cachedBranch(this.reader);
14
15
  let parent = null;
15
16
  const seen = new Set();
16
17
  for (const entry of entries) {
17
18
  if (!entry.id || seen.has(entry.id) || entry.parentId !== parent)
18
- throw new Error("Invalid Astra session ancestry");
19
+ throw new Error("Invalid OpenAI effort session ancestry");
19
20
  seen.add(entry.id);
20
21
  parent = entry.id;
21
22
  }
22
- if (parent !== this.reader.getLeafId()) throw new Error("Astra session leaf changed");
23
+ if (parent !== this.reader.getLeafId()) throw new Error("OpenAI effort session leaf changed");
23
24
  return entries;
24
25
  }
25
26
  async records() {
26
27
  if (this.fault) throw new Error(this.fault);
27
28
  const branch = this.branch();
28
- // Model visits are not history replacement. Derive both identities from the
29
- // selected native ancestry so old v1 records remain readable without rewriting them.
30
- const boundaries = new Map();
29
+ // Model visits and branch summaries preserve compatible history; compaction does not. A branch summary
30
+ // follows its branch point, leaving the path before it unchanged. Records written while branch summaries
31
+ // still started a generation carry that earlier boundary and stay readable.
32
+ const boundaries = new Map(),
33
+ earlier = new Map();
31
34
  let epoch = "root",
32
- legacy = "root",
33
- model;
35
+ earlierEpoch = "root";
34
36
  for (const entry of branch) {
35
- if (entry.type === "compaction" || entry.type === "branch_summary") epoch = legacy = entry.id;
36
- if (entry.type === "model_change") {
37
- const next = `${entry.provider}/${entry.modelId}`;
38
- if (model !== next) legacy = entry.id;
39
- model = next;
40
- }
41
- boundaries.set(entry.id, { epoch, legacy });
37
+ if (entry.type === "compaction") epoch = entry.id;
38
+ if (entry.type === "compaction" || entry.type === "branch_summary") earlierEpoch = entry.id;
39
+ boundaries.set(entry.id, epoch);
40
+ earlier.set(entry.id, earlierEpoch);
42
41
  }
43
42
  const entries = branch.filter((e) => e.type === "custom" && e.customType === ENTRY_TYPE);
44
43
  const records = entries.map((e) => {
@@ -47,13 +46,12 @@ export class SessionState {
47
46
  record.basis !== e.parentId &&
48
47
  !(this.reader.getHeader?.()?.parentSession && !branch.some((entry) => entry.id === record.basis))
49
48
  )
50
- throw new Error("Astra attempt anchor mismatch");
51
- const boundary = boundaries.get(e.id);
52
- if (record.generation !== boundary.epoch && record.generation !== boundary.legacy)
53
- throw new Error("Astra history generation unavailable");
54
- return { ...record, generation: boundary.epoch };
49
+ throw new Error("OpenAI effort attempt anchor mismatch");
50
+ if (record.generation !== boundaries.get(e.id) && record.generation !== earlier.get(e.id))
51
+ throw new Error("OpenAI effort history generation unavailable");
52
+ return record;
55
53
  });
56
- if (new Set(records.map((r) => r.id)).size !== records.length) throw new Error("Duplicate Astra attempt");
54
+ if (new Set(records.map((r) => r.id)).size !== records.length) throw new Error("Duplicate OpenAI effort attempt");
57
55
  const parents = new Map();
58
56
  for (const record of records) {
59
57
  const parent = record.parent === null ? undefined : parents.get(record.parent);
@@ -66,25 +64,18 @@ export class SessionState {
66
64
  parent.baseline !== record.baseline ||
67
65
  parent.length > record.length)
68
66
  )
69
- throw new Error("Invalid Astra history parent");
67
+ throw new Error("Invalid OpenAI effort history parent");
70
68
  const expected = record.update?.effort ?? parent?.effective ?? record.baseline;
71
69
  if (record.effective !== expected || (record.update && record.update.at < (parent?.length ?? 0)))
72
- throw new Error("Invalid Astra effective effort");
70
+ throw new Error("Invalid OpenAI effective effort");
73
71
  parents.set(record.id, record);
74
72
  }
75
73
  const unknown = entries.some((e) => this.acknowledged.get(e.id) !== JSON.stringify(e));
76
74
  const file = this.reader.getSessionFile();
77
75
  if (unknown && file && branch.some((e) => e.type === "message" && e.message?.role === "assistant")) {
78
- const sessionId = this.reader.getSessionId(),
79
- leaf = this.reader.getLeafId();
80
- const snapshot = await readOnlySessionSnapshot(file);
81
- const persisted = activeReadOnlySessionBranch(snapshot, leaf);
82
- if (
83
- snapshot.sessionId !== sessionId ||
84
- this.reader.getLeafId() !== leaf ||
85
- JSON.stringify(persisted) !== JSON.stringify(branch)
86
- )
87
- throw new Error("Astra persisted ancestry differs");
76
+ const leaf = this.reader.getLeafId();
77
+ if (!(await persistedBranchMatches(this.reader, leaf, branch)) || this.reader.getLeafId() !== leaf)
78
+ throw new Error("OpenAI effort persisted ancestry differs");
88
79
  }
89
80
  for (const entry of entries) this.acknowledged.set(entry.id, JSON.stringify(entry));
90
81
  return records;
@@ -92,22 +83,22 @@ export class SessionState {
92
83
  generation() {
93
84
  let generation = "root";
94
85
  for (const entry of this.branch()) {
95
- if (entry.type === "compaction" || entry.type === "branch_summary") generation = entry.id;
86
+ if (entry.type === "compaction") generation = entry.id;
96
87
  }
97
88
  return generation;
98
89
  }
99
90
  append(record) {
100
91
  try {
101
- if (this.reader.getLeafId() !== record.basis) throw new Error("Astra request ancestry changed");
92
+ if (this.reader.getLeafId() !== record.basis) throw new Error("OpenAI effort request ancestry changed");
102
93
  this.pi.appendEntry(ENTRY_TYPE, record);
103
94
  const entry = this.branch().find(
104
95
  (e) => e.type === "custom" && e.customType === ENTRY_TYPE && e.data?.id === record.id,
105
96
  );
106
97
  if (!entry || entry.parentId !== record.basis || JSON.stringify(entry.data) !== JSON.stringify(record))
107
- throw new Error("Astra append not acknowledged");
98
+ throw new Error("OpenAI effort append not acknowledged");
108
99
  this.acknowledged.set(entry.id, JSON.stringify(entry));
109
100
  } catch (error) {
110
- this.fault = "Astra append uncertain; native effort retained until session recovery";
101
+ this.fault = "OpenAI effort append uncertain; native effort retained until session recovery";
111
102
  throw error;
112
103
  }
113
104
  }
@@ -0,0 +1,17 @@
1
+ const OPENAI_API = "https://api.openai.com/v1";
2
+ /**
3
+ * OpenAI's Sign in with ChatGPT (Pi 0.99): the openai provider's Responses API at api.openai.com with a ChatGPT
4
+ * credential instead of an API key. It reaches the ChatGPT backend. Live on gpt-6-luna and gpt-6.1-sol (2026-09-30) it
5
+ * rejected prompt_cache_breakpoint, max_output_tokens and configuration_update items with HTTP 400, and reused the
6
+ * whole cached prefix after 25 appended message endings without breakpoints. Freeflow sends its requests as Pi
7
+ * built them.
8
+ */
9
+ export function isChatGPTSignIn(model, ctx) {
10
+ if (model?.provider !== "openai" || model.api !== "openai-responses") return false;
11
+ if ((model.baseUrl ?? OPENAI_API).replace(/\/+$/, "") !== OPENAI_API) return false;
12
+ try {
13
+ return ctx?.modelRegistry?.isUsingOAuth?.(model) === true;
14
+ } catch {
15
+ return false;
16
+ }
17
+ }
@@ -0,0 +1,23 @@
1
+ // The apply_patch Lark grammar from OpenAI Codex (codex-rs/core/assets/tools/apply_patch.lark at bcd6d9ab),
2
+ // Copyright 2025 OpenAI, licensed under the Apache License 2.0 (https://www.apache.org/licenses/LICENSE-2.0).
3
+ // GPT models are trained on this exact grammar; keep it byte-identical to upstream.
4
+ export const APPLY_PATCH_LARK_GRAMMAR = `start: begin_patch hunk+ end_patch
5
+ begin_patch: "*** Begin Patch" LF
6
+ end_patch: "*** End Patch" LF?
7
+
8
+ hunk: add_hunk | delete_hunk | update_hunk
9
+ add_hunk: "*** Add File: " filename LF add_line+
10
+ delete_hunk: "*** Delete File: " filename LF
11
+ update_hunk: "*** Update File: " filename LF change_move? change?
12
+
13
+ filename: /(.+)/
14
+ add_line: "+" /(.*)/ LF -> line
15
+
16
+ change_move: "*** Move to: " filename LF
17
+ change: (change_context | change_line)+ eof_line?
18
+ change_context: ("@@" | "@@ " /(.+)/) LF
19
+ change_line: ("+" | "-" | " ") /(.*)/ LF
20
+ eof_line: "*** End of File" LF
21
+
22
+ %import common.LF
23
+ `;
@@ -0,0 +1,38 @@
1
+ // Locating a hunk's lines in a file, with the four passes of OpenAI Codex's seek_sequence
2
+ // (codex-rs/apply-patch/src/seek_sequence.rs at bcd6d9ab, Apache-2.0): exact, then ignoring trailing whitespace,
3
+ // then ignoring surrounding whitespace, then after normalizing Unicode punctuation and spaces.
4
+ function normalise(line) {
5
+ return line
6
+ .trim()
7
+ .replace(/[‐‑‒–—―−]/g, "-")
8
+ .replace(/[‘’‚‛]/g, "'")
9
+ .replace(/[“”„‟]/g, '"')
10
+ .replace(/[  -    ]/g, " ");
11
+ }
12
+ const PASSES = [(line) => line, (line) => line.trimEnd(), (line) => line.trim(), normalise];
13
+ function matchesAt(lines, pattern, at, pass) {
14
+ const compare = PASSES[pass];
15
+ for (let i = 0; i < pattern.length; i++) if (compare(lines[at + i]) !== compare(pattern[i])) return false;
16
+ return true;
17
+ }
18
+ /**
19
+ * The first index at or after `start` where `pattern` matches, and the pass that matched. With `endOfFile`, the
20
+ * search starts where the pattern would end the file (falling back to `start` when that is earlier than the file).
21
+ */
22
+ export function seekSequence(lines, pattern, start, endOfFile) {
23
+ if (pattern.length === 0) return { index: start, pass: 0 };
24
+ if (pattern.length > lines.length) return undefined;
25
+ const from = endOfFile ? Math.max(lines.length - pattern.length, start) : start;
26
+ for (let pass = 0; pass < PASSES.length; pass++)
27
+ for (let i = from; i <= lines.length - pattern.length; i++)
28
+ if (matchesAt(lines, pattern, i, pass)) return { index: i, pass };
29
+ return undefined;
30
+ }
31
+ /** Other places in the whole file where the pattern matches with the same pass: a visible sign of ambiguity. */
32
+ export function otherMatches(lines, pattern, index, pass) {
33
+ if (pattern.length === 0) return [];
34
+ const found = [];
35
+ for (let i = 0; i <= lines.length - pattern.length; i++)
36
+ if (i !== index && matchesAt(lines, pattern, i, pass)) found.push(i);
37
+ return found;
38
+ }
@@ -0,0 +1,177 @@
1
+ // A port of OpenAI Codex's apply_patch parser (codex-rs/apply-patch/src/{parser,streaming_parser}.rs at bcd6d9ab,
2
+ // Apache-2.0), including its lenient forms, so patches GPT models write for Codex parse the same way here.
3
+ export class PatchParseError extends Error {
4
+ line;
5
+ constructor(message, line) {
6
+ super(line === undefined ? message : `${message} (patch line ${line})`);
7
+ this.line = line;
8
+ }
9
+ }
10
+ const BEGIN = "*** Begin Patch";
11
+ const END = "*** End Patch";
12
+ const ADD = "*** Add File: ";
13
+ const DELETE = "*** Delete File: ";
14
+ const UPDATE = "*** Update File: ";
15
+ const MOVE = "*** Move to: ";
16
+ const EOF_MARKER = "*** End of File";
17
+ const ENVIRONMENT = "*** Environment ID: ";
18
+ const HEADERS = "Valid hunk headers: '*** Add File: {path}', '*** Delete File: {path}', '*** Update File: {path}'";
19
+ const UNEXPECTED = (line) =>
20
+ `Unexpected line found in update hunk: '${line}'. Every line should start with ' ' (context line), '+' (added line), or '-' (removed line)`;
21
+ function boundaries(lines) {
22
+ const strict = (candidate) => {
23
+ const first = candidate[0]?.trim(),
24
+ last = candidate.at(-1)?.trim();
25
+ if (first !== BEGIN) throw new PatchParseError(`The first line of the patch must be '${BEGIN}'`);
26
+ if (last !== END) throw new PatchParseError(`The last line of the patch must be '${END}'`);
27
+ return candidate;
28
+ };
29
+ try {
30
+ return strict(lines);
31
+ } catch (error) {
32
+ // Codex's lenient mode: a heredoc wrapper, as GPT models write for a shell invocation. The optional
33
+ // `apply_patch` prefix covers the same wrapper copied from a shell command.
34
+ const first = lines[0]?.replace(/^apply_patch\s+/, "");
35
+ if (["<<EOF", "<<'EOF'", '<<"EOF"'].includes(first ?? "") && lines.at(-1)?.endsWith("EOF") && lines.length >= 4)
36
+ return strict(lines.slice(1, -1));
37
+ throw error;
38
+ }
39
+ }
40
+ export function parsePatch(text) {
41
+ const lines = boundaries(text.trim().split(/\r?\n/));
42
+ const hunks = [];
43
+ let mode = "started";
44
+ let updateHeaderLine = 0;
45
+ let environment = false;
46
+ const lastUpdate = () => {
47
+ const hunk = hunks.at(-1);
48
+ return hunk?.kind === "update" ? hunk : undefined;
49
+ };
50
+ const ensureUpdateNotEmpty = (line, number) => {
51
+ const update = mode === "update" ? lastUpdate() : undefined;
52
+ if (!update) return;
53
+ if (update.chunks.length === 0)
54
+ throw new PatchParseError(`Update file hunk for path '${update.path}' is empty`, updateHeaderLine);
55
+ const chunk = update.chunks.at(-1);
56
+ if (chunk.oldLines.length === 0 && chunk.newLines.length === 0)
57
+ throw new PatchParseError(line === END ? "Update hunk does not contain any lines" : UNEXPECTED(line), number);
58
+ };
59
+ const header = (trimmed, number) => {
60
+ if (mode === "started" && trimmed.startsWith(ENVIRONMENT) && hunks.length === 0) {
61
+ if (environment) throw new PatchParseError("apply_patch environment_id cannot be specified more than once");
62
+ if (!trimmed.slice(ENVIRONMENT.length).trim())
63
+ throw new PatchParseError("apply_patch environment_id cannot be empty");
64
+ environment = true;
65
+ return true;
66
+ }
67
+ for (const [marker, kind] of [
68
+ [END, "end"],
69
+ [ADD, "add"],
70
+ [DELETE, "delete"],
71
+ [UPDATE, "update"],
72
+ ]) {
73
+ if (kind === "end" ? trimmed !== END : !trimmed.startsWith(marker)) continue;
74
+ ensureUpdateNotEmpty(trimmed, number);
75
+ const path = trimmed.slice(marker.length);
76
+ if (kind === "end") mode = "ended";
77
+ else if (kind === "add") {
78
+ hunks.push({ kind: "add", path, contents: "" });
79
+ mode = "add";
80
+ } else if (kind === "delete") {
81
+ hunks.push({ kind: "delete", path });
82
+ mode = "delete";
83
+ } else {
84
+ hunks.push({ kind: "update", path, chunks: [] });
85
+ mode = "update";
86
+ updateHeaderLine = number;
87
+ }
88
+ return true;
89
+ }
90
+ return false;
91
+ };
92
+ const chunk = (update) => {
93
+ if (update.chunks.length === 0) update.chunks.push({ oldLines: [], newLines: [], context: [], endOfFile: false });
94
+ return update.chunks.at(-1);
95
+ };
96
+ lines.slice(1).forEach((line, offset) => {
97
+ const number = offset + 2;
98
+ const trimmed = line.trim();
99
+ if (mode === "ended") {
100
+ if (trimmed) throw new PatchParseError(`The last line of the patch must be '${END}'`, number);
101
+ return;
102
+ }
103
+ if (mode === "started") {
104
+ if (!header(trimmed, number))
105
+ throw new PatchParseError(`'${trimmed}' is not a valid hunk header. ${HEADERS}`, number);
106
+ return;
107
+ }
108
+ if (mode === "add") {
109
+ if (header(trimmed, number)) return;
110
+ const add = hunks.at(-1);
111
+ if (line.startsWith("+") && add?.kind === "add") {
112
+ add.contents += `${line.slice(1)}\n`;
113
+ return;
114
+ }
115
+ throw new PatchParseError(`'${trimmed}' is not a valid hunk header. ${HEADERS}`, number);
116
+ }
117
+ if (mode === "delete") {
118
+ if (header(trimmed, number)) return;
119
+ throw new PatchParseError(`'${trimmed}' is not a valid hunk header. ${HEADERS}`, number);
120
+ }
121
+ // Update file.
122
+ const updateLine = line.trimEnd();
123
+ if (header(updateLine, number)) return;
124
+ const update = lastUpdate();
125
+ const current = update.chunks.at(-1);
126
+ if (current?.endOfFile) {
127
+ if (!updateLine) return;
128
+ if (updateLine !== "@@" && !updateLine.startsWith("@@ "))
129
+ throw new PatchParseError(`Expected update hunk to start with a @@ context marker, got: '${line}'`, number);
130
+ }
131
+ if (update.chunks.length === 0 && update.moveTo === undefined && updateLine.startsWith(MOVE)) {
132
+ update.moveTo = updateLine.slice(MOVE.length);
133
+ return;
134
+ }
135
+ const isAnchor = updateLine === "@@" || updateLine.startsWith("@@ ");
136
+ if (isAnchor && current && current.oldLines.length === 0 && current.newLines.length === 0)
137
+ throw new PatchParseError(UNEXPECTED(line), number);
138
+ if (isAnchor) {
139
+ update.chunks.push({
140
+ ...(updateLine === "@@" ? {} : { anchor: updateLine.slice(3) }),
141
+ oldLines: [],
142
+ newLines: [],
143
+ context: [],
144
+ endOfFile: false,
145
+ });
146
+ return;
147
+ }
148
+ if (updateLine === EOF_MARKER) {
149
+ if (!current || (current.oldLines.length === 0 && current.newLines.length === 0))
150
+ throw new PatchParseError("Update hunk does not contain any lines", number);
151
+ current.endOfFile = true;
152
+ return;
153
+ }
154
+ if (line === "" || line.startsWith(" ")) {
155
+ const target = chunk(update);
156
+ target.context.push([target.oldLines.length, target.newLines.length]);
157
+ const text = line === "" ? "" : line.slice(1);
158
+ target.oldLines.push(text);
159
+ target.newLines.push(text);
160
+ return;
161
+ }
162
+ if (line.startsWith("+")) {
163
+ chunk(update).newLines.push(line.slice(1));
164
+ return;
165
+ }
166
+ if (line.startsWith("-")) {
167
+ chunk(update).oldLines.push(line.slice(1));
168
+ return;
169
+ }
170
+ if (current && (current.oldLines.length > 0 || current.newLines.length > 0))
171
+ throw new PatchParseError(`Expected update hunk to start with a @@ context marker, got: '${line}'`, number);
172
+ throw new PatchParseError(UNEXPECTED(line), number);
173
+ });
174
+ // `mode` changes inside the callback above, which the compiler cannot see.
175
+ if (mode !== "ended") throw new PatchParseError(`The last line of the patch must be '${END}'`);
176
+ return hunks;
177
+ }