mixdog 0.9.87 → 0.9.89
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/package.json +13 -7
- package/scripts/tool-failures.mjs +60 -24
- package/src/agents/heavy-worker/AGENT.md +3 -4
- package/src/agents/worker/AGENT.md +1 -2
- package/src/cli.mjs +8 -0
- package/src/defaults/cycle3-review-prompt.md +4 -4
- package/src/rules/agent/00-core.md +3 -0
- package/src/rules/agent/42-cycle3-agent.md +5 -6
- package/src/rules/lead/01-general.md +2 -0
- package/src/rules/shared/01-tool.md +29 -35
- package/src/runtime/agent/orchestrator/agent-runtime/agent-dispatch.mjs +3 -51
- package/src/runtime/agent/orchestrator/agent-runtime/maintenance-route.mjs +59 -0
- package/src/runtime/agent/orchestrator/agent-runtime/session-builder.mjs +2 -2
- package/src/runtime/agent/orchestrator/agent-runtime/title-completion.mjs +67 -0
- package/src/runtime/agent/orchestrator/agent-trace-format.mjs +57 -4
- package/src/runtime/agent/orchestrator/agent-trace-io.mjs +2 -3
- package/src/runtime/agent/orchestrator/config.mjs +29 -11
- package/src/runtime/agent/orchestrator/context/collect.mjs +1 -1
- package/src/runtime/agent/orchestrator/internal-agents.mjs +1 -1
- package/src/runtime/agent/orchestrator/mcp/client.mjs +0 -24
- package/src/runtime/agent/orchestrator/providers/admission-scheduler.mjs +3 -3
- package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +25 -17
- package/src/runtime/agent/orchestrator/providers/anthropic-sse.mjs +321 -22
- package/src/runtime/agent/orchestrator/providers/anthropic.mjs +35 -22
- package/src/runtime/agent/orchestrator/providers/gemini-stream.mjs +28 -23
- package/src/runtime/agent/orchestrator/providers/gemini.mjs +10 -2
- package/src/runtime/agent/orchestrator/providers/grok-oauth-login.mjs +0 -1
- package/src/runtime/agent/orchestrator/providers/grok-oauth-tokens.mjs +0 -1
- package/src/runtime/agent/orchestrator/providers/grok-oauth.mjs +2 -5
- package/src/runtime/agent/orchestrator/providers/lib/anthropic-native-blocks.mjs +27 -0
- package/src/runtime/agent/orchestrator/providers/lib/anthropic-request-utils.mjs +23 -0
- package/src/runtime/agent/orchestrator/providers/lib/stream-outcome.mjs +329 -0
- package/src/runtime/agent/orchestrator/providers/oauth-usage.mjs +23 -1
- package/src/runtime/agent/orchestrator/providers/openai-compat-stream.mjs +107 -20
- package/src/runtime/agent/orchestrator/providers/openai-compat.mjs +4 -3
- package/src/runtime/agent/orchestrator/providers/openai-oauth-http-sse.mjs +124 -12
- package/src/runtime/agent/orchestrator/providers/openai-oauth-ws.mjs +19 -2
- package/src/runtime/agent/orchestrator/providers/openai-oauth.mjs +1 -1
- package/src/runtime/agent/orchestrator/providers/openai-responses-payload.mjs +1 -1
- package/src/runtime/agent/orchestrator/providers/openai-ws-stream.mjs +110 -75
- package/src/runtime/agent/orchestrator/providers/retry-classifier.mjs +96 -113
- package/src/runtime/agent/orchestrator/session/agent-loop.mjs +160 -11
- package/src/runtime/agent/orchestrator/session/eager-dispatch.mjs +18 -1
- package/src/runtime/agent/orchestrator/session/lifecycle-scan.mjs +147 -117
- package/src/runtime/agent/orchestrator/session/loop/stop-hooks.mjs +88 -0
- package/src/runtime/agent/orchestrator/session/loop/stored-tool-args.mjs +50 -8
- package/src/runtime/agent/orchestrator/session/loop/termination.mjs +25 -0
- package/src/runtime/agent/orchestrator/session/loop/tool-exec.mjs +1 -1
- package/src/runtime/agent/orchestrator/session/manager/ask-session.mjs +47 -6
- package/src/runtime/agent/orchestrator/session/manager/pending-messages.mjs +485 -24
- package/src/runtime/agent/orchestrator/session/manager/session-close.mjs +100 -2
- package/src/runtime/agent/orchestrator/session/manager/session-crud.mjs +28 -0
- package/src/runtime/agent/orchestrator/session/manager/session-lifecycle.mjs +80 -14
- package/src/runtime/agent/orchestrator/session/manager/turn-checkpoint.mjs +142 -2
- package/src/runtime/agent/orchestrator/session/manager/usage-metrics.mjs +128 -32
- package/src/runtime/agent/orchestrator/session/manager.mjs +5 -1
- package/src/runtime/agent/orchestrator/session/save-session-worker.mjs +77 -4
- package/src/runtime/agent/orchestrator/session/send-with-recovery.mjs +70 -39
- package/src/runtime/agent/orchestrator/session/store/fs-probe.mjs +91 -0
- package/src/runtime/agent/orchestrator/session/store/listing.mjs +141 -64
- package/src/runtime/agent/orchestrator/session/store/live-state.mjs +253 -0
- package/src/runtime/agent/orchestrator/session/store/load-cache.mjs +211 -28
- package/src/runtime/agent/orchestrator/session/store/save-fault.mjs +313 -0
- package/src/runtime/agent/orchestrator/session/store/save-worker.mjs +622 -53
- package/src/runtime/agent/orchestrator/session/store/serialize.mjs +43 -10
- package/src/runtime/agent/orchestrator/session/store/summary-cache.mjs +53 -8
- package/src/runtime/agent/orchestrator/session/store/summary-rebuild-worker.mjs +20 -0
- package/src/runtime/agent/orchestrator/session/store/write-guards.mjs +113 -9
- package/src/runtime/agent/orchestrator/session/store-summary-index.mjs +2 -0
- package/src/runtime/agent/orchestrator/session/store-summary-reader.mjs +476 -52
- package/src/runtime/agent/orchestrator/session/store.mjs +721 -196
- package/src/runtime/agent/orchestrator/session/token-native.mjs +2 -4
- package/src/runtime/agent/orchestrator/session/tool-batch.mjs +19 -0
- package/src/runtime/agent/orchestrator/tools/bash-session.mjs +4 -6
- package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +5 -5
- package/src/runtime/agent/orchestrator/tools/builtin/list-tool.mjs +146 -90
- package/src/runtime/agent/orchestrator/tools/builtin/read-single-tool.mjs +6 -0
- package/src/runtime/agent/orchestrator/tools/builtin/rg-runner.mjs +93 -17
- package/src/runtime/agent/orchestrator/tools/builtin/search-path-diagnostics.mjs +5 -0
- package/src/runtime/agent/orchestrator/tools/builtin/search-tool.mjs +7 -7
- package/src/runtime/agent/orchestrator/tools/builtin/shell-job-paths.mjs +6 -1
- package/src/runtime/agent/orchestrator/tools/builtin/shell-job-spawn.mjs +17 -1
- package/src/runtime/agent/orchestrator/tools/builtin/shell-jobs.mjs +85 -5
- package/src/runtime/agent/orchestrator/tools/builtin/snapshot-store.mjs +52 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/disk-cache.mjs +54 -51
- package/src/runtime/agent/orchestrator/tools/code-graph/dispatch.mjs +14 -15
- package/src/runtime/agent/orchestrator/tools/patch/dispatch.mjs +132 -8
- package/src/runtime/agent/orchestrator/tools/patch/matcher.mjs +86 -3
- package/src/runtime/agent/orchestrator/tools/patch/native-server.mjs +6 -1
- package/src/runtime/agent/orchestrator/tools/patch/orchestrator.mjs +210 -38
- package/src/runtime/agent/orchestrator/tools/patch/paths.mjs +23 -1
- package/src/runtime/agent/orchestrator/tools/patch/v4a-convert.mjs +87 -19
- package/src/runtime/agent/orchestrator/tools/patch-manifest.json +11 -11
- package/src/runtime/agent/orchestrator/tools/patch-tool-defs.mjs +33 -8
- package/src/runtime/agent/orchestrator/tools/shell-command.mjs +12 -2
- package/src/runtime/media/lanes.mjs +2 -2
- package/src/runtime/media/renditions.mjs +101 -17
- package/src/runtime/media/renditions.test.mjs +54 -0
- package/src/runtime/media/store.mjs +111 -43
- package/src/runtime/media/store.test.mjs +53 -11
- package/src/runtime/memory/index.mjs +26 -50
- package/src/runtime/memory/lib/core-memory-candidates.mjs +2 -7
- package/src/runtime/memory/lib/core-memory-store.mjs +2 -9
- package/src/runtime/memory/lib/cycle-scheduler.mjs +10 -0
- package/src/runtime/memory/lib/embedding-provider.mjs +3 -3
- package/src/runtime/memory/lib/embedding-worker.mjs +60 -15
- package/src/runtime/memory/lib/http-router.mjs +4 -0
- package/src/runtime/memory/lib/ko-morph.mjs +49 -6
- package/src/runtime/memory/lib/memory-action-handlers.mjs +21 -18
- package/src/runtime/memory/lib/memory-cycle2.mjs +3 -3
- package/src/runtime/memory/lib/memory-cycle3.mjs +1 -3
- package/src/runtime/memory/lib/memory-recall-store.mjs +24 -0
- package/src/runtime/memory/lib/query-handlers.mjs +26 -28
- package/src/runtime/memory/tool-defs.mjs +3 -3
- package/src/runtime/shared/atomic-file.mjs +5 -1
- package/src/runtime/shared/child-guardian.mjs +73 -2
- package/src/runtime/shared/provider-api-key.mjs +22 -5
- package/src/runtime/shared/tool-execution-contract.mjs +53 -0
- package/src/session-runtime/config-helpers.mjs +6 -5
- package/src/session-runtime/lifecycle-api.mjs +16 -4
- package/src/session-runtime/media-api.mjs +31 -16
- package/src/session-runtime/prewarm.mjs +2 -24
- package/src/session-runtime/runtime-core.mjs +33 -15
- package/src/session-runtime/runtime-tunables.mjs +0 -3
- package/src/session-runtime/session-lifecycle.mjs +0 -6
- package/src/session-runtime/session-title.mjs +228 -0
- package/src/session-runtime/session-turn-api.mjs +11 -7
- package/src/session-runtime/workflow-agents-api.mjs +16 -0
- package/src/session-runtime/workflow.mjs +7 -5
- package/src/standalone/agent-tool/job-views.mjs +90 -36
- package/src/standalone/agent-tool/spawn-flow.mjs +25 -216
- package/src/standalone/agent-tool/worker-index.mjs +8 -0
- package/src/standalone/agent-tool/worker-rows.mjs +2 -1
- package/src/standalone/agent-tool.mjs +17 -17
- package/src/standalone/agent-watchdog-registry.mjs +19 -2
- package/src/standalone/channel-daemon.mjs +8 -0
- package/src/standalone/explore-tool.mjs +1 -1
- package/src/standalone/memory-runtime-proxy.mjs +4 -7
- package/src/standalone/projects.mjs +4 -1
- package/src/standalone/usage-dashboard.mjs +25 -3
- package/src/tui/App.jsx +5 -3
- package/src/tui/app/resume-picker.mjs +7 -2
- package/src/tui/app/route-pickers.mjs +11 -117
- package/src/tui/app/slash-dispatch.mjs +13 -3
- package/src/tui/app/transcript-row-estimate.mjs +1 -1
- package/src/tui/app/transcript-window.mjs +17 -93
- package/src/tui/app/use-transcript-scroll.mjs +32 -2
- package/src/tui/app/use-transcript-window.mjs +64 -114
- package/src/tui/components/Markdown.jsx +19 -2
- package/src/tui/components/Message.jsx +8 -3
- package/src/tui/components/ToolExecution.jsx +7 -3
- package/src/tui/dist/index.mjs +243 -223
- package/src/tui/engine/live-share.mjs +15 -5
- package/src/tui/engine/render-timing.mjs +2 -2
- package/src/tui/engine/session-api-ext.mjs +74 -4
- package/src/tui/engine/session-flow.mjs +3 -1
- package/src/tui/engine.mjs +2 -2
- package/src/tui/hooks/useSharedTick.mjs +0 -2
- package/src/tui/index.jsx +5 -5
- package/src/ui/statusline-agents.mjs +44 -11
- package/src/vendor/statusline/bin/statusline-route.mjs +26 -3
- package/src/workflows/default/WORKFLOW.md +3 -3
- package/src/workflows/solo/WORKFLOW.md +5 -3
- package/src/runtime/media/index.mjs +0 -18
- package/src/runtime/memory/lib/embedding-warmup.mjs +0 -68
- package/src/standalone/agent-shard/shard-child.mjs +0 -300
- package/src/standalone/agent-shard/shard-pool.mjs +0 -443
|
@@ -3,8 +3,8 @@
|
|
|
3
3
|
// executePatchTool entry point + replay capture. Moved verbatim from
|
|
4
4
|
// patch.mjs; control flow and output are unchanged.
|
|
5
5
|
|
|
6
|
-
import { chmodSync, existsSync, readFileSync, readdirSync, writeFileSync, mkdirSync, rmSync
|
|
7
|
-
import { dirname as pathDirname, resolve as pathResolve, isAbsolute, join as pathJoin } from 'node:path';
|
|
6
|
+
import { chmodSync, existsSync, statSync, readFileSync, readdirSync, writeFileSync, mkdirSync, rmSync } from 'node:fs';
|
|
7
|
+
import { dirname as pathDirname, resolve as pathResolve, relative as pathRelative, isAbsolute, join as pathJoin } from 'node:path';
|
|
8
8
|
import { parsePatch } from 'diff';
|
|
9
9
|
import { getAbortSignalForSession } from '../../session/abort-lookup.mjs';
|
|
10
10
|
import {
|
|
@@ -20,6 +20,7 @@ import { markCodeGraphDirtyPaths } from '../code-graph-state.mjs';
|
|
|
20
20
|
import { prepareInput, isV4APatchInput, hasUnifiedBareV4AHunk, canFallbackCountedUnified, parseV4APatch, parseUnifiedBareV4APatch, parseUnifiedCountedAsV4APatch, isCompactedPlaceholderPatch, salvageV4AOpening } from './parsing.mjs';
|
|
21
21
|
import {
|
|
22
22
|
resolveBasePath,
|
|
23
|
+
resolveEntryPath,
|
|
23
24
|
resolveV4AEntryPath,
|
|
24
25
|
parsedEntryResolvedPath,
|
|
25
26
|
isResolvedPathOutsideBase,
|
|
@@ -32,14 +33,13 @@ import {
|
|
|
32
33
|
} from './paths.mjs';
|
|
33
34
|
import { ensureNativePatchBinaryAvailable } from './native-server.mjs';
|
|
34
35
|
import { assertPathReachable } from '../builtin/fs-reachability.mjs';
|
|
36
|
+
import { resolveReadPathRedirect } from '../builtin/snapshot-store.mjs';
|
|
35
37
|
import { dispatchNativePatch, dispatchJsPatchEntries } from './dispatch.mjs';
|
|
36
38
|
import {
|
|
37
39
|
planV4ARenameSections,
|
|
38
40
|
applyV4ARenameSections,
|
|
39
41
|
formatV4ARenameSuccessLines,
|
|
40
42
|
convertV4ASectionsToUnifiedPatch,
|
|
41
|
-
convertUnifiedBareV4AToUnifiedPatch,
|
|
42
|
-
convertUnifiedCountedToUnifiedPatchViaV4A,
|
|
43
43
|
isV4ARenameSection,
|
|
44
44
|
} from './v4a-convert.mjs';
|
|
45
45
|
|
|
@@ -47,24 +47,80 @@ function isPatchErrorText(text) {
|
|
|
47
47
|
return /^Error:/i.test(String(text ?? '').trimStart());
|
|
48
48
|
}
|
|
49
49
|
|
|
50
|
+
function patchPathKey(fullPath) {
|
|
51
|
+
const value = String(fullPath || '');
|
|
52
|
+
return process.platform === 'win32' ? value.toLowerCase() : value;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function redirectedPatchPath(requestedFullPath, readStateScope) {
|
|
56
|
+
// A newly-created exact requested path always wins over an older redirect.
|
|
57
|
+
if (!requestedFullPath || existsSync(requestedFullPath)) return requestedFullPath;
|
|
58
|
+
const redirected = resolveReadPathRedirect(requestedFullPath, readStateScope);
|
|
59
|
+
return redirected && existsSync(redirected) ? redirected : requestedFullPath;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function patchHeaderPathForResolved(basePath, fullPath) {
|
|
63
|
+
const rel = pathRelative(pathResolve(basePath), pathResolve(fullPath));
|
|
64
|
+
if (rel && !isAbsolute(rel) && !rel.split(/[\\/]+/).some((part) => part === '..')) {
|
|
65
|
+
return rel.replace(/\\/g, '/');
|
|
66
|
+
}
|
|
67
|
+
return fullPath;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function rewriteV4AReadRedirects(sections, basePath, readStateScope) {
|
|
71
|
+
return (sections || []).map((section) => {
|
|
72
|
+
if (!section || section.kind === 'add' || !section.path) return section;
|
|
73
|
+
const requested = resolveV4AEntryPath(basePath, section.path);
|
|
74
|
+
const redirected = redirectedPatchPath(requested, readStateScope);
|
|
75
|
+
if (patchPathKey(redirected) === patchPathKey(requested)) return section;
|
|
76
|
+
return { ...section, path: patchHeaderPathForResolved(basePath, redirected) };
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function rewriteParsedReadRedirects(parsed, basePath, readStateScope) {
|
|
81
|
+
return (parsed || []).map((entry) => {
|
|
82
|
+
const kind = classifyEntry(entry);
|
|
83
|
+
if (kind === 'create' || !entry?.oldFileName) return entry;
|
|
84
|
+
const requested = resolveEntryPath(basePath, entry.oldFileName);
|
|
85
|
+
const redirected = redirectedPatchPath(requested, readStateScope);
|
|
86
|
+
if (patchPathKey(redirected) === patchPathKey(requested)) return entry;
|
|
87
|
+
const rewritten = {
|
|
88
|
+
...entry,
|
|
89
|
+
oldFileName: patchHeaderPathForResolved(basePath, redirected),
|
|
90
|
+
};
|
|
91
|
+
if (kind === 'modify' && entry.newFileName) {
|
|
92
|
+
const newRequested = resolveEntryPath(basePath, entry.newFileName);
|
|
93
|
+
if (patchPathKey(newRequested) === patchPathKey(requested)) {
|
|
94
|
+
rewritten.newFileName = rewritten.oldFileName;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
return rewritten;
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Pre-patch snapshot of every path this operation may touch: the bytes and
|
|
102
|
+
// mode of each existing target, or an "absent" marker. Restoring them undoes
|
|
103
|
+
// the whole batch. Hostile concurrent replacement of a path between capture
|
|
104
|
+
// and restore is out of scope.
|
|
50
105
|
function capturePatchRollbackState(paths) {
|
|
51
106
|
return paths.map((fullPath) => {
|
|
52
|
-
|
|
53
|
-
|
|
107
|
+
let stat;
|
|
108
|
+
try {
|
|
109
|
+
stat = statSync(fullPath);
|
|
110
|
+
} catch (err) {
|
|
111
|
+
if (err?.code === 'ENOENT') return { fullPath, existed: false, content: null, mode: null };
|
|
112
|
+
throw new Error(`apply_patch: rollback snapshot target unreadable: ${normalizeOutputPath(fullPath)} (${err?.code || err?.message || String(err)})`);
|
|
113
|
+
}
|
|
54
114
|
if (!stat.isFile()) throw new Error(`apply_patch: rollback snapshot target is not a regular file: ${normalizeOutputPath(fullPath)}`);
|
|
55
|
-
return {
|
|
56
|
-
fullPath,
|
|
57
|
-
existed: true,
|
|
58
|
-
content: readFileSync(fullPath),
|
|
59
|
-
mode: stat.mode,
|
|
60
|
-
};
|
|
115
|
+
return { fullPath, existed: true, content: readFileSync(fullPath), mode: stat.mode };
|
|
61
116
|
});
|
|
62
117
|
}
|
|
63
118
|
|
|
64
119
|
function restorePatchRollbackState(snapshots, readStateScope) {
|
|
65
120
|
const errors = [];
|
|
66
|
-
const paths =
|
|
121
|
+
const paths = [];
|
|
67
122
|
for (const snapshot of snapshots) {
|
|
123
|
+
const display = normalizeOutputPath(snapshot.fullPath);
|
|
68
124
|
try {
|
|
69
125
|
if (snapshot.existed) {
|
|
70
126
|
mkdirSync(pathDirname(snapshot.fullPath), { recursive: true });
|
|
@@ -72,9 +128,12 @@ function restorePatchRollbackState(snapshots, readStateScope) {
|
|
|
72
128
|
if (snapshot.mode != null) chmodSync(snapshot.fullPath, snapshot.mode);
|
|
73
129
|
} else if (existsSync(snapshot.fullPath)) {
|
|
74
130
|
rmSync(snapshot.fullPath, { force: true });
|
|
131
|
+
} else {
|
|
132
|
+
continue; // nothing written, nothing to undo
|
|
75
133
|
}
|
|
134
|
+
paths.push(snapshot.fullPath);
|
|
76
135
|
} catch (err) {
|
|
77
|
-
errors.push(`${
|
|
136
|
+
errors.push(`${display} — ${err?.message || String(err)}`);
|
|
78
137
|
}
|
|
79
138
|
}
|
|
80
139
|
invalidateBuiltinResultCache(paths);
|
|
@@ -186,7 +245,11 @@ async function applyPatchSequence(patchStr, requestedFormat, basePath, ctx) {
|
|
|
186
245
|
if (isV4APatchInput(patchStr, requestedFormat)) {
|
|
187
246
|
let allSections;
|
|
188
247
|
try {
|
|
189
|
-
allSections =
|
|
248
|
+
allSections = rewriteV4AReadRedirects(
|
|
249
|
+
parseV4APatch(patchStr),
|
|
250
|
+
basePath,
|
|
251
|
+
readStateScope,
|
|
252
|
+
);
|
|
190
253
|
} catch (err) {
|
|
191
254
|
throw new Error(`apply_patch: V4A parse failed — ${err?.message || String(err)}`);
|
|
192
255
|
}
|
|
@@ -197,7 +260,11 @@ async function applyPatchSequence(patchStr, requestedFormat, basePath, ctx) {
|
|
|
197
260
|
// than converting the whole patch up front.
|
|
198
261
|
let sections;
|
|
199
262
|
try {
|
|
200
|
-
sections =
|
|
263
|
+
sections = rewriteV4AReadRedirects(
|
|
264
|
+
parseUnifiedBareV4APatch(patchStr),
|
|
265
|
+
basePath,
|
|
266
|
+
readStateScope,
|
|
267
|
+
);
|
|
201
268
|
} catch (err) {
|
|
202
269
|
throw new Error(`apply_patch: bare @@ parse failed — ${err?.message || String(err)}`);
|
|
203
270
|
}
|
|
@@ -215,7 +282,11 @@ async function applyPatchSequence(patchStr, requestedFormat, basePath, ctx) {
|
|
|
215
282
|
// V4A-style sections and defer per-section conversion — same ordered-stop
|
|
216
283
|
// guarantee as the V4A path (no whole-patch up-front convert).
|
|
217
284
|
try {
|
|
218
|
-
countedSections =
|
|
285
|
+
countedSections = rewriteV4AReadRedirects(
|
|
286
|
+
parseUnifiedCountedAsV4APatch(patchStr),
|
|
287
|
+
basePath,
|
|
288
|
+
readStateScope,
|
|
289
|
+
);
|
|
219
290
|
} catch (fallbackErr) {
|
|
220
291
|
throw new Error(`apply_patch: parse failed — ${err?.message || String(err)}; V4A fallback failed — ${fallbackErr?.message || String(fallbackErr)}`);
|
|
221
292
|
}
|
|
@@ -223,9 +294,17 @@ async function applyPatchSequence(patchStr, requestedFormat, basePath, ctx) {
|
|
|
223
294
|
if (countedSections) {
|
|
224
295
|
for (const section of countedSections) pushSectionUnit(section);
|
|
225
296
|
} else {
|
|
297
|
+
parsed = rewriteParsedReadRedirects(parsed, basePath, readStateScope);
|
|
226
298
|
for (const entry of parsed || []) {
|
|
227
299
|
const kind = classifyEntry(entry);
|
|
228
300
|
const headerName = kind === 'create' ? entry.newFileName : entry.oldFileName;
|
|
301
|
+
if (!headerName) {
|
|
302
|
+
throw new Error(
|
|
303
|
+
'apply_patch: a file section header could not be parsed (no target path) — the patch body is not a valid diff. '
|
|
304
|
+
+ 'Each section must start with `*** Update File: <path>` / `*** Add File: <path>` / `*** Delete File: <path>` '
|
|
305
|
+
+ '(V4A, wrapped in `*** Begin Patch` / `*** End Patch`), or a `--- a/<path>` + `+++ b/<path>` pair (unified).',
|
|
306
|
+
);
|
|
307
|
+
}
|
|
229
308
|
units.push({
|
|
230
309
|
displayPath: normalizeOutputPath(stripDiffPrefix(headerName || '')),
|
|
231
310
|
fullPath: parsedEntryResolvedPath(entry, basePath),
|
|
@@ -247,6 +326,42 @@ async function applyPatchSequence(patchStr, requestedFormat, basePath, ctx) {
|
|
|
247
326
|
|
|
248
327
|
return withBuiltinPathLocks(lockPaths, () =>
|
|
249
328
|
withAdvisoryLocks(lockPaths, async () => {
|
|
329
|
+
// Most multi-file failures are stale context in a later, independent
|
|
330
|
+
// file. For unique targets, validate every section against current bytes
|
|
331
|
+
// before the first write. Same-target sequences retain the existing
|
|
332
|
+
// ordered conversion/rollback path because later sections may
|
|
333
|
+
// intentionally depend on earlier edits.
|
|
334
|
+
const preparedUnits = new Map();
|
|
335
|
+
const uniqueTargets = new Set(lockPaths.map(patchPathKey)).size === units.length;
|
|
336
|
+
if (!dryRun && rollbackOnFailure && uniqueTargets) {
|
|
337
|
+
for (let i = 0; i < units.length; i++) {
|
|
338
|
+
const unit = units[i];
|
|
339
|
+
if (abortSignal?.aborted) return 'Error: apply_patch aborted during preflight; no files were written';
|
|
340
|
+
let parsed;
|
|
341
|
+
try {
|
|
342
|
+
parsed = await unit.buildParsed();
|
|
343
|
+
} catch (err) {
|
|
344
|
+
return `Error: apply_patch preflight rejected section ${i + 1}/${units.length} (${unit.displayPath}); no files were written.\n${err?.message || String(err)}`;
|
|
345
|
+
}
|
|
346
|
+
if (!Array.isArray(parsed) || parsed.length === 0) {
|
|
347
|
+
preparedUnits.set(i, { parsed, wave: null });
|
|
348
|
+
continue;
|
|
349
|
+
}
|
|
350
|
+
let wave;
|
|
351
|
+
try {
|
|
352
|
+
const { entries, headerRewrites } = await preValidateNativeBatch(parsed, basePath);
|
|
353
|
+
wave = { parsed, entries, headerRewrites };
|
|
354
|
+
} catch (err) {
|
|
355
|
+
return `Error: apply_patch preflight rejected section ${i + 1}/${units.length} (${unit.displayPath}); no files were written.\n${err?.message || String(err)}`;
|
|
356
|
+
}
|
|
357
|
+
const checked = await applyParsedWave(wave, basePath, { ...waveOpts, dryRun: true });
|
|
358
|
+
if (checked.error) {
|
|
359
|
+
const detail = checked.error.replace(/^Error:\s*/, '');
|
|
360
|
+
return `Error: apply_patch preflight rejected section ${i + 1}/${units.length} (${unit.displayPath}); no files were written.\n${detail}`;
|
|
361
|
+
}
|
|
362
|
+
preparedUnits.set(i, { parsed, wave });
|
|
363
|
+
}
|
|
364
|
+
}
|
|
250
365
|
let rollbackSnapshots = [];
|
|
251
366
|
if (!dryRun && rollbackOnFailure) {
|
|
252
367
|
try {
|
|
@@ -268,13 +383,15 @@ async function applyPatchSequence(patchStr, requestedFormat, basePath, ctx) {
|
|
|
268
383
|
failedIndex = i;
|
|
269
384
|
continue;
|
|
270
385
|
}
|
|
271
|
-
let parsed;
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
386
|
+
let parsed = preparedUnits.get(i)?.parsed;
|
|
387
|
+
if (!preparedUnits.has(i)) {
|
|
388
|
+
try {
|
|
389
|
+
parsed = await unit.buildParsed();
|
|
390
|
+
} catch (err) {
|
|
391
|
+
failed = { displayPath: unit.displayPath, error: `Error: ${err?.message || String(err)}` };
|
|
392
|
+
failedIndex = i;
|
|
393
|
+
continue;
|
|
394
|
+
}
|
|
278
395
|
}
|
|
279
396
|
if (!Array.isArray(parsed) || parsed.length === 0) {
|
|
280
397
|
// Section produced no applicable hunks (all skipped / no-op). Nothing
|
|
@@ -282,14 +399,16 @@ async function applyPatchSequence(patchStr, requestedFormat, basePath, ctx) {
|
|
|
282
399
|
applied.push({ displayPath: unit.displayPath, text: `(no changes) ${unit.displayPath}` });
|
|
283
400
|
continue;
|
|
284
401
|
}
|
|
285
|
-
let wave;
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
402
|
+
let wave = preparedUnits.get(i)?.wave;
|
|
403
|
+
if (!wave) {
|
|
404
|
+
try {
|
|
405
|
+
const { entries, headerRewrites } = await preValidateNativeBatch(parsed, basePath);
|
|
406
|
+
wave = { parsed, entries, headerRewrites };
|
|
407
|
+
} catch (err) {
|
|
408
|
+
failed = { displayPath: unit.displayPath, error: `Error: ${err?.message || String(err)}` };
|
|
409
|
+
failedIndex = i;
|
|
410
|
+
continue;
|
|
411
|
+
}
|
|
293
412
|
}
|
|
294
413
|
const res = await applyParsedWave(wave, basePath, waveOpts);
|
|
295
414
|
backend = res.backend;
|
|
@@ -473,7 +592,11 @@ async function apply_patch(args, cwd, options = {}) {
|
|
|
473
592
|
let v4aRenamePlan = null;
|
|
474
593
|
if (isV4APatchInput(patchStr, requestedFormat)) {
|
|
475
594
|
try {
|
|
476
|
-
const allSections =
|
|
595
|
+
const allSections = rewriteV4AReadRedirects(
|
|
596
|
+
parseV4APatch(patchStr),
|
|
597
|
+
basePath,
|
|
598
|
+
readStateScope,
|
|
599
|
+
);
|
|
477
600
|
v4aRenamePlan = await planV4ARenameSections(allSections, basePath);
|
|
478
601
|
inputPatchStr = await convertV4ASectionsToUnifiedPatch(v4aRenamePlan.remainingSections, basePath, v4aConvertOpts);
|
|
479
602
|
if (v4aRenamePlan.renameSections.length > 0) {
|
|
@@ -486,7 +609,12 @@ async function apply_patch(args, cwd, options = {}) {
|
|
|
486
609
|
}
|
|
487
610
|
} else if (requestedFormat !== 'unified' && hasUnifiedBareV4AHunk(patchStr)) {
|
|
488
611
|
try {
|
|
489
|
-
|
|
612
|
+
const sections = rewriteV4AReadRedirects(
|
|
613
|
+
parseUnifiedBareV4APatch(patchStr),
|
|
614
|
+
basePath,
|
|
615
|
+
readStateScope,
|
|
616
|
+
);
|
|
617
|
+
inputPatchStr = await convertV4ASectionsToUnifiedPatch(sections, basePath, v4aConvertOpts);
|
|
490
618
|
} catch (err) {
|
|
491
619
|
throw new Error(`apply_patch: bare @@ parse failed — ${err?.message || String(err)}`);
|
|
492
620
|
}
|
|
@@ -502,7 +630,12 @@ async function apply_patch(args, cwd, options = {}) {
|
|
|
502
630
|
throw new Error(`apply_patch: parse failed — ${err?.message || String(err)}; prefer V4A envelope for multi-hunk edits (no @@ line counts)`);
|
|
503
631
|
}
|
|
504
632
|
try {
|
|
505
|
-
|
|
633
|
+
const sections = rewriteV4AReadRedirects(
|
|
634
|
+
parseUnifiedCountedAsV4APatch(patchStr),
|
|
635
|
+
basePath,
|
|
636
|
+
readStateScope,
|
|
637
|
+
);
|
|
638
|
+
inputPatchStr = await convertV4ASectionsToUnifiedPatch(sections, basePath, v4aConvertOpts);
|
|
506
639
|
normalizedPatchStr = prepareInput(inputPatchStr);
|
|
507
640
|
parsed = parsePatch(normalizedPatchStr);
|
|
508
641
|
mutationPlan = {
|
|
@@ -514,6 +647,9 @@ async function apply_patch(args, cwd, options = {}) {
|
|
|
514
647
|
throw new Error(`apply_patch: parse failed — ${err?.message || String(err)}; V4A fallback failed — ${fallbackErr?.message || String(fallbackErr)}`);
|
|
515
648
|
}
|
|
516
649
|
}
|
|
650
|
+
if (!v4aRenameOnly) {
|
|
651
|
+
parsed = rewriteParsedReadRedirects(parsed, basePath, readStateScope);
|
|
652
|
+
}
|
|
517
653
|
if (!v4aRenameOnly && (!Array.isArray(parsed) || parsed.length === 0)) {
|
|
518
654
|
return 'Error: patch contained no file sections';
|
|
519
655
|
}
|
|
@@ -560,8 +696,7 @@ async function apply_patch(args, cwd, options = {}) {
|
|
|
560
696
|
}),
|
|
561
697
|
];
|
|
562
698
|
|
|
563
|
-
|
|
564
|
-
withAdvisoryLocks(_lockPaths, async () => {
|
|
699
|
+
const runLegacyBulkBatch = async () => {
|
|
565
700
|
let v4aRenameResults = [];
|
|
566
701
|
if (v4aRenamePlan?.renameSections?.length) {
|
|
567
702
|
v4aRenameResults = await applyV4ARenameSections(v4aRenamePlan.renameSections, basePath, v4aConvertOpts);
|
|
@@ -629,7 +764,44 @@ async function apply_patch(args, cwd, options = {}) {
|
|
|
629
764
|
return wrapPatchMutationOutput(`${combined}\n${tail.join('\n')}`, mutationPlan, { backend });
|
|
630
765
|
}
|
|
631
766
|
return wrapPatchMutationOutput(combined, mutationPlan, { backend });
|
|
632
|
-
}
|
|
767
|
+
};
|
|
768
|
+
|
|
769
|
+
return withBuiltinPathLocks(_lockPaths, () =>
|
|
770
|
+
withAdvisoryLocks(_lockPaths, async () => {
|
|
771
|
+
// Legacy bulk/atomic mode applies every wave in one shot, and a wave that
|
|
772
|
+
// mixes native (in-base) with JS (out-of-base) entries commits the native
|
|
773
|
+
// writes before the JS entries run. Snapshot every touched path up front
|
|
774
|
+
// and restore it whenever the batch fails — by returned Error text OR by
|
|
775
|
+
// a thrown error (V4A rename, persistence) — so mode:"atomic" really is
|
|
776
|
+
// all-or-nothing instead of leaving an earlier commit in place.
|
|
777
|
+
let rollbackSnapshots = [];
|
|
778
|
+
if (!dryRun) {
|
|
779
|
+
try {
|
|
780
|
+
rollbackSnapshots = capturePatchRollbackState(_lockPaths);
|
|
781
|
+
} catch (err) {
|
|
782
|
+
return `Error: ${err?.message || String(err)}`;
|
|
783
|
+
}
|
|
784
|
+
}
|
|
785
|
+
// Restoration errors are never swallowed: an incomplete rollback is
|
|
786
|
+
// reported verbatim so the caller never reads a false all-or-nothing.
|
|
787
|
+
const withRollback = (outcome) => {
|
|
788
|
+
const rollbackErrors = restorePatchRollbackState(rollbackSnapshots, readStateScope);
|
|
789
|
+
return rollbackErrors.length === 0
|
|
790
|
+
? `${outcome}\n--- rolled back: every touched path was restored to its pre-patch state ---`
|
|
791
|
+
: [outcome, '--- rollback incomplete ---', ...rollbackErrors].join('\n');
|
|
792
|
+
};
|
|
793
|
+
let outcome;
|
|
794
|
+
try {
|
|
795
|
+
outcome = await runLegacyBulkBatch();
|
|
796
|
+
} catch (err) {
|
|
797
|
+
// A thrown failure (e.g. V4A rename) took the same path to disk as a
|
|
798
|
+
// returned one, so it takes the same path back out.
|
|
799
|
+
if (dryRun) throw err;
|
|
800
|
+
return withRollback(`Error: ${err?.message || String(err)}`);
|
|
801
|
+
}
|
|
802
|
+
if (dryRun || !isPatchErrorText(outcome)) return outcome;
|
|
803
|
+
return withRollback(outcome);
|
|
804
|
+
}));
|
|
633
805
|
}
|
|
634
806
|
|
|
635
807
|
export async function executePatchTool(name, args, cwd, options = {}) {
|
|
@@ -24,6 +24,16 @@ export function stripDiffPrefix(name) {
|
|
|
24
24
|
}
|
|
25
25
|
|
|
26
26
|
export function resolveEntryPath(basePath, rawName) {
|
|
27
|
+
// A parsed entry without a header path (jsdiff parses arbitrary non-diff
|
|
28
|
+
// text into a headerless entry) must fail as a patch-format error, not as a
|
|
29
|
+
// raw `The "path" argument must be of type string` TypeError from node:path.
|
|
30
|
+
if (rawName == null || rawName === '') {
|
|
31
|
+
throw new Error(
|
|
32
|
+
'apply_patch: a file section header could not be parsed (no target path) — the patch body is not a valid diff. '
|
|
33
|
+
+ 'Each section must start with `*** Update File: <path>` / `*** Add File: <path>` / `*** Delete File: <path>` '
|
|
34
|
+
+ '(V4A, wrapped in `*** Begin Patch` / `*** End Patch`), or a `--- a/<path>` + `+++ b/<path>` pair (unified).',
|
|
35
|
+
);
|
|
36
|
+
}
|
|
27
37
|
const stripped = stripDiffPrefix(rawName);
|
|
28
38
|
const norm = normalizeInputPath(stripped);
|
|
29
39
|
return isAbsolute(norm) ? pathResolve(norm) : resolveAgainstCwd(norm, basePath);
|
|
@@ -156,6 +166,18 @@ function unifiedRange(start, lines) {
|
|
|
156
166
|
return `${s},${n}`;
|
|
157
167
|
}
|
|
158
168
|
|
|
169
|
+
// jsdiff's parser normalizes the unified-diff "zero count" quirk by bumping the
|
|
170
|
+
// start by one (`parse.js`: `if (hunk.oldLines === 0) hunk.oldStart += 1`), so a
|
|
171
|
+
// re-render of a parsed hunk must undo it. Without this, an insert-only hunk
|
|
172
|
+
// `@@ -N,0 @@` reaches the native engine as `-(N+1),0` and is anchored one line
|
|
173
|
+
// too late — or rejected outright when N is already the last line (the engine
|
|
174
|
+
// treats N as a 0-based "insert after line N" skip count, valid 0..=line_count).
|
|
175
|
+
function unifiedRangeFromParsed(start, lines) {
|
|
176
|
+
const n = Math.max(0, Number(lines) || 0);
|
|
177
|
+
const s = Math.max(0, Number(start) || 0);
|
|
178
|
+
return unifiedRange(n === 0 ? Math.max(0, s - 1) : s, n);
|
|
179
|
+
}
|
|
180
|
+
|
|
159
181
|
export function renderParsedUnifiedPatch(parsed) {
|
|
160
182
|
const out = [];
|
|
161
183
|
for (const entry of parsed || []) {
|
|
@@ -163,7 +185,7 @@ export function renderParsedUnifiedPatch(parsed) {
|
|
|
163
185
|
out.push(`+++ ${entry.newFileName || '/dev/null'}`);
|
|
164
186
|
for (const hunk of entry.hunks || []) {
|
|
165
187
|
const section = hunk.section ? ` ${hunk.section}` : '';
|
|
166
|
-
out.push(`@@ -${
|
|
188
|
+
out.push(`@@ -${unifiedRangeFromParsed(hunk.oldStart, hunk.oldLines)} +${unifiedRangeFromParsed(hunk.newStart, hunk.newLines)} @@${section}`);
|
|
167
189
|
for (const line of hunk.lines || []) out.push(line);
|
|
168
190
|
}
|
|
169
191
|
}
|
|
@@ -34,6 +34,7 @@ import {
|
|
|
34
34
|
formatPatchSourceExcerpt,
|
|
35
35
|
compactPatchPreviewLine,
|
|
36
36
|
decodeValidUtf8OrNull,
|
|
37
|
+
assertSafeReplacementPlan,
|
|
37
38
|
} from './matcher.mjs';
|
|
38
39
|
|
|
39
40
|
function v4AHunkLineStats(hunk) {
|
|
@@ -154,6 +155,15 @@ function cloneTextLinesForPatch(sourceLines) {
|
|
|
154
155
|
return lines;
|
|
155
156
|
}
|
|
156
157
|
|
|
158
|
+
// Codex parity (codex-rs/apply-patch/src/lib.rs compute_replacements): a chunk
|
|
159
|
+
// with no old lines is a pure addition and is inserted at end-of-file — before
|
|
160
|
+
// the trailing empty line when the file ends with a blank line — never at the
|
|
161
|
+
// current anchor cursor.
|
|
162
|
+
function eofInsertionIndex(sourceLines) {
|
|
163
|
+
const len = (sourceLines || []).length;
|
|
164
|
+
return len > 0 && sourceLines[len - 1] === '' ? len - 1 : len;
|
|
165
|
+
}
|
|
166
|
+
|
|
157
167
|
function resolveV4AHunkPosition(sourceLines, hunk, nextSearchLine, options = {}) {
|
|
158
168
|
const stats = v4AHunkLineStats(hunk);
|
|
159
169
|
if (stats.oldCount === 0 && stats.newCount === 0) return { skip: true };
|
|
@@ -170,7 +180,7 @@ function resolveV4AHunkPosition(sourceLines, hunk, nextSearchLine, options = {})
|
|
|
170
180
|
let trimmedTrailing = 0;
|
|
171
181
|
let trimmedTrailingNew = 0;
|
|
172
182
|
if (stats.oldCount === 0) {
|
|
173
|
-
oldStartIdx =
|
|
183
|
+
oldStartIdx = eofInsertionIndex(sourceLines);
|
|
174
184
|
} else {
|
|
175
185
|
const searchFrom = Math.max(0, anchorLine - 1);
|
|
176
186
|
oldStartIdx = findLineSequence(
|
|
@@ -180,20 +190,32 @@ function resolveV4AHunkPosition(sourceLines, hunk, nextSearchLine, options = {})
|
|
|
180
190
|
searchFrom,
|
|
181
191
|
{ fuzzy, eof },
|
|
182
192
|
);
|
|
183
|
-
|
|
193
|
+
// Codex parity (compute_replacements): when the first seek fails and the
|
|
194
|
+
// pattern's last line is the empty string that stands in for the region's
|
|
195
|
+
// terminating newline, retry without it. Codex runs this retry for EVERY
|
|
196
|
+
// chunk, not only `*** End of File` ones, so a hunk whose trailing blank
|
|
197
|
+
// context line sits at end-of-file resolves instead of reporting a context
|
|
198
|
+
// miss. When the retry empties the pattern (the old side was that single
|
|
199
|
+
// blank line) Codex's seek returns the cursor, i.e. the hunk becomes a
|
|
200
|
+
// zero-length insertion at the anchor cursor; `anchorLine` IS that cursor
|
|
201
|
+
// (Codex `line_index`), so use it directly instead of the seek's
|
|
202
|
+
// preferred-line fallback.
|
|
203
|
+
if (oldStartIdx < 0 && oldLinesPattern.length > 0 && oldLinesPattern[oldLinesPattern.length - 1] === '') {
|
|
184
204
|
oldLinesPattern = oldLinesPattern.slice(0, -1);
|
|
185
205
|
trimmedTrailing = 1;
|
|
186
206
|
if (newLinesPattern.length > 0 && newLinesPattern[newLinesPattern.length - 1] === '') {
|
|
187
207
|
newLinesPattern = newLinesPattern.slice(0, -1);
|
|
188
208
|
trimmedTrailingNew = 1;
|
|
189
209
|
}
|
|
190
|
-
oldStartIdx =
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
210
|
+
oldStartIdx = oldLinesPattern.length === 0
|
|
211
|
+
? anchorLine
|
|
212
|
+
: findLineSequence(
|
|
213
|
+
sourceLines,
|
|
214
|
+
oldLinesPattern,
|
|
215
|
+
searchFrom,
|
|
216
|
+
searchFrom,
|
|
217
|
+
{ fuzzy, eof },
|
|
218
|
+
);
|
|
197
219
|
}
|
|
198
220
|
}
|
|
199
221
|
// Escape-equivalence fallback (fuzzy, non-EOF only): accept a window where each old
|
|
@@ -229,7 +251,11 @@ function resolveV4AHunkPosition(sourceLines, hunk, nextSearchLine, options = {})
|
|
|
229
251
|
oldStartIdx,
|
|
230
252
|
matchLen,
|
|
231
253
|
newLines: newLinesPattern,
|
|
232
|
-
|
|
254
|
+
// Codex parity: a zero-length replacement never advances the cursor
|
|
255
|
+
// (compute_replacements `continue`s on the pure-addition branch), so a
|
|
256
|
+
// following hunk can still resolve against lines before the insertion
|
|
257
|
+
// point instead of being pushed past end-of-file.
|
|
258
|
+
nextSearchLine: matchLen === 0 ? Math.max(0, nextSearchLine || 0) : oldStartIdx + matchLen,
|
|
233
259
|
trimmedTrailing,
|
|
234
260
|
trimmedTrailingNew,
|
|
235
261
|
};
|
|
@@ -251,6 +277,13 @@ export function applyV4AHunksToLines(sourceLines, hunks, options = {}) {
|
|
|
251
277
|
});
|
|
252
278
|
nextSearchLine = loc.nextSearchLine;
|
|
253
279
|
}
|
|
280
|
+
// Codex parity (compute_replacements sorts by start index before
|
|
281
|
+
// apply_replacements walks them in reverse): descending application is only
|
|
282
|
+
// position-safe when the list is ordered by start index. Resolution order is
|
|
283
|
+
// normally already ascending; the sort is stable, so equal starts keep their
|
|
284
|
+
// resolution order.
|
|
285
|
+
replacements.sort((a, b) => a.oldStartIdx - b.oldStartIdx);
|
|
286
|
+
assertSafeReplacementPlan(replacements.map((rep) => ({ start: rep.oldStartIdx, oldLen: rep.oldLen })));
|
|
254
287
|
for (const rep of replacements.reverse()) {
|
|
255
288
|
lines.splice(rep.oldStartIdx, rep.oldLen, ...rep.newLines);
|
|
256
289
|
}
|
|
@@ -628,6 +661,12 @@ export async function convertV4ASectionsToUnifiedPatch(sections, basePath, optio
|
|
|
628
661
|
throw new Error(`V4A update target unreadable: ${section.path} (${err?.code || err?.message || String(err)}).`);
|
|
629
662
|
}
|
|
630
663
|
const sectionHunks = [];
|
|
664
|
+
// Resolved-position ordering for the emitted unified hunks: the unique-
|
|
665
|
+
// verbatim fallback in findLineSequence can resolve a later-listed hunk
|
|
666
|
+
// BEFORE an earlier one, and the native engine applies unified hunks with
|
|
667
|
+
// a forward-only cursor — emission must therefore be sorted by resolved
|
|
668
|
+
// start (stable on ties) instead of resolution order.
|
|
669
|
+
const sectionHunkEntries = [];
|
|
631
670
|
const orderedHunks = orderV4AHunksByFilePosition(sourceLines, section.hunks, fuzzy);
|
|
632
671
|
let nextSearchLine = 0;
|
|
633
672
|
for (const hunk of orderedHunks) {
|
|
@@ -641,12 +680,7 @@ export async function convertV4ASectionsToUnifiedPatch(sections, basePath, optio
|
|
|
641
680
|
if (rejectedHunks) rejectedHunks.push({ file: section.path, hunk, reason: msg });
|
|
642
681
|
continue;
|
|
643
682
|
}
|
|
644
|
-
const oldStart = stats.oldCount === 0 ? loc.oldStartIdx : loc.oldStartIdx + 1;
|
|
645
|
-
const newStart = oldStart;
|
|
646
683
|
const tail = (hunk.anchors || []).filter(Boolean).join(' ');
|
|
647
|
-
const oldCount = stats.oldCount === 0 ? 0 : loc.matchLen;
|
|
648
|
-
const newCount = stats.newCount - (loc.trimmedTrailingNew || 0);
|
|
649
|
-
sectionHunks.push(`@@ -${oldStart},${oldCount} +${newStart},${newCount} @@${tail ? ` ${tail}` : ''}`);
|
|
650
684
|
let dropOldAt = -1;
|
|
651
685
|
let dropNewAt = -1;
|
|
652
686
|
if (loc.trimmedTrailing) {
|
|
@@ -659,6 +693,13 @@ export async function convertV4ASectionsToUnifiedPatch(sections, basePath, optio
|
|
|
659
693
|
if (dropOldAt >= 0 && (!loc.trimmedTrailingNew || dropNewAt >= 0)) break;
|
|
660
694
|
}
|
|
661
695
|
}
|
|
696
|
+
// Emit the body FIRST and derive the header counts from what was
|
|
697
|
+
// actually emitted: the trailing-newline sentinel drop can remove a line
|
|
698
|
+
// from one side only, so a header computed from the raw hunk stats can
|
|
699
|
+
// disagree with the body and make the emitted unified patch unparseable.
|
|
700
|
+
const bodyLines = [];
|
|
701
|
+
let emittedOld = 0;
|
|
702
|
+
let emittedNew = 0;
|
|
662
703
|
let srcIdx = loc.oldStartIdx;
|
|
663
704
|
const srcEnd = loc.oldStartIdx + loc.matchLen;
|
|
664
705
|
for (let i = 0; i < hunk.lines.length; i++) {
|
|
@@ -666,20 +707,47 @@ export async function convertV4ASectionsToUnifiedPatch(sections, basePath, optio
|
|
|
666
707
|
if (isV4AEndOfFileMarker(line)) continue;
|
|
667
708
|
const prefix = line[0];
|
|
668
709
|
if (prefix === ' ' || prefix === '-') {
|
|
669
|
-
if (i === dropOldAt
|
|
710
|
+
if (i === dropOldAt) {
|
|
711
|
+
// The sentinel line leaves the OLD side. When it is a context line
|
|
712
|
+
// that the new side still keeps (Codex only trims the new slice
|
|
713
|
+
// when it ends with the sentinel), it survives as an addition.
|
|
714
|
+
if (prefix === ' ' && i !== dropNewAt) {
|
|
715
|
+
bodyLines.push(`+${line.slice(1)}`);
|
|
716
|
+
emittedNew++;
|
|
717
|
+
}
|
|
718
|
+
continue;
|
|
719
|
+
}
|
|
720
|
+
if (i === dropNewAt) continue;
|
|
670
721
|
if (srcIdx < srcEnd && srcIdx < sourceLines.length) {
|
|
671
|
-
|
|
722
|
+
bodyLines.push(prefix + sourceLines[srcIdx]);
|
|
672
723
|
} else {
|
|
673
|
-
|
|
724
|
+
bodyLines.push(line);
|
|
674
725
|
}
|
|
726
|
+
emittedOld++;
|
|
727
|
+
if (prefix === ' ') emittedNew++;
|
|
675
728
|
srcIdx++;
|
|
676
729
|
} else {
|
|
677
730
|
if (i === dropNewAt) continue;
|
|
678
|
-
|
|
731
|
+
bodyLines.push(line);
|
|
732
|
+
emittedNew++;
|
|
679
733
|
}
|
|
680
734
|
}
|
|
735
|
+
// A zero-length old range is an insertion: its header start is the 0-based
|
|
736
|
+
// insertion index (`-N,0` inserts after source line N), while a real
|
|
737
|
+
// replacement starts at the 1-based first replaced line.
|
|
738
|
+
const oldStart = emittedOld === 0 ? loc.oldStartIdx : loc.oldStartIdx + 1;
|
|
739
|
+
sectionHunkEntries.push({
|
|
740
|
+
start: loc.oldStartIdx,
|
|
741
|
+
order: sectionHunkEntries.length,
|
|
742
|
+
lines: [
|
|
743
|
+
`@@ -${oldStart},${emittedOld} +${oldStart},${emittedNew} @@${tail ? ` ${tail}` : ''}`,
|
|
744
|
+
...bodyLines,
|
|
745
|
+
],
|
|
746
|
+
});
|
|
681
747
|
nextSearchLine = loc.nextSearchLine;
|
|
682
748
|
}
|
|
749
|
+
sectionHunkEntries.sort((a, b) => (a.start - b.start) || (a.order - b.order));
|
|
750
|
+
for (const entry of sectionHunkEntries) sectionHunks.push(...entry.lines);
|
|
683
751
|
if (sectionHunks.length > 0) {
|
|
684
752
|
out.push(`--- a/${displayPath}`);
|
|
685
753
|
out.push(`+++ b/${displayPath}`);
|
|
@@ -1,26 +1,26 @@
|
|
|
1
1
|
{
|
|
2
|
-
"version": "0.7.
|
|
2
|
+
"version": "0.7.22",
|
|
3
3
|
"_comment": "Rewritten by .github/workflows/patch-release.yml on each tagged release. assets maps platformKey (process.platform-process.arch, e.g. win32-x64, linux-x64, darwin-arm64) to { url, sha256 } of the mixdog-patch binary on the GitHub release. A local cargo build under native/mixdog-patch/target/release always takes precedence; otherwise the binary is fetched per this manifest into the data dir (apply is native-only — no JS apply engine).",
|
|
4
4
|
"assets": {
|
|
5
5
|
"darwin-arm64": {
|
|
6
|
-
"url": "https://github.com/tribgames/mixdog/releases/download/patch-v0.7.
|
|
7
|
-
"sha256": "
|
|
6
|
+
"url": "https://github.com/tribgames/mixdog/releases/download/patch-v0.7.22/mixdog-patch-darwin-arm64",
|
|
7
|
+
"sha256": "6990ee4442aa4f5ec4c07926b2f22e8fcbc17b668a1be005d71551bfcf61fc73"
|
|
8
8
|
},
|
|
9
9
|
"darwin-x64": {
|
|
10
|
-
"url": "https://github.com/tribgames/mixdog/releases/download/patch-v0.7.
|
|
11
|
-
"sha256": "
|
|
10
|
+
"url": "https://github.com/tribgames/mixdog/releases/download/patch-v0.7.22/mixdog-patch-darwin-x64",
|
|
11
|
+
"sha256": "0118298483df38af7e0463bede8412c2267c8df96b586ce98120c9139ad22977"
|
|
12
12
|
},
|
|
13
13
|
"linux-arm64": {
|
|
14
|
-
"url": "https://github.com/tribgames/mixdog/releases/download/patch-v0.7.
|
|
15
|
-
"sha256": "
|
|
14
|
+
"url": "https://github.com/tribgames/mixdog/releases/download/patch-v0.7.22/mixdog-patch-linux-arm64",
|
|
15
|
+
"sha256": "91330967bf089b943e6175f9ade45fbac2d4e6d4449c9ea28853c1acf0f472db"
|
|
16
16
|
},
|
|
17
17
|
"linux-x64": {
|
|
18
|
-
"url": "https://github.com/tribgames/mixdog/releases/download/patch-v0.7.
|
|
19
|
-
"sha256": "
|
|
18
|
+
"url": "https://github.com/tribgames/mixdog/releases/download/patch-v0.7.22/mixdog-patch-linux-x64",
|
|
19
|
+
"sha256": "399e7c68a42f6e15fca28270ea3e216b8d34e48b53702bbefa2354cf0cc956f5"
|
|
20
20
|
},
|
|
21
21
|
"win32-x64": {
|
|
22
|
-
"url": "https://github.com/tribgames/mixdog/releases/download/patch-v0.7.
|
|
23
|
-
"sha256": "
|
|
22
|
+
"url": "https://github.com/tribgames/mixdog/releases/download/patch-v0.7.22/mixdog-patch-win32-x64.exe",
|
|
23
|
+
"sha256": "13e2373bceb6c6f9cafe7b815b5d20bffcb20414d8c9dfbb287dd7fd110a811e"
|
|
24
24
|
}
|
|
25
25
|
}
|
|
26
26
|
}
|