opencode-swarm 7.186.7 → 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.
Files changed (121) hide show
  1. package/.opencode/skills/swarm-pr-review/SKILL.md +3 -3
  2. package/.opencode/skills/swarm-pr-review/references/lane-output-recoverability.md +4 -3
  3. package/binaries/win32-arm64/swarm-sandbox-runner.exe +0 -0
  4. package/binaries/win32-x64/swarm-sandbox-runner.exe +0 -0
  5. package/dist/agents/architect.d.ts +1 -1
  6. package/dist/background/pending-delegations.d.ts +4 -1
  7. package/dist/cli/{auto-checkpoint-2wez0vzz.js → auto-checkpoint-r69e82bh.js} +10 -11
  8. package/dist/cli/{checkpoint-ywqsp7d7.js → checkpoint-1sqwbgra.js} +18 -19
  9. package/dist/cli/{coder-settlement-nfzcjeda.js → coder-settlement-b9qx0732.js} +14 -15
  10. package/dist/cli/{config-doctor-1fjsn7zq.js → config-doctor-s4fr5qwr.js} +34 -4
  11. package/dist/cli/{core-xs68b2jw.js → core-bezezjy9.js} +5 -6
  12. package/dist/cli/{curation-policy-3dvythny.js → curation-policy-2x2w3b57.js} +7 -8
  13. package/dist/cli/{curator-drift-cz65fcz0.js → curator-drift-785xnxdv.js} +10 -11
  14. package/dist/cli/curator-llm-factory-p0wwyswg.js +80 -0
  15. package/dist/cli/{curator-nejfgxfm.js → curator-w4hp7a10.js} +44 -45
  16. package/dist/cli/{dispatch-0ejzwb45.js → dispatch-qvcztbpf.js} +222 -48
  17. package/dist/cli/{evidence-summary-service-0jt68dq5.js → evidence-summary-service-33qcm9xc.js} +21 -22
  18. package/dist/cli/{gate-evidence-ypmdcgrt.js → gate-evidence-9ehj968e.js} +9 -10
  19. package/dist/cli/guardrail-explain-5715fdzv.js +81 -0
  20. package/dist/cli/guardrail-log-k1dfzfzm.js +22 -0
  21. package/dist/cli/{guardrail-reset-cpw83epe.js → guardrail-reset-nt88n9dm.js} +44 -45
  22. package/dist/cli/hive-promoter-c8fjn186.js +95 -0
  23. package/dist/cli/{index-mr86ywsj.js → index-0yj79yk1.js} +1 -1
  24. package/dist/cli/{index-9qgncxbv.js → index-1vvr69ka.js} +1 -1
  25. package/dist/cli/{index-q93b3xdh.js → index-2s8wdagg.js} +44 -45
  26. package/dist/cli/{index-2rkw7nft.js → index-2wgvjwpr.js} +5 -5
  27. package/dist/cli/{index-gaf95msf.js → index-469xmpd6.js} +1 -1
  28. package/dist/cli/{index-9n1va1c4.js → index-4g6sw4eq.js} +1 -1
  29. package/dist/cli/{index-gmwzspch.js → index-5m56c5r6.js} +6 -6
  30. package/dist/cli/{index-yan163x1.js → index-6ersmqjd.js} +9 -9
  31. package/dist/cli/{index-mdxqv3r9.js → index-9hag235h.js} +2 -2
  32. package/dist/cli/{index-zyx7p0kb.js → index-a8gvszr2.js} +2 -2
  33. package/dist/cli/{index-63e62vkg.js → index-akt0arnt.js} +2745 -2596
  34. package/dist/cli/{index-qwd1qpmg.js → index-b2je0z0t.js} +4 -4
  35. package/dist/cli/{index-rsrsv4c6.js → index-bbqxt1p5.js} +28 -11
  36. package/dist/cli/{index-j2m9q6w1.js → index-bejxkee0.js} +2 -2
  37. package/dist/cli/{index-8y9p665v.js → index-c8ffgcvz.js} +3 -3
  38. package/dist/cli/{index-gvch83kc.js → index-cc1vzq4m.js} +1 -1
  39. package/dist/cli/{index-qy60v48q.js → index-cppqxjw9.js} +2 -2
  40. package/dist/cli/{index-93mgmhe5.js → index-cx4s7cak.js} +46 -47
  41. package/dist/cli/{index-059dnh78.js → index-d6kvatwp.js} +12 -12
  42. package/dist/cli/{index-8jhpedx9.js → index-df2p0k0s.js} +2 -2
  43. package/dist/cli/{index-h9gkgwn6.js → index-dypmvvga.js} +1 -1
  44. package/dist/cli/{index-vtbehfx1.js → index-ectvhv8e.js} +5 -5
  45. package/dist/cli/{index-e27aph2j.js → index-en7vzpeq.js} +4 -4
  46. package/dist/cli/{index-v3w5y2zp.js → index-evy5yrsw.js} +4 -4
  47. package/dist/cli/{index-mc72pk74.js → index-fzpt2djt.js} +32 -1
  48. package/dist/cli/{index-w4q83wf9.js → index-hvr82s1t.js} +565 -76
  49. package/dist/cli/{index-xdjwefa5.js → index-jtncfvhe.js} +1 -1
  50. package/dist/cli/{index-d7sxckqj.js → index-knbjtqwt.js} +5 -5
  51. package/dist/cli/{index-5z3qzesd.js → index-krqq8kbq.js} +7 -7
  52. package/dist/cli/{index-q4vf9k47.js → index-m1qy7f1e.js} +1 -1
  53. package/dist/cli/{index-5z1xrkaa.js → index-mf19yqss.js} +3 -3
  54. package/dist/cli/{index-j0j82th1.js → index-pfpnzf6w.js} +1 -1
  55. package/dist/cli/{index-fzah4ctw.js → index-pts38j6x.js} +5 -5
  56. package/dist/cli/{index-8t5z1yxt.js → index-pymr99as.js} +8 -8
  57. package/dist/cli/{index-qc8r1gy4.js → index-qzgfheb4.js} +3 -3
  58. package/dist/cli/{index-g319bdeh.js → index-r6e8syr3.js} +2 -2
  59. package/dist/cli/{index-emjnn90k.js → index-r7gt2w1b.js} +4 -4
  60. package/dist/cli/{index-m9xhje7p.js → index-rm676s0p.js} +2 -2
  61. package/dist/cli/{index-mpvvgktj.js → index-sztx0vyt.js} +5 -5
  62. package/dist/cli/{index-4dkmehar.js → index-t3hcd6zz.js} +2 -2
  63. package/dist/cli/{index-4c4s8yww.js → index-ta48pe8j.js} +3 -3
  64. package/dist/cli/{index-kb30szp0.js → index-v08ksq0b.js} +2 -2
  65. package/dist/cli/{index-7bxssqaf.js → index-vx8a8w3b.js} +142 -2
  66. package/dist/cli/{index-743kwz1r.js → index-ww8payq9.js} +2 -2
  67. package/dist/cli/{index-xadre3f0.js → index-xs96nry2.js} +24 -13
  68. package/dist/cli/{index-tq8f6ar8.js → index-ycz8z2db.js} +8 -8
  69. package/dist/cli/{index-671k5bzj.js → index-yf0cc6c5.js} +2 -2
  70. package/dist/cli/{index-48jvqqfa.js → index-yw1g78v9.js} +7 -3
  71. package/dist/cli/{index-qs706r9x.js → index-zzxnssy3.js} +4 -4
  72. package/dist/cli/index.js +45 -46
  73. package/dist/cli/{knowledge-escalator-1g4ewbtd.js → knowledge-escalator-qttydcx2.js} +15 -16
  74. package/dist/cli/{knowledge-events-t4axcrwt.js → knowledge-events-4vf0vj82.js} +13 -14
  75. package/dist/cli/{knowledge-link-xwn2ww1d.js → knowledge-link-p2c8agc8.js} +6 -7
  76. package/dist/cli/{knowledge-store-dfdktsc9.js → knowledge-store-bw1dk2eb.js} +7 -8
  77. package/dist/cli/{knowledge-validator-kraghq1k.js → knowledge-validator-gefcc36h.js} +9 -10
  78. package/dist/cli/{mcp-gv7qrj3a.js → mcp-d5p0k3p1.js} +1 -1
  79. package/dist/cli/{model-preflight-gztv8xqx.js → model-preflight-fmx49pwy.js} +44 -45
  80. package/dist/cli/{pending-delegations-rxyjgkrw.js → pending-delegations-8333cb3y.js} +13 -14
  81. package/dist/cli/{pr-subscriptions-csr7n0mx.js → pr-subscriptions-p42ny2sn.js} +13 -14
  82. package/dist/cli/{pr-workflow-gate-3nrrg0je.js → pr-workflow-gate-csy32z8w.js} +44 -45
  83. package/dist/cli/{runner-mfxyvzv5.js → runner-cbpr45cs.js} +7 -8
  84. package/dist/cli/{scan-cursor-0v2a2kya.js → scan-cursor-14mwymtz.js} +8 -9
  85. package/dist/cli/{schema-zzz1ddnv.js → schema-6426g0gw.js} +2 -2
  86. package/dist/cli/{scope-persistence-5p4kkrgt.js → scope-persistence-avkv3s4m.js} +17 -18
  87. package/dist/cli/{server-96a0yyge.js → server-d9rk2zeb.js} +45 -47
  88. package/dist/cli/{skill-generator-v5q1vkxk.js → skill-generator-epdvdsh5.js} +17 -18
  89. package/dist/cli/{snapshot-coordination-init-7cr259p8.js → snapshot-coordination-init-35f7wrqb.js} +44 -45
  90. package/dist/cli/{speckit-checkoff-j9skdc02.js → speckit-checkoff-erwa7fbj.js} +5 -5
  91. package/dist/cli/{telemetry-rb8hrvbx.js → telemetry-2vwb3n42.js} +2 -3
  92. package/dist/cli/{workspace-snapshot-hcgztg7w.js → workspace-snapshot-y60ay5eq.js} +4 -5
  93. package/dist/cli/{worktree-collision-ownership-k5kcnw6t.js → worktree-collision-ownership-3tqkw81h.js} +14 -15
  94. package/dist/cli/{worktree-isolation-p0p13n1e.js → worktree-isolation-0rrjqypt.js} +44 -45
  95. package/dist/commands/registry.d.ts +4 -4
  96. package/dist/commands/reset-backup.d.ts +20 -5
  97. package/dist/commands/rollback-gate.d.ts +73 -0
  98. package/dist/hooks/pr-workflow-gate.d.ts +1 -1
  99. package/dist/index.js +618 -603
  100. package/dist/lang/backends/java.d.ts +76 -0
  101. package/dist/lang/java-extraction.d.ts +88 -0
  102. package/dist/pr-review/circuit.d.ts +1 -1
  103. package/dist/pr-review/completion.d.ts +1 -0
  104. package/dist/pr-review/reducer.d.ts +2 -1
  105. package/dist/pr-review/types.d.ts +2 -1
  106. package/dist/services/config-doctor.d.ts +19 -1
  107. package/dist/services/preflight-integration.d.ts +8 -1
  108. package/dist/state.d.ts +13 -0
  109. package/dist/tools/cancel-lane-batch.d.ts +38 -0
  110. package/dist/tools/dispatch-lanes.d.ts +19 -0
  111. package/dist/tools/index.d.ts +1 -0
  112. package/dist/tools/manifest.d.ts +1 -0
  113. package/dist/tools/symbols.d.ts +18 -0
  114. package/dist/tools/tool-metadata.d.ts +4 -0
  115. package/opencode-swarm.schema.json +1 -1
  116. package/package.json +1 -1
  117. package/dist/cli/curator-llm-factory-p9r4sb0v.js +0 -81
  118. package/dist/cli/guardrail-explain-c42va9fp.js +0 -82
  119. package/dist/cli/guardrail-log-1hv4awyg.js +0 -23
  120. package/dist/cli/hive-promoter-y1f7zsrj.js +0 -96
  121. 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-059dnh78.js";
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-gmwzspch.js";
26
+ } from "./index-5m56c5r6.js";
24
27
  import {
25
28
  appendCoreEventSync
26
- } from "./index-743kwz1r.js";
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-gaf95msf.js";
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 fs from "fs";
63
- import * as path from "path";
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 = path.normalize(workingDirectory);
109
- const resolvedDir = path.resolve(normalizedDir);
537
+ const normalizedDir = path4.normalize(workingDirectory);
538
+ const resolvedDir = path4.resolve(normalizedDir);
110
539
  let statResult;
111
540
  try {
112
- statResult = fs.statSync(resolvedDir);
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 = path.resolve(fallbackDirectory);
565
+ const resolvedFallback = path4.resolve(fallbackDirectory);
137
566
  let fallbackExists = false;
138
567
  try {
139
- fs.statSync(resolvedFallback);
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 _internals = {
670
+ var _internals2 = {
242
671
  tryAcquireLock,
243
672
  loadPlan,
244
673
  gitExec,
245
- sleep: (ms) => new Promise((resolve2) => setTimeout(resolve2, ms)),
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 path2.join(directory, CHECKPOINT_LOG_PATH);
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 (fs2.existsSync(logPath)) {
305
- const content = fs2.readFileSync(logPath, "utf-8");
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(log, directory) {
744
+ function writeCheckpointLog(log2, directory) {
316
745
  const logPath = getCheckpointLogPath(directory);
317
- const dir = path2.dirname(logPath);
318
- if (!fs2.existsSync(dir)) {
319
- fs2.mkdirSync(dir, { recursive: true });
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
- fs2.writeFileSync(tempPath, JSON.stringify(log, null, 2), "utf-8");
323
- fs2.renameSync(tempPath, logPath);
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, log, entry, maxCheckpoints) {
344
- const existing = log.checkpoints.find((checkpoint) => checkpoint.label === entry.label);
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
- log.checkpoints.push(entry);
352
- if (log.checkpoints.length > maxCheckpoints) {
353
- const evicted = log.checkpoints.splice(0, log.checkpoints.length - maxCheckpoints);
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: log.checkpoints.length
787
+ remaining_count: log2.checkpoints.length
359
788
  });
360
789
  }
361
790
  }
362
- function upsertTaskCompletionCheckpointEntry(directory, log, entry, maxCheckpoints) {
363
- const existingIndex = log.checkpoints.findIndex((checkpoint) => checkpoint.label === entry.label);
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 = log.checkpoints[existingIndex];
794
+ const existing = log2.checkpoints[existingIndex];
366
795
  if (existing.sha === entry.sha) {
367
796
  return;
368
797
  }
369
- log.checkpoints[existingIndex] = entry;
798
+ log2.checkpoints[existingIndex] = entry;
370
799
  return;
371
800
  }
372
- log.checkpoints.push(entry);
373
- if (log.checkpoints.length > maxCheckpoints) {
374
- const evicted = log.checkpoints.splice(0, log.checkpoints.length - maxCheckpoints);
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: log.checkpoints.length
808
+ remaining_count: log2.checkpoints.length
380
809
  });
381
810
  }
382
811
  }
383
812
  function stageAllExcludingSwarm(directory) {
384
- _internals.gitExec(["add", "--all", "--", ...NON_SWARM_COMMIT_PATHSPEC], directory);
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(_internals.resolveGitExecutable(), args, {
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 = _internals.gitExec([
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 _internals.tryAcquireLock(input.directory, PLAN_LOCK_PATH, CHECKPOINT_AGENT_NAME, `save-task-completion-plan-${input.taskId}`);
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 _internals.sleep(CHECKPOINT_LOCK_DELAY_MS);
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 _internals.tryAcquireLock(input.directory, CHECKPOINT_LOG_PATH, CHECKPOINT_AGENT_NAME, `save-task-completion-checkpoint-${input.taskId}`);
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 _internals.sleep(CHECKPOINT_LOCK_DELAY_MS);
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 _internals.tryAcquireLock(input.directory, CHECKPOINT_LOG_PATH, CHECKPOINT_AGENT_NAME, input.operationKey);
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 _internals.sleep(CHECKPOINT_LOCK_DELAY_MS);
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 log = readCheckpointLog(directory);
619
- const existingCheckpoint = log.checkpoints.find((checkpoint) => checkpoint.label === label);
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
- _internals.stageAllExcludingSwarm(directory);
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, log, {
1071
+ appendCheckpointEntry(directory, log2, {
643
1072
  label,
644
1073
  sha: newSha,
645
1074
  timestamp
646
1075
  }, maxCheckpoints);
647
- writeCheckpointLog(log, directory);
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 log = readCheckpointLog(directory);
672
- if (log.checkpoints.find((checkpoint) => checkpoint.label === label)) {
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, log, {
1113
+ appendCheckpointEntry(directory, log2, {
685
1114
  label,
686
1115
  sha,
687
1116
  timestamp: new Date().toISOString()
688
1117
  }, loadCheckpointConfig(directory).maxCheckpoints);
689
- writeCheckpointLog(log, directory);
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 log = readCheckpointLog(directory);
709
- const checkpoint = log.checkpoints.find((candidate) => candidate.label === label);
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 logBeforeReset = log;
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 log = readCheckpointLog(directory);
734
- const sorted = [...log.checkpoints].sort((a, b) => b.timestamp.localeCompare(a.timestamp));
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 log = readCheckpointLog(directory);
744
- const initialLength = log.checkpoints.length;
745
- log.checkpoints = log.checkpoints.filter((checkpoint) => checkpoint.label !== label);
746
- if (log.checkpoints.length === initialLength) {
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(log, directory);
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 _internals.loadPlan(directory);
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 _internals.afterTaskEligibilityRead?.(directory, task.id);
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 = _internals.findCommitByExactSubject(directory, descriptor.subject);
1312
+ const recoveredSha = _internals2.findCommitByExactSubject(directory, descriptor.subject);
827
1313
  let committedSha = null;
828
1314
  committedSha = recoveredSha ?? receipt.sha;
829
1315
  if (!committedSha) {
830
- _internals.stageAllExcludingSwarm(directory);
831
- _internals.gitExec([
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 log = readCheckpointLog(directory);
846
- upsertTaskCompletionCheckpointEntry(directory, log, {
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(log, directory);
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 };