@hassangameryt/freeflow 0.7.2 → 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 (267) 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 +151 -136
  7. package/capabilities/cognitive-routing/SKILL.md +110 -96
  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 +40 -21
  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/cognitive-routing/event-store.js +280 -0
  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/projection.js +503 -0
  29. package/pi-extension/dist/cognitive-routing/provenance.js +81 -0
  30. package/pi-extension/dist/{cognitive-routing-v2 → cognitive-routing}/render.js +31 -10
  31. package/pi-extension/dist/cognitive-routing/runtime.js +396 -0
  32. package/pi-extension/dist/cognitive-routing/schemas.js +209 -0
  33. package/pi-extension/dist/cognitive-routing/session.js +221 -0
  34. package/pi-extension/dist/cognitive-routing/sources.js +291 -0
  35. package/pi-extension/dist/cognitive-routing/state.js +487 -0
  36. package/pi-extension/dist/cognitive-routing/status.js +80 -0
  37. package/pi-extension/dist/cognitive-routing/tools.js +60 -0
  38. package/pi-extension/dist/{cognitive-routing-v2 → cognitive-routing}/types.js +10 -12
  39. package/pi-extension/dist/compaction/carry.js +191 -0
  40. package/pi-extension/dist/compaction/config.js +22 -0
  41. package/pi-extension/dist/compaction/controller.js +471 -0
  42. package/pi-extension/dist/compaction/harness.js +77 -0
  43. package/pi-extension/dist/compaction/results.js +158 -0
  44. package/pi-extension/dist/compaction/thresholds.js +35 -0
  45. package/pi-extension/dist/compaction/tool.js +57 -0
  46. package/pi-extension/dist/host/branch.js +13 -0
  47. package/pi-extension/dist/host/canonical.js +12 -0
  48. package/pi-extension/dist/host/catalog.js +77 -0
  49. package/pi-extension/dist/host/config.js +374 -0
  50. package/pi-extension/dist/host/projection-tags.js +60 -0
  51. package/pi-extension/dist/host/prompts.js +82 -0
  52. package/pi-extension/dist/{session-sources → host}/read-only-session.js +112 -1
  53. package/pi-extension/dist/host/request-history.js +298 -0
  54. package/pi-extension/dist/host/runtime-state.js +189 -0
  55. package/pi-extension/dist/host/settings/freeflow-command.js +1570 -0
  56. package/pi-extension/dist/{settings/settings-tui.js → host/settings/settings-view.js} +186 -73
  57. package/pi-extension/dist/host/staging.js +28 -0
  58. package/pi-extension/dist/host/status.js +101 -0
  59. package/pi-extension/dist/index.js +441 -170
  60. package/pi-extension/dist/provider-support/cache/anchor.js +21 -0
  61. package/pi-extension/dist/provider-support/cache/anthropic.js +102 -0
  62. package/pi-extension/dist/provider-support/cache/health.js +73 -0
  63. package/pi-extension/dist/provider-support/cache/index.js +58 -0
  64. package/pi-extension/dist/provider-support/cache/keepalive.js +210 -0
  65. package/pi-extension/dist/provider-support/cache/monitor.js +17 -0
  66. package/pi-extension/dist/provider-support/cache/openai-responses.js +48 -0
  67. package/pi-extension/dist/provider-support/effort.js +13 -0
  68. package/pi-extension/dist/provider-support/index.js +45 -0
  69. package/pi-extension/dist/provider-support/openai/adapter.js +127 -0
  70. package/pi-extension/dist/provider-support/openai/history.js +127 -0
  71. package/pi-extension/dist/provider-support/openai/index.js +15 -0
  72. package/pi-extension/dist/provider-support/openai/session-state.js +105 -0
  73. package/pi-extension/dist/provider-support/routes.js +17 -0
  74. package/pi-extension/dist/tool-execution/apply-patch/grammar.js +23 -0
  75. package/pi-extension/dist/tool-execution/apply-patch/match.js +38 -0
  76. package/pi-extension/dist/tool-execution/apply-patch/parser.js +177 -0
  77. package/pi-extension/dist/tool-execution/apply-patch/plan.js +201 -0
  78. package/pi-extension/dist/tool-execution/apply-patch/tool.js +84 -0
  79. package/pi-extension/dist/tool-execution/background.js +238 -0
  80. package/pi-extension/dist/tool-execution/bash-guard.js +23 -0
  81. package/pi-extension/dist/tool-execution/config.js +22 -0
  82. package/pi-extension/dist/tool-execution/edit-messages.js +120 -0
  83. package/pi-extension/dist/tool-execution/file-state.js +138 -0
  84. package/pi-extension/dist/tool-execution/file-tracking.js +143 -0
  85. package/pi-extension/dist/tool-execution/prompt.js +54 -0
  86. package/pi-extension/dist/tool-execution/tools.js +16 -0
  87. package/pi-extension/freeflow/README.md +9 -0
  88. package/plugin.json +1 -1
  89. package/runtime/prompts/cognitive-routing.md +4 -4
  90. package/runtime/prompts/core.md +22 -75
  91. package/runtime/prompts/interaction-contract.md +15 -8
  92. package/runtime/prompts/tool-execution.md +38 -0
  93. package/runtime/prompts/working-method.md +108 -0
  94. package/skills/action-selection/SKILL.md +39 -47
  95. package/skills/action-selection/references/trajectory-stalls.md +2 -0
  96. package/skills/bypass/SKILL.md +2 -2
  97. package/skills/commit-work/SKILL.md +31 -83
  98. package/skills/commit-work/references/git-state-edges.md +1 -1
  99. package/skills/commit-work/references/simple-push.md +2 -0
  100. package/skills/commit-work/references/staging-decisions.md +1 -1
  101. package/skills/decision-gate/SKILL.md +31 -50
  102. package/skills/design-for-depth/SKILL.md +44 -53
  103. package/skills/design-for-depth/references/design-pressure-signals.md +46 -144
  104. package/skills/design-for-depth/references/implementation-decisions.md +4 -4
  105. package/skills/design-for-depth/references/interface-design-loop.md +5 -36
  106. package/skills/design-for-depth/references/module-and-dependency-design.md +2 -32
  107. package/skills/design-for-depth/references/state-and-failure-boundaries.md +2 -13
  108. package/skills/diagnose-failure/SKILL.md +39 -24
  109. package/skills/diagnose-failure/references/diagnostic-loop-catalog.md +2 -0
  110. package/skills/diagnose-failure/references/flaky-and-performance-diagnosis.md +2 -2
  111. package/skills/discuss/SKILL.md +37 -13
  112. package/skills/discuss/references/discussion-continuity.md +7 -27
  113. package/skills/evaluate-skill/SKILL.md +49 -80
  114. package/skills/evaluate-skill/references/evaluation-definition-schema.md +35 -5
  115. package/skills/evaluate-skill/references/evaluation-design.md +14 -14
  116. package/skills/evaluate-skill/references/execution-and-evidence.md +26 -5
  117. package/skills/evaluate-skill/references/review-and-revision.md +3 -1
  118. package/skills/evaluate-skill/scripts/lib/definitions.mjs +49 -8
  119. package/skills/evaluate-skill/scripts/lib/pi.mjs +14 -7
  120. package/skills/evaluate-skill/scripts/lib/review.mjs +196 -0
  121. package/skills/evaluate-skill/scripts/lib/runner.mjs +45 -8
  122. package/skills/evaluate-skill/scripts/lib/sandbox.mjs +2 -0
  123. package/skills/evaluate-skill/scripts/lib/trials.mjs +103 -0
  124. package/skills/evaluate-skill/scripts/lib/view.mjs +67 -1
  125. package/skills/evaluate-skill/scripts/pi-command-tool.mjs +144 -0
  126. package/skills/evaluate-skill/scripts/pi-guard.mjs +30 -13
  127. package/skills/evaluate-skill/scripts/skill-eval.mjs +114 -34
  128. package/skills/execute-work/SKILL.md +41 -9
  129. package/skills/execute-work/references/code-practices.md +2 -0
  130. package/skills/execute-work/references/execute-work-edges.md +11 -1
  131. package/skills/handoff/SKILL.md +1 -1
  132. package/skills/handoff/references/handoff-templates.md +1 -1
  133. package/skills/review-artifact/SKILL.md +42 -41
  134. package/skills/review-artifact/references/adjudicate-artifact-review.md +1 -1
  135. package/skills/review-artifact/references/independent-artifact-reviewer-contract.md +1 -1
  136. package/skills/review-work/SKILL.md +38 -32
  137. package/skills/review-work/references/adjudicate-work-review.md +1 -1
  138. package/skills/review-work/references/independent-work-reviewer-contract.md +1 -1
  139. package/skills/review-work/references/security-risk-lens.md +2 -4
  140. package/skills/setup-freeflow/SKILL.md +3 -1
  141. package/skills/setup-freeflow/references/activation-contract.md +7 -13
  142. package/skills/setup-freeflow/references/host-setup.md +3 -3
  143. package/skills/track-work/SKILL.md +113 -161
  144. package/skills/track-work/references/working-record-format.md +189 -350
  145. package/skills/track-work/scripts/lib/document.mjs +62 -6
  146. package/skills/track-work/scripts/lib/format.mjs +16 -1
  147. package/skills/track-work/scripts/lib/store.mjs +9 -8
  148. package/skills/track-work/scripts/lib/transitions.mjs +79 -4
  149. package/skills/track-work/scripts/working-record.mjs +34 -12
  150. package/skills/verify-work/SKILL.md +29 -20
  151. package/skills/verify-work/references/browser-runtime-evidence.md +1 -5
  152. package/skills/verify-work/references/integration-evidence.md +2 -0
  153. package/skills/verify-work/references/performance-evidence.md +3 -1
  154. package/skills/verify-work/references/test-design.md +3 -1
  155. package/skills/workflow/SKILL.md +65 -21
  156. package/skills/workflow/references/domain-skill-composition.md +5 -3
  157. package/skills/write-docs/SKILL.md +83 -0
  158. package/skills/write-docs/references/documentation-shapes.md +119 -0
  159. package/skills/write-docs/scripts/docs-check.mjs +139 -0
  160. package/skills/write-plan/SKILL.md +40 -40
  161. package/skills/write-plan/references/plan-shapes.md +1 -1
  162. package/skills/write-skill/SKILL.md +107 -147
  163. package/skills/write-skill/references/activation-boundaries.md +2 -0
  164. package/skills/write-skill/references/agent-first-instructions.md +22 -10
  165. package/skills/write-skill/references/progressive-disclosure.md +4 -2
  166. package/skills/write-skill/references/skill-dependency-graphs.md +2 -0
  167. package/skills/write-skill/scripts/lib/similarity.mjs +56 -0
  168. package/skills/write-skill/scripts/skill-author.mjs +18 -2
  169. package/skills/write-spec/SKILL.md +34 -41
  170. package/skills/write-spec/references/artifact-standards.md +1 -1
  171. package/skills/write-spec/references/decision-records.md +2 -0
  172. package/skills/write-spec/references/spec-shapes.md +1 -1
  173. package/capabilities/context-virtualization/SKILL.md +0 -103
  174. package/capabilities/conversation-history/SKILL.md +0 -53
  175. package/hooks/tests/check-host-adapters.test.mjs +0 -82
  176. package/hooks/tests/check-runtime-context-hook.sh +0 -212
  177. package/pi-extension/dist/cognitive-routing-v2/projection.js +0 -321
  178. package/pi-extension/dist/cognitive-routing-v2/runtime.js +0 -1585
  179. package/pi-extension/dist/cognitive-routing-v2/schemas.js +0 -107
  180. package/pi-extension/dist/cognitive-routing-v2/state.js +0 -612
  181. package/pi-extension/dist/cognitive-routing-v2/tools.js +0 -91
  182. package/pi-extension/dist/context-virtualization/commands.js +0 -89
  183. package/pi-extension/dist/context-virtualization/projector.js +0 -42
  184. package/pi-extension/dist/context-virtualization/runtime.js +0 -356
  185. package/pi-extension/dist/context-virtualization/state.js +0 -57
  186. package/pi-extension/dist/context-virtualization/types.js +0 -10
  187. package/pi-extension/dist/conversation-history/passages.js +0 -66
  188. package/pi-extension/dist/conversation-history/ranking.js +0 -105
  189. package/pi-extension/dist/conversation-history/runtime.js +0 -226
  190. package/pi-extension/dist/conversation-history/search.js +0 -56
  191. package/pi-extension/dist/conversation-history/types.js +0 -1
  192. package/pi-extension/dist/freeflow-context/resolver.js +0 -120
  193. package/pi-extension/dist/freeflow-context/runtime.js +0 -80
  194. package/pi-extension/dist/freeflow-context/source-projector.js +0 -95
  195. package/pi-extension/dist/freeflow-context/tool.js +0 -313
  196. package/pi-extension/dist/freeflow-context/types.js +0 -12
  197. package/pi-extension/dist/freeflow-context/visibility.js +0 -28
  198. package/pi-extension/dist/integrations/mcp-config.js +0 -29
  199. package/pi-extension/dist/runtime/runtime-context.js +0 -735
  200. package/pi-extension/dist/runtime/runtime-identity.js +0 -5
  201. package/pi-extension/dist/session-sources/events.js +0 -195
  202. package/pi-extension/dist/session-sources/sources.js +0 -233
  203. package/pi-extension/dist/settings/settings-ui.js +0 -1160
  204. package/pi-extension/dist/ui/text-component.js +0 -57
  205. package/pi-extension/src/cognitive-routing-v2/budget.ts +0 -42
  206. package/pi-extension/src/cognitive-routing-v2/config.ts +0 -143
  207. package/pi-extension/src/cognitive-routing-v2/projection.ts +0 -365
  208. package/pi-extension/src/cognitive-routing-v2/render.ts +0 -256
  209. package/pi-extension/src/cognitive-routing-v2/runtime.ts +0 -1634
  210. package/pi-extension/src/cognitive-routing-v2/schemas.ts +0 -109
  211. package/pi-extension/src/cognitive-routing-v2/state.ts +0 -622
  212. package/pi-extension/src/cognitive-routing-v2/tools.ts +0 -93
  213. package/pi-extension/src/cognitive-routing-v2/types.ts +0 -222
  214. package/pi-extension/src/context-virtualization/commands.ts +0 -111
  215. package/pi-extension/src/context-virtualization/projector.ts +0 -62
  216. package/pi-extension/src/context-virtualization/runtime.ts +0 -406
  217. package/pi-extension/src/context-virtualization/state.ts +0 -79
  218. package/pi-extension/src/context-virtualization/types.ts +0 -92
  219. package/pi-extension/src/conversation-history/passages.ts +0 -81
  220. package/pi-extension/src/conversation-history/ranking.ts +0 -155
  221. package/pi-extension/src/conversation-history/runtime.ts +0 -250
  222. package/pi-extension/src/conversation-history/search.ts +0 -79
  223. package/pi-extension/src/conversation-history/types.ts +0 -31
  224. package/pi-extension/src/freeflow-context/resolver.ts +0 -139
  225. package/pi-extension/src/freeflow-context/runtime.ts +0 -100
  226. package/pi-extension/src/freeflow-context/source-projector.ts +0 -106
  227. package/pi-extension/src/freeflow-context/tool.ts +0 -365
  228. package/pi-extension/src/freeflow-context/types.ts +0 -70
  229. package/pi-extension/src/freeflow-context/visibility.ts +0 -53
  230. package/pi-extension/src/index.ts +0 -368
  231. package/pi-extension/src/integrations/mcp-config.ts +0 -32
  232. package/pi-extension/src/runtime/runtime-context.ts +0 -838
  233. package/pi-extension/src/runtime/runtime-identity.ts +0 -8
  234. package/pi-extension/src/session-sources/events.ts +0 -208
  235. package/pi-extension/src/session-sources/read-only-session.ts +0 -170
  236. package/pi-extension/src/session-sources/sources.ts +0 -263
  237. package/pi-extension/src/settings/settings-tui.ts +0 -501
  238. package/pi-extension/src/settings/settings-ui.ts +0 -1422
  239. package/pi-extension/src/ui/text-component.ts +0 -66
  240. package/pi-extension/tests/cognitive-routing-v2/communication.test.js +0 -484
  241. package/pi-extension/tests/cognitive-routing-v2/compaction-input.test.js +0 -183
  242. package/pi-extension/tests/cognitive-routing-v2/native.test.js +0 -441
  243. package/pi-extension/tests/cognitive-routing-v2/performance.test.js +0 -87
  244. package/pi-extension/tests/cognitive-routing-v2/persistence.test.js +0 -245
  245. package/pi-extension/tests/cognitive-routing-v2/projection.test.js +0 -241
  246. package/pi-extension/tests/cognitive-routing-v2/recovery.test.js +0 -236
  247. package/pi-extension/tests/cognitive-routing-v2/render.test.js +0 -230
  248. package/pi-extension/tests/cognitive-routing-v2/runtime.test.js +0 -153
  249. package/pi-extension/tests/cognitive-routing-v2/stabilization.test.js +0 -521
  250. package/pi-extension/tests/cognitive-routing-v2/state.test.js +0 -275
  251. package/pi-extension/tests/context-virtualization/extension.test.js +0 -765
  252. package/pi-extension/tests/context-virtualization/projector.test.js +0 -110
  253. package/pi-extension/tests/context-virtualization/runtime.test.js +0 -216
  254. package/pi-extension/tests/conversation-history/runtime.test.js +0 -98
  255. package/pi-extension/tests/conversation-history/search.test.js +0 -97
  256. package/pi-extension/tests/fixtures/pi-host.js +0 -4
  257. package/pi-extension/tests/fixtures/routing-native.js +0 -188
  258. package/pi-extension/tests/integration/mode-free-core.test.js +0 -219
  259. package/pi-extension/tests/integration/pi-extension.test.js +0 -968
  260. package/pi-extension/tests/integration/prompt-architecture.test.js +0 -431
  261. package/pi-extension/tests/integration/unaffected-regressions.test.js +0 -478
  262. package/pi-extension/tests/settings/settings-tui.test.js +0 -243
  263. package/pi-extension/tsconfig.json +0 -16
  264. package/runtime/prompts/context-virtualization.md +0 -3
  265. package/runtime/prompts/conversation-history.md +0 -3
  266. package/skills/design-for-depth/references/software-design-philosophy.md +0 -202
  267. /package/pi-extension/dist/{cognitive-routing-v2 → cognitive-routing}/budget.js +0 -0
@@ -0,0 +1,127 @@
1
+ import { assemble, EFFORTS } from "./history.js";
2
+ import { SessionState } from "./session-state.js";
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;
9
+ const codex = model.provider === "openai-codex" && model.api === "openai-codex-responses";
10
+ const api = model.provider === "openai" && model.api === "openai-responses";
11
+ if (!codex && !api) return;
12
+ if (model.baseUrl !== undefined && typeof model.baseUrl !== "string") return;
13
+ const base = (model.baseUrl ?? (codex ? "https://chatgpt.com/backend-api" : "https://api.openai.com/v1")).replace(
14
+ /\/+$/,
15
+ "",
16
+ );
17
+ const allowed = codex
18
+ ? [
19
+ "https://chatgpt.com/backend-api",
20
+ "https://chatgpt.com/backend-api/codex",
21
+ "https://chatgpt.com/backend-api/codex/responses",
22
+ ]
23
+ : ["https://api.openai.com/v1"];
24
+ if (!allowed.includes(base)) return;
25
+ return `${model.provider}/${model.api}/${model.id}`;
26
+ }
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 {
47
+ pi;
48
+ enabled;
49
+ monitor;
50
+ session;
51
+ generation = 0;
52
+ compacting = false;
53
+ queue = Promise.resolve();
54
+ statusSignature;
55
+ constructor(
56
+ pi,
57
+ // Global Freeflow disablement leaves provider requests and session history untouched.
58
+ enabled = () => true,
59
+ monitor,
60
+ ) {
61
+ this.pi = pi;
62
+ this.enabled = enabled;
63
+ this.monitor = monitor;
64
+ }
65
+ reset(ctx) {
66
+ this.generation++;
67
+ this.session = undefined;
68
+ this.compacting = false;
69
+ this.status(ctx, undefined);
70
+ }
71
+ setCompacting(value) {
72
+ this.compacting = value;
73
+ }
74
+ status(_ctx, label) {
75
+ if (this.statusSignature === label) return;
76
+ this.statusSignature = label;
77
+ // A cache diagnostic, reported to /freeflow status rather than the settings footer.
78
+ this.monitor?.set("openai-effort", label);
79
+ }
80
+ async adapt(payload, ctx) {
81
+ const generation = this.generation;
82
+ const operation = this.queue.then(async () => {
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()) {
86
+ this.status(ctx, undefined);
87
+ return payload;
88
+ }
89
+ const label = ctx.model.id === "gpt-6-astra" ? "Astra" : ctx.model.id;
90
+ try {
91
+ const reader = ctx.sessionManager;
92
+ if (
93
+ typeof this.pi.appendEntry !== "function" ||
94
+ !reader?.getLeafId ||
95
+ !reader.getSessionId ||
96
+ !reader.getSessionFile
97
+ )
98
+ throw new Error("Native session persistence unavailable");
99
+ const id = reader.getSessionId(),
100
+ file = reader.getSessionFile();
101
+ if (!this.session || this.session.id !== id || this.session.file !== file)
102
+ this.session = { id, file, store: new SessionState(this.pi, reader) };
103
+ const store = this.session.store,
104
+ basis = reader.getLeafId();
105
+ const records = await store.records();
106
+ if (
107
+ generation !== this.generation ||
108
+ reader.getLeafId() !== basis ||
109
+ this.session?.store !== store ||
110
+ key !== requestKey(payload, ctx.model)
111
+ )
112
+ throw new Error("OpenAI effort request changed during preparation");
113
+ const result = assemble(payload, key, store.generation(), basis, records);
114
+ if (result.record) store.append(result.record);
115
+ this.status(ctx, `${label} ${result.effective} · cache baseline ${result.baseline}`);
116
+ return result.payload;
117
+ } catch {
118
+ // Pi swallows hook exceptions. Return the untouched full-history request,
119
+ // including its requested effort, instead of a partially pinned request.
120
+ this.status(ctx, `${label} cache adaptation unavailable · native effort`);
121
+ return payload;
122
+ }
123
+ });
124
+ this.queue = operation.catch(() => {});
125
+ return operation;
126
+ }
127
+ }
@@ -0,0 +1,127 @@
1
+ import { createHash, randomUUID } from "node:crypto";
2
+ // This namespace starts a fresh effort chain; old Astra entries are deliberately ignored.
3
+ export const ENTRY_TYPE = "freeflow-openai-effort-v1";
4
+ export const EFFORTS = ["low", "medium", "high", "xhigh", "max"];
5
+ export const hash = (value) => createHash("sha256").update(JSON.stringify(value)).digest("hex");
6
+ const digest = (x) => typeof x === "string" && /^[a-f0-9]{64}$/.test(x);
7
+ const effort = (x) => EFFORTS.includes(x);
8
+ const identity = (x) => typeof x === "string" && x.length > 0 && x.length <= 1024;
9
+ export function parseAttempt(value) {
10
+ if (
11
+ !value ||
12
+ value.version !== 1 ||
13
+ !identity(value.id) ||
14
+ !(value.basis === null || identity(value.basis)) ||
15
+ !identity(value.key) ||
16
+ !identity(value.generation) ||
17
+ !digest(value.envelope) ||
18
+ !digest(value.inputHash) ||
19
+ !effort(value.baseline) ||
20
+ !effort(value.effective) ||
21
+ !(value.parent === null || identity(value.parent)) ||
22
+ !Number.isSafeInteger(value.length) ||
23
+ value.length < 0 ||
24
+ (value.update !== undefined &&
25
+ (!value.update ||
26
+ !Number.isSafeInteger(value.update.at) ||
27
+ value.update.at < 0 ||
28
+ value.update.at > value.length ||
29
+ !digest(value.update.prefixHash) ||
30
+ !effort(value.update.effort))) ||
31
+ Object.keys(value).some(
32
+ (k) =>
33
+ ![
34
+ "version",
35
+ "id",
36
+ "basis",
37
+ "key",
38
+ "generation",
39
+ "envelope",
40
+ "baseline",
41
+ "effective",
42
+ "parent",
43
+ "length",
44
+ "inputHash",
45
+ "update",
46
+ ].includes(k),
47
+ )
48
+ )
49
+ throw new Error("Invalid OpenAI effort history");
50
+ return value;
51
+ }
52
+ // Hash complete serialized items, including opaque reasoning; never persist their bodies.
53
+ export function prefixHashes(input) {
54
+ const result = [hash([])];
55
+ for (const item of input) result.push(hash([result.at(-1), item]));
56
+ return result;
57
+ }
58
+ export function envelopeHash(payload) {
59
+ const { input: _input, prompt_cache_key: _key, max_output_tokens: _max, stream: _stream, ...rest } = payload;
60
+ const { effort: _effort, ...reasoning } = rest.reasoning;
61
+ return hash({ ...rest, reasoning });
62
+ }
63
+ export function assemble(payload, key, generation, basis, records) {
64
+ const hashes = prefixHashes(payload.input),
65
+ envelope = envelopeHash(payload);
66
+ const applicable = records.filter((r) => r.key === key && r.generation === generation);
67
+ const compatible = applicable.filter((r) => r.envelope === envelope && hashes[r.length] === r.inputHash);
68
+ const prior = compatible.reduce((best, r) => (!best || r.length >= best.length ? r : best), undefined);
69
+ const baseline = prior?.baseline ?? applicable[0]?.baseline ?? payload.reasoning.effort;
70
+ const desired = payload.reasoning.effort;
71
+ const byId = new Map(records.map((r) => [r.id, r]));
72
+ const updates = [];
73
+ const seen = new Set();
74
+ let cursor = prior;
75
+ while (cursor) {
76
+ if (seen.has(cursor.id)) throw new Error("Cyclic OpenAI effort history");
77
+ seen.add(cursor.id);
78
+ if (cursor.update) {
79
+ if (hashes[cursor.update.at] !== cursor.update.prefixHash) throw new Error("OpenAI effort anchor changed");
80
+ updates.unshift(cursor.update);
81
+ }
82
+ if (cursor.parent === null) break;
83
+ const parent = byId.get(cursor.parent);
84
+ if (!parent || parent.key !== key || parent.generation !== generation || parent.baseline !== baseline)
85
+ throw new Error("OpenAI effort history parent unavailable");
86
+ cursor = parent;
87
+ }
88
+ let update;
89
+ if (desired !== (prior?.effective ?? baseline)) {
90
+ // Only insert before a new user item. Within a tool loop, append after all completed exchanges.
91
+ const last = payload.input.length - 1;
92
+ const at = payload.input[last]?.role === "user" && last >= (prior?.length ?? 0) ? last : payload.input.length;
93
+ update = { at, prefixHash: hashes[at], effort: desired };
94
+ if (updates.at(-1)?.at === at) throw new Error("Adjacent OpenAI effort updates");
95
+ updates.push(update);
96
+ }
97
+ if (updates.some((u, i) => i > 0 && u.at <= updates[i - 1].at)) throw new Error("Invalid OpenAI effort update order");
98
+ const output = structuredClone(payload);
99
+ output.input = [];
100
+ let nextUpdate = 0;
101
+ for (let i = 0; i <= payload.input.length; i++) {
102
+ if (updates[nextUpdate]?.at === i) {
103
+ output.input.push({ type: "configuration_update", reasoning: { effort: updates[nextUpdate++].effort } });
104
+ }
105
+ if (i < payload.input.length) output.input.push(structuredClone(payload.input[i]));
106
+ }
107
+ output.reasoning.effort = baseline;
108
+ // Records mark baselines and effort changes; a longer input under the same effort needs none.
109
+ const identical = !!prior && !update;
110
+ const record = identical
111
+ ? undefined
112
+ : {
113
+ version: 1,
114
+ id: randomUUID(),
115
+ basis,
116
+ key,
117
+ generation,
118
+ envelope,
119
+ baseline,
120
+ effective: desired,
121
+ parent: prior?.id ?? null,
122
+ length: payload.input.length,
123
+ inputHash: hashes.at(-1),
124
+ ...(update ? { update } : {}),
125
+ };
126
+ return { payload: output, record, baseline, effective: desired, rebased: !prior && applicable.length > 0 };
127
+ }
@@ -0,0 +1,15 @@
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
+ });
9
+ pi.on("session_shutdown", (_event, ctx) => adapter.reset(ctx));
10
+ pi.on("session_tree", (_event, ctx) => adapter.reset(ctx));
11
+ pi.on("session_before_compact", () => adapter.setCompacting(true));
12
+ pi.on("session_compact", (_event, ctx) => adapter.reset(ctx));
13
+ pi.on("session_compact_failed", () => adapter.setCompacting(false));
14
+ pi.on("before_provider_request", (event, ctx) => adapter.adapt(event.payload, ctx));
15
+ }
@@ -0,0 +1,105 @@
1
+ import { persistedBranchMatches } from "../../host/read-only-session.js";
2
+ import { ENTRY_TYPE, parseAttempt } from "./history.js";
3
+ import { cachedBranch } from "../../host/branch.js";
4
+ export class SessionState {
5
+ pi;
6
+ reader;
7
+ acknowledged = new Map();
8
+ fault;
9
+ constructor(pi, reader) {
10
+ this.pi = pi;
11
+ this.reader = reader;
12
+ }
13
+ branch() {
14
+ const entries = cachedBranch(this.reader);
15
+ let parent = null;
16
+ const seen = new Set();
17
+ for (const entry of entries) {
18
+ if (!entry.id || seen.has(entry.id) || entry.parentId !== parent)
19
+ throw new Error("Invalid OpenAI effort session ancestry");
20
+ seen.add(entry.id);
21
+ parent = entry.id;
22
+ }
23
+ if (parent !== this.reader.getLeafId()) throw new Error("OpenAI effort session leaf changed");
24
+ return entries;
25
+ }
26
+ async records() {
27
+ if (this.fault) throw new Error(this.fault);
28
+ const branch = this.branch();
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();
34
+ let epoch = "root",
35
+ earlierEpoch = "root";
36
+ for (const entry of branch) {
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);
41
+ }
42
+ const entries = branch.filter((e) => e.type === "custom" && e.customType === ENTRY_TYPE);
43
+ const records = entries.map((e) => {
44
+ const record = parseAttempt(e.data);
45
+ if (
46
+ record.basis !== e.parentId &&
47
+ !(this.reader.getHeader?.()?.parentSession && !branch.some((entry) => entry.id === record.basis))
48
+ )
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;
53
+ });
54
+ if (new Set(records.map((r) => r.id)).size !== records.length) throw new Error("Duplicate OpenAI effort attempt");
55
+ const parents = new Map();
56
+ for (const record of records) {
57
+ const parent = record.parent === null ? undefined : parents.get(record.parent);
58
+ if (
59
+ record.parent !== null &&
60
+ (!parent ||
61
+ parent.key !== record.key ||
62
+ parent.generation !== record.generation ||
63
+ parent.envelope !== record.envelope ||
64
+ parent.baseline !== record.baseline ||
65
+ parent.length > record.length)
66
+ )
67
+ throw new Error("Invalid OpenAI effort history parent");
68
+ const expected = record.update?.effort ?? parent?.effective ?? record.baseline;
69
+ if (record.effective !== expected || (record.update && record.update.at < (parent?.length ?? 0)))
70
+ throw new Error("Invalid OpenAI effective effort");
71
+ parents.set(record.id, record);
72
+ }
73
+ const unknown = entries.some((e) => this.acknowledged.get(e.id) !== JSON.stringify(e));
74
+ const file = this.reader.getSessionFile();
75
+ if (unknown && file && branch.some((e) => e.type === "message" && e.message?.role === "assistant")) {
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");
79
+ }
80
+ for (const entry of entries) this.acknowledged.set(entry.id, JSON.stringify(entry));
81
+ return records;
82
+ }
83
+ generation() {
84
+ let generation = "root";
85
+ for (const entry of this.branch()) {
86
+ if (entry.type === "compaction") generation = entry.id;
87
+ }
88
+ return generation;
89
+ }
90
+ append(record) {
91
+ try {
92
+ if (this.reader.getLeafId() !== record.basis) throw new Error("OpenAI effort request ancestry changed");
93
+ this.pi.appendEntry(ENTRY_TYPE, record);
94
+ const entry = this.branch().find(
95
+ (e) => e.type === "custom" && e.customType === ENTRY_TYPE && e.data?.id === record.id,
96
+ );
97
+ if (!entry || entry.parentId !== record.basis || JSON.stringify(entry.data) !== JSON.stringify(record))
98
+ throw new Error("OpenAI effort append not acknowledged");
99
+ this.acknowledged.set(entry.id, JSON.stringify(entry));
100
+ } catch (error) {
101
+ this.fault = "OpenAI effort append uncertain; native effort retained until session recovery";
102
+ throw error;
103
+ }
104
+ }
105
+ }
@@ -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
+ }