spidersan 0.9.0 → 0.10.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 (37) hide show
  1. package/CHANGELOG.md +24 -0
  2. package/dist/commands/bot.d.ts.map +1 -1
  3. package/dist/commands/bot.js +32 -14
  4. package/dist/commands/bot.js.map +1 -1
  5. package/dist/commands/conflicts.d.ts.map +1 -1
  6. package/dist/commands/conflicts.js +247 -258
  7. package/dist/commands/conflicts.js.map +1 -1
  8. package/dist/commands/cross-conflicts.d.ts.map +1 -1
  9. package/dist/commands/cross-conflicts.js +9 -3
  10. package/dist/commands/cross-conflicts.js.map +1 -1
  11. package/dist/commands/pulse.d.ts.map +1 -1
  12. package/dist/commands/pulse.js +41 -112
  13. package/dist/commands/pulse.js.map +1 -1
  14. package/dist/commands/register.d.ts.map +1 -1
  15. package/dist/commands/register.js +15 -8
  16. package/dist/commands/register.js.map +1 -1
  17. package/dist/commands/watch.d.ts +6 -0
  18. package/dist/commands/watch.d.ts.map +1 -1
  19. package/dist/commands/watch.js +109 -1
  20. package/dist/commands/watch.js.map +1 -1
  21. package/dist/lib/pulse-renderer.d.ts +19 -0
  22. package/dist/lib/pulse-renderer.d.ts.map +1 -0
  23. package/dist/lib/pulse-renderer.js +108 -0
  24. package/dist/lib/pulse-renderer.js.map +1 -0
  25. package/dist/lib/register-renderer.d.ts +10 -0
  26. package/dist/lib/register-renderer.d.ts.map +1 -0
  27. package/dist/lib/register-renderer.js +19 -0
  28. package/dist/lib/register-renderer.js.map +1 -0
  29. package/dist/lib/remote-drift.d.ts +1 -0
  30. package/dist/lib/remote-drift.d.ts.map +1 -1
  31. package/dist/lib/remote-drift.js +3 -1
  32. package/dist/lib/remote-drift.js.map +1 -1
  33. package/dist/lib/watch-renderer.d.ts +13 -0
  34. package/dist/lib/watch-renderer.d.ts.map +1 -0
  35. package/dist/lib/watch-renderer.js +35 -0
  36. package/dist/lib/watch-renderer.js.map +1 -0
  37. package/package.json +90 -92
@@ -9,38 +9,116 @@
9
9
  */
10
10
  import { Command } from 'commander';
11
11
  import { compilePatterns } from '../lib/regex-utils.js';
12
- import { spawnSync } from 'child_process';
12
+ import { execFileSync, spawnSync } from 'child_process';
13
13
  import { basename } from 'path';
14
14
  import { homedir } from 'os';
15
15
  import { getStorage } from '../storage/index.js';
16
16
  import { ASTParser } from '../lib/ast.js';
17
- import { analyzeConflicts } from '../lib/conflict-analyzer.js';
18
- import { renderConflictReport } from '../lib/conflict-renderer.js';
19
- import { getCurrentBranch, getFileAtRef } from '../lib/git.js';
20
17
  import { validateBranchName } from '../lib/security.js';
21
18
  import { isExcludedPath } from './register.js';
22
19
  import { loadConfig } from '../lib/config.js';
23
20
  import { logActivity } from '../lib/activity.js';
24
21
  import { isGhAvailable, getPRDetails } from '../lib/github.js';
25
- import { TIER_LABELS } from '../lib/conflict-tier.js';
26
- import { createHubClient } from '../lib/hub.js';
27
- const hub = createHubClient();
22
+ import { classifyWithLabel } from '../lib/conflict-tier.js';
23
+ // Config
24
+ const HUB_URL = process.env.HUB_URL || 'https://hub.treebird.uk';
25
+ function getCurrentBranch() {
26
+ try {
27
+ return execFileSync('git', ['rev-parse', '--abbrev-ref', 'HEAD'], { encoding: 'utf-8' }).trim();
28
+ }
29
+ catch {
30
+ throw new Error('Not in a git repository');
31
+ }
32
+ }
33
+ async function notifyHub(branch, conflicts) {
34
+ let hasTier3 = false;
35
+ let hasTier2 = false;
36
+ for (const c of conflicts) {
37
+ if (c.tier === 3)
38
+ hasTier3 = true;
39
+ else if (c.tier === 2)
40
+ hasTier2 = true;
41
+ if (hasTier3 && hasTier2)
42
+ break;
43
+ }
44
+ if (!hasTier3 && !hasTier2)
45
+ return;
46
+ const severity = hasTier3 ? 'šŸ”“ TIER 3 BLOCK' : '🟠 TIER 2 PAUSE';
47
+ const message = `šŸ•·ļøāš ļø **Conflict Alert** on \`${branch}\`\n\n${severity}\n\nConflicting files require coordination.`;
48
+ try {
49
+ await fetch(`${HUB_URL}/api/chat`, {
50
+ method: 'POST',
51
+ headers: { 'Content-Type': 'application/json' },
52
+ body: JSON.stringify({
53
+ agent: 'spidersan',
54
+ name: 'Spidersan',
55
+ message,
56
+ glyph: 'šŸ•·ļø'
57
+ })
58
+ });
59
+ }
60
+ catch {
61
+ // Hub offline - silent fail
62
+ }
63
+ }
64
+ /**
65
+ * Wake a conflicting agent and send them a message about what to fix
66
+ */
67
+ async function wakeConflictingAgent(agentId, myBranch, theirBranch, _conflictingFiles) {
68
+ // 1. Wake the agent via Hub
69
+ try {
70
+ const wakeResponse = await fetch(`${HUB_URL}/api/wake/${agentId}`, {
71
+ method: 'POST',
72
+ headers: { 'Content-Type': 'application/json' },
73
+ body: JSON.stringify({
74
+ sender: 'spidersan',
75
+ reason: `Conflict on ${theirBranch} - need resolution`
76
+ })
77
+ });
78
+ if (wakeResponse.ok) {
79
+ console.log(` šŸ”” Wake signal sent to ${agentId}`);
80
+ }
81
+ }
82
+ catch {
83
+ console.log(` āš ļø Could not wake ${agentId} via Hub`);
84
+ }
85
+ return true;
86
+ }
28
87
  /**
29
88
  * Wait for a specified time
30
89
  */
31
90
  function sleep(ms) {
32
91
  return new Promise(resolve => setTimeout(resolve, ms));
33
92
  }
34
- function writeStdout(message = '') {
35
- process.stdout.write(`${message}\n`);
36
- }
37
- function writeStderr(message) {
38
- process.stderr.write(`${message}\n`);
39
- }
40
- function fail(messages) {
41
- const output = Array.isArray(messages) ? messages.join('\n') : messages;
42
- writeStderr(output);
43
- process.exit(1);
93
+ /**
94
+ * Suggest simple resolutions for add/add style conflicts where both branches added the same file
95
+ * Typically occurs with docs or generated files. This prints guidance and a recommended sequence
96
+ * to preserve both variants for auditability (e.g., create a .incoming copy).
97
+ */
98
+ function suggestAddAddResolution(conflicts) {
99
+ const addAddFiles = new Set();
100
+ for (const conflict of conflicts) {
101
+ for (const f of conflict.files) {
102
+ // Simple heuristic: docs and markdown files are common add/add candidates
103
+ if (/\.md$/.test(f) || /docs\//.test(f)) {
104
+ addAddFiles.add(f);
105
+ }
106
+ }
107
+ }
108
+ if (addAddFiles.size === 0)
109
+ return;
110
+ console.log('\nšŸ’” Add/Add Conflict Helper');
111
+ console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
112
+ console.log('Detected files added in both branches. Recommended actions:');
113
+ console.log(' 1. Preserve incoming variant for auditability:');
114
+ console.log(' git checkout --theirs <file> && mv <file> <file>.<their-branch>.incoming && git add <file>.<their-branch>.incoming');
115
+ console.log(' 2. Keep canonical version in main branch and include note linking the incoming variant.');
116
+ console.log(' 3. Commit and continue rebase: git add -A && git rebase --continue');
117
+ console.log('\nExamples:');
118
+ for (const f of addAddFiles) {
119
+ console.log(` • ${f} -> preserve incoming as ${f}.incoming`);
120
+ }
121
+ console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n');
44
122
  }
45
123
  /**
46
124
  * Prompt user for Y/N confirmation (prevents Ralph Wiggum loops)
@@ -49,7 +127,7 @@ function fail(messages) {
49
127
  */
50
128
  async function confirmAction(prompt, autoConfirm = false) {
51
129
  if (autoConfirm) {
52
- writeStdout(`${prompt} [Y/n]: Y (auto)`);
130
+ console.log(`${prompt} [Y/n]: Y (auto)`);
53
131
  return true;
54
132
  }
55
133
  const readline = await import('readline');
@@ -65,141 +143,6 @@ async function confirmAction(prompt, autoConfirm = false) {
65
143
  });
66
144
  });
67
145
  }
68
- function filterConflictReport(report, minTier) {
69
- const conflicts = report.conflicts.filter((conflict) => conflict.tier >= minTier);
70
- const byTier = {
71
- 1: conflicts.filter((conflict) => conflict.tier === 1),
72
- 2: conflicts.filter((conflict) => conflict.tier === 2),
73
- 3: conflicts.filter((conflict) => conflict.tier === 3),
74
- };
75
- const highest = conflicts.length > 0
76
- ? conflicts.reduce((max, conflict) => (conflict.tier > max ? conflict.tier : max), 1)
77
- : 1;
78
- return {
79
- conflicts,
80
- byTier,
81
- highest,
82
- shouldBlock: highest === 3,
83
- };
84
- }
85
- function buildBranchConflicts(report) {
86
- const branchOrder = inferBranchOrder(report);
87
- const branchOrderIndex = new Map(branchOrder.map((branch, index) => [branch, index]));
88
- const grouped = new Map();
89
- for (const conflict of report.conflicts) {
90
- for (const branch of conflict.branches) {
91
- let entry = grouped.get(branch.name);
92
- if (!entry) {
93
- entry = {
94
- branch: branch.name,
95
- files: [],
96
- tier: 1,
97
- tierInfo: TIER_LABELS[1],
98
- };
99
- grouped.set(branch.name, entry);
100
- }
101
- if (!entry.files.includes(conflict.file)) {
102
- entry.files.push(conflict.file);
103
- }
104
- if (conflict.tier > entry.tier) {
105
- entry.tier = conflict.tier;
106
- entry.tierInfo = TIER_LABELS[conflict.tier];
107
- }
108
- }
109
- }
110
- return [...grouped.values()].sort((left, right) => {
111
- if (right.tier !== left.tier) {
112
- return right.tier - left.tier;
113
- }
114
- return (branchOrderIndex.get(left.branch) ?? Number.MAX_SAFE_INTEGER)
115
- - (branchOrderIndex.get(right.branch) ?? Number.MAX_SAFE_INTEGER);
116
- });
117
- }
118
- function inferBranchOrder(report) {
119
- const adjacency = new Map();
120
- const indegree = new Map();
121
- const firstSeen = new Map();
122
- let seenIndex = 0;
123
- const ensureBranch = (branch) => {
124
- if (!adjacency.has(branch)) {
125
- adjacency.set(branch, new Set());
126
- }
127
- if (!indegree.has(branch)) {
128
- indegree.set(branch, 0);
129
- }
130
- if (!firstSeen.has(branch)) {
131
- firstSeen.set(branch, seenIndex++);
132
- }
133
- };
134
- for (const conflict of report.conflicts) {
135
- const branchNames = conflict.branches.map((branch) => branch.name);
136
- for (const branch of branchNames) {
137
- ensureBranch(branch);
138
- }
139
- for (let index = 0; index < branchNames.length; index++) {
140
- for (let nextIndex = index + 1; nextIndex < branchNames.length; nextIndex++) {
141
- const from = branchNames[index];
142
- const to = branchNames[nextIndex];
143
- const edges = adjacency.get(from);
144
- if (!edges || edges.has(to)) {
145
- continue;
146
- }
147
- edges.add(to);
148
- indegree.set(to, (indegree.get(to) ?? 0) + 1);
149
- }
150
- }
151
- }
152
- const queue = [...indegree.entries()]
153
- .filter(([, degree]) => degree === 0)
154
- .map(([branch]) => branch)
155
- .sort((left, right) => (firstSeen.get(left) ?? 0) - (firstSeen.get(right) ?? 0));
156
- const ordered = [];
157
- while (queue.length > 0) {
158
- const branch = queue.shift();
159
- if (!branch) {
160
- break;
161
- }
162
- ordered.push(branch);
163
- for (const next of adjacency.get(branch) ?? []) {
164
- const nextDegree = (indegree.get(next) ?? 0) - 1;
165
- indegree.set(next, nextDegree);
166
- if (nextDegree === 0) {
167
- queue.push(next);
168
- queue.sort((left, right) => (firstSeen.get(left) ?? 0) - (firstSeen.get(right) ?? 0));
169
- }
170
- }
171
- }
172
- if (ordered.length === indegree.size) {
173
- return ordered;
174
- }
175
- return [...firstSeen.entries()]
176
- .sort((left, right) => left[1] - right[1])
177
- .map(([branch]) => branch);
178
- }
179
- function countByTier(report) {
180
- const conflicts = buildBranchConflicts(report);
181
- return {
182
- tier1: conflicts.filter((conflict) => conflict.tier === 1).length,
183
- tier2: conflicts.filter((conflict) => conflict.tier === 2).length,
184
- tier3: conflicts.filter((conflict) => conflict.tier === 3).length,
185
- };
186
- }
187
- function createJsonReport(report, branch, blocked) {
188
- const summary = countByTier(report);
189
- return {
190
- ...report,
191
- branch,
192
- conflicts: report.conflicts.map((conflict) => ({
193
- ...conflict,
194
- files: [conflict.file],
195
- branch: conflict.branches[0]?.name,
196
- })),
197
- summary: {
198
- ...summary,
199
- blocked,
200
- },
201
- };
202
- }
203
146
  // ── Ecosystem scan ────────────────────────────────────────────────────────────
204
147
  /**
205
148
  * Default ecosystem repos. Configurable via SPIDERSAN_ECOSYSTEM env var.
@@ -280,42 +223,41 @@ function runEcosystemScan(repos, asJson) {
280
223
  const totals = results.reduce((acc, r) => ({ tier3: acc.tier3 + r.tier3, tier2: acc.tier2 + r.tier2, tier1: acc.tier1 + r.tier1 }), { tier3: 0, tier2: 0, tier1: 0 });
281
224
  const reposWithConflicts = results.filter(r => r.tier3 + r.tier2 + r.tier1 > 0).length;
282
225
  if (asJson) {
283
- writeStdout(JSON.stringify({
226
+ console.log(JSON.stringify({
284
227
  ecosystem: true,
285
228
  repos: results,
286
229
  summary: { ...totals, repos_scanned: results.length, repos_with_conflicts: reposWithConflicts },
287
230
  }, null, 2));
288
231
  return;
289
232
  }
290
- writeStdout(`
233
+ console.log(`
291
234
  šŸ•·ļø ECOSYSTEM CONFLICT SCAN
292
235
  ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
293
236
  `);
294
237
  for (const r of results) {
295
238
  if (r.skipped) {
296
- writeStdout(` ⬜ ${r.name.padEnd(22)} (not initialized)`);
239
+ console.log(` ⬜ ${r.name.padEnd(22)} (not initialized)`);
297
240
  continue;
298
241
  }
299
242
  if (r.error) {
300
- writeStdout(` ā“ ${r.name.padEnd(22)} (${r.error})`);
243
+ console.log(` ā“ ${r.name.padEnd(22)} (${r.error})`);
301
244
  continue;
302
245
  }
303
246
  const icon = r.tier3 > 0 ? 'šŸ”“' : r.tier2 > 0 ? '🟠' : r.tier1 > 0 ? '🟔' : 'āœ…';
304
247
  const counts = r.tier3 + r.tier2 + r.tier1 > 0
305
248
  ? ` T3:${r.tier3} T2:${r.tier2} T1:${r.tier1}`
306
249
  : ' clean';
307
- writeStdout(` ${icon} ${r.name.padEnd(22)}${counts}`);
250
+ console.log(` ${icon} ${r.name.padEnd(22)}${counts}`);
308
251
  }
309
- writeStdout('\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
310
- writeStdout(`šŸ“Š Total: šŸ”“ ${totals.tier3} BLOCK | 🟠 ${totals.tier2} PAUSE | 🟔 ${totals.tier1} WARN`);
311
- writeStdout(` Repos scanned: ${results.length} | With conflicts: ${reposWithConflicts}`);
252
+ console.log(`\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━`);
253
+ console.log(`šŸ“Š Total: šŸ”“ ${totals.tier3} BLOCK | 🟠 ${totals.tier2} PAUSE | 🟔 ${totals.tier1} WARN`);
254
+ console.log(` Repos scanned: ${results.length} | With conflicts: ${reposWithConflicts}`);
312
255
  }
313
256
  export const conflictsCommand = new Command('conflicts')
314
257
  .description('Detect file conflicts between branches (with tiered blocking)')
315
258
  .option('--branch <name>', 'Check conflicts for specific branch')
316
259
  .option('--pr <number>', 'Check conflicts for a GitHub pull request by number')
317
260
  .option('--json', 'Output as JSON')
318
- .option('--verbose', 'Show detailed symbol overlap information in human output')
319
261
  .option('--tier <level>', 'Filter by minimum tier (1, 2, or 3)', '1')
320
262
  .option('--strict', 'Strict mode: exit with error if TIER 2+ conflicts found')
321
263
  .option('--notify', 'Notify Hub of TIER 2+ conflicts')
@@ -338,7 +280,8 @@ export const conflictsCommand = new Command('conflicts')
338
280
  const storage = await getStorage();
339
281
  const config = await loadConfig();
340
282
  if (!await storage.isInitialized()) {
341
- fail('āŒ Spidersan not initialized. Run: spidersan init');
283
+ console.error('āŒ Spidersan not initialized. Run: spidersan init');
284
+ process.exit(1);
342
285
  }
343
286
  const minTier = parseInt(options.tier, 10);
344
287
  // Load custom patterns from config
@@ -352,13 +295,13 @@ export const conflictsCommand = new Command('conflicts')
352
295
  // --pr mode: fetch PR head branch + changed files from GitHub
353
296
  const prNumber = parseInt(options.pr, 10);
354
297
  if (isNaN(prNumber) || prNumber <= 0) {
355
- fail('āŒ Invalid PR number. Provide a positive integer, e.g. --pr 42');
298
+ console.error('āŒ Invalid PR number. Provide a positive integer, e.g. --pr 42');
299
+ process.exit(1);
356
300
  }
357
301
  if (!isGhAvailable()) {
358
- fail([
359
- 'āŒ GitHub CLI (gh) is not available or not authenticated.',
360
- ' Install: https://cli.github.com → gh auth login',
361
- ]);
302
+ console.error('āŒ GitHub CLI (gh) is not available or not authenticated.');
303
+ console.error(' Install: https://cli.github.com → gh auth login');
304
+ process.exit(1);
362
305
  }
363
306
  let prDetails;
364
307
  try {
@@ -366,47 +309,69 @@ export const conflictsCommand = new Command('conflicts')
366
309
  }
367
310
  catch (err) {
368
311
  const msg = err instanceof Error ? err.message : String(err);
369
- fail(`āŒ Failed to fetch PR #${prNumber}: ${msg}`);
312
+ console.error(`āŒ Failed to fetch PR #${prNumber}: ${msg}`);
313
+ process.exit(1);
370
314
  }
371
315
  targetBranch = prDetails.headBranch;
372
316
  targetFiles = prDetails.files;
373
- writeStdout(`šŸ•·ļø Checking conflicts for PR #${prNumber}: "${prDetails.title}"`);
374
- writeStdout(` Branch: ${targetBranch} (${targetFiles.length} file(s) changed)`);
317
+ console.log(`šŸ•·ļø Checking conflicts for PR #${prNumber}: "${prDetails.title}"`);
318
+ console.log(` Branch: ${targetBranch} (${targetFiles.length} file(s) changed)`);
375
319
  }
376
320
  else {
377
321
  // Normal --branch or current branch mode
378
322
  targetBranch = options.branch || getCurrentBranch();
379
323
  const target = await storage.get(targetBranch);
380
324
  if (!target) {
381
- fail([
382
- `āŒ Branch "${targetBranch}" is not registered.`,
383
- ' Run: spidersan register --files "..."',
384
- ]);
325
+ console.error(`āŒ Branch "${targetBranch}" is not registered.`);
326
+ console.error(' Run: spidersan register --files "..."');
327
+ process.exit(1);
385
328
  }
386
329
  targetFiles = target.files;
387
330
  }
388
331
  const allBranches = await storage.list();
389
- const analysisBranches = allBranches
390
- .filter(branch => branch.name !== targetBranch && branch.status === 'active')
391
- .map(branch => ({
392
- ...branch,
393
- files: branch.files.filter(file => !isExcludedPath(file)),
394
- }));
395
- const rawReport = analyzeConflicts({
396
- branches: analysisBranches,
397
- targetFiles: targetFiles.filter(file => !isExcludedPath(file)),
398
- options: {
399
- extraTier3: compiledHigh,
400
- extraTier2: compiledMedium,
401
- useSymbolAware: false,
402
- },
403
- });
404
- const report = filterConflictReport(rawReport, minTier);
405
- const conflicts = buildBranchConflicts(report);
332
+ const conflicts = [];
333
+ // Performance Optimization: Convert target files to Set for O(1) lookup
334
+ // Reduces complexity from O(N*M*K) to O(N*M) where N=branches, M=files/branch, K=target_files
335
+ // Filter excluded paths (node_modules/, dist/, etc.) at query time to handle stale registrations
336
+ const targetFilesSet = new Set(targetFiles.filter(f => !isExcludedPath(f)));
337
+ for (const branch of allBranches) {
338
+ if (branch.name === targetBranch || branch.status !== 'active')
339
+ continue;
340
+ // Performance Optimization: Replace .filter() with a standard loop to avoid array allocation.
341
+ // Short-circuit using O(1) Set lookup (targetFilesSet.has) before calling the expensive isExcludedPath.
342
+ const overlappingFiles = [];
343
+ for (let i = 0; i < branch.files.length; i++) {
344
+ const f = branch.files[i];
345
+ if (targetFilesSet.has(f) && !isExcludedPath(f)) {
346
+ overlappingFiles.push(f);
347
+ }
348
+ }
349
+ if (overlappingFiles.length > 0) {
350
+ // Get highest tier for this conflict
351
+ let maxTier = { tier: 1, label: 'WARN', icon: '🟔', action: '' };
352
+ for (const file of overlappingFiles) {
353
+ const fileTier = classifyWithLabel(file, {
354
+ extraTier3: compiledHigh,
355
+ extraTier2: compiledMedium,
356
+ });
357
+ if (fileTier.tier > maxTier.tier) {
358
+ maxTier = fileTier;
359
+ }
360
+ }
361
+ if (maxTier.tier >= minTier) {
362
+ conflicts.push({
363
+ branch: branch.name,
364
+ files: overlappingFiles,
365
+ tier: maxTier.tier,
366
+ tierInfo: maxTier
367
+ });
368
+ }
369
+ }
370
+ }
406
371
  // SEMANTIC ANALYSIS with AST parser
407
372
  const semanticConflicts = [];
408
373
  if (options.semantic && conflicts.length > 0) {
409
- writeStdout('\nšŸ”¬ Running semantic (AST) analysis...');
374
+ console.log('\nšŸ”¬ Running semantic (AST) analysis...');
410
375
  const astParser = new ASTParser();
411
376
  for (const conflict of conflicts) {
412
377
  for (const file of conflict.files) {
@@ -414,11 +379,10 @@ export const conflictsCommand = new Command('conflicts')
414
379
  if (!/\.(ts|js|tsx|jsx)$/.test(file))
415
380
  continue;
416
381
  try {
417
- const currentContent = getFileAtRef('HEAD', file);
418
- const otherContent = getFileAtRef(conflict.branch, file);
419
- if (currentContent === null || otherContent === null) {
420
- continue;
421
- }
382
+ // Get file content from both branches
383
+ // Security: Use execFileSync to prevent command injection via file names
384
+ const currentContent = execFileSync('git', ['show', `HEAD:${file}`], { encoding: 'utf-8' });
385
+ const otherContent = execFileSync('git', ['show', `${conflict.branch}:${file}`], { encoding: 'utf-8' });
422
386
  const symbolConflicts = astParser.findSymbolConflicts(currentContent, `${targetBranch}:${file}`, otherContent, `${conflict.branch}:${file}`);
423
387
  semanticConflicts.push(...symbolConflicts);
424
388
  }
@@ -428,73 +392,104 @@ export const conflictsCommand = new Command('conflicts')
428
392
  }
429
393
  }
430
394
  }
395
+ // Sort by tier (highest first)
396
+ conflicts.sort((a, b) => b.tier - a.tier);
397
+ let tier3Count = 0;
398
+ let tier2Count = 0;
399
+ let tier1Count = 0;
400
+ for (const c of conflicts) {
401
+ if (c.tier === 3)
402
+ tier3Count++;
403
+ else if (c.tier === 2)
404
+ tier2Count++;
405
+ else if (c.tier === 1)
406
+ tier1Count++;
407
+ }
431
408
  // Log conflict detection to activity log
432
409
  if (conflicts.length > 0) {
433
- try {
434
- const summary = countByTier(report);
435
- logActivity({
436
- event: 'conflict_detected',
437
- branch: targetBranch,
438
- details: {
439
- tier3: summary.tier3,
440
- tier2: summary.tier2,
441
- tier1: summary.tier1,
442
- conflicting_branches: conflicts.map(c => c.branch),
443
- }
444
- });
445
- }
446
- catch {
447
- // Telemetry should never block conflict detection.
448
- }
410
+ logActivity({
411
+ event: 'conflict_detected',
412
+ branch: targetBranch,
413
+ details: {
414
+ tier3: tier3Count,
415
+ tier2: tier2Count,
416
+ tier1: tier1Count,
417
+ conflicting_branches: conflicts.map(c => c.branch),
418
+ }
419
+ });
449
420
  }
450
421
  // Check for blocking conditions
451
- const { tier2: tier2Count, tier3: tier3Count } = countByTier(report);
452
422
  const shouldBlock = options.strict && (tier3Count > 0 || tier2Count > 0);
453
423
  // Notify Hub if requested
454
424
  if (options.notify && (tier3Count > 0 || tier2Count > 0)) {
455
- await hub.notifyConflict(targetBranch, conflicts);
425
+ await notifyHub(targetBranch, conflicts);
456
426
  }
457
427
  if (options.json) {
458
- console.log(JSON.stringify(createJsonReport(report, targetBranch, shouldBlock), null, 2));
428
+ console.log(JSON.stringify({
429
+ branch: targetBranch,
430
+ conflicts,
431
+ summary: {
432
+ tier3: tier3Count,
433
+ tier2: tier2Count,
434
+ tier1: tier1Count,
435
+ blocked: shouldBlock
436
+ }
437
+ }, null, 2));
459
438
  if (shouldBlock)
460
439
  process.exit(1);
461
440
  return;
462
441
  }
463
- const renderedReport = renderConflictReport(report, { verbose: !!options.verbose });
464
- const humanOutput = conflicts.length === 0
465
- ? renderedReport.replace('šŸ•·ļø No conflicts detected', `šŸ•·ļø No conflicts detected for "${targetBranch}"`)
466
- : `
442
+ if (conflicts.length === 0) {
443
+ console.log(`šŸ•·ļø No conflicts detected for "${targetBranch}"`);
444
+ console.log(' āœ… You\'re good to merge!');
445
+ return;
446
+ }
447
+ console.log(`
467
448
  šŸ•·ļø CONFLICT ANALYSIS: "${targetBranch}"
468
449
  ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
469
-
470
- ${renderedReport}`;
471
- console.log(humanOutput);
450
+ `);
451
+ for (const conflict of conflicts) {
452
+ console.log(`${conflict.tierInfo.icon} TIER ${conflict.tier} (${conflict.tierInfo.label}): ${conflict.branch}`);
453
+ for (const file of conflict.files) {
454
+ const fileTier = classifyWithLabel(file, {
455
+ extraTier3: compiledHigh,
456
+ extraTier2: compiledMedium,
457
+ });
458
+ console.log(` ${fileTier.icon} ${file}`);
459
+ }
460
+ console.log(` → ${conflict.tierInfo.action}`);
461
+ console.log('');
462
+ }
463
+ console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
464
+ console.log(`šŸ“Š Summary: šŸ”“ ${tier3Count} BLOCK | 🟠 ${tier2Count} PAUSE | 🟔 ${tier1Count} WARN`);
465
+ // Offer add/add resolution suggestions for likely add/add files
466
+ suggestAddAddResolution(conflicts);
472
467
  // Show semantic analysis results
473
468
  if (options.semantic && semanticConflicts.length > 0) {
474
- writeStdout(`\nšŸ”¬ SEMANTIC CONFLICTS DETECTED (${semanticConflicts.length} symbols):`);
475
- writeStdout('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
469
+ console.log(`\nšŸ”¬ SEMANTIC CONFLICTS DETECTED (${semanticConflicts.length} symbols):`);
470
+ console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
476
471
  for (const sc of semanticConflicts) {
477
- writeStdout(` ⚔ ${sc.symbolType} '${sc.symbolName}'`);
478
- writeStdout(' Modified in BOTH branches (different content)');
472
+ console.log(` ⚔ ${sc.symbolType} '${sc.symbolName}'`);
473
+ console.log(` Modified in BOTH branches (different content)`);
479
474
  }
480
- writeStdout('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
481
- writeStdout('\nšŸ’” TIP: Coordinate on these specific functions/classes,');
482
- writeStdout(' not just the files. One of you should rebase.');
475
+ console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
476
+ console.log('\nšŸ’” TIP: Coordinate on these specific functions/classes,');
477
+ console.log(' not just the files. One of you should rebase.');
483
478
  }
484
479
  else if (options.semantic && semanticConflicts.length === 0) {
485
- writeStdout('\nšŸ”¬ SEMANTIC ANALYSIS: No symbol-level conflicts!');
486
- writeStdout(' Files overlap, but different functions were modified.');
487
- writeStdout(' āœ… Likely safe to merge (git will auto-merge).');
480
+ console.log('\nšŸ”¬ SEMANTIC ANALYSIS: No symbol-level conflicts!');
481
+ console.log(' Files overlap, but different functions were modified.');
482
+ console.log(' āœ… Likely safe to merge (git will auto-merge).');
488
483
  }
489
484
  if (tier3Count > 0) {
490
- writeStdout(`
485
+ console.log(`
491
486
  šŸ”“ TIER 3 CONFLICTS FOUND
492
487
  These files are security-critical and MUST be resolved.
493
488
  Merge is BLOCKED until conflicts are cleared.
494
489
  `);
495
490
  }
496
491
  else if (tier2Count > 0) {
497
- writeStdout(`
492
+ console.log(`
498
493
  🟠 TIER 2 CONFLICTS FOUND
499
494
  Coordinate with the other agent before proceeding.
500
495
  `);
@@ -514,39 +509,33 @@ ${renderedReport}`;
514
509
  }
515
510
  }
516
511
  if (agentsToWake.size === 0) {
517
- writeStdout('\nāš ļø No agents registered on conflicting branches.');
512
+ console.log('\nāš ļø No agents registered on conflicting branches.');
518
513
  }
519
514
  else {
520
515
  // Show what will happen and ask for confirmation
521
- writeStdout('\nšŸ”” WAKE AGENTS?');
522
- writeStdout('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
516
+ console.log('\nšŸ”” WAKE AGENTS?');
517
+ console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
523
518
  for (const [agent, info] of agentsToWake) {
524
- writeStdout(` • ${agent} (${info.branch})`);
519
+ console.log(` • ${agent} (${info.branch})`);
525
520
  }
526
- writeStdout('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
527
- writeStdout('This will:');
528
- writeStdout(' 1. Send wake signal via Hub\n');
521
+ console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
522
+ console.log('This will:');
523
+ console.log(' 1. Send wake signal via Hub\n');
529
524
  const confirmed = await confirmAction('Wake these agents?', options.auto);
530
525
  if (confirmed) {
531
- writeStdout('\nšŸ”” WAKING AGENTS...\n');
526
+ console.log('\nšŸ”” WAKING AGENTS...\n');
532
527
  for (const [agentId, info] of agentsToWake) {
533
- const woke = await hub.wakeAgent(agentId, `Conflict on ${info.branch} - need resolution`);
534
- if (woke) {
535
- writeStdout(` šŸ”” Wake signal sent to ${agentId}`);
536
- }
537
- else {
538
- writeStdout(` āš ļø Could not wake ${agentId} via Hub`);
539
- }
528
+ await wakeConflictingAgent(agentId, targetBranch, info.branch, info.files);
540
529
  }
541
- writeStdout(`\nāœ… Woke ${agentsToWake.size} agent(s)`);
530
+ console.log(`\nāœ… Woke ${agentsToWake.size} agent(s)`);
542
531
  // If --retry is set, ask before waiting
543
532
  if (options.retry) {
544
533
  const waitSeconds = parseInt(options.retry, 10);
545
534
  const retryConfirmed = await confirmAction(`\nWait ${waitSeconds}s and re-check conflicts?`, options.auto);
546
535
  if (retryConfirmed) {
547
- writeStdout(`\nā³ Waiting ${waitSeconds}s for agents to resolve conflicts...`);
536
+ console.log(`\nā³ Waiting ${waitSeconds}s for agents to resolve conflicts...`);
548
537
  await sleep(waitSeconds * 1000);
549
- writeStdout('\nšŸ”„ RE-CHECKING CONFLICTS...\n');
538
+ console.log('\nšŸ”„ RE-CHECKING CONFLICTS...\n');
550
539
  const { execFileSync } = await import('child_process');
551
540
  try {
552
541
  const { getCLIPath } = await import('../lib/security.js');
@@ -560,21 +549,21 @@ ${renderedReport}`;
560
549
  return;
561
550
  }
562
551
  catch {
563
- writeStdout('āŒ Conflicts still exist after retry.');
552
+ console.log('āŒ Conflicts still exist after retry.');
564
553
  }
565
554
  }
566
555
  else {
567
- writeStdout('ā­ļø Skipping retry.');
556
+ console.log('ā­ļø Skipping retry.');
568
557
  }
569
558
  }
570
559
  }
571
560
  else {
572
- writeStdout('ā­ļø Skipping wake.');
561
+ console.log('ā­ļø Skipping wake.');
573
562
  }
574
563
  }
575
564
  }
576
565
  if (shouldBlock) {
577
- writeStdout('āŒ Strict mode: Exiting with error due to TIER 2+ conflicts.');
566
+ console.log('āŒ Strict mode: Exiting with error due to TIER 2+ conflicts.');
578
567
  process.exit(1);
579
568
  }
580
569
  });