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.
- package/CHANGELOG.md +24 -0
- package/dist/commands/bot.d.ts.map +1 -1
- package/dist/commands/bot.js +32 -14
- package/dist/commands/bot.js.map +1 -1
- package/dist/commands/conflicts.d.ts.map +1 -1
- package/dist/commands/conflicts.js +247 -258
- package/dist/commands/conflicts.js.map +1 -1
- package/dist/commands/cross-conflicts.d.ts.map +1 -1
- package/dist/commands/cross-conflicts.js +9 -3
- package/dist/commands/cross-conflicts.js.map +1 -1
- package/dist/commands/pulse.d.ts.map +1 -1
- package/dist/commands/pulse.js +41 -112
- package/dist/commands/pulse.js.map +1 -1
- package/dist/commands/register.d.ts.map +1 -1
- package/dist/commands/register.js +15 -8
- package/dist/commands/register.js.map +1 -1
- package/dist/commands/watch.d.ts +6 -0
- package/dist/commands/watch.d.ts.map +1 -1
- package/dist/commands/watch.js +109 -1
- package/dist/commands/watch.js.map +1 -1
- package/dist/lib/pulse-renderer.d.ts +19 -0
- package/dist/lib/pulse-renderer.d.ts.map +1 -0
- package/dist/lib/pulse-renderer.js +108 -0
- package/dist/lib/pulse-renderer.js.map +1 -0
- package/dist/lib/register-renderer.d.ts +10 -0
- package/dist/lib/register-renderer.d.ts.map +1 -0
- package/dist/lib/register-renderer.js +19 -0
- package/dist/lib/register-renderer.js.map +1 -0
- package/dist/lib/remote-drift.d.ts +1 -0
- package/dist/lib/remote-drift.d.ts.map +1 -1
- package/dist/lib/remote-drift.js +3 -1
- package/dist/lib/remote-drift.js.map +1 -1
- package/dist/lib/watch-renderer.d.ts +13 -0
- package/dist/lib/watch-renderer.d.ts.map +1 -0
- package/dist/lib/watch-renderer.js +35 -0
- package/dist/lib/watch-renderer.js.map +1 -0
- 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 {
|
|
26
|
-
|
|
27
|
-
const
|
|
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
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
const
|
|
42
|
-
|
|
43
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
239
|
+
console.log(` ⬠${r.name.padEnd(22)} (not initialized)`);
|
|
297
240
|
continue;
|
|
298
241
|
}
|
|
299
242
|
if (r.error) {
|
|
300
|
-
|
|
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
|
-
|
|
250
|
+
console.log(` ${icon} ${r.name.padEnd(22)}${counts}`);
|
|
308
251
|
}
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
359
|
-
|
|
360
|
-
|
|
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
|
-
|
|
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
|
-
|
|
374
|
-
|
|
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
|
-
|
|
382
|
-
|
|
383
|
-
|
|
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
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
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
|
-
|
|
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
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
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
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
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
|
|
425
|
+
await notifyHub(targetBranch, conflicts);
|
|
456
426
|
}
|
|
457
427
|
if (options.json) {
|
|
458
|
-
console.log(JSON.stringify(
|
|
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
|
-
|
|
464
|
-
|
|
465
|
-
|
|
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
|
-
|
|
471
|
-
|
|
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
|
-
|
|
475
|
-
|
|
469
|
+
console.log(`\nš¬ SEMANTIC CONFLICTS DETECTED (${semanticConflicts.length} symbols):`);
|
|
470
|
+
console.log('āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā');
|
|
476
471
|
for (const sc of semanticConflicts) {
|
|
477
|
-
|
|
478
|
-
|
|
472
|
+
console.log(` ā” ${sc.symbolType} '${sc.symbolName}'`);
|
|
473
|
+
console.log(` Modified in BOTH branches (different content)`);
|
|
479
474
|
}
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
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
|
-
|
|
486
|
-
|
|
487
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
522
|
-
|
|
516
|
+
console.log('\nš WAKE AGENTS?');
|
|
517
|
+
console.log('āāāāāāāāāāāāāāāāāāāāāāāāāāāāāā');
|
|
523
518
|
for (const [agent, info] of agentsToWake) {
|
|
524
|
-
|
|
519
|
+
console.log(` ⢠${agent} (${info.branch})`);
|
|
525
520
|
}
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
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
|
-
|
|
526
|
+
console.log('\nš WAKING AGENTS...\n');
|
|
532
527
|
for (const [agentId, info] of agentsToWake) {
|
|
533
|
-
|
|
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
|
-
|
|
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
|
-
|
|
536
|
+
console.log(`\nā³ Waiting ${waitSeconds}s for agents to resolve conflicts...`);
|
|
548
537
|
await sleep(waitSeconds * 1000);
|
|
549
|
-
|
|
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
|
-
|
|
552
|
+
console.log('ā Conflicts still exist after retry.');
|
|
564
553
|
}
|
|
565
554
|
}
|
|
566
555
|
else {
|
|
567
|
-
|
|
556
|
+
console.log('āļø Skipping retry.');
|
|
568
557
|
}
|
|
569
558
|
}
|
|
570
559
|
}
|
|
571
560
|
else {
|
|
572
|
-
|
|
561
|
+
console.log('āļø Skipping wake.');
|
|
573
562
|
}
|
|
574
563
|
}
|
|
575
564
|
}
|
|
576
565
|
if (shouldBlock) {
|
|
577
|
-
|
|
566
|
+
console.log('ā Strict mode: Exiting with error due to TIER 2+ conflicts.');
|
|
578
567
|
process.exit(1);
|
|
579
568
|
}
|
|
580
569
|
});
|