opencode-swarm 7.186.8 → 7.187.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/binaries/win32-arm64/swarm-sandbox-runner.exe +0 -0
- package/binaries/win32-x64/swarm-sandbox-runner.exe +0 -0
- package/dist/agents/architect.d.ts +1 -1
- package/dist/cli/{auto-checkpoint-hw4rttv4.js → auto-checkpoint-r69e82bh.js} +10 -11
- package/dist/cli/{checkpoint-fdkh943k.js → checkpoint-1sqwbgra.js} +18 -19
- package/dist/cli/{coder-settlement-91r5s8ar.js → coder-settlement-b9qx0732.js} +14 -15
- package/dist/cli/{config-doctor-nzds5xzf.js → config-doctor-s4fr5qwr.js} +34 -4
- package/dist/cli/{core-w0gq7vwa.js → core-bezezjy9.js} +5 -6
- package/dist/cli/{curation-policy-kw6x1cba.js → curation-policy-2x2w3b57.js} +7 -8
- package/dist/cli/{curator-drift-svnc70x4.js → curator-drift-785xnxdv.js} +10 -11
- package/dist/cli/curator-llm-factory-p0wwyswg.js +80 -0
- package/dist/cli/{curator-gkrn8wz3.js → curator-w4hp7a10.js} +44 -45
- package/dist/cli/{dispatch-0ejzwb45.js → dispatch-qvcztbpf.js} +222 -48
- package/dist/cli/{evidence-summary-service-a2tx8tbn.js → evidence-summary-service-33qcm9xc.js} +21 -22
- package/dist/cli/{gate-evidence-28bys0r0.js → gate-evidence-9ehj968e.js} +9 -10
- package/dist/cli/guardrail-explain-5715fdzv.js +81 -0
- package/dist/cli/guardrail-log-k1dfzfzm.js +22 -0
- package/dist/cli/{guardrail-reset-w5kpwfxb.js → guardrail-reset-nt88n9dm.js} +44 -45
- package/dist/cli/hive-promoter-c8fjn186.js +95 -0
- package/dist/cli/{index-mr86ywsj.js → index-0yj79yk1.js} +1 -1
- package/dist/cli/{index-mtb763x4.js → index-1vvr69ka.js} +1 -1
- package/dist/cli/{index-b9zqye89.js → index-2s8wdagg.js} +44 -45
- package/dist/cli/{index-e47fhj4k.js → index-2wgvjwpr.js} +5 -5
- package/dist/cli/{index-cvwkjnr5.js → index-469xmpd6.js} +1 -1
- package/dist/cli/{index-0ghn241n.js → index-4g6sw4eq.js} +1 -1
- package/dist/cli/{index-2nbws8bd.js → index-5m56c5r6.js} +5 -5
- package/dist/cli/{index-e6tjg7t9.js → index-6ersmqjd.js} +9 -9
- package/dist/cli/{index-0k283qtq.js → index-9hag235h.js} +2 -2
- package/dist/cli/{index-sjwpvte3.js → index-a8gvszr2.js} +2 -2
- package/dist/cli/{index-qc1x5vvb.js → index-akt0arnt.js} +2695 -2589
- package/dist/cli/{index-kg4yp11g.js → index-b2je0z0t.js} +4 -4
- package/dist/cli/{index-ha4r51k4.js → index-bbqxt1p5.js} +28 -11
- package/dist/cli/{index-znc71e1c.js → index-bejxkee0.js} +2 -2
- package/dist/cli/{index-06pcq86m.js → index-c8ffgcvz.js} +3 -3
- package/dist/cli/{index-n1szkmbz.js → index-cc1vzq4m.js} +1 -1
- package/dist/cli/{index-c12cz80m.js → index-cppqxjw9.js} +2 -2
- package/dist/cli/{index-d21545hp.js → index-cx4s7cak.js} +46 -47
- package/dist/cli/{index-xgsy3hzf.js → index-d6kvatwp.js} +12 -12
- package/dist/cli/{index-k55ep5se.js → index-df2p0k0s.js} +2 -2
- package/dist/cli/{index-x5mkva6s.js → index-dypmvvga.js} +1 -1
- package/dist/cli/{index-7ae4sgp4.js → index-ectvhv8e.js} +5 -5
- package/dist/cli/{index-whk4027k.js → index-en7vzpeq.js} +4 -4
- package/dist/cli/{index-4y6pndcf.js → index-evy5yrsw.js} +4 -4
- package/dist/cli/{index-mc72pk74.js → index-fzpt2djt.js} +32 -1
- package/dist/cli/{index-5dgg9vhz.js → index-hvr82s1t.js} +565 -76
- package/dist/cli/{index-xdjwefa5.js → index-jtncfvhe.js} +1 -1
- package/dist/cli/{index-c5x4yzt6.js → index-knbjtqwt.js} +5 -5
- package/dist/cli/{index-pntrhnj8.js → index-krqq8kbq.js} +7 -7
- package/dist/cli/{index-extgtmxm.js → index-m1qy7f1e.js} +1 -1
- package/dist/cli/{index-84wt7q5y.js → index-mf19yqss.js} +3 -3
- package/dist/cli/{index-7pstt3w2.js → index-pfpnzf6w.js} +1 -1
- package/dist/cli/{index-144jx1jn.js → index-pts38j6x.js} +5 -5
- package/dist/cli/{index-dbwpcjhw.js → index-pymr99as.js} +8 -8
- package/dist/cli/{index-616pp1nq.js → index-qzgfheb4.js} +3 -3
- package/dist/cli/{index-e4hy4des.js → index-r6e8syr3.js} +1 -1
- package/dist/cli/{index-t6na8zpb.js → index-r7gt2w1b.js} +4 -4
- package/dist/cli/{index-gsqmefdg.js → index-rm676s0p.js} +2 -2
- package/dist/cli/{index-n5ekzhad.js → index-sztx0vyt.js} +5 -5
- package/dist/cli/{index-227te5a7.js → index-t3hcd6zz.js} +2 -2
- package/dist/cli/{index-prksynjw.js → index-ta48pe8j.js} +3 -3
- package/dist/cli/{index-q8dj8b88.js → index-v08ksq0b.js} +2 -2
- package/dist/cli/{index-7bxssqaf.js → index-vx8a8w3b.js} +142 -2
- package/dist/cli/{index-zcpd36zm.js → index-ww8payq9.js} +2 -2
- package/dist/cli/{index-tw513bvn.js → index-xs96nry2.js} +8 -8
- package/dist/cli/{index-0qpa53jg.js → index-ycz8z2db.js} +8 -8
- package/dist/cli/{index-gapyat7s.js → index-yf0cc6c5.js} +2 -2
- package/dist/cli/{index-03z05sbb.js → index-yw1g78v9.js} +2 -2
- package/dist/cli/{index-agm6hegf.js → index-zzxnssy3.js} +4 -4
- package/dist/cli/index.js +45 -46
- package/dist/cli/{knowledge-escalator-0dj3bvh2.js → knowledge-escalator-qttydcx2.js} +15 -16
- package/dist/cli/{knowledge-events-zwhb8jnb.js → knowledge-events-4vf0vj82.js} +13 -14
- package/dist/cli/{knowledge-link-pmbyqe3b.js → knowledge-link-p2c8agc8.js} +6 -7
- package/dist/cli/{knowledge-store-67n0qbhg.js → knowledge-store-bw1dk2eb.js} +7 -8
- package/dist/cli/{knowledge-validator-b58w129y.js → knowledge-validator-gefcc36h.js} +9 -10
- package/dist/cli/{mcp-ayc74bm2.js → mcp-d5p0k3p1.js} +1 -1
- package/dist/cli/{model-preflight-nvjb3bv5.js → model-preflight-fmx49pwy.js} +44 -45
- package/dist/cli/{pending-delegations-yvh7b951.js → pending-delegations-8333cb3y.js} +13 -14
- package/dist/cli/{pr-subscriptions-pkj5mmra.js → pr-subscriptions-p42ny2sn.js} +13 -14
- package/dist/cli/{pr-workflow-gate-zg6tg3c0.js → pr-workflow-gate-csy32z8w.js} +44 -45
- package/dist/cli/{runner-jev9vea3.js → runner-cbpr45cs.js} +7 -8
- package/dist/cli/{scan-cursor-8nkt0n1b.js → scan-cursor-14mwymtz.js} +8 -9
- package/dist/cli/{schema-ahy4ps8z.js → schema-6426g0gw.js} +2 -2
- package/dist/cli/{scope-persistence-h8b2hcy9.js → scope-persistence-avkv3s4m.js} +17 -18
- package/dist/cli/{server-14wr2fz2.js → server-d9rk2zeb.js} +45 -47
- package/dist/cli/{skill-generator-2pk8fvfz.js → skill-generator-epdvdsh5.js} +17 -18
- package/dist/cli/{snapshot-coordination-init-keeqdwph.js → snapshot-coordination-init-35f7wrqb.js} +44 -45
- package/dist/cli/{speckit-checkoff-yf09ctgv.js → speckit-checkoff-erwa7fbj.js} +5 -5
- package/dist/cli/{telemetry-rb8hrvbx.js → telemetry-2vwb3n42.js} +2 -3
- package/dist/cli/{workspace-snapshot-28p0ekng.js → workspace-snapshot-y60ay5eq.js} +4 -5
- package/dist/cli/{worktree-collision-ownership-7ctmpqx6.js → worktree-collision-ownership-3tqkw81h.js} +14 -15
- package/dist/cli/{worktree-isolation-2c1hvct4.js → worktree-isolation-0rrjqypt.js} +44 -45
- package/dist/commands/registry.d.ts +4 -4
- package/dist/commands/reset-backup.d.ts +20 -5
- package/dist/commands/rollback-gate.d.ts +73 -0
- package/dist/index.js +612 -597
- package/dist/lang/backends/java.d.ts +76 -0
- package/dist/lang/java-extraction.d.ts +88 -0
- package/dist/services/config-doctor.d.ts +19 -1
- package/dist/services/preflight-integration.d.ts +8 -1
- package/dist/state.d.ts +13 -0
- package/dist/tools/symbols.d.ts +18 -0
- package/opencode-swarm.schema.json +1 -1
- package/package.json +1 -1
- package/dist/cli/curator-llm-factory-j51cttb2.js +0 -81
- package/dist/cli/guardrail-explain-qjyz300y.js +0 -82
- package/dist/cli/guardrail-log-rw25bt8g.js +0 -23
- package/dist/cli/hive-promoter-qzz0veev.js +0 -96
- package/dist/cli/index-c1ggtmpm.js +0 -33
|
@@ -7,7 +7,7 @@ import {
|
|
|
7
7
|
loadPlan,
|
|
8
8
|
transientBackoff,
|
|
9
9
|
updateTaskCheckpointReceipt
|
|
10
|
-
} from "./index-
|
|
10
|
+
} from "./index-d6kvatwp.js";
|
|
11
11
|
import {
|
|
12
12
|
tool
|
|
13
13
|
} from "./index-9nyqt3kb.js";
|
|
@@ -18,12 +18,15 @@ import {
|
|
|
18
18
|
isSpawnCwdMissing,
|
|
19
19
|
isSpawnCwdUnreadable
|
|
20
20
|
} from "./index-g4fs6z4e.js";
|
|
21
|
+
import {
|
|
22
|
+
validateSwarmPath
|
|
23
|
+
} from "./index-bejxkee0.js";
|
|
21
24
|
import {
|
|
22
25
|
readLedgerEvents
|
|
23
|
-
} from "./index-
|
|
26
|
+
} from "./index-5m56c5r6.js";
|
|
24
27
|
import {
|
|
25
28
|
appendCoreEventSync
|
|
26
|
-
} from "./index-
|
|
29
|
+
} from "./index-ww8payq9.js";
|
|
27
30
|
import {
|
|
28
31
|
derivePlanId,
|
|
29
32
|
derivePlanIdentityHash
|
|
@@ -36,9 +39,12 @@ import {
|
|
|
36
39
|
import {
|
|
37
40
|
tryAcquireLock
|
|
38
41
|
} from "./index-ejhxgyvm.js";
|
|
42
|
+
import {
|
|
43
|
+
atomicWriteSwarmFileSync
|
|
44
|
+
} from "./index-r6e8syr3.js";
|
|
39
45
|
import {
|
|
40
46
|
loadPluginConfigWithMeta
|
|
41
|
-
} from "./index-
|
|
47
|
+
} from "./index-469xmpd6.js";
|
|
42
48
|
import {
|
|
43
49
|
init_git_executable,
|
|
44
50
|
resolveGitExecutable
|
|
@@ -47,20 +53,443 @@ import {
|
|
|
47
53
|
exports_external,
|
|
48
54
|
init_zod
|
|
49
55
|
} from "./index-gknz3bv1.js";
|
|
56
|
+
import {
|
|
57
|
+
init_logger,
|
|
58
|
+
log
|
|
59
|
+
} from "./index-zgwm4ryv.js";
|
|
50
60
|
|
|
51
61
|
// src/tools/checkpoint.ts
|
|
52
62
|
init_zod();
|
|
53
63
|
import * as child_process from "child_process";
|
|
64
|
+
import * as fs4 from "fs";
|
|
65
|
+
import * as path5 from "path";
|
|
66
|
+
|
|
67
|
+
// src/commands/destructive-purge.ts
|
|
68
|
+
import { createHash, randomBytes } from "crypto";
|
|
69
|
+
import * as fs from "fs";
|
|
70
|
+
import * as path from "path";
|
|
71
|
+
init_logger();
|
|
72
|
+
var PENDING_PURGE_PATH = "pending-purge.json";
|
|
73
|
+
var PENDING_PURGE_TTL_MS = 15 * 60 * 1000;
|
|
74
|
+
var _internals = {
|
|
75
|
+
readFileSync: fs.readFileSync,
|
|
76
|
+
rmSync: fs.rmSync.bind(fs),
|
|
77
|
+
existsSync: fs.existsSync.bind(fs),
|
|
78
|
+
atomicWriteSwarmFileSync,
|
|
79
|
+
now: () => Date.now(),
|
|
80
|
+
randomBytes,
|
|
81
|
+
scopeDigest
|
|
82
|
+
};
|
|
83
|
+
function pendingPath(directory) {
|
|
84
|
+
return validateSwarmPath(directory, PENDING_PURGE_PATH);
|
|
85
|
+
}
|
|
86
|
+
function resolveCandidates(scopeTarget, extra) {
|
|
87
|
+
if (extra?.candidates && extra.candidates.length > 0) {
|
|
88
|
+
return { kind: extra.kind ?? "set", candidates: extra.candidates };
|
|
89
|
+
}
|
|
90
|
+
return {
|
|
91
|
+
kind: extra?.kind ?? "single",
|
|
92
|
+
candidates: [{ path: scopeTarget, reason: "operator-requested" }]
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
function scopeDigest(kind, candidates) {
|
|
96
|
+
const paths = candidates.map((c) => {
|
|
97
|
+
const resolved = path.resolve(c.path);
|
|
98
|
+
if (resolved.includes("\x00") || resolved.includes(`
|
|
99
|
+
`)) {
|
|
100
|
+
throw new Error(`Purge candidate path contains a NUL or newline separator character and cannot be scope-bound: ${JSON.stringify(resolved)}`);
|
|
101
|
+
}
|
|
102
|
+
return resolved;
|
|
103
|
+
});
|
|
104
|
+
const sorted = paths.sort().join(`
|
|
105
|
+
`);
|
|
106
|
+
return createHash("sha256").update(`${kind}\x00${sorted}`).digest("hex");
|
|
107
|
+
}
|
|
108
|
+
function mintToken(digest) {
|
|
109
|
+
return createHash("sha256").update(`${digest}:${_internals.randomBytes(16).toString("hex")}`).digest("hex").slice(0, 24);
|
|
110
|
+
}
|
|
111
|
+
function readPending(directory) {
|
|
112
|
+
try {
|
|
113
|
+
const raw = _internals.readFileSync(pendingPath(directory), "utf-8");
|
|
114
|
+
const parsed = JSON.parse(raw);
|
|
115
|
+
if (!parsed || parsed.schemaVersion !== 1 || typeof parsed.scopeDigest !== "string" || typeof parsed.confirmToken !== "string" || typeof parsed.createdAt !== "number") {
|
|
116
|
+
return null;
|
|
117
|
+
}
|
|
118
|
+
if (!Number.isFinite(parsed.createdAt))
|
|
119
|
+
return null;
|
|
120
|
+
if (parsed.createdAt > _internals.now() + 60000)
|
|
121
|
+
return null;
|
|
122
|
+
if (_internals.now() - parsed.createdAt > PENDING_PURGE_TTL_MS) {
|
|
123
|
+
return null;
|
|
124
|
+
}
|
|
125
|
+
return {
|
|
126
|
+
schemaVersion: 1,
|
|
127
|
+
scopeDigest: parsed.scopeDigest,
|
|
128
|
+
confirmToken: parsed.confirmToken,
|
|
129
|
+
createdAt: parsed.createdAt
|
|
130
|
+
};
|
|
131
|
+
} catch {
|
|
132
|
+
return null;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
function writePending(directory, record) {
|
|
136
|
+
_internals.atomicWriteSwarmFileSync(pendingPath(directory), JSON.stringify(record, null, 2));
|
|
137
|
+
}
|
|
138
|
+
function previewDestructivePurge(scopeTarget, projectRoot, extra) {
|
|
139
|
+
const scope = resolveCandidates(scopeTarget, extra);
|
|
140
|
+
const previewLines = [
|
|
141
|
+
`Destructive purge preview (${scope.candidates.length} candidate(s)) for ${projectRoot}:`,
|
|
142
|
+
...scope.candidates.map((c) => ` - ${c.path} (${c.reason})`),
|
|
143
|
+
"Uncommitted work inside these directories will be DESTROYED."
|
|
144
|
+
];
|
|
145
|
+
return {
|
|
146
|
+
previewLines,
|
|
147
|
+
counts: { total: scope.candidates.length },
|
|
148
|
+
optionLabel: `--confirm=<token>`,
|
|
149
|
+
confirmToken: "<run again to receive your confirm token>",
|
|
150
|
+
candidates: scope.candidates
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
function issueConfirmToken(scopeTarget, projectRoot, extra) {
|
|
154
|
+
const scope = resolveCandidates(scopeTarget, extra);
|
|
155
|
+
const digest = scopeDigest(scope.kind, scope.candidates);
|
|
156
|
+
const token = mintToken(digest);
|
|
157
|
+
writePending(projectRoot, {
|
|
158
|
+
schemaVersion: 1,
|
|
159
|
+
scopeDigest: digest,
|
|
160
|
+
confirmToken: token,
|
|
161
|
+
createdAt: _internals.now()
|
|
162
|
+
});
|
|
163
|
+
return token;
|
|
164
|
+
}
|
|
165
|
+
function consumeConfirmToken(scopeTarget, projectRoot, token, extra) {
|
|
166
|
+
const verdict = verifyPendingConfirmToken(scopeTarget, projectRoot, token, extra);
|
|
167
|
+
if (!verdict.ok)
|
|
168
|
+
return verdict;
|
|
169
|
+
try {
|
|
170
|
+
_internals.rmSync(pendingPath(projectRoot), { force: true });
|
|
171
|
+
} catch (error) {
|
|
172
|
+
log(`[destructive-purge] could not consume pending record: ${error instanceof Error ? error.message : String(error)}`);
|
|
173
|
+
}
|
|
174
|
+
return { ok: true };
|
|
175
|
+
}
|
|
176
|
+
function verifyPendingConfirmToken(scopeTarget, projectRoot, token, extra) {
|
|
177
|
+
const pending = readPending(projectRoot);
|
|
178
|
+
if (!pending) {
|
|
179
|
+
return {
|
|
180
|
+
ok: false,
|
|
181
|
+
reason: "no pending purge for this scope (expired, overwritten, or already executed)"
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
const scope = resolveCandidates(scopeTarget, extra);
|
|
185
|
+
const digest = scopeDigest(scope.kind, scope.candidates);
|
|
186
|
+
if (pending.confirmToken !== token) {
|
|
187
|
+
return { ok: false, reason: "confirm token mismatch" };
|
|
188
|
+
}
|
|
189
|
+
if (pending.scopeDigest !== digest) {
|
|
190
|
+
return {
|
|
191
|
+
ok: false,
|
|
192
|
+
reason: "purge scope changed since the token was issued \u2014 re-run the preview and confirm the new token"
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
return { ok: true };
|
|
196
|
+
}
|
|
197
|
+
function executeDestructivePurge(scopeTarget, projectRoot, token, extra) {
|
|
198
|
+
const verdict = verifyPendingConfirmToken(scopeTarget, projectRoot, token, extra);
|
|
199
|
+
if (!verdict.ok)
|
|
200
|
+
return verdict;
|
|
201
|
+
const { candidates } = resolveCandidates(scopeTarget, extra);
|
|
202
|
+
const purged = [];
|
|
203
|
+
for (const candidate of candidates) {
|
|
204
|
+
if (!_internals.existsSync(candidate.path))
|
|
205
|
+
continue;
|
|
206
|
+
try {
|
|
207
|
+
_internals.rmSync(candidate.path, { recursive: true, force: true });
|
|
208
|
+
purged.push(candidate.path);
|
|
209
|
+
} catch (error) {
|
|
210
|
+
try {
|
|
211
|
+
_internals.rmSync(pendingPath(projectRoot), { force: true });
|
|
212
|
+
} catch {}
|
|
213
|
+
return {
|
|
214
|
+
ok: false,
|
|
215
|
+
reason: `candidate ${candidate.path} could not be purged: ${error instanceof Error ? error.message : String(error)}`
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
try {
|
|
220
|
+
_internals.rmSync(pendingPath(projectRoot), { force: true });
|
|
221
|
+
} catch (error) {
|
|
222
|
+
log(`[destructive-purge] could not consume pending record: ${error instanceof Error ? error.message : String(error)}`);
|
|
223
|
+
}
|
|
224
|
+
return { ok: true, purged };
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// src/commands/reset-backup.ts
|
|
54
228
|
import * as fs2 from "fs";
|
|
55
229
|
import * as path2 from "path";
|
|
230
|
+
var RESET_BACKUP_RETENTION = 5;
|
|
231
|
+
function backupSwarmStateBeforeReset(directory, kind, relEntries, opts) {
|
|
232
|
+
const warnings = [];
|
|
233
|
+
const copied = [];
|
|
234
|
+
const swarmDir = path2.join(directory, ".swarm");
|
|
235
|
+
try {
|
|
236
|
+
const stat = fs2.lstatSync(swarmDir);
|
|
237
|
+
if (stat.isSymbolicLink() || !stat.isDirectory()) {
|
|
238
|
+
warnings.push("backup skipped: .swarm is a symlink or not a directory \u2014 nothing was backed up");
|
|
239
|
+
return { backupDir: null, copied, warnings };
|
|
240
|
+
}
|
|
241
|
+
} catch {
|
|
242
|
+
warnings.push("backup skipped: .swarm directory is missing \u2014 nothing was backed up");
|
|
243
|
+
return { backupDir: null, copied, warnings };
|
|
244
|
+
}
|
|
245
|
+
const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
|
|
246
|
+
const backupsRoot = path2.join(swarmDir, opts?.backupsRoot ?? "reset-backups");
|
|
247
|
+
const backupDir = path2.join(backupsRoot, `${kind}-${timestamp}`);
|
|
248
|
+
let backupDirCreated = false;
|
|
249
|
+
const ensureBackupDir = () => {
|
|
250
|
+
if (!backupDirCreated) {
|
|
251
|
+
fs2.mkdirSync(backupDir, { recursive: true });
|
|
252
|
+
backupDirCreated = true;
|
|
253
|
+
}
|
|
254
|
+
};
|
|
255
|
+
for (const rel of relEntries) {
|
|
256
|
+
let src;
|
|
257
|
+
try {
|
|
258
|
+
src = validateSwarmPath(directory, rel);
|
|
259
|
+
} catch {
|
|
260
|
+
warnings.push(`skipped backup of ${rel} (path validation failed)`);
|
|
261
|
+
continue;
|
|
262
|
+
}
|
|
263
|
+
if (!fs2.existsSync(src))
|
|
264
|
+
continue;
|
|
265
|
+
try {
|
|
266
|
+
ensureBackupDir();
|
|
267
|
+
const dest = path2.join(backupDir, rel);
|
|
268
|
+
fs2.mkdirSync(path2.dirname(dest), { recursive: true });
|
|
269
|
+
fs2.cpSync(src, dest, { recursive: true });
|
|
270
|
+
copied.push(rel);
|
|
271
|
+
} catch (err) {
|
|
272
|
+
warnings.push(`failed to back up ${rel}: ${err instanceof Error ? err.message : String(err)}`);
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
if (copied.length === 0) {
|
|
276
|
+
if (backupDirCreated) {
|
|
277
|
+
try {
|
|
278
|
+
fs2.rmSync(backupDir, { recursive: true, force: true });
|
|
279
|
+
} catch {}
|
|
280
|
+
}
|
|
281
|
+
return { backupDir: null, copied, warnings };
|
|
282
|
+
}
|
|
283
|
+
pruneOldResetBackups(backupsRoot, warnings, path2.basename(backupDir));
|
|
284
|
+
return { backupDir, copied, warnings };
|
|
285
|
+
}
|
|
286
|
+
var BACKUP_KIND_PREFIXES = [
|
|
287
|
+
"reset-session-",
|
|
288
|
+
"rollback-git-",
|
|
289
|
+
"rollback-",
|
|
290
|
+
"reset-"
|
|
291
|
+
];
|
|
292
|
+
function backupTimestampSortKey(name) {
|
|
293
|
+
for (const prefix of BACKUP_KIND_PREFIXES) {
|
|
294
|
+
if (name.startsWith(prefix)) {
|
|
295
|
+
return name.slice(prefix.length).replace(/\D/g, "");
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
return "";
|
|
299
|
+
}
|
|
300
|
+
function pruneOldResetBackups(backupsRoot, warnings, keepDirName) {
|
|
301
|
+
try {
|
|
302
|
+
const entries = fs2.readdirSync(backupsRoot, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => ({ name: e.name, key: backupTimestampSortKey(e.name) })).sort((a, b) => a.key === b.key ? b.name.localeCompare(a.name) : b.key.localeCompare(a.key));
|
|
303
|
+
for (const stale of entries.slice(RESET_BACKUP_RETENTION)) {
|
|
304
|
+
if (stale.name === keepDirName)
|
|
305
|
+
continue;
|
|
306
|
+
try {
|
|
307
|
+
fs2.rmSync(path2.join(backupsRoot, stale.name), {
|
|
308
|
+
recursive: true,
|
|
309
|
+
force: true
|
|
310
|
+
});
|
|
311
|
+
} catch (err) {
|
|
312
|
+
warnings.push(`failed to prune old backup ${stale.name}: ${err instanceof Error ? err.message : String(err)}`);
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
} catch (err) {
|
|
316
|
+
warnings.push(`failed to prune old reset backups: ${err instanceof Error ? err.message : String(err)}`);
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
function backupTrackedChangesBeforeRollback(directory, repoRelPaths) {
|
|
320
|
+
const warnings = [];
|
|
321
|
+
const copied = [];
|
|
322
|
+
const swarmDir = path2.join(directory, ".swarm");
|
|
323
|
+
try {
|
|
324
|
+
const stat = fs2.lstatSync(swarmDir);
|
|
325
|
+
if (stat.isSymbolicLink() || !stat.isDirectory()) {
|
|
326
|
+
warnings.push("backup skipped: .swarm is a symlink or not a directory \u2014 nothing was backed up");
|
|
327
|
+
return { backupDir: null, copied, warnings };
|
|
328
|
+
}
|
|
329
|
+
} catch {
|
|
330
|
+
warnings.push("backup skipped: .swarm directory is missing \u2014 nothing was backed up");
|
|
331
|
+
return { backupDir: null, copied, warnings };
|
|
332
|
+
}
|
|
333
|
+
const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
|
|
334
|
+
const backupsRoot = path2.join(swarmDir, "rollback-backups");
|
|
335
|
+
const backupDir = path2.join(backupsRoot, `rollback-git-${timestamp}`);
|
|
336
|
+
let backupDirCreated = false;
|
|
337
|
+
const ensureBackupDir = () => {
|
|
338
|
+
if (!backupDirCreated) {
|
|
339
|
+
fs2.mkdirSync(backupDir, { recursive: true });
|
|
340
|
+
backupDirCreated = true;
|
|
341
|
+
}
|
|
342
|
+
};
|
|
343
|
+
for (const rel of repoRelPaths) {
|
|
344
|
+
if (!rel)
|
|
345
|
+
continue;
|
|
346
|
+
const src = path2.join(directory, rel);
|
|
347
|
+
if (!fs2.existsSync(src))
|
|
348
|
+
continue;
|
|
349
|
+
try {
|
|
350
|
+
ensureBackupDir();
|
|
351
|
+
const dest = path2.join(backupDir, "tracked", rel);
|
|
352
|
+
fs2.mkdirSync(path2.dirname(dest), { recursive: true });
|
|
353
|
+
fs2.cpSync(src, dest, { recursive: true });
|
|
354
|
+
copied.push(rel);
|
|
355
|
+
} catch (err) {
|
|
356
|
+
warnings.push(`failed to back up ${rel}: ${err instanceof Error ? err.message : String(err)}`);
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
if (copied.length === 0) {
|
|
360
|
+
if (backupDirCreated) {
|
|
361
|
+
try {
|
|
362
|
+
fs2.rmSync(backupDir, { recursive: true, force: true });
|
|
363
|
+
} catch {}
|
|
364
|
+
}
|
|
365
|
+
return { backupDir: null, copied, warnings };
|
|
366
|
+
}
|
|
367
|
+
pruneOldResetBackups(backupsRoot, warnings, path2.basename(backupDir));
|
|
368
|
+
return { backupDir, copied, warnings };
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
// src/commands/rollback-gate.ts
|
|
372
|
+
init_git_executable();
|
|
373
|
+
import { spawnSync } from "child_process";
|
|
374
|
+
import * as fsSync from "fs";
|
|
375
|
+
import * as path3 from "path";
|
|
376
|
+
var ROLLBACK_GIT_KIND = "swarm-rollback-git";
|
|
377
|
+
var ROLLBACK_PHASE_KIND = "swarm-rollback-phase";
|
|
378
|
+
var GIT_STATUS_TIMEOUT_MS = 1e4;
|
|
379
|
+
var GIT_LIST_MAX_BUFFER_BYTES = 32 * 1024 * 1024;
|
|
380
|
+
var _rollbackGateInternals = {
|
|
381
|
+
runGit: (args, cwd) => {
|
|
382
|
+
const result = spawnSync(resolveGitExecutable(), args, {
|
|
383
|
+
cwd,
|
|
384
|
+
encoding: "utf-8",
|
|
385
|
+
timeout: GIT_STATUS_TIMEOUT_MS,
|
|
386
|
+
windowsHide: true,
|
|
387
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
388
|
+
});
|
|
389
|
+
if (result.error || result.status !== 0)
|
|
390
|
+
return null;
|
|
391
|
+
return result.stdout ?? "";
|
|
392
|
+
},
|
|
393
|
+
runGitBuffered: (args, cwd) => {
|
|
394
|
+
const result = spawnSync(resolveGitExecutable(), args, {
|
|
395
|
+
cwd,
|
|
396
|
+
encoding: "utf-8",
|
|
397
|
+
timeout: GIT_STATUS_TIMEOUT_MS,
|
|
398
|
+
windowsHide: true,
|
|
399
|
+
maxBuffer: GIT_LIST_MAX_BUFFER_BYTES,
|
|
400
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
401
|
+
});
|
|
402
|
+
if (result.error || result.status !== 0)
|
|
403
|
+
return null;
|
|
404
|
+
return result.stdout ?? "";
|
|
405
|
+
}
|
|
406
|
+
};
|
|
407
|
+
function isRealGitRepository(directory) {
|
|
408
|
+
try {
|
|
409
|
+
if (fsSync.statSync(path3.join(directory, ".git")).isFile())
|
|
410
|
+
return true;
|
|
411
|
+
} catch {
|
|
412
|
+
return false;
|
|
413
|
+
}
|
|
414
|
+
return fsSync.existsSync(path3.join(directory, ".git", "HEAD"));
|
|
415
|
+
}
|
|
416
|
+
function parseTrackedDirtyPaths(statusOutput) {
|
|
417
|
+
const dirtyPaths = [];
|
|
418
|
+
const tokens = statusOutput.split("\x00");
|
|
419
|
+
for (let i = 0;i < tokens.length; i += 1) {
|
|
420
|
+
const token = tokens[i];
|
|
421
|
+
if (token === "" || token.length < 4)
|
|
422
|
+
continue;
|
|
423
|
+
const xy = token.slice(0, 2);
|
|
424
|
+
if (xy === "??")
|
|
425
|
+
continue;
|
|
426
|
+
const filePath = token.slice(3);
|
|
427
|
+
if (!filePath)
|
|
428
|
+
continue;
|
|
429
|
+
dirtyPaths.push(filePath);
|
|
430
|
+
if (/[RC]/.test(xy) && i + 1 < tokens.length && tokens[i + 1] !== "") {
|
|
431
|
+
dirtyPaths.push(tokens[i + 1]);
|
|
432
|
+
i += 1;
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
return dirtyPaths;
|
|
436
|
+
}
|
|
437
|
+
function untrackedPathsInTargetTree(directory, targetSha) {
|
|
438
|
+
if (!isRealGitRepository(directory))
|
|
439
|
+
return [];
|
|
440
|
+
const targetTree = _rollbackGateInternals.runGitBuffered(["ls-tree", "-r", "-z", "--name-only", targetSha], directory);
|
|
441
|
+
const index = _rollbackGateInternals.runGitBuffered(["ls-files", "-z"], directory);
|
|
442
|
+
if (targetTree === null || index === null)
|
|
443
|
+
return [];
|
|
444
|
+
const indexed = new Set(index.split("\x00").filter((p) => p !== ""));
|
|
445
|
+
const inWay = [];
|
|
446
|
+
for (const rel of targetTree.split("\x00")) {
|
|
447
|
+
if (rel === "" || indexed.has(rel))
|
|
448
|
+
continue;
|
|
449
|
+
if (fsSync.existsSync(path3.join(directory, rel)))
|
|
450
|
+
inWay.push(rel);
|
|
451
|
+
}
|
|
452
|
+
return inWay;
|
|
453
|
+
}
|
|
454
|
+
function trackedDirtyCandidates(directory, dirtyRelPaths) {
|
|
455
|
+
return dirtyRelPaths.map((rel) => ({
|
|
456
|
+
path: path3.join(directory, rel),
|
|
457
|
+
reason: "uncommitted tracked change \u2014 destroyed by git reset --hard"
|
|
458
|
+
}));
|
|
459
|
+
}
|
|
460
|
+
function swarmOverwriteCandidates(directory, overwriteRelPaths) {
|
|
461
|
+
return overwriteRelPaths.map((rel) => ({
|
|
462
|
+
path: path3.join(directory, ".swarm", rel),
|
|
463
|
+
reason: "live .swarm state replaced by the checkpoint copy"
|
|
464
|
+
}));
|
|
465
|
+
}
|
|
466
|
+
function measureGitDestruction(directory, targetSha) {
|
|
467
|
+
if (!isRealGitRepository(directory)) {
|
|
468
|
+
return { kind: "ok", dirtyRelPaths: [] };
|
|
469
|
+
}
|
|
470
|
+
const status = _rollbackGateInternals.runGit(["status", "--porcelain", "-z"], directory);
|
|
471
|
+
if (status === null) {
|
|
472
|
+
return { kind: "unreadable" };
|
|
473
|
+
}
|
|
474
|
+
const dirtyRelPaths = parseTrackedDirtyPaths(status);
|
|
475
|
+
if (targetSha) {
|
|
476
|
+
for (const rel of untrackedPathsInTargetTree(directory, targetSha)) {
|
|
477
|
+
if (!dirtyRelPaths.includes(rel))
|
|
478
|
+
dirtyRelPaths.push(rel);
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
return { kind: "ok", dirtyRelPaths };
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
// src/tools/checkpoint.ts
|
|
56
485
|
init_git_executable();
|
|
57
486
|
|
|
58
487
|
// src/tools/create-tool.ts
|
|
59
488
|
init_zod();
|
|
60
489
|
|
|
61
490
|
// src/tools/resolve-working-directory.ts
|
|
62
|
-
import * as
|
|
63
|
-
import * as
|
|
491
|
+
import * as fs3 from "fs";
|
|
492
|
+
import * as path4 from "path";
|
|
64
493
|
function resolveWorkingDirectory(workingDirectory, fallbackDirectory) {
|
|
65
494
|
if (workingDirectory == null || workingDirectory === "") {
|
|
66
495
|
if (typeof fallbackDirectory !== "string" || fallbackDirectory === "") {
|
|
@@ -105,11 +534,11 @@ function resolveWorkingDirectory(workingDirectory, fallbackDirectory) {
|
|
|
105
534
|
message: "Invalid working_directory: path traversal sequences (..) are not allowed"
|
|
106
535
|
};
|
|
107
536
|
}
|
|
108
|
-
const normalizedDir =
|
|
109
|
-
const resolvedDir =
|
|
537
|
+
const normalizedDir = path4.normalize(workingDirectory);
|
|
538
|
+
const resolvedDir = path4.resolve(normalizedDir);
|
|
110
539
|
let statResult;
|
|
111
540
|
try {
|
|
112
|
-
statResult =
|
|
541
|
+
statResult = fs3.statSync(resolvedDir);
|
|
113
542
|
} catch {
|
|
114
543
|
return {
|
|
115
544
|
success: false,
|
|
@@ -133,10 +562,10 @@ function resolveWorkingDirectory(workingDirectory, fallbackDirectory) {
|
|
|
133
562
|
if (typeof fallbackDirectory !== "string" || fallbackDirectory === "") {
|
|
134
563
|
return { success: true, directory: resolvedDir };
|
|
135
564
|
}
|
|
136
|
-
const resolvedFallback =
|
|
565
|
+
const resolvedFallback = path4.resolve(fallbackDirectory);
|
|
137
566
|
let fallbackExists = false;
|
|
138
567
|
try {
|
|
139
|
-
|
|
568
|
+
fs3.statSync(resolvedFallback);
|
|
140
569
|
fallbackExists = true;
|
|
141
570
|
} catch {
|
|
142
571
|
fallbackExists = false;
|
|
@@ -238,11 +667,11 @@ var SHELL_METACHARACTERS = /[;|&$`(){}<>!'"]/;
|
|
|
238
667
|
var SAFE_LABEL_PATTERN = /^[a-zA-Z0-9_ -]+$/;
|
|
239
668
|
var CONTROL_CHAR_PATTERN = /[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]/;
|
|
240
669
|
var NON_ASCII_PATTERN = /[^\x20-\x7E]/;
|
|
241
|
-
var
|
|
670
|
+
var _internals2 = {
|
|
242
671
|
tryAcquireLock,
|
|
243
672
|
loadPlan,
|
|
244
673
|
gitExec,
|
|
245
|
-
sleep: (ms) => new Promise((
|
|
674
|
+
sleep: (ms) => new Promise((resolve3) => setTimeout(resolve3, ms)),
|
|
246
675
|
findCommitByExactSubject,
|
|
247
676
|
stageAllExcludingSwarm,
|
|
248
677
|
resolveGitExecutable
|
|
@@ -296,13 +725,13 @@ function validateLabel(label) {
|
|
|
296
725
|
return null;
|
|
297
726
|
}
|
|
298
727
|
function getCheckpointLogPath(directory) {
|
|
299
|
-
return
|
|
728
|
+
return path5.join(directory, CHECKPOINT_LOG_PATH);
|
|
300
729
|
}
|
|
301
730
|
function readCheckpointLog(directory) {
|
|
302
731
|
const logPath = getCheckpointLogPath(directory);
|
|
303
732
|
try {
|
|
304
|
-
if (
|
|
305
|
-
const content =
|
|
733
|
+
if (fs4.existsSync(logPath)) {
|
|
734
|
+
const content = fs4.readFileSync(logPath, "utf-8");
|
|
306
735
|
const parsed = JSON.parse(content);
|
|
307
736
|
if (!parsed.checkpoints || !Array.isArray(parsed.checkpoints)) {
|
|
308
737
|
return { version: 1, checkpoints: [] };
|
|
@@ -312,15 +741,15 @@ function readCheckpointLog(directory) {
|
|
|
312
741
|
} catch {}
|
|
313
742
|
return { version: 1, checkpoints: [] };
|
|
314
743
|
}
|
|
315
|
-
function writeCheckpointLog(
|
|
744
|
+
function writeCheckpointLog(log2, directory) {
|
|
316
745
|
const logPath = getCheckpointLogPath(directory);
|
|
317
|
-
const dir =
|
|
318
|
-
if (!
|
|
319
|
-
|
|
746
|
+
const dir = path5.dirname(logPath);
|
|
747
|
+
if (!fs4.existsSync(dir)) {
|
|
748
|
+
fs4.mkdirSync(dir, { recursive: true });
|
|
320
749
|
}
|
|
321
750
|
const tempPath = `${logPath}.tmp`;
|
|
322
|
-
|
|
323
|
-
|
|
751
|
+
fs4.writeFileSync(tempPath, JSON.stringify(log2, null, 2), "utf-8");
|
|
752
|
+
fs4.renameSync(tempPath, logPath);
|
|
324
753
|
}
|
|
325
754
|
function appendRetentionEvent(directory, event) {
|
|
326
755
|
try {
|
|
@@ -340,52 +769,52 @@ function loadCheckpointConfig(directory) {
|
|
|
340
769
|
} catch {}
|
|
341
770
|
return { maxCheckpoints, allowEmptyCommits };
|
|
342
771
|
}
|
|
343
|
-
function appendCheckpointEntry(directory,
|
|
344
|
-
const existing =
|
|
772
|
+
function appendCheckpointEntry(directory, log2, entry, maxCheckpoints) {
|
|
773
|
+
const existing = log2.checkpoints.find((checkpoint) => checkpoint.label === entry.label);
|
|
345
774
|
if (existing) {
|
|
346
775
|
if (existing.sha !== entry.sha) {
|
|
347
776
|
throw new Error(`checkpoint log conflict: label "${entry.label}" already exists with a different SHA`);
|
|
348
777
|
}
|
|
349
778
|
return;
|
|
350
779
|
}
|
|
351
|
-
|
|
352
|
-
if (
|
|
353
|
-
const evicted =
|
|
780
|
+
log2.checkpoints.push(entry);
|
|
781
|
+
if (log2.checkpoints.length > maxCheckpoints) {
|
|
782
|
+
const evicted = log2.checkpoints.splice(0, log2.checkpoints.length - maxCheckpoints);
|
|
354
783
|
appendRetentionEvent(directory, {
|
|
355
784
|
event: "checkpoint_retention_applied",
|
|
356
785
|
evicted_labels: evicted.map((checkpoint) => checkpoint.label),
|
|
357
786
|
evicted_count: evicted.length,
|
|
358
|
-
remaining_count:
|
|
787
|
+
remaining_count: log2.checkpoints.length
|
|
359
788
|
});
|
|
360
789
|
}
|
|
361
790
|
}
|
|
362
|
-
function upsertTaskCompletionCheckpointEntry(directory,
|
|
363
|
-
const existingIndex =
|
|
791
|
+
function upsertTaskCompletionCheckpointEntry(directory, log2, entry, maxCheckpoints) {
|
|
792
|
+
const existingIndex = log2.checkpoints.findIndex((checkpoint) => checkpoint.label === entry.label);
|
|
364
793
|
if (existingIndex >= 0) {
|
|
365
|
-
const existing =
|
|
794
|
+
const existing = log2.checkpoints[existingIndex];
|
|
366
795
|
if (existing.sha === entry.sha) {
|
|
367
796
|
return;
|
|
368
797
|
}
|
|
369
|
-
|
|
798
|
+
log2.checkpoints[existingIndex] = entry;
|
|
370
799
|
return;
|
|
371
800
|
}
|
|
372
|
-
|
|
373
|
-
if (
|
|
374
|
-
const evicted =
|
|
801
|
+
log2.checkpoints.push(entry);
|
|
802
|
+
if (log2.checkpoints.length > maxCheckpoints) {
|
|
803
|
+
const evicted = log2.checkpoints.splice(0, log2.checkpoints.length - maxCheckpoints);
|
|
375
804
|
appendRetentionEvent(directory, {
|
|
376
805
|
event: "checkpoint_retention_applied",
|
|
377
806
|
evicted_labels: evicted.map((checkpoint) => checkpoint.label),
|
|
378
807
|
evicted_count: evicted.length,
|
|
379
|
-
remaining_count:
|
|
808
|
+
remaining_count: log2.checkpoints.length
|
|
380
809
|
});
|
|
381
810
|
}
|
|
382
811
|
}
|
|
383
812
|
function stageAllExcludingSwarm(directory) {
|
|
384
|
-
|
|
813
|
+
_internals2.gitExec(["add", "--all", "--", ...NON_SWARM_COMMIT_PATHSPEC], directory);
|
|
385
814
|
}
|
|
386
815
|
function gitExec(args, cwd) {
|
|
387
816
|
for (let attempt = 0;attempt < MAX_TRANSIENT_RETRIES; attempt++) {
|
|
388
|
-
const result = child_process.spawnSync(
|
|
817
|
+
const result = child_process.spawnSync(_internals2.resolveGitExecutable(), args, {
|
|
389
818
|
cwd,
|
|
390
819
|
encoding: "utf-8",
|
|
391
820
|
timeout: GIT_TIMEOUT_MS,
|
|
@@ -442,7 +871,7 @@ function isGitRepo(directory) {
|
|
|
442
871
|
}
|
|
443
872
|
}
|
|
444
873
|
function findCommitByExactSubject(directory, subject) {
|
|
445
|
-
const output =
|
|
874
|
+
const output = _internals2.gitExec([
|
|
446
875
|
"log",
|
|
447
876
|
"HEAD",
|
|
448
877
|
"--format=%H%x00%s",
|
|
@@ -472,7 +901,7 @@ async function withTaskCompletionLocksResult(input) {
|
|
|
472
901
|
attempts++;
|
|
473
902
|
let planLockResult;
|
|
474
903
|
try {
|
|
475
|
-
planLockResult = await
|
|
904
|
+
planLockResult = await _internals2.tryAcquireLock(input.directory, PLAN_LOCK_PATH, CHECKPOINT_AGENT_NAME, `save-task-completion-plan-${input.taskId}`);
|
|
476
905
|
} catch (error) {
|
|
477
906
|
if (input.onFailure) {
|
|
478
907
|
return input.onFailure(error);
|
|
@@ -486,13 +915,13 @@ async function withTaskCompletionLocksResult(input) {
|
|
|
486
915
|
if (attempts >= CHECKPOINT_LOCK_MAX_ATTEMPTS || Date.now() + CHECKPOINT_LOCK_DELAY_MS > deadline) {
|
|
487
916
|
break;
|
|
488
917
|
}
|
|
489
|
-
await
|
|
918
|
+
await _internals2.sleep(CHECKPOINT_LOCK_DELAY_MS);
|
|
490
919
|
continue;
|
|
491
920
|
}
|
|
492
921
|
try {
|
|
493
922
|
let checkpointLockResult;
|
|
494
923
|
try {
|
|
495
|
-
checkpointLockResult = await
|
|
924
|
+
checkpointLockResult = await _internals2.tryAcquireLock(input.directory, CHECKPOINT_LOG_PATH, CHECKPOINT_AGENT_NAME, `save-task-completion-checkpoint-${input.taskId}`);
|
|
496
925
|
} catch (error) {
|
|
497
926
|
if (input.onFailure) {
|
|
498
927
|
return input.onFailure(error);
|
|
@@ -506,7 +935,7 @@ async function withTaskCompletionLocksResult(input) {
|
|
|
506
935
|
if (attempts >= CHECKPOINT_LOCK_MAX_ATTEMPTS || Date.now() + CHECKPOINT_LOCK_DELAY_MS > deadline) {
|
|
507
936
|
break;
|
|
508
937
|
}
|
|
509
|
-
await
|
|
938
|
+
await _internals2.sleep(CHECKPOINT_LOCK_DELAY_MS);
|
|
510
939
|
continue;
|
|
511
940
|
}
|
|
512
941
|
try {
|
|
@@ -562,7 +991,7 @@ async function withCheckpointMutationLockResult(input) {
|
|
|
562
991
|
attempts++;
|
|
563
992
|
let lockResult;
|
|
564
993
|
try {
|
|
565
|
-
lockResult = await
|
|
994
|
+
lockResult = await _internals2.tryAcquireLock(input.directory, CHECKPOINT_LOG_PATH, CHECKPOINT_AGENT_NAME, input.operationKey);
|
|
566
995
|
} catch (error) {
|
|
567
996
|
if (input.onFailure) {
|
|
568
997
|
return input.onFailure(error);
|
|
@@ -599,7 +1028,7 @@ async function withCheckpointMutationLockResult(input) {
|
|
|
599
1028
|
if (attempts >= CHECKPOINT_LOCK_MAX_ATTEMPTS || Date.now() + CHECKPOINT_LOCK_DELAY_MS > deadline) {
|
|
600
1029
|
break;
|
|
601
1030
|
}
|
|
602
|
-
await
|
|
1031
|
+
await _internals2.sleep(CHECKPOINT_LOCK_DELAY_MS);
|
|
603
1032
|
}
|
|
604
1033
|
if (input.onBusy) {
|
|
605
1034
|
return input.onBusy(attempts);
|
|
@@ -615,8 +1044,8 @@ async function withCheckpointMutationLockResult(input) {
|
|
|
615
1044
|
function handleSave(label, directory) {
|
|
616
1045
|
try {
|
|
617
1046
|
const { maxCheckpoints, allowEmptyCommits } = loadCheckpointConfig(directory);
|
|
618
|
-
const
|
|
619
|
-
const existingCheckpoint =
|
|
1047
|
+
const log2 = readCheckpointLog(directory);
|
|
1048
|
+
const existingCheckpoint = log2.checkpoints.find((checkpoint) => checkpoint.label === label);
|
|
620
1049
|
if (existingCheckpoint) {
|
|
621
1050
|
return serializeResult("save", {
|
|
622
1051
|
success: false,
|
|
@@ -624,7 +1053,7 @@ function handleSave(label, directory) {
|
|
|
624
1053
|
});
|
|
625
1054
|
}
|
|
626
1055
|
const timestamp = new Date().toISOString();
|
|
627
|
-
|
|
1056
|
+
_internals2.stageAllExcludingSwarm(directory);
|
|
628
1057
|
const hasStagedChanges = (() => {
|
|
629
1058
|
try {
|
|
630
1059
|
gitExec(["diff", "--cached", "--quiet"], directory);
|
|
@@ -639,12 +1068,12 @@ function handleSave(label, directory) {
|
|
|
639
1068
|
gitExec(["commit", "--allow-empty", "-m", `checkpoint: ${label}`], directory);
|
|
640
1069
|
}
|
|
641
1070
|
const newSha = getCurrentSha(directory);
|
|
642
|
-
appendCheckpointEntry(directory,
|
|
1071
|
+
appendCheckpointEntry(directory, log2, {
|
|
643
1072
|
label,
|
|
644
1073
|
sha: newSha,
|
|
645
1074
|
timestamp
|
|
646
1075
|
}, maxCheckpoints);
|
|
647
|
-
writeCheckpointLog(
|
|
1076
|
+
writeCheckpointLog(log2, directory);
|
|
648
1077
|
return serializeResult("save", {
|
|
649
1078
|
success: true,
|
|
650
1079
|
label,
|
|
@@ -668,8 +1097,8 @@ async function saveCheckpointRecord(label, directory) {
|
|
|
668
1097
|
directory,
|
|
669
1098
|
operationKey: `save-record-${label}`,
|
|
670
1099
|
run: async () => {
|
|
671
|
-
const
|
|
672
|
-
if (
|
|
1100
|
+
const log2 = readCheckpointLog(directory);
|
|
1101
|
+
if (log2.checkpoints.find((checkpoint) => checkpoint.label === label)) {
|
|
673
1102
|
return { success: false, error: `duplicate label: "${label}"` };
|
|
674
1103
|
}
|
|
675
1104
|
let sha = "";
|
|
@@ -681,12 +1110,12 @@ async function saveCheckpointRecord(label, directory) {
|
|
|
681
1110
|
sha = "";
|
|
682
1111
|
}
|
|
683
1112
|
}
|
|
684
|
-
appendCheckpointEntry(directory,
|
|
1113
|
+
appendCheckpointEntry(directory, log2, {
|
|
685
1114
|
label,
|
|
686
1115
|
sha,
|
|
687
1116
|
timestamp: new Date().toISOString()
|
|
688
1117
|
}, loadCheckpointConfig(directory).maxCheckpoints);
|
|
689
|
-
writeCheckpointLog(
|
|
1118
|
+
writeCheckpointLog(log2, directory);
|
|
690
1119
|
const result = { success: true, sha };
|
|
691
1120
|
if (!sha) {
|
|
692
1121
|
result.warning = "no git restore target - checkpoint recorded without a SHA (directory may not be a git repository or HEAD is unavailable)";
|
|
@@ -703,17 +1132,74 @@ async function saveCheckpointRecord(label, directory) {
|
|
|
703
1132
|
})
|
|
704
1133
|
});
|
|
705
1134
|
}
|
|
706
|
-
function handleRestore(label, directory) {
|
|
1135
|
+
function handleRestore(label, directory, confirmToken) {
|
|
707
1136
|
try {
|
|
708
|
-
const
|
|
709
|
-
const checkpoint =
|
|
1137
|
+
const log2 = readCheckpointLog(directory);
|
|
1138
|
+
const checkpoint = log2.checkpoints.find((candidate) => candidate.label === label);
|
|
710
1139
|
if (!checkpoint) {
|
|
711
1140
|
return serializeResult("restore", {
|
|
712
1141
|
success: false,
|
|
713
1142
|
error: `checkpoint not found: "${label}"`
|
|
714
1143
|
});
|
|
715
1144
|
}
|
|
716
|
-
const
|
|
1145
|
+
const measurement = measureGitDestruction(directory, checkpoint.sha);
|
|
1146
|
+
if (measurement.kind === "unreadable") {
|
|
1147
|
+
return serializeResult("restore", {
|
|
1148
|
+
success: false,
|
|
1149
|
+
error: "cannot verify the working tree (git status unreadable) \u2014 refusing to restore without a preview. Retry, or inspect git status manually."
|
|
1150
|
+
});
|
|
1151
|
+
}
|
|
1152
|
+
if (measurement.dirtyRelPaths.length > 0) {
|
|
1153
|
+
const candidates = trackedDirtyCandidates(directory, measurement.dirtyRelPaths);
|
|
1154
|
+
const scopeAnchor = candidates[0].path;
|
|
1155
|
+
if (!confirmToken) {
|
|
1156
|
+
const token = issueConfirmToken(scopeAnchor, directory, {
|
|
1157
|
+
kind: ROLLBACK_GIT_KIND,
|
|
1158
|
+
candidates
|
|
1159
|
+
});
|
|
1160
|
+
const shown = measurement.dirtyRelPaths.slice(0, 10);
|
|
1161
|
+
const overflow = measurement.dirtyRelPaths.length - shown.length;
|
|
1162
|
+
return serializeResult("restore", {
|
|
1163
|
+
success: false,
|
|
1164
|
+
requires_confirm: true,
|
|
1165
|
+
preview: [
|
|
1166
|
+
`Restoring to checkpoint "${label}" runs a hard git reset and would DESTROY ${candidates.length} uncommitted tracked file(s):`,
|
|
1167
|
+
...shown.map((p) => ` - ${p}`),
|
|
1168
|
+
...overflow > 0 ? [` (+${overflow} more)`] : [],
|
|
1169
|
+
"Backups are taken automatically once confirmed."
|
|
1170
|
+
],
|
|
1171
|
+
confirm_token: token,
|
|
1172
|
+
message: "restore preview \u2014 re-run with confirm_token to execute (valid 15 minutes)"
|
|
1173
|
+
});
|
|
1174
|
+
}
|
|
1175
|
+
const verdict = consumeConfirmToken(scopeAnchor, directory, confirmToken, { kind: ROLLBACK_GIT_KIND, candidates });
|
|
1176
|
+
if (!verdict.ok) {
|
|
1177
|
+
return serializeResult("restore", {
|
|
1178
|
+
success: false,
|
|
1179
|
+
error: verdict.reason
|
|
1180
|
+
});
|
|
1181
|
+
}
|
|
1182
|
+
const backup = backupTrackedChangesBeforeRollback(directory, measurement.dirtyRelPaths);
|
|
1183
|
+
const atRiskExisting = measurement.dirtyRelPaths.filter((rel) => fs4.existsSync(path5.join(directory, rel))).length;
|
|
1184
|
+
if (!backup.backupDir || backup.copied.length < atRiskExisting) {
|
|
1185
|
+
return serializeResult("restore", {
|
|
1186
|
+
success: false,
|
|
1187
|
+
error: `backup incomplete (${backup.copied.length}/${atRiskExisting} at-risk file(s) copied) \u2014 restore refused so nothing is destroyed.${backup.warnings.length > 0 ? ` Warnings: ${backup.warnings.join("; ")}` : ""}`
|
|
1188
|
+
});
|
|
1189
|
+
}
|
|
1190
|
+
const logBeforeReset2 = log2;
|
|
1191
|
+
gitExec(["reset", "--hard", checkpoint.sha], directory);
|
|
1192
|
+
writeCheckpointLog(logBeforeReset2, directory);
|
|
1193
|
+
return serializeResult("restore", {
|
|
1194
|
+
success: true,
|
|
1195
|
+
label,
|
|
1196
|
+
sha: checkpoint.sha,
|
|
1197
|
+
backup_dir: backup.backupDir,
|
|
1198
|
+
...backup.warnings.length > 0 ? { backup_warnings: backup.warnings } : {},
|
|
1199
|
+
message: `Restored to checkpoint: "${label}" (hard reset)${backup.backupDir ? `; destroyed bytes backed up to ${path5.relative(directory, backup.backupDir)}` : ""}${backup.warnings.length > 0 ? `; warnings: ${backup.warnings.join("; ")}` : ""}`
|
|
1200
|
+
});
|
|
1201
|
+
}
|
|
1202
|
+
const logBeforeReset = log2;
|
|
717
1203
|
gitExec(["reset", "--hard", checkpoint.sha], directory);
|
|
718
1204
|
writeCheckpointLog(logBeforeReset, directory);
|
|
719
1205
|
return serializeResult("restore", {
|
|
@@ -730,8 +1216,8 @@ function handleRestore(label, directory) {
|
|
|
730
1216
|
}
|
|
731
1217
|
}
|
|
732
1218
|
function handleList(directory) {
|
|
733
|
-
const
|
|
734
|
-
const sorted = [...
|
|
1219
|
+
const log2 = readCheckpointLog(directory);
|
|
1220
|
+
const sorted = [...log2.checkpoints].sort((a, b) => b.timestamp.localeCompare(a.timestamp));
|
|
735
1221
|
return serializeResult("list", {
|
|
736
1222
|
success: true,
|
|
737
1223
|
count: sorted.length,
|
|
@@ -740,16 +1226,16 @@ function handleList(directory) {
|
|
|
740
1226
|
}
|
|
741
1227
|
function handleDelete(label, directory) {
|
|
742
1228
|
try {
|
|
743
|
-
const
|
|
744
|
-
const initialLength =
|
|
745
|
-
|
|
746
|
-
if (
|
|
1229
|
+
const log2 = readCheckpointLog(directory);
|
|
1230
|
+
const initialLength = log2.checkpoints.length;
|
|
1231
|
+
log2.checkpoints = log2.checkpoints.filter((checkpoint) => checkpoint.label !== label);
|
|
1232
|
+
if (log2.checkpoints.length === initialLength) {
|
|
747
1233
|
return serializeResult("delete", {
|
|
748
1234
|
success: false,
|
|
749
1235
|
error: `checkpoint not found: "${label}"`
|
|
750
1236
|
});
|
|
751
1237
|
}
|
|
752
|
-
writeCheckpointLog(
|
|
1238
|
+
writeCheckpointLog(log2, directory);
|
|
753
1239
|
return serializeResult("delete", {
|
|
754
1240
|
success: true,
|
|
755
1241
|
label,
|
|
@@ -767,7 +1253,7 @@ async function handleSaveTaskCompletion(taskId, directory) {
|
|
|
767
1253
|
directory,
|
|
768
1254
|
taskId,
|
|
769
1255
|
run: async () => {
|
|
770
|
-
const plan = await
|
|
1256
|
+
const plan = await _internals2.loadPlan(directory);
|
|
771
1257
|
if (!plan) {
|
|
772
1258
|
return serializeResult("save_task_completion", {
|
|
773
1259
|
success: false,
|
|
@@ -787,7 +1273,7 @@ async function handleSaveTaskCompletion(taskId, directory) {
|
|
|
787
1273
|
error: `save_task_completion failed: task "${taskId}" is ${task.status}, not completed`
|
|
788
1274
|
});
|
|
789
1275
|
}
|
|
790
|
-
await
|
|
1276
|
+
await _internals2.afterTaskEligibilityRead?.(directory, task.id);
|
|
791
1277
|
const planIdentityHash = derivePlanIdentityHash(plan);
|
|
792
1278
|
const planId = derivePlanId(plan);
|
|
793
1279
|
const ledgerEvents = await readLedgerEvents(directory);
|
|
@@ -823,12 +1309,12 @@ async function handleSaveTaskCompletion(taskId, directory) {
|
|
|
823
1309
|
message: `Checkpoint task completion already logged for "${descriptor.taskId}"`
|
|
824
1310
|
});
|
|
825
1311
|
}
|
|
826
|
-
const recoveredSha =
|
|
1312
|
+
const recoveredSha = _internals2.findCommitByExactSubject(directory, descriptor.subject);
|
|
827
1313
|
let committedSha = null;
|
|
828
1314
|
committedSha = recoveredSha ?? receipt.sha;
|
|
829
1315
|
if (!committedSha) {
|
|
830
|
-
|
|
831
|
-
|
|
1316
|
+
_internals2.stageAllExcludingSwarm(directory);
|
|
1317
|
+
_internals2.gitExec([
|
|
832
1318
|
"commit",
|
|
833
1319
|
"--allow-empty",
|
|
834
1320
|
"-m",
|
|
@@ -842,13 +1328,13 @@ async function handleSaveTaskCompletion(taskId, directory) {
|
|
|
842
1328
|
throw new Error(`save_task_completion failed: could not resolve commit SHA for task "${descriptor.taskId}"`);
|
|
843
1329
|
}
|
|
844
1330
|
updateTaskCheckpointReceipt(directory, descriptor, "committed", committedSha);
|
|
845
|
-
const
|
|
846
|
-
upsertTaskCompletionCheckpointEntry(directory,
|
|
1331
|
+
const log2 = readCheckpointLog(directory);
|
|
1332
|
+
upsertTaskCompletionCheckpointEntry(directory, log2, {
|
|
847
1333
|
label: descriptor.label,
|
|
848
1334
|
sha: committedSha,
|
|
849
1335
|
timestamp: new Date().toISOString()
|
|
850
1336
|
}, loadCheckpointConfig(directory).maxCheckpoints);
|
|
851
|
-
writeCheckpointLog(
|
|
1337
|
+
writeCheckpointLog(log2, directory);
|
|
852
1338
|
const logged = updateTaskCheckpointReceipt(directory, descriptor, "logged", committedSha);
|
|
853
1339
|
return serializeResult("save_task_completion", {
|
|
854
1340
|
success: true,
|
|
@@ -862,10 +1348,11 @@ async function handleSaveTaskCompletion(taskId, directory) {
|
|
|
862
1348
|
}
|
|
863
1349
|
var checkpoint = createSwarmTool({
|
|
864
1350
|
allowWorkingDirectoryOverride: true,
|
|
865
|
-
description: "Save, restore, list, and delete git checkpoints. " + "Use save to create a named snapshot, restore to return tracked files to a checkpoint, " + "list to see all checkpoints, delete to remove a checkpoint from the log, and save_task_completion to record a retry-safe task completion checkpoint. " + "Git commits are preserved on delete.",
|
|
1351
|
+
description: "Save, restore, list, and delete git checkpoints. " + "Use save to create a named snapshot, restore to return tracked files to a checkpoint (a destructive restore previews first and requires a confirm token issued in the preview; destroyed bytes are backed up under .swarm/rollback-backups/), " + "list to see all checkpoints, delete to remove a checkpoint from the log, and save_task_completion to record a retry-safe task completion checkpoint. " + "Git commits are preserved on delete.",
|
|
866
1352
|
args: {
|
|
867
1353
|
action: exports_external.string().describe("Action to perform: save, restore, list, delete, or save_task_completion"),
|
|
868
1354
|
label: exports_external.string().optional().describe("Checkpoint label (required for save, restore, delete)"),
|
|
1355
|
+
confirm_token: exports_external.string().optional().describe("Confirm token from a destructive-restore preview (required when restore would destroy uncommitted tracked work; the preview response carries the token)"),
|
|
869
1356
|
task_id: exports_external.string().optional().describe('Plan task ID (required for save_task_completion), e.g. "1.1"')
|
|
870
1357
|
},
|
|
871
1358
|
execute: async (args, directory) => {
|
|
@@ -878,10 +1365,12 @@ var checkpoint = createSwarmTool({
|
|
|
878
1365
|
}
|
|
879
1366
|
let action;
|
|
880
1367
|
let label;
|
|
1368
|
+
let confirmToken;
|
|
881
1369
|
let taskId;
|
|
882
1370
|
try {
|
|
883
1371
|
action = String(args.action);
|
|
884
1372
|
label = args.label !== undefined && args.label !== null ? String(args.label) : undefined;
|
|
1373
|
+
confirmToken = args.confirm_token !== undefined && args.confirm_token !== null ? String(args.confirm_token) : undefined;
|
|
885
1374
|
taskId = args.task_id !== undefined && args.task_id !== null ? String(args.task_id) : undefined;
|
|
886
1375
|
} catch {
|
|
887
1376
|
return serializeResult("unknown", {
|
|
@@ -938,7 +1427,7 @@ var checkpoint = createSwarmTool({
|
|
|
938
1427
|
action: "restore",
|
|
939
1428
|
directory,
|
|
940
1429
|
operationKey: `restore-${label}`,
|
|
941
|
-
run: async () => handleRestore(label, directory)
|
|
1430
|
+
run: async () => handleRestore(label, directory, confirmToken)
|
|
942
1431
|
});
|
|
943
1432
|
case "list":
|
|
944
1433
|
return handleList(directory);
|
|
@@ -960,4 +1449,4 @@ var checkpoint = createSwarmTool({
|
|
|
960
1449
|
}
|
|
961
1450
|
});
|
|
962
1451
|
|
|
963
|
-
export { resolveWorkingDirectory, createSwarmTool, _internals, saveCheckpointRecord, checkpoint };
|
|
1452
|
+
export { previewDestructivePurge, issueConfirmToken, consumeConfirmToken, executeDestructivePurge, backupSwarmStateBeforeReset, ROLLBACK_GIT_KIND, ROLLBACK_PHASE_KIND, trackedDirtyCandidates, swarmOverwriteCandidates, measureGitDestruction, resolveWorkingDirectory, createSwarmTool, _internals2 as _internals, saveCheckpointRecord, checkpoint };
|