@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.
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +1 -1
- package/.cursor-plugin/plugin.json +1 -1
- package/NOTICE +14 -0
- package/README.md +26 -31
- package/capabilities/cognitive-routing/SKILL.md +106 -161
- package/capabilities/cognitive-routing/references/both-mode.md +36 -0
- package/capabilities/cognitive-routing/references/executor-mode.md +48 -0
- package/capabilities/cognitive-routing/references/helper-mode.md +47 -0
- package/capabilities/compaction/SKILL.md +49 -0
- package/capabilities/compaction/references/summary-format.md +239 -0
- package/command-surface.json +4 -0
- package/gemini-extension.json +1 -1
- package/hooks/shared/runtime-context.mjs +28 -30
- package/licenses/Apache-2.0.txt +55 -0
- package/package.json +22 -9
- package/pi-extension/README.md +41 -0
- package/pi-extension/dist/cognitive-routing/assembler.js +393 -0
- package/pi-extension/dist/{cognitive-routing-v2 → cognitive-routing}/config.js +5 -2
- package/pi-extension/dist/cognitive-routing/economics.js +45 -0
- package/pi-extension/dist/cognitive-routing/event-schema.js +388 -0
- package/pi-extension/dist/{session-sources/events.js → cognitive-routing/event-store.js} +103 -40
- package/pi-extension/dist/cognitive-routing/gate.js +241 -0
- package/pi-extension/dist/cognitive-routing/handlers.js +899 -0
- package/pi-extension/dist/cognitive-routing/handoffs.js +350 -0
- package/pi-extension/dist/cognitive-routing/model-control.js +467 -0
- package/pi-extension/dist/{cognitive-routing-v2 → cognitive-routing}/projection.js +229 -56
- package/pi-extension/dist/cognitive-routing/provenance.js +81 -0
- package/pi-extension/dist/cognitive-routing/runtime.js +396 -0
- package/pi-extension/dist/{cognitive-routing-v2 → cognitive-routing}/schemas.js +43 -7
- package/pi-extension/dist/cognitive-routing/session.js +221 -0
- package/pi-extension/dist/{session-sources → cognitive-routing}/sources.js +99 -85
- package/pi-extension/dist/{cognitive-routing-v2 → cognitive-routing}/state.js +37 -378
- package/pi-extension/dist/cognitive-routing/status.js +80 -0
- package/pi-extension/dist/{cognitive-routing-v2 → cognitive-routing}/tools.js +12 -12
- package/pi-extension/dist/{cognitive-routing-v2 → cognitive-routing}/types.js +5 -11
- package/pi-extension/dist/compaction/carry.js +191 -0
- package/pi-extension/dist/compaction/config.js +22 -0
- package/pi-extension/dist/compaction/controller.js +471 -0
- package/pi-extension/dist/compaction/harness.js +77 -0
- package/pi-extension/dist/compaction/results.js +158 -0
- package/pi-extension/dist/compaction/thresholds.js +35 -0
- package/pi-extension/dist/compaction/tool.js +57 -0
- package/pi-extension/dist/host/branch.js +13 -0
- package/pi-extension/dist/host/canonical.js +12 -0
- package/pi-extension/dist/host/catalog.js +77 -0
- package/pi-extension/dist/host/config.js +374 -0
- package/pi-extension/dist/host/projection-tags.js +60 -0
- package/pi-extension/dist/host/prompts.js +82 -0
- package/pi-extension/dist/{session-sources → host}/read-only-session.js +112 -1
- package/pi-extension/dist/{runtime → host}/request-history.js +121 -39
- package/pi-extension/dist/host/runtime-state.js +189 -0
- package/pi-extension/dist/{settings/settings-ui.js → host/settings/freeflow-command.js} +620 -410
- package/pi-extension/dist/{settings/settings-tui.js → host/settings/settings-view.js} +186 -73
- package/pi-extension/dist/host/staging.js +28 -0
- package/pi-extension/dist/host/status.js +101 -0
- package/pi-extension/dist/index.js +423 -173
- package/pi-extension/dist/provider-support/cache/anchor.js +21 -0
- package/pi-extension/dist/provider-support/cache/anthropic.js +102 -0
- package/pi-extension/dist/provider-support/cache/health.js +73 -0
- package/pi-extension/dist/provider-support/cache/index.js +58 -0
- package/pi-extension/dist/provider-support/cache/keepalive.js +210 -0
- package/pi-extension/dist/provider-support/cache/monitor.js +17 -0
- package/pi-extension/dist/provider-support/cache/openai-responses.js +48 -0
- package/pi-extension/dist/provider-support/effort.js +13 -0
- package/pi-extension/dist/provider-support/index.js +44 -3
- package/pi-extension/dist/provider-support/{astra → openai}/adapter.js +46 -29
- package/pi-extension/dist/provider-support/{astra → openai}/history.js +10 -8
- package/pi-extension/dist/provider-support/{astra → openai}/index.js +8 -4
- package/pi-extension/dist/provider-support/{astra → openai}/session-state.js +29 -38
- package/pi-extension/dist/provider-support/routes.js +17 -0
- package/pi-extension/dist/tool-execution/apply-patch/grammar.js +23 -0
- package/pi-extension/dist/tool-execution/apply-patch/match.js +38 -0
- package/pi-extension/dist/tool-execution/apply-patch/parser.js +177 -0
- package/pi-extension/dist/tool-execution/apply-patch/plan.js +201 -0
- package/pi-extension/dist/tool-execution/apply-patch/tool.js +84 -0
- package/pi-extension/dist/tool-execution/background.js +238 -0
- package/pi-extension/dist/tool-execution/bash-guard.js +23 -0
- package/pi-extension/dist/tool-execution/config.js +22 -0
- package/pi-extension/dist/tool-execution/edit-messages.js +120 -0
- package/pi-extension/dist/tool-execution/file-state.js +138 -0
- package/pi-extension/dist/tool-execution/file-tracking.js +143 -0
- package/pi-extension/dist/tool-execution/prompt.js +54 -0
- package/pi-extension/dist/tool-execution/tools.js +16 -0
- package/pi-extension/freeflow/README.md +9 -0
- package/plugin.json +1 -1
- package/runtime/prompts/cognitive-routing.md +4 -4
- package/runtime/prompts/core.md +22 -75
- package/runtime/prompts/interaction-contract.md +15 -8
- package/runtime/prompts/tool-execution.md +38 -0
- package/runtime/prompts/working-method.md +108 -0
- package/skills/action-selection/SKILL.md +39 -47
- package/skills/action-selection/references/trajectory-stalls.md +2 -0
- package/skills/bypass/SKILL.md +2 -2
- package/skills/commit-work/SKILL.md +31 -83
- package/skills/commit-work/references/git-state-edges.md +1 -1
- package/skills/commit-work/references/simple-push.md +2 -0
- package/skills/commit-work/references/staging-decisions.md +1 -1
- package/skills/decision-gate/SKILL.md +31 -50
- package/skills/design-for-depth/SKILL.md +44 -53
- package/skills/design-for-depth/references/design-pressure-signals.md +46 -144
- package/skills/design-for-depth/references/implementation-decisions.md +1 -1
- package/skills/design-for-depth/references/interface-design-loop.md +5 -36
- package/skills/design-for-depth/references/module-and-dependency-design.md +2 -32
- package/skills/design-for-depth/references/state-and-failure-boundaries.md +2 -13
- package/skills/diagnose-failure/SKILL.md +39 -24
- package/skills/diagnose-failure/references/diagnostic-loop-catalog.md +2 -0
- package/skills/diagnose-failure/references/flaky-and-performance-diagnosis.md +2 -2
- package/skills/discuss/SKILL.md +35 -11
- package/skills/discuss/references/discussion-continuity.md +7 -27
- package/skills/evaluate-skill/SKILL.md +49 -80
- package/skills/evaluate-skill/references/evaluation-definition-schema.md +35 -5
- package/skills/evaluate-skill/references/evaluation-design.md +14 -14
- package/skills/evaluate-skill/references/execution-and-evidence.md +26 -5
- package/skills/evaluate-skill/references/review-and-revision.md +3 -1
- package/skills/evaluate-skill/scripts/lib/definitions.mjs +49 -8
- package/skills/evaluate-skill/scripts/lib/pi.mjs +14 -7
- package/skills/evaluate-skill/scripts/lib/review.mjs +196 -0
- package/skills/evaluate-skill/scripts/lib/runner.mjs +45 -8
- package/skills/evaluate-skill/scripts/lib/sandbox.mjs +2 -0
- package/skills/evaluate-skill/scripts/lib/trials.mjs +103 -0
- package/skills/evaluate-skill/scripts/lib/view.mjs +67 -1
- package/skills/evaluate-skill/scripts/pi-command-tool.mjs +144 -0
- package/skills/evaluate-skill/scripts/pi-guard.mjs +30 -13
- package/skills/evaluate-skill/scripts/skill-eval.mjs +114 -34
- package/skills/execute-work/SKILL.md +41 -9
- package/skills/execute-work/references/code-practices.md +2 -0
- package/skills/execute-work/references/execute-work-edges.md +11 -1
- package/skills/handoff/SKILL.md +1 -1
- package/skills/handoff/references/handoff-templates.md +1 -1
- package/skills/review-artifact/SKILL.md +42 -41
- package/skills/review-artifact/references/adjudicate-artifact-review.md +1 -1
- package/skills/review-artifact/references/independent-artifact-reviewer-contract.md +1 -1
- package/skills/review-work/SKILL.md +38 -32
- package/skills/review-work/references/adjudicate-work-review.md +1 -1
- package/skills/review-work/references/independent-work-reviewer-contract.md +1 -1
- package/skills/review-work/references/security-risk-lens.md +2 -4
- package/skills/setup-freeflow/SKILL.md +3 -1
- package/skills/setup-freeflow/references/activation-contract.md +7 -13
- package/skills/setup-freeflow/references/host-setup.md +3 -3
- package/skills/track-work/SKILL.md +113 -161
- package/skills/track-work/references/working-record-format.md +189 -350
- package/skills/track-work/scripts/lib/document.mjs +62 -6
- package/skills/track-work/scripts/lib/format.mjs +16 -1
- package/skills/track-work/scripts/lib/store.mjs +9 -8
- package/skills/track-work/scripts/lib/transitions.mjs +79 -4
- package/skills/track-work/scripts/working-record.mjs +34 -12
- package/skills/verify-work/SKILL.md +29 -20
- package/skills/verify-work/references/browser-runtime-evidence.md +1 -5
- package/skills/verify-work/references/integration-evidence.md +2 -0
- package/skills/verify-work/references/performance-evidence.md +3 -1
- package/skills/verify-work/references/test-design.md +3 -1
- package/skills/workflow/SKILL.md +65 -21
- package/skills/workflow/references/domain-skill-composition.md +5 -3
- package/skills/write-docs/SKILL.md +83 -0
- package/skills/write-docs/references/documentation-shapes.md +119 -0
- package/skills/write-docs/scripts/docs-check.mjs +139 -0
- package/skills/write-plan/SKILL.md +40 -40
- package/skills/write-plan/references/plan-shapes.md +1 -1
- package/skills/write-skill/SKILL.md +107 -147
- package/skills/write-skill/references/activation-boundaries.md +2 -0
- package/skills/write-skill/references/agent-first-instructions.md +22 -10
- package/skills/write-skill/references/progressive-disclosure.md +4 -2
- package/skills/write-skill/references/skill-dependency-graphs.md +2 -0
- package/skills/write-skill/scripts/lib/similarity.mjs +56 -0
- package/skills/write-skill/scripts/skill-author.mjs +18 -2
- package/skills/write-spec/SKILL.md +34 -41
- package/skills/write-spec/references/artifact-standards.md +1 -1
- package/skills/write-spec/references/decision-records.md +2 -0
- package/skills/write-spec/references/spec-shapes.md +1 -1
- package/capabilities/context-virtualization/SKILL.md +0 -103
- package/capabilities/conversation-history/SKILL.md +0 -53
- package/hooks/tests/check-host-adapters.test.mjs +0 -82
- package/hooks/tests/check-runtime-context-hook.sh +0 -212
- package/pi-extension/dist/cognitive-routing-v2/runtime.js +0 -2181
- package/pi-extension/dist/context-virtualization/commands.js +0 -89
- package/pi-extension/dist/context-virtualization/projector.js +0 -42
- package/pi-extension/dist/context-virtualization/runtime.js +0 -378
- package/pi-extension/dist/context-virtualization/state.js +0 -57
- package/pi-extension/dist/context-virtualization/types.js +0 -10
- package/pi-extension/dist/conversation-history/passages.js +0 -66
- package/pi-extension/dist/conversation-history/ranking.js +0 -105
- package/pi-extension/dist/conversation-history/runtime.js +0 -226
- package/pi-extension/dist/conversation-history/search.js +0 -56
- package/pi-extension/dist/conversation-history/types.js +0 -1
- package/pi-extension/dist/freeflow-context/resolver.js +0 -120
- package/pi-extension/dist/freeflow-context/runtime.js +0 -80
- package/pi-extension/dist/freeflow-context/source-projector.js +0 -95
- package/pi-extension/dist/freeflow-context/tool.js +0 -314
- package/pi-extension/dist/freeflow-context/types.js +0 -12
- package/pi-extension/dist/freeflow-context/visibility.js +0 -28
- package/pi-extension/dist/integrations/mcp-config.js +0 -29
- package/pi-extension/dist/runtime/runtime-context.js +0 -754
- package/pi-extension/dist/runtime/runtime-identity.js +0 -5
- package/pi-extension/dist/session-sources/provenance.js +0 -32
- package/pi-extension/dist/ui/text-component.js +0 -57
- package/pi-extension/src/cognitive-routing-v2/budget.ts +0 -42
- package/pi-extension/src/cognitive-routing-v2/config.ts +0 -173
- package/pi-extension/src/cognitive-routing-v2/projection.ts +0 -375
- package/pi-extension/src/cognitive-routing-v2/render.ts +0 -277
- package/pi-extension/src/cognitive-routing-v2/runtime.ts +0 -2241
- package/pi-extension/src/cognitive-routing-v2/schemas.ts +0 -180
- package/pi-extension/src/cognitive-routing-v2/state.ts +0 -839
- package/pi-extension/src/cognitive-routing-v2/tools.ts +0 -62
- package/pi-extension/src/cognitive-routing-v2/types.ts +0 -254
- package/pi-extension/src/context-virtualization/commands.ts +0 -111
- package/pi-extension/src/context-virtualization/projector.ts +0 -62
- package/pi-extension/src/context-virtualization/runtime.ts +0 -428
- package/pi-extension/src/context-virtualization/state.ts +0 -79
- package/pi-extension/src/context-virtualization/types.ts +0 -92
- package/pi-extension/src/conversation-history/passages.ts +0 -81
- package/pi-extension/src/conversation-history/ranking.ts +0 -155
- package/pi-extension/src/conversation-history/runtime.ts +0 -250
- package/pi-extension/src/conversation-history/search.ts +0 -79
- package/pi-extension/src/conversation-history/types.ts +0 -31
- package/pi-extension/src/freeflow-context/resolver.ts +0 -139
- package/pi-extension/src/freeflow-context/runtime.ts +0 -100
- package/pi-extension/src/freeflow-context/source-projector.ts +0 -106
- package/pi-extension/src/freeflow-context/tool.ts +0 -366
- package/pi-extension/src/freeflow-context/types.ts +0 -70
- package/pi-extension/src/freeflow-context/visibility.ts +0 -53
- package/pi-extension/src/index.ts +0 -390
- package/pi-extension/src/integrations/mcp-config.ts +0 -32
- package/pi-extension/src/provider-support/astra/adapter.ts +0 -108
- package/pi-extension/src/provider-support/astra/history.ts +0 -145
- package/pi-extension/src/provider-support/astra/index.ts +0 -12
- package/pi-extension/src/provider-support/astra/session-state.ts +0 -113
- package/pi-extension/src/provider-support/index.ts +0 -5
- package/pi-extension/src/runtime/request-history.ts +0 -243
- package/pi-extension/src/runtime/runtime-context.ts +0 -860
- package/pi-extension/src/runtime/runtime-identity.ts +0 -8
- package/pi-extension/src/session-sources/events.ts +0 -230
- package/pi-extension/src/session-sources/provenance.ts +0 -39
- package/pi-extension/src/session-sources/read-only-session.ts +0 -170
- package/pi-extension/src/session-sources/sources.ts +0 -334
- package/pi-extension/src/settings/settings-tui.ts +0 -501
- package/pi-extension/src/settings/settings-ui.ts +0 -1652
- package/pi-extension/src/ui/text-component.ts +0 -66
- package/pi-extension/tests/cache-reuse/history.test.js +0 -117
- package/pi-extension/tests/cache-reuse/native.test.js +0 -91
- package/pi-extension/tests/cache-reuse/settings.test.js +0 -211
- package/pi-extension/tests/cognitive-routing-v2/communication.test.js +0 -577
- package/pi-extension/tests/cognitive-routing-v2/compaction-input.test.js +0 -183
- package/pi-extension/tests/cognitive-routing-v2/delegation.test.js +0 -557
- package/pi-extension/tests/cognitive-routing-v2/native.test.js +0 -474
- package/pi-extension/tests/cognitive-routing-v2/performance.test.js +0 -87
- package/pi-extension/tests/cognitive-routing-v2/persistence.test.js +0 -300
- package/pi-extension/tests/cognitive-routing-v2/projection.test.js +0 -241
- package/pi-extension/tests/cognitive-routing-v2/recovery.test.js +0 -1034
- package/pi-extension/tests/cognitive-routing-v2/render.test.js +0 -244
- package/pi-extension/tests/cognitive-routing-v2/runtime.test.js +0 -423
- package/pi-extension/tests/cognitive-routing-v2/stabilization.test.js +0 -521
- package/pi-extension/tests/cognitive-routing-v2/state.test.js +0 -586
- package/pi-extension/tests/context-virtualization/extension.test.js +0 -775
- package/pi-extension/tests/context-virtualization/projector.test.js +0 -110
- package/pi-extension/tests/context-virtualization/runtime.test.js +0 -230
- package/pi-extension/tests/conversation-history/runtime.test.js +0 -98
- package/pi-extension/tests/conversation-history/search.test.js +0 -97
- package/pi-extension/tests/fixtures/pi-host.js +0 -4
- package/pi-extension/tests/fixtures/routing-native.js +0 -191
- package/pi-extension/tests/integration/mode-free-core.test.js +0 -220
- package/pi-extension/tests/integration/pi-extension.test.js +0 -1447
- package/pi-extension/tests/integration/prompt-architecture.test.js +0 -447
- package/pi-extension/tests/integration/unaffected-regressions.test.js +0 -478
- package/pi-extension/tests/provider-support/astra-boundaries.test.js +0 -75
- package/pi-extension/tests/provider-support/astra.test.js +0 -306
- package/pi-extension/tests/provider-support/cli.test.js +0 -92
- package/pi-extension/tests/provider-support/native.test.js +0 -183
- package/pi-extension/tests/settings/settings-tui.test.js +0 -243
- package/pi-extension/tsconfig.json +0 -16
- package/runtime/prompts/context-virtualization.md +0 -3
- package/runtime/prompts/conversation-history.md +0 -3
- package/skills/design-for-depth/references/software-design-philosophy.md +0 -202
- /package/pi-extension/dist/{cognitive-routing-v2 → cognitive-routing}/budget.js +0 -0
- /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
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
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
|
|
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(
|
|
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(
|
|
74
|
+
status(_ctx, label) {
|
|
58
75
|
if (this.statusSignature === label) return;
|
|
59
76
|
this.statusSignature = label;
|
|
60
|
-
//
|
|
61
|
-
|
|
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
|
-
|
|
69
|
-
|
|
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("
|
|
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,
|
|
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,
|
|
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
|
-
|
|
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
|
|
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
|
|
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("
|
|
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("
|
|
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
|
|
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
|
|
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
|
-
|
|
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 {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
pi
|
|
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 {
|
|
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
|
|
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
|
|
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("
|
|
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
|
|
29
|
-
//
|
|
30
|
-
|
|
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
|
-
|
|
33
|
-
model;
|
|
35
|
+
earlierEpoch = "root";
|
|
34
36
|
for (const entry of branch) {
|
|
35
|
-
if (entry.type === "compaction"
|
|
36
|
-
if (entry.type === "
|
|
37
|
-
|
|
38
|
-
|
|
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("
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
79
|
-
|
|
80
|
-
|
|
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"
|
|
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("
|
|
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("
|
|
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 = "
|
|
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
|
+
}
|