@wrongstack/core 0.310.1 → 0.313.1

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 (187) hide show
  1. package/dist/chronicle/index.js +84 -6391
  2. package/dist/chronicle/project-server.js +84 -4868
  3. package/dist/chunk-2BUUIMQB.js +225 -0
  4. package/dist/chunk-2FK4WTFF.js +207 -0
  5. package/dist/chunk-2FQL4PDC.js +292 -0
  6. package/dist/chunk-2KKODIQU.js +2256 -0
  7. package/dist/chunk-2YU4WVBW.js +134 -0
  8. package/dist/chunk-3E244UDA.js +356 -0
  9. package/dist/chunk-3XPVB7BA.js +135 -0
  10. package/dist/chunk-4LJJYBAU.js +388 -0
  11. package/dist/chunk-4VWO3ZXL.js +342 -0
  12. package/dist/chunk-4XV6IR37.js +187 -0
  13. package/dist/chunk-4ZHTKJHM.js +183 -0
  14. package/dist/chunk-52RTVH4M.js +361 -0
  15. package/dist/chunk-57GEWK5X.js +26 -0
  16. package/dist/chunk-5DSMEVA4.js +931 -0
  17. package/dist/chunk-5RP3QTRL.js +1223 -0
  18. package/dist/chunk-5S437PRC.js +50 -0
  19. package/dist/chunk-5XDUGTKN.js +511 -0
  20. package/dist/chunk-62V2AFVG.js +422 -0
  21. package/dist/chunk-65RDIOR2.js +2470 -0
  22. package/dist/chunk-6AKOTGLM.js +597 -0
  23. package/dist/chunk-7ZHZE5EO.js +28 -0
  24. package/dist/chunk-A455ATZB.js +140 -0
  25. package/dist/chunk-AD7ZFZCS.js +46 -0
  26. package/dist/chunk-AI4LLQ2U.js +856 -0
  27. package/dist/chunk-ASX6AW36.js +789 -0
  28. package/dist/chunk-AT4V4XGI.js +3522 -0
  29. package/dist/chunk-AWNQ6ED6.js +311 -0
  30. package/dist/chunk-B4AMLZPB.js +9 -0
  31. package/dist/chunk-BAKVJLEV.js +417 -0
  32. package/dist/chunk-BJHVULHO.js +55 -0
  33. package/dist/chunk-CB2HFKTP.js +9 -0
  34. package/dist/chunk-CE4ZOCOI.js +1640 -0
  35. package/dist/chunk-CQQL4ZOQ.js +151 -0
  36. package/dist/chunk-DEIQMBD2.js +65 -0
  37. package/dist/chunk-E6I5RXVK.js +58 -0
  38. package/dist/chunk-EFEHDQGY.js +1589 -0
  39. package/dist/chunk-EODDVJBM.js +82 -0
  40. package/dist/chunk-EPC3GZD4.js +308 -0
  41. package/dist/chunk-FAYRX2AD.js +13 -0
  42. package/dist/chunk-FFSX2QDQ.js +362 -0
  43. package/dist/chunk-FHAMQS7Q.js +5856 -0
  44. package/dist/chunk-FIV4IJL7.js +2263 -0
  45. package/dist/chunk-FNXVT7HF.js +52 -0
  46. package/dist/chunk-FSWCWSTS.js +395 -0
  47. package/dist/chunk-FW5LDDMJ.js +16464 -0
  48. package/dist/chunk-FXQLIX3K.js +211 -0
  49. package/dist/chunk-G5AAKEEZ.js +228 -0
  50. package/dist/chunk-G5S3S5ZN.js +38 -0
  51. package/dist/chunk-GAZEWQEZ.js +231 -0
  52. package/dist/chunk-GFJOMSHG.js +125 -0
  53. package/dist/chunk-GFXZTHNI.js +158 -0
  54. package/dist/chunk-GM54GJBU.js +960 -0
  55. package/dist/chunk-GPQ3NJPR.js +900 -0
  56. package/dist/chunk-GTCU6DZ3.js +1 -0
  57. package/dist/chunk-H5E77KUJ.js +15 -0
  58. package/dist/chunk-H72H4XDU.js +1023 -0
  59. package/dist/chunk-HA4ZYA6T.js +178 -0
  60. package/dist/chunk-I45KAW2N.js +5020 -0
  61. package/dist/chunk-IQ35HMQL.js +1396 -0
  62. package/dist/chunk-IYFBFX34.js +86 -0
  63. package/dist/chunk-JJPMWPB6.js +109 -0
  64. package/dist/chunk-JTHKOYWU.js +663 -0
  65. package/dist/chunk-JVGPR3TS.js +365 -0
  66. package/dist/chunk-JVKB2TBL.js +48 -0
  67. package/dist/chunk-JX7VBZDK.js +31 -0
  68. package/dist/chunk-K3G6JBUC.js +5232 -0
  69. package/dist/chunk-K4YUNKU7.js +589 -0
  70. package/dist/chunk-KBZGE6LN.js +710 -0
  71. package/dist/chunk-KDQJCT2J.js +45 -0
  72. package/dist/chunk-KI4L2DUL.js +560 -0
  73. package/dist/chunk-KJ2ZTQSL.js +47 -0
  74. package/dist/chunk-KSJ5UTP7.js +154 -0
  75. package/dist/chunk-LHOA5DWA.js +28 -0
  76. package/dist/chunk-LX5QWLBL.js +27 -0
  77. package/dist/chunk-LZDDLNCZ.js +279 -0
  78. package/dist/chunk-M2WF6AXG.js +115 -0
  79. package/dist/chunk-M7BS2K6U.js +1 -0
  80. package/dist/chunk-NDBIPFNY.js +102 -0
  81. package/dist/chunk-NEMIWEXC.js +1199 -0
  82. package/dist/chunk-NFAYHPRH.js +323 -0
  83. package/dist/chunk-O37BQ65S.js +21 -0
  84. package/dist/chunk-O74MJHXU.js +187 -0
  85. package/dist/chunk-OJZEX73F.js +112 -0
  86. package/dist/chunk-PAAWLLZX.js +74 -0
  87. package/dist/chunk-PFXFQQRT.js +339 -0
  88. package/dist/chunk-PFZO3C6H.js +14 -0
  89. package/dist/chunk-PHH7LD45.js +113 -0
  90. package/dist/chunk-QRTG4XTI.js +188 -0
  91. package/dist/chunk-QVOVMXJR.js +1351 -0
  92. package/dist/chunk-RCQYT2I4.js +757 -0
  93. package/dist/chunk-RD6DEE36.js +1178 -0
  94. package/dist/chunk-RJIZEJ42.js +30 -0
  95. package/dist/chunk-RPOA7IAE.js +150 -0
  96. package/dist/chunk-SN2DQRL6.js +1172 -0
  97. package/dist/chunk-SQJVFEBV.js +41 -0
  98. package/dist/chunk-SQP64RCS.js +5391 -0
  99. package/dist/chunk-SVT4RNTR.js +49 -0
  100. package/dist/chunk-SWUFN7XM.js +43 -0
  101. package/dist/chunk-SXMVYC5H.js +174 -0
  102. package/dist/chunk-SZ6AYUIZ.js +23 -0
  103. package/dist/chunk-T3KGZBU5.js +3171 -0
  104. package/dist/chunk-TBRL2RCC.js +9 -0
  105. package/dist/chunk-TRT2HRWZ.js +18 -0
  106. package/dist/chunk-TYW2GI5I.js +1077 -0
  107. package/dist/chunk-UIWONWVG.js +4959 -0
  108. package/dist/chunk-VB3H5WWB.js +151 -0
  109. package/dist/chunk-W4A6QA3D.js +258 -0
  110. package/dist/chunk-WBPVKEEE.js +227 -0
  111. package/dist/chunk-WEBMSQP2.js +123 -0
  112. package/dist/chunk-WOEZV3Q4.js +333 -0
  113. package/dist/chunk-WQQON3SV.js +473 -0
  114. package/dist/chunk-WQTOVMGV.js +119 -0
  115. package/dist/chunk-WZO23AWO.js +2779 -0
  116. package/dist/chunk-XBDP4QPW.js +7 -0
  117. package/dist/chunk-XEBHSJLT.js +103 -0
  118. package/dist/chunk-XQ2ECNK3.js +1 -0
  119. package/dist/chunk-XQ2XGIBF.js +2086 -0
  120. package/dist/chunk-Y2GE5TSI.js +355 -0
  121. package/dist/chunk-YBUJCZ57.js +351 -0
  122. package/dist/chunk-YNCVSFGF.js +149 -0
  123. package/dist/chunk-YUPMXOGP.js +282 -0
  124. package/dist/chunk-YY6APYUM.js +49 -0
  125. package/dist/chunk-Z2IJUO45.js +691 -0
  126. package/dist/chunk-ZEHUCJHY.js +129 -0
  127. package/dist/chunk-ZYU2YOJ6.js +1496 -0
  128. package/dist/chunk-ZZ4V3WIZ.js +163 -0
  129. package/dist/coordination/agents/index.js +145 -6260
  130. package/dist/coordination/index.js +365 -34323
  131. package/dist/coordination/mailbox-project-server.js +48 -3220
  132. package/dist/coordination/mailbox-types.d.ts +13 -1
  133. package/dist/coordination/provider-status-tracker.d.ts +4 -0
  134. package/dist/coordination/sqlite-mailbox.d.ts +2 -1
  135. package/dist/core/agent.d.ts +14 -1
  136. package/dist/core/index.js +267 -12479
  137. package/dist/core/model-ref.js +5 -23
  138. package/dist/core/provider-runner.d.ts +36 -1
  139. package/dist/design/index.js +50 -1087
  140. package/dist/execution/index.d.ts +1 -1
  141. package/dist/execution/index.js +1599 -21887
  142. package/dist/execution/prompt-enhancer.js +16 -535
  143. package/dist/execution/retry-policy.d.ts +31 -1
  144. package/dist/extension/index.js +7 -311
  145. package/dist/goal/index.js +33 -3435
  146. package/dist/hooks/index.js +14 -1138
  147. package/dist/hq/index.js +112 -5139
  148. package/dist/hq/protocol.js +21 -668
  149. package/dist/index.js +1410 -92655
  150. package/dist/infrastructure/index.js +64 -4811
  151. package/dist/kernel/events/provider-events.d.ts +15 -0
  152. package/dist/kernel/events/wrongtrace-events.d.ts +26 -0
  153. package/dist/kernel/events.d.ts +16 -7
  154. package/dist/kernel/index.js +14 -992
  155. package/dist/models/index.js +40 -1117
  156. package/dist/notifications/index.js +3 -108
  157. package/dist/observability/index.js +17 -279
  158. package/dist/plugin/index.js +584 -14835
  159. package/dist/prompts/index.js +14 -948
  160. package/dist/registry/index.js +9 -822
  161. package/dist/registry-R23UBE7H.js +14 -0
  162. package/dist/replay/index.js +6 -84
  163. package/dist/review-finding-store-DKAV3AS6.js +21 -0
  164. package/dist/review-report-store-OWY3EC3N.js +19 -0
  165. package/dist/security/index.js +114 -2202
  166. package/dist/session-catalog/index.js +36 -2103
  167. package/dist/session-catalog/project-server.js +42 -1435
  168. package/dist/skills/index.js +35 -1578
  169. package/dist/storage/index.js +182 -15600
  170. package/dist/tasking/index.js +10 -676
  171. package/dist/tools/index.js +52 -9589
  172. package/dist/types/config/autonomy.d.ts +5 -0
  173. package/dist/types/config/tools.d.ts +2 -2
  174. package/dist/types/config/ui.d.ts +1 -1
  175. package/dist/types/errors.d.ts +18 -0
  176. package/dist/types/index.js +121 -1341
  177. package/dist/types/provider-runner.d.ts +8 -0
  178. package/dist/types/session-markers.js +8 -144
  179. package/dist/utils/error.js +3 -4
  180. package/dist/utils/expect-defined.js +3 -9
  181. package/dist/utils/heap-watchdog.js +6 -706
  182. package/dist/utils/index.js +269 -5685
  183. package/dist/utils/sage-output-block.js +5 -42
  184. package/dist/wiring/proxy-rewrite.d.ts +92 -4
  185. package/dist/wiring/proxy-rewrite.js +20 -61
  186. package/dist/worktree/index.js +6 -887
  187. package/package.json +4 -4
@@ -0,0 +1,757 @@
1
+ import {
2
+ buildChildEnv
3
+ } from "./chunk-VB3H5WWB.js";
4
+ import {
5
+ toErrorMessage
6
+ } from "./chunk-TBRL2RCC.js";
7
+
8
+ // src/worktree/worktree-manager.ts
9
+ import { mkdir, readFile } from "node:fs/promises";
10
+ import { join, resolve as resolve3, sep as sep3 } from "node:path";
11
+
12
+ // src/worktree/worktree-git.ts
13
+ import { spawn } from "node:child_process";
14
+ import { resolve, sep } from "node:path";
15
+ var MAX_SLUG = 40;
16
+ function makeSlug(hint, usedSlugs) {
17
+ let base = hint.toLowerCase().replace(/[^a-z0-9._-]+/g, "-").replace(/-+/g, "-").replace(/^[-.]+/, "").replace(/[-.]+$/, "").slice(0, MAX_SLUG).replace(/[-.]+$/, "");
18
+ if (!base) base = "wt";
19
+ let slug = `${base}-${crypto.randomUUID().slice(0, 6)}`;
20
+ while (usedSlugs.has(slug)) slug = `${base}-${crypto.randomUUID().slice(0, 6)}`;
21
+ usedSlugs.add(slug);
22
+ return slug;
23
+ }
24
+ async function collectStats(runGit, dir) {
25
+ const sha = (await runGit(["rev-parse", "HEAD"], dir)).stdout.trim();
26
+ const numstat = await runGit(["show", "--numstat", "--format=", "HEAD"], dir);
27
+ let insertions = 0;
28
+ let deletions = 0;
29
+ let files = 0;
30
+ for (const line of numstat.stdout.split("\n")) {
31
+ const m = line.trim().match(/^(\d+|-)\t(\d+|-)\t(.+)$/);
32
+ if (!m) continue;
33
+ files++;
34
+ if (m[1] !== "-") insertions += Number(m[1]);
35
+ if (m[2] !== "-") deletions += Number(m[2]);
36
+ }
37
+ return { insertions, deletions, files, sha };
38
+ }
39
+ async function identityArgs(runGit, cwd) {
40
+ const name = (await runGit(["config", "user.name"], cwd)).stdout.trim();
41
+ const email = (await runGit(["config", "user.email"], cwd)).stdout.trim();
42
+ if (name && email) return [];
43
+ return ["-c", `user.name=${name || "Goal"}`, "-c", `user.email=${email || "goal@agent.local"}`];
44
+ }
45
+ function defaultRun(gitBin, args, cwd) {
46
+ return new Promise((res) => {
47
+ let stdout = "";
48
+ let stderr = "";
49
+ const MAX_GIT_OUTPUT = 1e6;
50
+ const child = spawn(gitBin, args, {
51
+ cwd,
52
+ env: buildChildEnv(),
53
+ stdio: ["ignore", "pipe", "pipe"],
54
+ signal: AbortSignal.timeout(3e4),
55
+ windowsHide: true
56
+ });
57
+ child.stdout?.on("data", (c) => {
58
+ if (stdout.length < MAX_GIT_OUTPUT) stdout += c.toString();
59
+ });
60
+ child.stderr?.on("data", (c) => {
61
+ if (stderr.length < MAX_GIT_OUTPUT) stderr += c.toString();
62
+ });
63
+ child.on("error", (err) => res({ code: 1, stdout, stderr: err.message }));
64
+ child.on("close", (code) => res({ code: code ?? 1, stdout, stderr }));
65
+ });
66
+ }
67
+ function parseConflictPaths(output) {
68
+ const paths = /* @__PURE__ */ new Set();
69
+ for (const line of output.split("\n")) {
70
+ const m = line.match(/^CONFLICT \([^)]*\): Merge conflict in (.+?)\s*$/);
71
+ if (m?.[1]) paths.add(m[1]);
72
+ }
73
+ return [...paths];
74
+ }
75
+ function assertSafePath(dir, projectRoot) {
76
+ const root = resolve(projectRoot);
77
+ const abs = resolve(dir);
78
+ if (abs !== root && !abs.startsWith(root + sep)) {
79
+ throw new Error(`worktree path escapes project root: ${dir}`);
80
+ }
81
+ }
82
+
83
+ // src/worktree/worktree-managed-ops.ts
84
+ import { resolve as resolve2, sep as sep2 } from "node:path";
85
+ async function cleanupAllManaged(ctx) {
86
+ const root = resolve2(ctx.worktreesRoot);
87
+ let removed = 0;
88
+ try {
89
+ const listed = await ctx.runGit(["worktree", "list", "--porcelain"], ctx.projectRoot);
90
+ for (const line of listed.stdout.split("\n")) {
91
+ const m = line.match(/^worktree\s+(.+?)\s*$/);
92
+ if (!m?.[1]) continue;
93
+ const dir = resolve2(m[1]);
94
+ if (dir !== root && (dir === root || dir.startsWith(root + sep2))) {
95
+ const rm = await ctx.runGit(["worktree", "remove", "--force", dir], ctx.projectRoot);
96
+ if (rm.code === 0) removed++;
97
+ }
98
+ }
99
+ } catch {
100
+ }
101
+ try {
102
+ const branches = await ctx.runGit(
103
+ ["branch", "--list", "--format=%(refname:short)", "wstack/ap/*"],
104
+ ctx.projectRoot
105
+ );
106
+ for (const b of branches.stdout.split("\n").map((s) => s.trim()).filter(Boolean)) {
107
+ await ctx.runGit(["branch", "-D", b], ctx.projectRoot);
108
+ }
109
+ } catch {
110
+ }
111
+ await ctx.runGit(["worktree", "prune"], ctx.projectRoot).catch(() => void 0);
112
+ ctx.handles.clear();
113
+ ctx.usedSlugs.clear();
114
+ ctx.emit("worktree.released", {
115
+ handleId: "cleanup-all",
116
+ ownerId: "cleanup-all",
117
+ branch: "wstack/ap/*",
118
+ kept: false
119
+ });
120
+ return { removed };
121
+ }
122
+ async function listManaged(ctx) {
123
+ const root = resolve2(ctx.worktreesRoot);
124
+ const worktrees = [];
125
+ try {
126
+ const listed = await ctx.runGit(["worktree", "list", "--porcelain"], ctx.projectRoot);
127
+ let curDir = null;
128
+ let curBranch;
129
+ const flush = () => {
130
+ if (curDir) {
131
+ const d = resolve2(curDir);
132
+ if (d !== root && d.startsWith(root + sep2)) {
133
+ worktrees.push({ dir: curDir, branch: curBranch });
134
+ }
135
+ }
136
+ curDir = null;
137
+ curBranch = void 0;
138
+ };
139
+ for (const line of listed.stdout.split("\n")) {
140
+ if (line.startsWith("worktree ")) {
141
+ flush();
142
+ curDir = line.slice("worktree ".length).trim();
143
+ } else if (line.startsWith("branch ")) {
144
+ curBranch = line.slice("branch ".length).trim().replace(/^refs\/heads\//, "");
145
+ } else if (line.trim() === "") {
146
+ flush();
147
+ }
148
+ }
149
+ flush();
150
+ } catch {
151
+ }
152
+ let branches = [];
153
+ try {
154
+ const b = await ctx.runGit(
155
+ ["branch", "--list", "--format=%(refname:short)", "wstack/ap/*"],
156
+ ctx.projectRoot
157
+ );
158
+ branches = b.stdout.split("\n").map((s) => s.trim()).filter(Boolean);
159
+ } catch {
160
+ }
161
+ return { worktrees, branches };
162
+ }
163
+ async function removeOne(ctx, dir, branch) {
164
+ const root = resolve2(ctx.worktreesRoot);
165
+ const abs = resolve2(dir);
166
+ if (abs === root || !abs.startsWith(root + sep2)) {
167
+ return { removed: false };
168
+ }
169
+ let removed = false;
170
+ try {
171
+ const rm = await ctx.runGit(["worktree", "remove", "--force", abs], ctx.projectRoot);
172
+ removed = rm.code === 0;
173
+ } catch {
174
+ }
175
+ if (branch && !branch.startsWith("-")) {
176
+ await ctx.runGit(["branch", "-D", "--", branch], ctx.projectRoot).catch(() => void 0);
177
+ }
178
+ await ctx.runGit(["worktree", "prune"], ctx.projectRoot).catch(() => void 0);
179
+ for (const [ownerId, h] of [...ctx.handles]) {
180
+ if (resolve2(h.dir) === abs) ctx.handles.delete(ownerId);
181
+ }
182
+ return { removed };
183
+ }
184
+ async function cleanupStale(ctx) {
185
+ const root = resolve2(ctx.worktreesRoot);
186
+ let detected = 0;
187
+ try {
188
+ const listed = await ctx.runGit(["worktree", "list", "--porcelain"], ctx.projectRoot);
189
+ for (const line of listed.stdout.split("\n")) {
190
+ const m = line.match(/^worktree\s+(.+?)\s*$/);
191
+ if (!m?.[1]) continue;
192
+ const dir = resolve2(m[1]);
193
+ if (dir !== root && (dir === root || dir.startsWith(root + sep2))) {
194
+ detected++;
195
+ }
196
+ }
197
+ } catch {
198
+ return { removed: 0, detected: 0 };
199
+ }
200
+ if (detected === 0) {
201
+ try {
202
+ const branches = await ctx.runGit(
203
+ ["branch", "--list", "--format=%(refname:short)", "wstack/ap/*"],
204
+ ctx.projectRoot
205
+ );
206
+ detected += branches.stdout.split("\n").map((s) => s.trim()).filter(Boolean).length;
207
+ } catch {
208
+ }
209
+ }
210
+ if (detected === 0) {
211
+ return { removed: 0, detected: 0 };
212
+ }
213
+ const { removed } = await cleanupAllManaged(ctx);
214
+ return { removed, detected };
215
+ }
216
+
217
+ // src/worktree/worktree-manager.ts
218
+ var WorktreeManager = class {
219
+ projectRoot;
220
+ events;
221
+ sessionIdSource;
222
+ gitBin;
223
+ runGit;
224
+ /** Keyed by ownerId. */
225
+ handles = /* @__PURE__ */ new Map();
226
+ usedSlugs = /* @__PURE__ */ new Set();
227
+ constructor(opts) {
228
+ this.projectRoot = resolve3(opts.projectRoot);
229
+ this.events = opts.events;
230
+ this.sessionIdSource = opts.sessionId;
231
+ this.gitBin = opts.gitBin ?? "git";
232
+ this.runGit = opts.run ?? ((args, cwd) => this.defaultRun(args, cwd));
233
+ }
234
+ /** Create a fresh worktree + branch forked from the current base branch. */
235
+ async allocate(ownerId, opts = {}) {
236
+ const existing = this.handles.get(ownerId);
237
+ if (existing && (existing.status === "allocating" || existing.status === "active")) {
238
+ return existing;
239
+ }
240
+ const slug = this.makeSlug(opts.slugHint ?? ownerId);
241
+ const branch = `wstack/ap/${slug}`;
242
+ const dir = join(this.worktreesRoot(), slug);
243
+ const absDir = resolve3(dir);
244
+ const absRoot = resolve3(this.projectRoot);
245
+ if (!absDir.startsWith(absRoot + sep3)) {
246
+ throw new Error(`Worktree dir "${absDir}" resolves outside project root`);
247
+ }
248
+ const baseBranch = opts.baseBranch ?? await this.detectBaseBranch();
249
+ const handle = {
250
+ id: slug,
251
+ ownerId,
252
+ ownerLabel: opts.ownerLabel ?? opts.slugHint ?? ownerId,
253
+ slug,
254
+ dir,
255
+ branch,
256
+ baseBranch,
257
+ status: "allocating",
258
+ createdAt: Date.now(),
259
+ updatedAt: Date.now(),
260
+ insertions: 0,
261
+ deletions: 0,
262
+ files: 0
263
+ };
264
+ this.handles.set(ownerId, handle);
265
+ try {
266
+ await mkdir(this.worktreesRoot(), { recursive: true });
267
+ const res = await this.runGit(
268
+ ["worktree", "add", "-b", branch, dir, baseBranch],
269
+ this.projectRoot
270
+ );
271
+ if (res.code !== 0) {
272
+ return this.fail(handle, res.stderr || "git worktree add failed");
273
+ }
274
+ } catch (err) {
275
+ return this.fail(handle, toErrorMessage(err));
276
+ }
277
+ this.setStatus(handle, "active");
278
+ this.emit("worktree.allocated", {
279
+ handleId: handle.id,
280
+ ownerId: handle.ownerId,
281
+ ownerLabel: handle.ownerLabel,
282
+ slug: handle.slug,
283
+ dir: handle.dir,
284
+ branch: handle.branch,
285
+ baseBranch: handle.baseBranch
286
+ });
287
+ return handle;
288
+ }
289
+ /** Stage everything and commit inside the worktree. */
290
+ async commitAll(handle, message) {
291
+ this.setStatus(handle, "committing");
292
+ await this.runGit(["add", "-A"], handle.dir);
293
+ const staged = await this.runGit(["diff", "--cached", "--quiet"], handle.dir);
294
+ if (staged.code === 0) {
295
+ this.emitCommitted(handle, false);
296
+ return { committed: false };
297
+ }
298
+ const idArgs = await this.identityArgs(handle.dir);
299
+ const committed = await this.runGit([...idArgs, "commit", "-m", message], handle.dir);
300
+ if (committed.code !== 0) {
301
+ this.fail(handle, committed.stderr || "git commit failed");
302
+ return { committed: false };
303
+ }
304
+ const stats = await this.collectStats(handle.dir);
305
+ handle.insertions = stats.insertions;
306
+ handle.deletions = stats.deletions;
307
+ handle.files = stats.files;
308
+ handle.sha = stats.sha;
309
+ handle.updatedAt = Date.now();
310
+ this.emitCommitted(handle, true);
311
+ return { committed: true };
312
+ }
313
+ /** Merge the worktree branch back into the base branch (squash by default). */
314
+ async merge(handle, opts = {}) {
315
+ const squash = opts.squash ?? true;
316
+ this.setStatus(handle, "merging");
317
+ const status = await this.runGit(
318
+ ["status", "--porcelain", "--untracked-files=no"],
319
+ this.projectRoot
320
+ );
321
+ if (status.stdout.trim().length > 0) {
322
+ const reason = "working tree has uncommitted changes \u2014 commit or stash first";
323
+ this.fail(handle, reason);
324
+ return { ok: false, stderr: reason };
325
+ }
326
+ const checkout = await this.runGit(["checkout", handle.baseBranch], this.projectRoot);
327
+ if (checkout.code !== 0) {
328
+ this.fail(handle, checkout.stderr || `checkout ${handle.baseBranch} failed`);
329
+ return { ok: false, stderr: checkout.stderr };
330
+ }
331
+ const mergeArgs = squash ? ["merge", "--squash", handle.branch] : ["merge", "--no-ff", handle.branch];
332
+ const merged = await this.runGit(mergeArgs, this.projectRoot);
333
+ const fromOutput = parseConflictPaths(`${merged.stdout}
334
+ ${merged.stderr}`);
335
+ const fromIndex = await this.unmergedFiles();
336
+ const conflictFiles = [.../* @__PURE__ */ new Set([...fromOutput, ...fromIndex])];
337
+ if (merged.code !== 0 || conflictFiles.length > 0) {
338
+ if (opts.resolve) {
339
+ const finalized = await this.tryResolveConflict(handle, conflictFiles, opts);
340
+ if (finalized) return finalized;
341
+ }
342
+ await this.runGit(["reset", "--hard", "HEAD"], this.projectRoot);
343
+ handle.conflictFiles = conflictFiles;
344
+ this.setStatus(handle, "needs-review", { lastError: merged.stderr });
345
+ this.emit("worktree.conflict", {
346
+ handleId: handle.id,
347
+ ownerId: handle.ownerId,
348
+ branch: handle.branch,
349
+ conflictFiles
350
+ });
351
+ return { ok: false, conflict: true, conflictFiles, stderr: merged.stderr };
352
+ }
353
+ if (squash) {
354
+ const msg = opts.message ?? `merge ${handle.branch} (squash)`;
355
+ const idArgs = await this.identityArgs(this.projectRoot);
356
+ const commit = await this.runGit([...idArgs, "commit", "-m", msg], this.projectRoot);
357
+ if (commit.code !== 0 && !/nothing to commit/i.test(commit.stdout + commit.stderr)) {
358
+ this.fail(handle, commit.stderr || "squash commit failed");
359
+ return { ok: false, stderr: commit.stderr };
360
+ }
361
+ }
362
+ this.setStatus(handle, "merged");
363
+ this.emit("worktree.merged", {
364
+ handleId: handle.id,
365
+ ownerId: handle.ownerId,
366
+ branch: handle.branch,
367
+ baseBranch: handle.baseBranch,
368
+ squash
369
+ });
370
+ return { ok: true };
371
+ }
372
+ /**
373
+ * Current tip SHA of a handle's base branch (without checking it out). Capture
374
+ * this before a merge so a regressed merge can be reverted to exactly this
375
+ * commit — unambiguous even when a squash produced no diff. Returns null on
376
+ * failure (caller then skips the revert).
377
+ */
378
+ async baseHead(handle) {
379
+ const res = await this.runGit(["rev-parse", handle.baseBranch], this.projectRoot);
380
+ const sha = res.stdout.trim();
381
+ return res.code === 0 && sha ? sha : null;
382
+ }
383
+ /**
384
+ * Hard-reset the base branch back to `sha` (a value previously returned by
385
+ * {@link baseHead}). Used to undo a squash-merge whose integrated result failed
386
+ * re-verification, so an auto-resolved-but-broken merge never sticks on base.
387
+ * Safe because SDD merges are serialized — no other commit lands in between.
388
+ */
389
+ async revertBaseTo(handle, sha) {
390
+ const status = await this.runGit(["status", "--porcelain"], this.projectRoot);
391
+ if (status.code !== 0) return false;
392
+ const hasTrackedDirt = status.stdout.split("\n").some((line) => line.trim().length > 0 && !line.startsWith("??"));
393
+ if (hasTrackedDirt) return false;
394
+ const co = await this.runGit(["checkout", handle.baseBranch], this.projectRoot);
395
+ if (co.code !== 0) return false;
396
+ const reset = await this.runGit(["reset", "--hard", sha], this.projectRoot);
397
+ return reset.code === 0;
398
+ }
399
+ /**
400
+ * Current base branch + tip SHA, captured WITHOUT a handle. The SDD run calls
401
+ * this once at start so a later rollback knows which branch the run's squash
402
+ * commits landed on. Returns null when not in a usable git state.
403
+ */
404
+ async currentBase() {
405
+ const branch = await this.detectBaseBranch();
406
+ const head = await this.runGit(["rev-parse", "HEAD"], this.projectRoot);
407
+ const sha = head.stdout.trim();
408
+ return head.code === 0 && sha ? { branch, sha } : null;
409
+ }
410
+ /**
411
+ * Force-remove EVERY managed worktree + branch this project owns, without
412
+ * relying on the in-memory `handles` map — so it works post-run (a fresh
413
+ * manager can clean up a previous run's leftovers). Enumerates
414
+ * `git worktree list --porcelain`, removes every checkout living under the
415
+ * `.wrongstack/worktrees` root, deletes every `wstack/ap/*` branch, then prunes.
416
+ * Returns the number of worktrees removed. Never throws — best-effort cleanup.
417
+ */
418
+ async cleanupAllManaged() {
419
+ return cleanupAllManaged(this.managedOpsContext());
420
+ }
421
+ /**
422
+ * Read-only inventory of this project's managed git artifacts: every worktree
423
+ * checkout under the `.wrongstack/worktrees` root (with its branch, when the
424
+ * checkout has one) plus every `wstack/ap/*` branch. Unlike `cleanupStale` /
425
+ * `cleanupAllManaged` this removes nothing — it powers the WebUI worktree panel
426
+ * so the user can SEE orphans before deciding to clean them. Never throws.
427
+ */
428
+ async listManaged() {
429
+ return listManaged(this.managedOpsContext());
430
+ }
431
+ /**
432
+ * Force-remove ONE managed worktree + (optionally) its branch — the targeted
433
+ * counterpart to `cleanupAllManaged`. Used by the WebUI worktree panel's
434
+ * per-row Remove/Discard. Path-guarded so only checkouts under the project's
435
+ * worktrees root can be removed. Best-effort; never throws.
436
+ */
437
+ async removeOne(dir, branch) {
438
+ return removeOne(this.managedOpsContext(), dir, branch);
439
+ }
440
+ /**
441
+ * Squash-merge an arbitrary `wstack/ap/*` branch into the base branch from the
442
+ * main checkout — the handle-free counterpart to {@link merge}, used by the
443
+ * WebUI worktree panel's per-row "Merge to base". On conflict it hard-resets
444
+ * the base tree (never leaves it dirty) and reports the conflicted paths.
445
+ * Refuses on a dirty base tree so uncommitted work is never clobbered.
446
+ */
447
+ async mergeBranch(branch, baseBranch) {
448
+ const base = baseBranch ?? await this.detectBaseBranch();
449
+ if (branch.startsWith("-") || base.startsWith("-")) {
450
+ return { ok: false, reason: "invalid ref" };
451
+ }
452
+ const status = await this.runGit(["status", "--porcelain"], this.projectRoot);
453
+ if (status.stdout.trim().length > 0) {
454
+ return { ok: false, reason: "working tree has uncommitted changes \u2014 commit or stash first" };
455
+ }
456
+ const co = await this.runGit(["checkout", base], this.projectRoot);
457
+ if (co.code !== 0) {
458
+ return { ok: false, reason: co.stderr || `checkout ${base} failed` };
459
+ }
460
+ const merged = await this.runGit(["merge", "--squash", branch], this.projectRoot);
461
+ if (merged.code !== 0) {
462
+ const fromOutput = parseConflictPaths(`${merged.stdout}
463
+ ${merged.stderr}`);
464
+ const fromIndex = await this.unmergedFiles();
465
+ const conflictFiles = [.../* @__PURE__ */ new Set([...fromOutput, ...fromIndex])];
466
+ await this.runGit(["reset", "--hard", "HEAD"], this.projectRoot).catch(() => void 0);
467
+ return {
468
+ ok: false,
469
+ conflict: true,
470
+ conflictFiles,
471
+ reason: merged.stderr || "merge conflict"
472
+ };
473
+ }
474
+ const idArgs = await this.identityArgs(this.projectRoot);
475
+ const commit = await this.runGit(
476
+ [...idArgs, "commit", "-m", `merge ${branch} (squash)`],
477
+ this.projectRoot
478
+ );
479
+ if (commit.code !== 0 && !/nothing to commit/i.test(commit.stdout + commit.stderr)) {
480
+ return { ok: false, reason: commit.stderr || "squash commit failed" };
481
+ }
482
+ return { ok: true };
483
+ }
484
+ /**
485
+ * Compact change summary for a worktree checkout: working-tree edits
486
+ * (numstat vs HEAD) + commit count ahead of `baseBranch`. Powers the panel's
487
+ * "View changes" without streaming a full diff. Never throws.
488
+ */
489
+ async diffSummary(dir, baseBranch) {
490
+ const files = [];
491
+ let insertions = 0;
492
+ let deletions = 0;
493
+ let commits = 0;
494
+ try {
495
+ const numstat = await this.runGit(["diff", "--numstat", "HEAD"], dir);
496
+ for (const line of numstat.stdout.split("\n")) {
497
+ const m = line.trim().match(/^(\d+|-)\t(\d+|-)\t(.+)$/);
498
+ if (!m) continue;
499
+ const ins = m[1] === "-" ? 0 : Number(m[1]);
500
+ const del = m[2] === "-" ? 0 : Number(m[2]);
501
+ files.push({ path: m[3], insertions: ins, deletions: del });
502
+ insertions += ins;
503
+ deletions += del;
504
+ }
505
+ } catch {
506
+ }
507
+ try {
508
+ const base = baseBranch ?? await this.detectBaseBranch();
509
+ const count = await this.runGit(["rev-list", "--count", `${base}..HEAD`], dir);
510
+ commits = Number(count.stdout.trim()) || 0;
511
+ } catch {
512
+ }
513
+ return { files, insertions, deletions, commits };
514
+ }
515
+ /**
516
+ * Detect and clean up stale worktrees left behind by crashed subagents.
517
+ *
518
+ * P2 #B6 (sprint2 audit): when a subagent crashes (OOM, SIGKILL), its
519
+ * worktree checkout and branch remain on disk. `cleanupAllManaged()`
520
+ * sweeps everything, but is never called automatically — only via
521
+ * `/worktree clean`. This method runs on Director boot (or any other
522
+ * entry point) to clean up leftovers from a previous crashed run.
523
+ *
524
+ * Unlike `cleanupAllManaged()`, this method is a no-op when no stale
525
+ * worktrees are detected — it doesn't clear in-memory handles or emit
526
+ * events unnecessarily. When stale worktrees ARE found, it delegates
527
+ * to `cleanupAllManaged()` for the actual removal.
528
+ *
529
+ * Returns the number of stale worktrees removed (0 = clean).
530
+ */
531
+ async cleanupStale() {
532
+ return cleanupStale(this.managedOpsContext());
533
+ }
534
+ /**
535
+ * Undo a run's squash commits by reverting each (newest → oldest) on the base
536
+ * branch — history-preserving, never a destructive reset. Refuses on a dirty
537
+ * working tree (so uncommitted work is never clobbered) and aborts cleanly if a
538
+ * revert conflicts, reporting which SHA. `shas` are the run commit SHAs in the
539
+ * order they landed; this reverses them. Returns the count reverted.
540
+ */
541
+ async revertCommits(baseBranch, shas) {
542
+ if (shas.length === 0) return { ok: true, reverted: 0, reason: "nothing to revert" };
543
+ const status = await this.runGit(["status", "--porcelain"], this.projectRoot);
544
+ if (status.stdout.trim().length > 0) {
545
+ return {
546
+ ok: false,
547
+ reverted: 0,
548
+ reason: "working tree has uncommitted changes \u2014 commit or stash first"
549
+ };
550
+ }
551
+ const co = await this.runGit(["checkout", baseBranch], this.projectRoot);
552
+ if (co.code !== 0) {
553
+ return { ok: false, reverted: 0, reason: co.stderr || `checkout ${baseBranch} failed` };
554
+ }
555
+ const idArgs = await this.identityArgs(this.projectRoot);
556
+ let reverted = 0;
557
+ for (const sha of [...shas].reverse()) {
558
+ const res = await this.runGit([...idArgs, "revert", "--no-edit", sha], this.projectRoot);
559
+ if (res.code !== 0) {
560
+ await this.runGit(["revert", "--abort"], this.projectRoot).catch(() => void 0);
561
+ return {
562
+ ok: false,
563
+ reverted,
564
+ reason: `revert of ${sha.slice(0, 8)} failed: ${(res.stderr || res.stdout).trim().split("\n")[0] ?? "conflict"}`
565
+ };
566
+ }
567
+ reverted++;
568
+ }
569
+ return { ok: true, reverted };
570
+ }
571
+ /**
572
+ * Run the caller-supplied resolver against a conflicted squash-merge, then
573
+ * commit if it cleared every marker. Returns a successful `MergeResult` on a
574
+ * clean resolution, or `null` to signal the caller should fall back to the
575
+ * abort path. Never leaves the base tree committed-but-dirty: a partial or
576
+ * failed resolution returns `null` and the caller hard-resets.
577
+ */
578
+ async tryResolveConflict(handle, conflictFiles, opts) {
579
+ let resolved = false;
580
+ try {
581
+ resolved = opts.resolve ? await opts.resolve({ conflictFiles, cwd: this.projectRoot }) : false;
582
+ } catch {
583
+ resolved = false;
584
+ }
585
+ if (!resolved) return null;
586
+ await this.runGit(["add", "-A"], this.projectRoot);
587
+ if (await this.hasConflictMarkers(conflictFiles)) return null;
588
+ const idArgs = await this.identityArgs(this.projectRoot);
589
+ const msg = opts.message ?? `merge ${handle.branch} (squash, conflict resolved)`;
590
+ const commit = await this.runGit([...idArgs, "commit", "-m", msg], this.projectRoot);
591
+ if (commit.code !== 0 && !/nothing to commit/i.test(commit.stdout + commit.stderr)) {
592
+ return null;
593
+ }
594
+ handle.conflictFiles = conflictFiles;
595
+ this.setStatus(handle, "merged");
596
+ this.emit("worktree.merged", {
597
+ handleId: handle.id,
598
+ ownerId: handle.ownerId,
599
+ branch: handle.branch,
600
+ baseBranch: handle.baseBranch,
601
+ squash: true
602
+ });
603
+ return { ok: true, resolved: true, conflictFiles };
604
+ }
605
+ /**
606
+ * True when staged content still carries conflict markers.
607
+ *
608
+ * Reads the working-tree files directly and matches a full marker line
609
+ * (`<<<<<<< `, `=======`, `>>>>>>> `, `||||||| `) with a lenient regex
610
+ * (`{7,}`) that stays platform- and config-independent — unlike the old
611
+ * `git diff --check` (only fires when `core.whitespace` opts in) and
612
+ * `git grep` (assumes the default 7-char markers).
613
+ *
614
+ * Scan set: when the caller supplies the conflicted paths (the normal
615
+ * path from {@link tryResolveConflict}), ONLY those files are scanned so
616
+ * an unrelated `=======` setext-heading underline in a staged document
617
+ * can't false-positive and discard valid resolution work. The staged-file
618
+ * listing (`git diff --cached --name-only`) is used only as a fallback
619
+ * when no paths are given.
620
+ *
621
+ * CRLF caveat: conflicted files checked out with CRLF (`core.autocrlf=true`,
622
+ * `.gitattributes eol=crlf`) carry bare markers as `=======\r\n`. Because
623
+ * `$` in multiline mode matches only before `\n`, carriage returns are
624
+ * stripped before matching so `\r` never defeats the end-of-line anchor.
625
+ */
626
+ async hasConflictMarkers(files) {
627
+ const listed = files ?? [];
628
+ const paths = listed.length > 0 ? listed : (await this.runGit(["diff", "--cached", "--name-only", "-z"], this.projectRoot)).stdout.split("\0").filter(Boolean);
629
+ const marker = /^(?:<{7,}(?: |$)|={7,}$|>{7,}(?: |$)|\|{7,}(?: |$))/m;
630
+ const startMarker = /^(?:<{7,}(?: |$)|\|{7,}(?: |$))/m;
631
+ for (const rel of paths) {
632
+ try {
633
+ const content = (await readFile(join(this.projectRoot, rel), "utf8")).replace(/\r/g, "");
634
+ const lines = content.split("\n");
635
+ let seenStart = false;
636
+ for (const line of lines) {
637
+ if (!marker.test(line)) continue;
638
+ if (line.startsWith("=====")) {
639
+ if (!seenStart) continue;
640
+ } else if (startMarker.test(line)) {
641
+ seenStart = true;
642
+ }
643
+ return true;
644
+ }
645
+ } catch {
646
+ }
647
+ }
648
+ return false;
649
+ }
650
+ /**
651
+ * Remove the worktree + branch. Conflicted/failed handles (or `keep:true`)
652
+ * are left on disk for inspection.
653
+ */
654
+ async release(handle, opts = {}) {
655
+ const keep = opts.keep || handle.status === "needs-review" || handle.status === "failed";
656
+ if (!keep) {
657
+ await this.runGit(["worktree", "remove", "--force", handle.dir], this.projectRoot);
658
+ await this.runGit(["branch", "-D", handle.branch], this.projectRoot);
659
+ await this.runGit(["worktree", "prune"], this.projectRoot);
660
+ this.handles.delete(handle.ownerId);
661
+ }
662
+ this.emit("worktree.released", {
663
+ handleId: handle.id,
664
+ ownerId: handle.ownerId,
665
+ branch: handle.branch,
666
+ kept: keep
667
+ });
668
+ }
669
+ get(ownerId) {
670
+ return this.handles.get(ownerId);
671
+ }
672
+ list() {
673
+ return [...this.handles.values()];
674
+ }
675
+ // ── internals ────────────────────────────────────────────────────────────
676
+ worktreesRoot() {
677
+ return join(this.projectRoot, ".wrongstack", "worktrees");
678
+ }
679
+ async detectBaseBranch() {
680
+ const head = await this.runGit(["rev-parse", "--abbrev-ref", "HEAD"], this.projectRoot);
681
+ const name = head.stdout.trim();
682
+ if (name && name !== "HEAD") return name;
683
+ const sha = await this.runGit(["rev-parse", "HEAD"], this.projectRoot);
684
+ return sha.stdout.trim() || "HEAD";
685
+ }
686
+ makeSlug(hint) {
687
+ return makeSlug(hint, this.usedSlugs);
688
+ }
689
+ async collectStats(dir) {
690
+ return collectStats(this.runGit, dir);
691
+ }
692
+ async identityArgs(cwd) {
693
+ return identityArgs(this.runGit, cwd);
694
+ }
695
+ async unmergedFiles() {
696
+ const res = await this.runGit(["diff", "--name-only", "--diff-filter=U"], this.projectRoot);
697
+ return res.stdout.split("\n").map((s) => s.trim()).filter(Boolean);
698
+ }
699
+ emitCommitted(handle, committed) {
700
+ this.emit("worktree.committed", {
701
+ handleId: handle.id,
702
+ ownerId: handle.ownerId,
703
+ branch: handle.branch,
704
+ committed,
705
+ insertions: handle.insertions,
706
+ deletions: handle.deletions,
707
+ files: handle.files,
708
+ sha: handle.sha
709
+ });
710
+ }
711
+ fail(handle, error) {
712
+ this.setStatus(handle, "failed", { lastError: error });
713
+ this.emit("worktree.failed", {
714
+ handleId: handle.id,
715
+ ownerId: handle.ownerId,
716
+ branch: handle.branch,
717
+ error
718
+ });
719
+ return handle;
720
+ }
721
+ setStatus(handle, status, patch) {
722
+ handle.status = status;
723
+ handle.updatedAt = Date.now();
724
+ if (patch) Object.assign(handle, patch);
725
+ }
726
+ emit(event, payload) {
727
+ const sessionId = this.currentSessionId();
728
+ this.events?.emit(
729
+ event,
730
+ sessionId && payload && typeof payload === "object" ? { ...payload, sessionId } : payload
731
+ );
732
+ }
733
+ currentSessionId() {
734
+ const value = typeof this.sessionIdSource === "function" ? this.sessionIdSource() : this.sessionIdSource;
735
+ return typeof value === "string" && value.length > 0 ? value : void 0;
736
+ }
737
+ defaultRun(args, cwd) {
738
+ return defaultRun(this.gitBin, args, cwd);
739
+ }
740
+ /** Bundle of manager state the handle-free managed-artifact ops operate on. */
741
+ managedOpsContext() {
742
+ return {
743
+ runGit: this.runGit,
744
+ projectRoot: this.projectRoot,
745
+ worktreesRoot: this.worktreesRoot(),
746
+ handles: this.handles,
747
+ usedSlugs: this.usedSlugs,
748
+ emit: (event, payload) => this.emit(event, payload)
749
+ };
750
+ }
751
+ };
752
+
753
+ export {
754
+ assertSafePath,
755
+ WorktreeManager
756
+ };
757
+ //# sourceMappingURL=chunk-RCQYT2I4.js.map