@nexrall/code-core 1.4.76 → 1.4.78

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 (143) hide show
  1. package/dist/agent/effortCap.d.ts +24 -0
  2. package/dist/agent/effortCap.d.ts.map +1 -0
  3. package/dist/agent/effortCap.js +66 -0
  4. package/dist/agent/goal.d.ts +55 -0
  5. package/dist/agent/goal.d.ts.map +1 -0
  6. package/dist/agent/goal.js +120 -0
  7. package/dist/agent/handoffBrief.d.ts +82 -0
  8. package/dist/agent/handoffBrief.d.ts.map +1 -0
  9. package/dist/agent/handoffBrief.js +121 -0
  10. package/dist/agent/hooks.d.ts +10 -2
  11. package/dist/agent/hooks.d.ts.map +1 -1
  12. package/dist/agent/hooks.js +9 -3
  13. package/dist/agent/modelCatalogue.d.ts +23 -0
  14. package/dist/agent/modelCatalogue.d.ts.map +1 -1
  15. package/dist/agent/modelCatalogue.js +28 -0
  16. package/dist/agent/outputStyles.d.ts +27 -0
  17. package/dist/agent/outputStyles.d.ts.map +1 -0
  18. package/dist/agent/outputStyles.js +174 -0
  19. package/dist/agent/skills.d.ts.map +1 -1
  20. package/dist/agent/skills.js +21 -1
  21. package/dist/api/billingLines.d.ts +11 -0
  22. package/dist/api/billingLines.d.ts.map +1 -0
  23. package/dist/api/billingLines.js +56 -0
  24. package/dist/api/client.d.ts +2 -0
  25. package/dist/api/client.d.ts.map +1 -1
  26. package/dist/api/scheduleLines.d.ts +53 -0
  27. package/dist/api/scheduleLines.d.ts.map +1 -0
  28. package/dist/api/scheduleLines.js +150 -0
  29. package/dist/context/contextFiles.d.ts +81 -0
  30. package/dist/context/contextFiles.d.ts.map +1 -0
  31. package/dist/context/contextFiles.js +310 -0
  32. package/dist/diff/collect.d.ts +25 -0
  33. package/dist/diff/collect.d.ts.map +1 -0
  34. package/dist/diff/collect.js +169 -0
  35. package/dist/diff/diffView.d.ts +57 -0
  36. package/dist/diff/diffView.d.ts.map +1 -0
  37. package/dist/diff/diffView.js +231 -0
  38. package/dist/diff/reviewQueue.d.ts +53 -0
  39. package/dist/diff/reviewQueue.d.ts.map +1 -0
  40. package/dist/diff/reviewQueue.js +349 -0
  41. package/dist/events/recap.d.ts +54 -0
  42. package/dist/events/recap.d.ts.map +1 -0
  43. package/dist/events/recap.js +161 -0
  44. package/dist/events/sessionEvents.d.ts +54 -0
  45. package/dist/events/sessionEvents.d.ts.map +1 -0
  46. package/dist/events/sessionEvents.js +324 -0
  47. package/dist/events/sessionStats.d.ts +89 -0
  48. package/dist/events/sessionStats.d.ts.map +1 -0
  49. package/dist/events/sessionStats.js +264 -0
  50. package/dist/import/claudeImport.d.ts +44 -0
  51. package/dist/import/claudeImport.d.ts.map +1 -0
  52. package/dist/import/claudeImport.js +337 -0
  53. package/dist/index.d.ts +38 -0
  54. package/dist/index.d.ts.map +1 -1
  55. package/dist/index.js +38 -0
  56. package/dist/mcp/manager.d.ts +8 -0
  57. package/dist/mcp/manager.d.ts.map +1 -1
  58. package/dist/mcp/manager.js +64 -57
  59. package/dist/permissions/destructiveTokens.d.ts +1 -0
  60. package/dist/permissions/destructiveTokens.d.ts.map +1 -1
  61. package/dist/permissions/destructiveTokens.js +93 -0
  62. package/dist/permissions/rules.d.ts +12 -0
  63. package/dist/permissions/rules.d.ts.map +1 -1
  64. package/dist/permissions/rules.js +89 -7
  65. package/dist/plugins/configStore.d.ts +43 -0
  66. package/dist/plugins/configStore.d.ts.map +1 -0
  67. package/dist/plugins/configStore.js +253 -0
  68. package/dist/plugins/files.d.ts +10 -0
  69. package/dist/plugins/files.d.ts.map +1 -0
  70. package/dist/plugins/files.js +85 -0
  71. package/dist/plugins/index.d.ts +3 -22
  72. package/dist/plugins/index.d.ts.map +1 -1
  73. package/dist/plugins/index.js +30 -40
  74. package/dist/plugins/resolve.d.ts +18 -0
  75. package/dist/plugins/resolve.d.ts.map +1 -0
  76. package/dist/plugins/resolve.js +152 -0
  77. package/dist/plugins/userConfig.d.ts +90 -0
  78. package/dist/plugins/userConfig.d.ts.map +1 -0
  79. package/dist/plugins/userConfig.js +332 -0
  80. package/dist/plugins/validate.d.ts +14 -0
  81. package/dist/plugins/validate.d.ts.map +1 -0
  82. package/dist/plugins/validate.js +222 -0
  83. package/dist/project/projectCommands.d.ts +33 -0
  84. package/dist/project/projectCommands.d.ts.map +1 -0
  85. package/dist/project/projectCommands.js +246 -0
  86. package/dist/search/conversationTurns.d.ts +53 -0
  87. package/dist/search/conversationTurns.d.ts.map +1 -0
  88. package/dist/search/conversationTurns.js +170 -0
  89. package/dist/search/globalSearch.d.ts +144 -0
  90. package/dist/search/globalSearch.d.ts.map +1 -0
  91. package/dist/search/globalSearch.js +350 -0
  92. package/dist/share/sessionChannel.d.ts +136 -0
  93. package/dist/share/sessionChannel.d.ts.map +1 -0
  94. package/dist/share/sessionChannel.js +373 -0
  95. package/dist/share/sessionShare.d.ts +104 -0
  96. package/dist/share/sessionShare.d.ts.map +1 -0
  97. package/dist/share/sessionShare.js +336 -0
  98. package/dist/ui/configCmd.d.ts +48 -0
  99. package/dist/ui/configCmd.d.ts.map +1 -0
  100. package/dist/ui/configCmd.js +146 -0
  101. package/dist/ui/deployPanel.d.ts +91 -0
  102. package/dist/ui/deployPanel.d.ts.map +1 -0
  103. package/dist/ui/deployPanel.js +442 -0
  104. package/dist/ui/mergeAssistant.d.ts +120 -0
  105. package/dist/ui/mergeAssistant.d.ts.map +1 -0
  106. package/dist/ui/mergeAssistant.js +391 -0
  107. package/dist/ui/notify.d.ts +114 -0
  108. package/dist/ui/notify.d.ts.map +1 -0
  109. package/dist/ui/notify.js +235 -0
  110. package/dist/ui/promptSuggest.d.ts +56 -0
  111. package/dist/ui/promptSuggest.d.ts.map +1 -0
  112. package/dist/ui/promptSuggest.js +160 -0
  113. package/dist/ui/taskGraph.d.ts +44 -0
  114. package/dist/ui/taskGraph.d.ts.map +1 -0
  115. package/dist/ui/taskGraph.js +289 -0
  116. package/dist/util/addDir.d.ts +31 -0
  117. package/dist/util/addDir.d.ts.map +1 -0
  118. package/dist/util/addDir.js +124 -0
  119. package/dist/util/batch.d.ts +16 -0
  120. package/dist/util/batch.d.ts.map +1 -0
  121. package/dist/util/batch.js +92 -0
  122. package/dist/util/cellWidth.d.ts +12 -0
  123. package/dist/util/cellWidth.d.ts.map +1 -0
  124. package/dist/util/cellWidth.js +161 -0
  125. package/dist/util/duration.d.ts +8 -0
  126. package/dist/util/duration.d.ts.map +1 -0
  127. package/dist/util/duration.js +50 -0
  128. package/dist/util/loopJobs.d.ts +67 -0
  129. package/dist/util/loopJobs.d.ts.map +1 -0
  130. package/dist/util/loopJobs.js +169 -0
  131. package/dist/util/settingsFile.d.ts +14 -0
  132. package/dist/util/settingsFile.d.ts.map +1 -0
  133. package/dist/util/settingsFile.js +99 -0
  134. package/dist/util/toolLabel.d.ts +2 -0
  135. package/dist/util/toolLabel.d.ts.map +1 -0
  136. package/dist/util/toolLabel.js +17 -0
  137. package/dist/verify/failureTimeline.d.ts +100 -0
  138. package/dist/verify/failureTimeline.d.ts.map +1 -0
  139. package/dist/verify/failureTimeline.js +253 -0
  140. package/dist/verify/verification.d.ts +75 -0
  141. package/dist/verify/verification.d.ts.map +1 -0
  142. package/dist/verify/verification.js +315 -0
  143. package/package.json +5 -4
@@ -0,0 +1,391 @@
1
+ "use strict";
2
+ // Lives in core (moved from the CLI's ui/mergeAssistant.ts, 2026-10-05) so the VS
3
+ // Code panel previews worktree merges with the same rules; the CLI module is a
4
+ // re-export shim. STATUS_GLYPH's exact glyphs must match agentTree's (pinned by test).
5
+ // Merge assistant (spec §80).
6
+ //
7
+ // WHY this exists: sub-agents can now write into the SAME checkout — silently, last write
8
+ // wins — or into an isolated worktree (`isolation: "worktree"`), which the loop reports in the
9
+ // task result as "[isolated worktree: this sub-agent's changes are in <path> (branch <b>) — NOT
10
+ // in the main checkout ...]". When several of them finish, the user's real question is "what has
11
+ // to be reconciled before I move on", and there are TWO different answers that must never be
12
+ // conflated:
13
+ //
14
+ // • Same checkout, same file: NOT a merge conflict. Git never sees it; the second writer
15
+ // simply overwrote the first, so the risk is lost intent, not a conflict marker.
16
+ // • Separate worktree branches: a real merge. `git merge-tree --write-tree <a> <b>` says
17
+ // whether they combine without touching the working tree, and it is the only way to answer
18
+ // without guessing — overlapping file sets are NOT a conflict (two agents can edit
19
+ // different regions of one file and merge cleanly, and git is the one who knows).
20
+ //
21
+ // So this module reports both, each labelled as what it is, and never calls something a conflict
22
+ // that git did not call one. Everything is pure except `checkBranchMerges`, which takes its git
23
+ // runner as an argument so the flow is testable against a fake repo, and none of it is
24
+ // speculative: a worktree branch is only checked if a report named it.
25
+ Object.defineProperty(exports, "__esModule", { value: true });
26
+ exports.MAX_WRITER_ROWS = exports.MAX_MERGE_TREE_CHECKS = exports.gitRunnerForMerge = exports.STATUS_GLYPH = exports.MAX_NODE_FILES = void 0;
27
+ exports.headRefFor = headRefFor;
28
+ exports.parseWorktreeNote = parseWorktreeNote;
29
+ exports.writersFrom = writersFrom;
30
+ exports.sharedFiles = sharedFiles;
31
+ exports.alsoByMain = alsoByMain;
32
+ exports.looksLikeRef = looksLikeRef;
33
+ exports.mergePairs = mergePairs;
34
+ exports.parseMergeTree = parseMergeTree;
35
+ exports.checkBranchMerges = checkBranchMerges;
36
+ exports.formatMergePanel = formatMergePanel;
37
+ const child_process_1 = require("child_process");
38
+ const globalSearch_1 = require("../search/globalSearch");
39
+ const collect_1 = require("../diff/collect");
40
+ exports.MAX_NODE_FILES = 40;
41
+ exports.STATUS_GLYPH = {
42
+ running: '⠋',
43
+ completed: '✓',
44
+ failed: '✗',
45
+ stopped: '⊘',
46
+ idle: '○',
47
+ };
48
+ /** The ref a worktree branch would be merged INTO: the branch name, or HEAD when detached. */
49
+ function headRefFor(dir) {
50
+ return (0, collect_1.gitBranch)(dir) ?? 'HEAD';
51
+ }
52
+ /**
53
+ * git with stderr captured, because `merge-tree` says "clean" and "I could not run" with
54
+ * different exit codes and treating the second as the first is exactly the lie this feature
55
+ * exists to avoid. execFileSync throws on a non-zero status, so the fields come off the error.
56
+ */
57
+ const gitRunnerForMerge = (cwd, args) => {
58
+ try {
59
+ const stdout = (0, child_process_1.execFileSync)('git', args, {
60
+ cwd,
61
+ encoding: 'utf-8',
62
+ stdio: ['ignore', 'pipe', 'pipe'],
63
+ timeout: 20000,
64
+ maxBuffer: 4 * 1024 * 1024,
65
+ });
66
+ return { status: 0, stdout, stderr: '' };
67
+ }
68
+ catch (err) {
69
+ const e = err;
70
+ const message = err instanceof Error ? err.message : String(err);
71
+ return {
72
+ status: typeof e.status === 'number' ? e.status : 1,
73
+ stdout: e.stdout ?? '',
74
+ stderr: e.stderr ?? message,
75
+ };
76
+ }
77
+ };
78
+ exports.gitRunnerForMerge = gitRunnerForMerge;
79
+ /** Cap on merge-tree invocations per /merge — one per branch plus every pair, all cheap. */
80
+ exports.MAX_MERGE_TREE_CHECKS = 12;
81
+ /** Agent rows before the panel stops listing and counts the rest. */
82
+ exports.MAX_WRITER_ROWS = 8;
83
+ /**
84
+ * The marker the agent loop appends to a sub-agent report when its isolated worktree still
85
+ * held work. Parsed with ASCII anchors only ("are in … [" and "NOT in the main checkout")
86
+ * because the note contains an em dash, and a path may legitimately contain one too.
87
+ */
88
+ const WORKTREE_NOTE_RE = /\[isolated worktree: this sub-agent's changes are in ([^\]]*)\]/;
89
+ function parseWorktreeNote(text) {
90
+ if (!text)
91
+ return null;
92
+ const m = WORKTREE_NOTE_RE.exec(text);
93
+ const body = m?.[1];
94
+ if (body === undefined)
95
+ return null;
96
+ let rest = body;
97
+ const cut = rest.indexOf('NOT in the main checkout');
98
+ // `>= 0`: a body that OPENS with that sentence carries no path at all, which is refused
99
+ // below — keeping it would return the sentence itself as the path.
100
+ if (cut >= 0)
101
+ rest = rest.slice(0, cut);
102
+ // Trailing punctuation is the separator before that sentence: " — ", "-", " - ".
103
+ rest = rest.replace(/[\s\u2014\u2013-]+$/, '').trim();
104
+ if (!rest)
105
+ return null;
106
+ const b = /\(branch ([^)]*)\)\s*$/.exec(rest);
107
+ const branch = b?.[1]?.trim();
108
+ if (branch) {
109
+ const path = rest.slice(0, b.index).trim();
110
+ return path ? { path, branch } : null;
111
+ }
112
+ return { path: rest };
113
+ }
114
+ /** Agents that wrote at least one file, or that kept a worktree worth reconciling. */
115
+ function writersFrom(nodes) {
116
+ const out = [];
117
+ for (const n of nodes) {
118
+ const worktree = parseWorktreeNote(n.report) ?? undefined;
119
+ if (!n.files.length && !worktree)
120
+ continue;
121
+ out.push({
122
+ id: n.id,
123
+ name: n.name,
124
+ label: n.label,
125
+ status: n.status,
126
+ files: [...n.files],
127
+ capped: n.files.length >= exports.MAX_NODE_FILES,
128
+ ...(worktree ? { worktree } : {}),
129
+ });
130
+ }
131
+ return out;
132
+ }
133
+ /**
134
+ * Files written by more than one agent in the same checkout.
135
+ *
136
+ * An isolated agent is skipped: its writes landed in its own worktree, so the same relative
137
+ * path is a different file on disk and pairing them here would invent a clash.
138
+ */
139
+ function sharedFiles(writers) {
140
+ const byPath = new Map();
141
+ for (const w of writers) {
142
+ if (w.worktree)
143
+ continue;
144
+ for (const f of w.files) {
145
+ const list = byPath.get(f);
146
+ if (list) {
147
+ if (!list.includes(w.id))
148
+ list.push(w.id);
149
+ }
150
+ else {
151
+ byPath.set(f, [w.id]);
152
+ }
153
+ }
154
+ }
155
+ return [...byPath.entries()]
156
+ .filter(([, ids]) => ids.length > 1)
157
+ .map(([path, ids]) => ({ path, writers: ids }))
158
+ .sort((a, b) => b.writers.length - a.writers.length || a.path.localeCompare(b.path));
159
+ }
160
+ /** Sub-agent writes that the main agent also made this session (same checkout only). */
161
+ function alsoByMain(writers, mainFiles) {
162
+ const main = new Set(mainFiles);
163
+ const out = new Set();
164
+ for (const w of writers) {
165
+ if (w.worktree)
166
+ continue;
167
+ for (const f of w.files)
168
+ if (main.has(f))
169
+ out.add(f);
170
+ }
171
+ return [...out].sort();
172
+ }
173
+ /** A ref we are willing to hand to git — no leading dash, no whitespace. */
174
+ function looksLikeRef(name) {
175
+ return name.length > 0 && !name.startsWith('-') && !/\s/.test(name);
176
+ }
177
+ /**
178
+ * Which merges to test, most useful first: every branch into the current checkout, then the
179
+ * branches against each other (merging A then B is exactly where two agents' overlapping work
180
+ * surfaces). Capped, and the number left out is reported rather than hidden.
181
+ */
182
+ function mergePairs(writers, head, max = exports.MAX_MERGE_TREE_CHECKS) {
183
+ const branches = [];
184
+ for (const w of writers) {
185
+ const b = w.worktree?.branch;
186
+ if (b && looksLikeRef(b) && !branches.includes(b))
187
+ branches.push(b);
188
+ }
189
+ const all = [];
190
+ if (looksLikeRef(head))
191
+ for (const b of branches)
192
+ all.push({ a: head, b });
193
+ for (let i = 0; i < branches.length; i++) {
194
+ for (let j = i + 1; j < branches.length; j++)
195
+ all.push({ a: branches[i], b: branches[j] });
196
+ }
197
+ const keep = Math.max(0, max);
198
+ const pairs = all.slice(0, keep);
199
+ return { pairs, skipped: all.length - pairs.length };
200
+ }
201
+ /** Conflicted paths from `merge-tree --write-tree`: the stage rows are the authoritative list. */
202
+ function parseMergeTree(stdout) {
203
+ const out = [];
204
+ for (const raw of stdout.split('\n')) {
205
+ const m = /^[0-7]{6} [0-9a-f]{4,} [123]\t(.+)$/.exec(raw.replace(/\r$/, ''));
206
+ const tail = m?.[1];
207
+ if (tail === undefined)
208
+ continue;
209
+ const path = unquotePath(tail);
210
+ if (path && !out.includes(path))
211
+ out.push(path);
212
+ }
213
+ return out;
214
+ }
215
+ /** git quotes paths with special bytes C-style ("a\tb"); undo it, same as `git status` parsing. */
216
+ function unquotePath(raw) {
217
+ const t = raw.trim();
218
+ const m = /^"(.*)"$/.exec(t);
219
+ const inner = m?.[1];
220
+ if (inner === undefined)
221
+ return t;
222
+ try {
223
+ return JSON.parse(`"${inner}"`);
224
+ }
225
+ catch {
226
+ return inner;
227
+ }
228
+ }
229
+ /**
230
+ * Run one dry-run merge per pair. Exit 0 = clean, 1 = conflicts (paths on stdout), anything
231
+ * else = the check did not happen, which is reported as such instead of as "no conflicts".
232
+ *
233
+ * Pairs whose two refs resolve to the same commit are marked `trivial` instead of being asked:
234
+ * the answer is fixed before git runs, and it is exactly the case a freshly spawned worktree
235
+ * agent is in (its writes are on disk in the worktree, not in any commit).
236
+ */
237
+ function checkBranchMerges(cwd, pairs, run) {
238
+ const checks = [];
239
+ let unsupported = false;
240
+ const oids = new Map();
241
+ const oidOf = (ref) => {
242
+ let hit = oids.get(ref);
243
+ if (hit === undefined) {
244
+ const r = run(cwd, ['rev-parse', '--verify', '--quiet', `${ref}^{commit}`]);
245
+ hit = r.status === 0 ? firstLine(r.stdout) || null : null;
246
+ oids.set(ref, hit);
247
+ }
248
+ return hit;
249
+ };
250
+ for (const p of pairs) {
251
+ const a = oidOf(p.a);
252
+ const b = oidOf(p.b);
253
+ if (a !== null && a === b) {
254
+ checks.push({ ...p, conflicts: [], trivial: true });
255
+ continue;
256
+ }
257
+ const r = run(cwd, ['merge-tree', '--write-tree', p.a, p.b]);
258
+ if (r.status === 0) {
259
+ checks.push({ ...p, conflicts: [] });
260
+ continue;
261
+ }
262
+ if (r.status === 1) {
263
+ checks.push({ ...p, conflicts: parseMergeTree(r.stdout) });
264
+ continue;
265
+ }
266
+ const detail = firstLine(r.stderr || r.stdout) || `git exited ${r.status}`;
267
+ if (/usage:|unknown option|--write-tree|not a git|bad revision/i.test(detail))
268
+ unsupported = true;
269
+ checks.push({ ...p, conflicts: [], error: detail });
270
+ }
271
+ return { checks, unsupported };
272
+ }
273
+ function firstLine(text) {
274
+ const line = text.split('\n').find((l) => l.trim().length > 0) ?? '';
275
+ return line.trim();
276
+ }
277
+ function formatMergePanel(input, cols) {
278
+ const { writers, totalAgents } = input;
279
+ const out = [];
280
+ const push = (body, indent = '') => {
281
+ out.push((0, globalSearch_1.fitLine)(body, cols, indent));
282
+ };
283
+ const nWriters = writers.length;
284
+ if (!nWriters) {
285
+ push(`No sub-agent has written a file in this session${totalAgents ? ` (${totalAgents} ran)` : ''} — nothing to merge.`);
286
+ return out;
287
+ }
288
+ const worktrees = writers.filter((w) => w.worktree);
289
+ const bits = [`${nWriters} of ${totalAgents} agent${totalAgents === 1 ? '' : 's'} wrote files`];
290
+ if (worktrees.length)
291
+ bits.push(`${worktrees.length} in an isolated worktree`);
292
+ push(`Merge review — ${bits.join(' · ')}`);
293
+ const shown = writers.slice(0, exports.MAX_WRITER_ROWS);
294
+ for (const w of shown) {
295
+ // Derived, not trusted: the board caps a node's list, so a caller that forgot the flag
296
+ // must still not be able to print a truncated list as a total.
297
+ const capped = w.capped || w.files.length >= exports.MAX_NODE_FILES;
298
+ const count = `${w.files.length}${capped ? '+' : ''} file${w.files.length === 1 && !capped ? '' : 's'}`;
299
+ const tail = [];
300
+ if (w.status === 'running')
301
+ tail.push('still running');
302
+ else if (w.status === 'failed')
303
+ tail.push('failed');
304
+ else if (w.status === 'stopped')
305
+ tail.push('stopped early');
306
+ if (w.worktree)
307
+ tail.push(w.worktree.branch ? `worktree, branch ${w.worktree.branch}` : 'isolated worktree');
308
+ push(`${exports.STATUS_GLYPH[w.status]} ${w.name.padEnd(14)} ${count}${tail.length ? ` · ${tail.join(' · ')}` : ''}`);
309
+ }
310
+ if (writers.length > shown.length)
311
+ push(` … and ${writers.length - shown.length} more agent(s)`);
312
+ const shared = sharedFiles(writers);
313
+ const byId = new Map(writers.map((w) => [w.id, w.name]));
314
+ if (shared.length) {
315
+ push(`Written by more than one agent in this checkout — last write wins, git sees no conflict:`);
316
+ for (const s of shared) {
317
+ push(`${s.path} ${s.writers.map((id) => byId.get(id) ?? id).join(' + ')}`, ' ');
318
+ }
319
+ }
320
+ else {
321
+ push('No file in this checkout was written by two agents.');
322
+ }
323
+ const main = alsoByMain(writers, input.mainFiles ?? []);
324
+ if (main.length) {
325
+ push(`Also changed by the main agent this session: ${main.slice(0, 4).join(', ')}${main.length > 4 ? ` (+${main.length - 4})` : ''}`);
326
+ }
327
+ const checks = input.checks ?? [];
328
+ if (checks.length) {
329
+ push('Worktree branches — merged in memory with git merge-tree (working tree untouched):');
330
+ for (const c of checks) {
331
+ // The VERDICT leads the row. With the branch names first, a 60-column terminal clipped
332
+ // the row right after the names — the one thing the user ran /merge to learn was the
333
+ // part that fell off the end.
334
+ const label = `${c.a} → ${c.b}`;
335
+ if (c.error) {
336
+ push(`not checked · ${label}`, ' ');
337
+ push(c.error, ' ');
338
+ }
339
+ else if (c.trivial) {
340
+ push(`clean (same commit) · ${label}`, ' ');
341
+ }
342
+ else if (!c.conflicts.length) {
343
+ push(`clean · ${label}`, ' ');
344
+ }
345
+ else {
346
+ push(`${c.conflicts.length} conflicting · ${label}`, ' ');
347
+ const paths = c.conflicts.slice(0, 4).join(', ') + (c.conflicts.length > 4 ? ` (+${c.conflicts.length - 4} more)` : '');
348
+ push(paths, ' ');
349
+ }
350
+ }
351
+ if (input.skippedChecks)
352
+ push(` ${input.skippedChecks} pair(s) not checked (cap ${exports.MAX_MERGE_TREE_CHECKS}).`);
353
+ }
354
+ if (input.unsupportedGit) {
355
+ // Never let "no conflicts" be read into a check that did not happen.
356
+ push('This git has no `merge-tree --write-tree`: branch merges could NOT be checked. The file lists above are the only measured evidence.');
357
+ }
358
+ else if (!checks.length && worktrees.length) {
359
+ push('Worktree branches were not checked against each other.');
360
+ }
361
+ const trivial = checks.filter((c) => c.trivial);
362
+ if (trivial.length) {
363
+ // Measured, not inferred: both refs are the same commit, so merge-tree compared one tree
364
+ // with itself. Saying only "clean" here would promise a merge that has not been tested.
365
+ const t0 = trivial[0];
366
+ // Either side of the pair may be the worktree branch — `main → b` and `a → b` both occur.
367
+ const wt = writers.find((w) => w.worktree?.branch === t0.a || w.worktree?.branch === t0.b)?.worktree;
368
+ // Split across lines on purpose: at 110 columns the one-sentence version was clipped
369
+ // mid-word, which is exactly where the reason and the fix became unreadable.
370
+ push(`${trivial.length} checked pair(s) compare the SAME commit — a worktree branch with no commits of its own.`);
371
+ push('merge-tree reads commits, not files on disk. Commit inside the worktree first:');
372
+ if (wt)
373
+ push(`git -C ${wt.path} commit -am "agent work"`, ' ');
374
+ }
375
+ if (input.running)
376
+ push('An agent is still running — anything here can still change.');
377
+ // Actionable, and never the same advice for the two situations: `/diff` reads THIS checkout,
378
+ // while a branch conflict is reviewed with `git diff <a>...<b>` (which shows branch-only files
379
+ // that /diff cannot see). Merging stays the user's call — this command only reports.
380
+ if (shared.length)
381
+ push(`Review: /diff ${shared[0].path}`);
382
+ const conflicted = checks.find((c) => c.conflicts.length);
383
+ if (conflicted) {
384
+ // Split for the same reason as the same-commit note: one long sentence put the `a...b`
385
+ // range past the 110-column clip, so the command the user needs was invisible.
386
+ push(`Branch conflict: ${conflicted.b} vs ${conflicted.a} — this merge is not clean.`);
387
+ push(`Review with git diff ${conflicted.a}...${conflicted.b}; merging stays your call.`, ' ');
388
+ }
389
+ return out;
390
+ }
391
+ //# sourceMappingURL=mergeAssistant.js.map
@@ -0,0 +1,114 @@
1
+ export type NotifyMode = 'auto' | 'bell' | 'osc' | 'off';
2
+ export declare const NOTIFY_MODES: readonly NotifyMode[];
3
+ export declare const NOTIFY_SETTING = "notifications";
4
+ export declare const NOTIFY_USAGE = "/notify [auto|bell|osc|off] [--project]";
5
+ /** Longest notification body kept. Emulators truncate anyway; ours should be predictable. */
6
+ export declare const MAX_NOTE = 120;
7
+ /**
8
+ * A turn only *notifies* when it ran long enough that the user plausibly walked away.
9
+ * Below this, the completion block is already on screen in front of them.
10
+ */
11
+ export declare const SLOW_TURN_MS = 30000;
12
+ /** The knobs the decision depends on — env vars here rather than read inside, so tests can. */
13
+ export interface NotifyEnv {
14
+ TERM?: string | undefined;
15
+ TERM_PROGRAM?: string | undefined;
16
+ KITTY_WINDOW_ID?: string | undefined;
17
+ WT_SESSION?: string | undefined;
18
+ TMUX?: string | undefined;
19
+ STY?: string | undefined;
20
+ }
21
+ export interface NotifyOpts {
22
+ mode?: NotifyMode;
23
+ env?: NotifyEnv;
24
+ /** stdout.isTTY. Callers pass it explicitly; the default is the conservative `false`. */
25
+ isTTY?: boolean;
26
+ /** Where the bytes go. Injected so tests assert the exact sequence without a terminal. */
27
+ write?: (bytes: string) => void;
28
+ }
29
+ /**
30
+ * A mode string from the settings file or `/notify`. `null` for anything that is not a
31
+ * mode — the caller decides whether that is "unset, keep the default" (settings) or
32
+ * "invalid, tell the user" (`/notify`), which are different answers to the same input.
33
+ */
34
+ export declare function parseNotifyMode(raw: unknown): NotifyMode | null;
35
+ /**
36
+ * The `notifications` setting, defaulting to `auto`: on, using the best technique this
37
+ * terminal supports. A hand-written `false`/`"off"` means off, matching `/livediff`'s
38
+ * convention (a settings value a person typed must not be silently overruled).
39
+ */
40
+ export declare function readNotifySetting(raw: Record<string, unknown> | undefined | null): NotifyMode;
41
+ /**
42
+ * Does this terminal show OSC 9 notifications? Only the ones that document it get a yes;
43
+ * an unknown TERM_PROGRAM (VS Code's `vscode`, Apple's `Apple_Terminal`, a bare xterm)
44
+ * gets a `false` and therefore the bell.
45
+ */
46
+ export declare function detectOscSupport(env: NotifyEnv): boolean;
47
+ /**
48
+ * A notification body safe to embed in an OSC sequence: no escape sequences, no control
49
+ * characters (a stray BEL would terminate the sequence early and print the rest as text),
50
+ * one line, bounded. Returns '' when nothing printable is left, which callers treat as
51
+ * "send nothing".
52
+ *
53
+ * The ANSI strip is not cosmetic. The permission prompts hand us strings that are already
54
+ * coloured (`paint.boldRed('Type "yes" …')`), and replacing the ESC byte alone leaves the
55
+ * parameters behind as visible text — measured on a real prompt:
56
+ * `waiting for you — [1m [31mType "yes" to run this destructive command:`.
57
+ */
58
+ export declare function sanitizeNote(text: string, max?: number): string;
59
+ /** The raw OSC 9 sequence. BEL-terminated: the form iTerm2 documents and every other
60
+ * implementation of it inherits. */
61
+ export declare function oscNotification(text: string): string;
62
+ /**
63
+ * tmux and screen do not forward an OSC sequence they do not recognise, so it has to be
64
+ * handed to the OUTER terminal wrapped in a DCS passthrough — with every ESC inside the
65
+ * payload doubled, which is what tmux's own parser un-escapes. Without a multiplexer the
66
+ * sequence passes through unchanged.
67
+ *
68
+ * If the user has not enabled `allow-passthrough` (tmux ≥ 3.3) the wrapper is discarded
69
+ * silently rather than printed — the reason to wrap even when unsure.
70
+ */
71
+ export declare function passthrough(seq: string, env: NotifyEnv): string;
72
+ /**
73
+ * The bytes for one notification, or null for "say nothing".
74
+ *
75
+ * `bell` and `osc` are explicit requests and are honoured; `auto` picks OSC only where it
76
+ * is known to be read, and the bell otherwise.
77
+ */
78
+ export declare function notificationBytes(mode: NotifyMode, env: NotifyEnv, text: string): string | null;
79
+ export interface EffectiveNotify {
80
+ /** What will actually be sent for an event that carries text. */
81
+ technique: 'osc' | 'bell' | 'off';
82
+ /**
83
+ * Explicit `osc` on a terminal we do not recognise. It is still sent — that is what
84
+ * asking for it means, and detection cannot see through ssh — but the caller should say
85
+ * so, because an emulator that does not read OSC 9 shows nothing at all.
86
+ */
87
+ unconfirmed: boolean;
88
+ }
89
+ export declare class Notifier {
90
+ private mode;
91
+ private readonly env;
92
+ private readonly isTTY;
93
+ private readonly write;
94
+ constructor(opts?: NotifyOpts);
95
+ setMode(mode: NotifyMode): void;
96
+ getMode(): NotifyMode;
97
+ /** What a notification would do right now — the honest answer `/notify` prints. */
98
+ effective(): EffectiveNotify;
99
+ /** Send one. False when nothing was written — not a TTY, mode off, or an empty note. */
100
+ notify(text: string): boolean;
101
+ }
102
+ /** One line naming what finished, for a `!command` that was moved to the background. */
103
+ export declare function bgShellNote(cmd: string, id: number, code: number | null, signal: string | null): string;
104
+ /** A background sub-agent came back. */
105
+ export declare function bgAgentNote(descriptions: string[]): string;
106
+ /** The agent is blocked on a human: a permission box, a destructive confirm, a question. */
107
+ export declare function needsYouNote(what: string): string;
108
+ /**
109
+ * A turn finished. Null unless it ran long enough to have lost the user AND they did not
110
+ * touch the keyboard since it started — a notification for work they were watching is
111
+ * just noise, and "long enough" is measured, not guessed.
112
+ */
113
+ export declare function turnDoneNote(durationMs: number, userActive: boolean, sessionId?: string): string | null;
114
+ //# sourceMappingURL=notify.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"notify.d.ts","sourceRoot":"","sources":["../../src/ui/notify.ts"],"names":[],"mappings":"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"}