spidersan 0.5.0 → 0.9.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 +46 -48
- package/README.md +241 -4
- package/dist/bin/spidersan.d.ts.map +1 -1
- package/dist/bin/spidersan.js +13 -1
- package/dist/bin/spidersan.js.map +1 -1
- package/dist/commands/abandon.d.ts.map +1 -1
- package/dist/commands/abandon.js +1 -9
- package/dist/commands/abandon.js.map +1 -1
- package/dist/commands/ai.d.ts +15 -0
- package/dist/commands/ai.d.ts.map +1 -0
- package/dist/commands/ai.js +498 -0
- package/dist/commands/ai.js.map +1 -0
- package/dist/commands/auto.d.ts.map +1 -1
- package/dist/commands/auto.js +2 -1
- package/dist/commands/auto.js.map +1 -1
- package/dist/commands/bot.d.ts +16 -0
- package/dist/commands/bot.d.ts.map +1 -0
- package/dist/commands/bot.js +398 -0
- package/dist/commands/bot.js.map +1 -0
- package/dist/commands/conflicts.d.ts.map +1 -1
- package/dist/commands/conflicts.js +264 -313
- package/dist/commands/conflicts.js.map +1 -1
- package/dist/commands/context.d.ts +8 -0
- package/dist/commands/context.d.ts.map +1 -0
- package/dist/commands/context.js +104 -0
- package/dist/commands/context.js.map +1 -0
- package/dist/commands/cross-conflicts.d.ts.map +1 -1
- package/dist/commands/cross-conflicts.js +1 -38
- package/dist/commands/cross-conflicts.js.map +1 -1
- package/dist/commands/depends.d.ts.map +1 -1
- package/dist/commands/depends.js +1 -9
- package/dist/commands/depends.js.map +1 -1
- package/dist/commands/doctor.d.ts.map +1 -1
- package/dist/commands/doctor.js +117 -59
- package/dist/commands/doctor.js.map +1 -1
- package/dist/commands/fleet-status.d.ts +14 -0
- package/dist/commands/fleet-status.d.ts.map +1 -0
- package/dist/commands/fleet-status.js +127 -0
- package/dist/commands/fleet-status.js.map +1 -0
- package/dist/commands/git-watch.d.ts +24 -0
- package/dist/commands/git-watch.d.ts.map +1 -0
- package/dist/commands/git-watch.js +84 -0
- package/dist/commands/git-watch.js.map +1 -0
- package/dist/commands/index.d.ts +5 -0
- package/dist/commands/index.d.ts.map +1 -1
- package/dist/commands/index.js +7 -0
- package/dist/commands/index.js.map +1 -1
- package/dist/commands/merge-order.d.ts.map +1 -1
- package/dist/commands/merge-order.js +18 -67
- package/dist/commands/merge-order.js.map +1 -1
- package/dist/commands/merged.d.ts.map +1 -1
- package/dist/commands/merged.js +1 -9
- package/dist/commands/merged.js.map +1 -1
- package/dist/commands/pulse.d.ts.map +1 -1
- package/dist/commands/pulse.js +137 -74
- package/dist/commands/pulse.js.map +1 -1
- package/dist/commands/queen.d.ts.map +1 -1
- package/dist/commands/queen.js +36 -19
- package/dist/commands/queen.js.map +1 -1
- package/dist/commands/ready-check.d.ts +2 -1
- package/dist/commands/ready-check.d.ts.map +1 -1
- package/dist/commands/ready-check.js +6 -30
- package/dist/commands/ready-check.js.map +1 -1
- package/dist/commands/register.d.ts.map +1 -1
- package/dist/commands/register.js +7 -29
- package/dist/commands/register.js.map +1 -1
- package/dist/commands/stale.d.ts +1 -9
- package/dist/commands/stale.d.ts.map +1 -1
- package/dist/commands/stale.js +1 -42
- package/dist/commands/stale.js.map +1 -1
- package/dist/commands/torrent.d.ts.map +1 -1
- package/dist/commands/torrent.js +29 -18
- package/dist/commands/torrent.js.map +1 -1
- package/dist/commands/watch.d.ts.map +1 -1
- package/dist/commands/watch.js +13 -34
- package/dist/commands/watch.js.map +1 -1
- package/dist/lib/agent-errors.js +2 -2
- package/dist/lib/agent-errors.js.map +1 -1
- package/dist/lib/ai/context-builder.d.ts +16 -0
- package/dist/lib/ai/context-builder.d.ts.map +1 -0
- package/dist/lib/ai/context-builder.js +216 -0
- package/dist/lib/ai/context-builder.js.map +1 -0
- package/dist/lib/ai/event-handler.d.ts +21 -0
- package/dist/lib/ai/event-handler.d.ts.map +1 -0
- package/dist/lib/ai/event-handler.js +98 -0
- package/dist/lib/ai/event-handler.js.map +1 -0
- package/dist/lib/ai/index.d.ts +13 -0
- package/dist/lib/ai/index.d.ts.map +1 -0
- package/dist/lib/ai/index.js +11 -0
- package/dist/lib/ai/index.js.map +1 -0
- package/dist/lib/ai/llm-client.d.ts +37 -0
- package/dist/lib/ai/llm-client.d.ts.map +1 -0
- package/dist/lib/ai/llm-client.js +225 -0
- package/dist/lib/ai/llm-client.js.map +1 -0
- package/dist/lib/ai/reasoner.d.ts +11 -0
- package/dist/lib/ai/reasoner.d.ts.map +1 -0
- package/dist/lib/ai/reasoner.js +246 -0
- package/dist/lib/ai/reasoner.js.map +1 -0
- package/dist/lib/ai/setup.d.ts +40 -0
- package/dist/lib/ai/setup.d.ts.map +1 -0
- package/dist/lib/ai/setup.js +154 -0
- package/dist/lib/ai/setup.js.map +1 -0
- package/dist/lib/ai/types.d.ts +135 -0
- package/dist/lib/ai/types.d.ts.map +1 -0
- package/dist/lib/ai/types.js +39 -0
- package/dist/lib/ai/types.js.map +1 -0
- package/dist/lib/colony-subscriber.d.ts +15 -12
- package/dist/lib/colony-subscriber.d.ts.map +1 -1
- package/dist/lib/colony-subscriber.js +146 -65
- package/dist/lib/colony-subscriber.js.map +1 -1
- package/dist/lib/config.d.ts.map +1 -1
- package/dist/lib/config.js +18 -4
- package/dist/lib/config.js.map +1 -1
- package/dist/lib/conflict-analyzer.d.ts +33 -0
- package/dist/lib/conflict-analyzer.d.ts.map +1 -0
- package/dist/lib/conflict-analyzer.js +114 -0
- package/dist/lib/conflict-analyzer.js.map +1 -0
- package/dist/lib/conflict-renderer.d.ts +7 -0
- package/dist/lib/conflict-renderer.d.ts.map +1 -0
- package/dist/lib/conflict-renderer.js +162 -0
- package/dist/lib/conflict-renderer.js.map +1 -0
- package/dist/lib/conflict-tier.d.ts +20 -0
- package/dist/lib/conflict-tier.d.ts.map +1 -0
- package/dist/lib/conflict-tier.js +49 -0
- package/dist/lib/conflict-tier.js.map +1 -0
- package/dist/lib/crypto.js +1 -1
- package/dist/lib/crypto.js.map +1 -1
- package/dist/lib/daily-bridge.d.ts.map +1 -1
- package/dist/lib/daily-bridge.js +3 -2
- package/dist/lib/daily-bridge.js.map +1 -1
- package/dist/lib/git-events-subscriber.d.ts +59 -0
- package/dist/lib/git-events-subscriber.d.ts.map +1 -0
- package/dist/lib/git-events-subscriber.js +779 -0
- package/dist/lib/git-events-subscriber.js.map +1 -0
- package/dist/lib/git.d.ts +15 -0
- package/dist/lib/git.d.ts.map +1 -0
- package/dist/lib/git.js +180 -0
- package/dist/lib/git.js.map +1 -0
- package/dist/lib/github.d.ts.map +1 -1
- package/dist/lib/github.js +14 -9
- package/dist/lib/github.js.map +1 -1
- package/dist/lib/graph.d.ts +23 -0
- package/dist/lib/graph.d.ts.map +1 -0
- package/dist/lib/graph.js +134 -0
- package/dist/lib/graph.js.map +1 -0
- package/dist/lib/hub.d.ts +31 -0
- package/dist/lib/hub.d.ts.map +1 -0
- package/dist/lib/hub.js +92 -0
- package/dist/lib/hub.js.map +1 -0
- package/dist/lib/regex-utils.d.ts +6 -0
- package/dist/lib/regex-utils.d.ts.map +1 -0
- package/dist/lib/regex-utils.js +24 -0
- package/dist/lib/regex-utils.js.map +1 -0
- package/dist/lib/remote-drift.d.ts +60 -0
- package/dist/lib/remote-drift.d.ts.map +1 -0
- package/dist/lib/remote-drift.js +225 -0
- package/dist/lib/remote-drift.js.map +1 -0
- package/dist/lib/repo-scanner.d.ts +0 -4
- package/dist/lib/repo-scanner.d.ts.map +1 -1
- package/dist/lib/repo-scanner.js +2 -1
- package/dist/lib/repo-scanner.js.map +1 -1
- package/dist/lib/salvage-analyzer.d.ts.map +1 -1
- package/dist/lib/salvage-analyzer.js +2 -3
- package/dist/lib/salvage-analyzer.js.map +1 -1
- package/dist/lib/security.d.ts +17 -0
- package/dist/lib/security.d.ts.map +1 -1
- package/dist/lib/security.js +34 -1
- package/dist/lib/security.js.map +1 -1
- package/dist/lib/session-logger.d.ts +54 -0
- package/dist/lib/session-logger.d.ts.map +1 -0
- package/dist/lib/session-logger.js +136 -0
- package/dist/lib/session-logger.js.map +1 -0
- package/dist/storage/adapter.d.ts +4 -0
- package/dist/storage/adapter.d.ts.map +1 -1
- package/dist/storage/branch-registry-store.d.ts +13 -0
- package/dist/storage/branch-registry-store.d.ts.map +1 -0
- package/dist/storage/branch-registry-store.js +2 -0
- package/dist/storage/branch-registry-store.js.map +1 -0
- package/dist/storage/factory.d.ts +4 -0
- package/dist/storage/factory.d.ts.map +1 -1
- package/dist/storage/factory.js +25 -9
- package/dist/storage/factory.js.map +1 -1
- package/dist/storage/index.d.ts +5 -5
- package/dist/storage/index.d.ts.map +1 -1
- package/dist/storage/index.js +5 -6
- package/dist/storage/index.js.map +1 -1
- package/dist/storage/json-branch-registry-store.d.ts +19 -0
- package/dist/storage/json-branch-registry-store.d.ts.map +1 -0
- package/dist/storage/json-branch-registry-store.js +112 -0
- package/dist/storage/json-branch-registry-store.js.map +1 -0
- package/dist/storage/local.d.ts +2 -6
- package/dist/storage/local.d.ts.map +1 -1
- package/dist/storage/local.js +13 -76
- package/dist/storage/local.js.map +1 -1
- package/dist/storage/memory-branch-registry-store.d.ts +16 -0
- package/dist/storage/memory-branch-registry-store.d.ts.map +1 -0
- package/dist/storage/memory-branch-registry-store.js +65 -0
- package/dist/storage/memory-branch-registry-store.js.map +1 -0
- package/dist/storage/mycmail-adapter.d.ts.map +1 -1
- package/dist/storage/mycmail-adapter.js +4 -6
- package/dist/storage/mycmail-adapter.js.map +1 -1
- package/dist/storage/supabase-registry-sync-client-impl.d.ts +46 -0
- package/dist/storage/supabase-registry-sync-client-impl.d.ts.map +1 -0
- package/dist/storage/supabase-registry-sync-client-impl.js +322 -0
- package/dist/storage/supabase-registry-sync-client-impl.js.map +1 -0
- package/dist/storage/supabase-registry-sync-client.d.ts +9 -0
- package/dist/storage/supabase-registry-sync-client.d.ts.map +1 -0
- package/dist/storage/supabase-registry-sync-client.js +2 -0
- package/dist/storage/supabase-registry-sync-client.js.map +1 -0
- package/dist/storage/supabase.d.ts +8 -46
- package/dist/storage/supabase.d.ts.map +1 -1
- package/dist/storage/supabase.js +30 -342
- package/dist/storage/supabase.js.map +1 -1
- package/dist/tui/screen.d.ts.map +1 -1
- package/dist/tui/screen.js +5 -3
- package/dist/tui/screen.js.map +1 -1
- package/package.json +1 -1
|
@@ -8,201 +8,39 @@
|
|
|
8
8
|
* TIER 3: Block (exit with error, prevent merge)
|
|
9
9
|
*/
|
|
10
10
|
import { Command } from 'commander';
|
|
11
|
-
import {
|
|
11
|
+
import { compilePatterns } from '../lib/regex-utils.js';
|
|
12
|
+
import { spawnSync } from 'child_process';
|
|
12
13
|
import { basename } from 'path';
|
|
13
14
|
import { homedir } from 'os';
|
|
14
15
|
import { getStorage } from '../storage/index.js';
|
|
15
16
|
import { ASTParser } from '../lib/ast.js';
|
|
16
|
-
import {
|
|
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
|
+
import { validateBranchName } from '../lib/security.js';
|
|
17
21
|
import { isExcludedPath } from './register.js';
|
|
18
22
|
import { loadConfig } from '../lib/config.js';
|
|
19
23
|
import { logActivity } from '../lib/activity.js';
|
|
20
24
|
import { isGhAvailable, getPRDetails } from '../lib/github.js';
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
const TIER_3_PATTERNS = [
|
|
25
|
-
/\.env$/,
|
|
26
|
-
/secrets?\./i,
|
|
27
|
-
/credentials/i,
|
|
28
|
-
/password/i,
|
|
29
|
-
/api[_-]?key/i,
|
|
30
|
-
/private[_-]?key/i,
|
|
31
|
-
/\.pem$/,
|
|
32
|
-
/auth\.(ts|js)$/,
|
|
33
|
-
/security\.(ts|js)$/,
|
|
34
|
-
];
|
|
35
|
-
// Important files that trigger TIER 2 pause
|
|
36
|
-
const TIER_2_PATTERNS = [
|
|
37
|
-
/package\.json$/,
|
|
38
|
-
/package-lock\.json$/,
|
|
39
|
-
/tsconfig\.json$/,
|
|
40
|
-
/CLAUDE\.md$/,
|
|
41
|
-
/\.gitignore$/,
|
|
42
|
-
/server\.(ts|js)$/,
|
|
43
|
-
/index\.(ts|js)$/,
|
|
44
|
-
/config\.(ts|js)$/,
|
|
45
|
-
];
|
|
46
|
-
function getConflictTier(file, highSeverityPatterns = [], mediumSeverityPatterns = []) {
|
|
47
|
-
// Check TIER 3 first (most critical)
|
|
48
|
-
const tier3 = [...TIER_3_PATTERNS, ...highSeverityPatterns];
|
|
49
|
-
for (const pattern of tier3) {
|
|
50
|
-
if (pattern.test(file)) {
|
|
51
|
-
return {
|
|
52
|
-
tier: 3,
|
|
53
|
-
label: 'BLOCK',
|
|
54
|
-
icon: '🔴',
|
|
55
|
-
action: 'Merge blocked. Resolve conflict first.'
|
|
56
|
-
};
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
// Check TIER 2
|
|
60
|
-
const tier2 = [...TIER_2_PATTERNS, ...mediumSeverityPatterns];
|
|
61
|
-
for (const pattern of tier2) {
|
|
62
|
-
if (pattern.test(file)) {
|
|
63
|
-
return {
|
|
64
|
-
tier: 2,
|
|
65
|
-
label: 'PAUSE',
|
|
66
|
-
icon: '🟠',
|
|
67
|
-
action: 'Coordinate with other agent before proceeding.'
|
|
68
|
-
};
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
// Default: TIER 1
|
|
72
|
-
return {
|
|
73
|
-
tier: 1,
|
|
74
|
-
label: 'WARN',
|
|
75
|
-
icon: '🟡',
|
|
76
|
-
action: 'Consider coordinating, but safe to proceed.'
|
|
77
|
-
};
|
|
78
|
-
}
|
|
79
|
-
function getCurrentBranch() {
|
|
80
|
-
try {
|
|
81
|
-
return execFileSync('git', ['rev-parse', '--abbrev-ref', 'HEAD'], { encoding: 'utf-8' }).trim();
|
|
82
|
-
}
|
|
83
|
-
catch {
|
|
84
|
-
throw new Error('Not in a git repository');
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
async function notifyHub(branch, conflicts) {
|
|
88
|
-
const tier3 = conflicts.filter(c => c.tier === 3);
|
|
89
|
-
const tier2 = conflicts.filter(c => c.tier === 2);
|
|
90
|
-
if (tier3.length === 0 && tier2.length === 0)
|
|
91
|
-
return;
|
|
92
|
-
const severity = tier3.length > 0 ? '🔴 TIER 3 BLOCK' : '🟠 TIER 2 PAUSE';
|
|
93
|
-
const message = `🕷️⚠️ **Conflict Alert** on \`${branch}\`\n\n${severity}\n\nConflicting files require coordination.`;
|
|
94
|
-
try {
|
|
95
|
-
await fetch(`${HUB_URL}/api/chat`, {
|
|
96
|
-
method: 'POST',
|
|
97
|
-
headers: { 'Content-Type': 'application/json' },
|
|
98
|
-
body: JSON.stringify({
|
|
99
|
-
agent: 'spidersan',
|
|
100
|
-
name: 'Spidersan',
|
|
101
|
-
message,
|
|
102
|
-
glyph: '🕷️'
|
|
103
|
-
})
|
|
104
|
-
});
|
|
105
|
-
}
|
|
106
|
-
catch {
|
|
107
|
-
// Hub offline - silent fail
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
|
-
/**
|
|
111
|
-
* Wake a conflicting agent and send them a message about what to fix
|
|
112
|
-
*/
|
|
113
|
-
async function wakeConflictingAgent(agentId, myBranch, theirBranch, conflictingFiles) {
|
|
114
|
-
const fileList = conflictingFiles.slice(0, 5).join(', ');
|
|
115
|
-
const more = conflictingFiles.length > 5 ? ` (+${conflictingFiles.length - 5} more)` : '';
|
|
116
|
-
// 1. Wake the agent via Hub
|
|
117
|
-
try {
|
|
118
|
-
const wakeResponse = await fetch(`${HUB_URL}/api/wake/${agentId}`, {
|
|
119
|
-
method: 'POST',
|
|
120
|
-
headers: { 'Content-Type': 'application/json' },
|
|
121
|
-
body: JSON.stringify({
|
|
122
|
-
sender: 'spidersan',
|
|
123
|
-
reason: `Conflict on ${theirBranch} - need resolution`
|
|
124
|
-
})
|
|
125
|
-
});
|
|
126
|
-
if (wakeResponse.ok) {
|
|
127
|
-
console.log(` 🔔 Wake signal sent to ${agentId}`);
|
|
128
|
-
}
|
|
129
|
-
}
|
|
130
|
-
catch {
|
|
131
|
-
console.log(` ⚠️ Could not wake ${agentId} via Hub`);
|
|
132
|
-
}
|
|
133
|
-
// 2. Send a detailed mycmail message
|
|
134
|
-
try {
|
|
135
|
-
const { execFileSync } = await import('child_process');
|
|
136
|
-
// Security: Validate inputs before shell execution
|
|
137
|
-
const safeAgentId = validateAgentId(agentId);
|
|
138
|
-
const safeBranch = validateBranchName(theirBranch);
|
|
139
|
-
const safeMyBranch = validateBranchName(myBranch);
|
|
140
|
-
const subject = `🕷️ Conflict Alert: ${safeBranch}`;
|
|
141
|
-
const body = [
|
|
142
|
-
`Hey ${safeAgentId}!`,
|
|
143
|
-
``,
|
|
144
|
-
`Spidersan detected a conflict between our branches:`,
|
|
145
|
-
``,
|
|
146
|
-
` My branch: ${safeMyBranch}`,
|
|
147
|
-
` Your branch: ${safeBranch}`,
|
|
148
|
-
``,
|
|
149
|
-
`Conflicting files: ${fileList}${more}`,
|
|
150
|
-
``,
|
|
151
|
-
`Action needed:`,
|
|
152
|
-
` 1. Check your changes on these files`,
|
|
153
|
-
` 2. Coordinate with me or rebase`,
|
|
154
|
-
` 3. Run: spidersan conflicts --branch ${safeBranch}`,
|
|
155
|
-
``,
|
|
156
|
-
`Let's sync up! 🕷️`
|
|
157
|
-
].join('\n');
|
|
158
|
-
// Security: Use execFileSync with argument array instead of string interpolation
|
|
159
|
-
execFileSync('mycmail', ['send', safeAgentId, subject, '-m', body], {
|
|
160
|
-
encoding: 'utf-8',
|
|
161
|
-
stdio: 'pipe'
|
|
162
|
-
});
|
|
163
|
-
console.log(` 📧 Message sent to ${agentId}`);
|
|
164
|
-
return true;
|
|
165
|
-
}
|
|
166
|
-
catch {
|
|
167
|
-
console.log(` ⚠️ Could not send mycmail to ${agentId}`);
|
|
168
|
-
return false;
|
|
169
|
-
}
|
|
170
|
-
}
|
|
25
|
+
import { TIER_LABELS } from '../lib/conflict-tier.js';
|
|
26
|
+
import { createHubClient } from '../lib/hub.js';
|
|
27
|
+
const hub = createHubClient();
|
|
171
28
|
/**
|
|
172
29
|
* Wait for a specified time
|
|
173
30
|
*/
|
|
174
31
|
function sleep(ms) {
|
|
175
32
|
return new Promise(resolve => setTimeout(resolve, ms));
|
|
176
33
|
}
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
if (/\.md$/.test(f) || /docs\//.test(f)) {
|
|
188
|
-
addAddFiles.add(f);
|
|
189
|
-
}
|
|
190
|
-
}
|
|
191
|
-
}
|
|
192
|
-
if (addAddFiles.size === 0)
|
|
193
|
-
return;
|
|
194
|
-
console.log('\n💡 Add/Add Conflict Helper');
|
|
195
|
-
console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
|
|
196
|
-
console.log('Detected files added in both branches. Recommended actions:');
|
|
197
|
-
console.log(' 1. Preserve incoming variant for auditability:');
|
|
198
|
-
console.log(' git checkout --theirs <file> && mv <file> <file>.<their-branch>.incoming && git add <file>.<their-branch>.incoming');
|
|
199
|
-
console.log(' 2. Keep canonical version in main branch and include note linking the incoming variant.');
|
|
200
|
-
console.log(' 3. Commit and continue rebase: git add -A && git rebase --continue');
|
|
201
|
-
console.log('\nExamples:');
|
|
202
|
-
for (const f of addAddFiles) {
|
|
203
|
-
console.log(` • ${f} -> preserve incoming as ${f}.incoming`);
|
|
204
|
-
}
|
|
205
|
-
console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n');
|
|
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);
|
|
206
44
|
}
|
|
207
45
|
/**
|
|
208
46
|
* Prompt user for Y/N confirmation (prevents Ralph Wiggum loops)
|
|
@@ -211,7 +49,7 @@ function suggestAddAddResolution(conflicts) {
|
|
|
211
49
|
*/
|
|
212
50
|
async function confirmAction(prompt, autoConfirm = false) {
|
|
213
51
|
if (autoConfirm) {
|
|
214
|
-
|
|
52
|
+
writeStdout(`${prompt} [Y/n]: Y (auto)`);
|
|
215
53
|
return true;
|
|
216
54
|
}
|
|
217
55
|
const readline = await import('readline');
|
|
@@ -227,6 +65,141 @@ async function confirmAction(prompt, autoConfirm = false) {
|
|
|
227
65
|
});
|
|
228
66
|
});
|
|
229
67
|
}
|
|
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
|
+
}
|
|
230
203
|
// ── Ecosystem scan ────────────────────────────────────────────────────────────
|
|
231
204
|
/**
|
|
232
205
|
* Default ecosystem repos. Configurable via SPIDERSAN_ECOSYSTEM env var.
|
|
@@ -307,41 +280,42 @@ function runEcosystemScan(repos, asJson) {
|
|
|
307
280
|
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 });
|
|
308
281
|
const reposWithConflicts = results.filter(r => r.tier3 + r.tier2 + r.tier1 > 0).length;
|
|
309
282
|
if (asJson) {
|
|
310
|
-
|
|
283
|
+
writeStdout(JSON.stringify({
|
|
311
284
|
ecosystem: true,
|
|
312
285
|
repos: results,
|
|
313
286
|
summary: { ...totals, repos_scanned: results.length, repos_with_conflicts: reposWithConflicts },
|
|
314
287
|
}, null, 2));
|
|
315
288
|
return;
|
|
316
289
|
}
|
|
317
|
-
|
|
290
|
+
writeStdout(`
|
|
318
291
|
🕷️ ECOSYSTEM CONFLICT SCAN
|
|
319
292
|
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
320
293
|
`);
|
|
321
294
|
for (const r of results) {
|
|
322
295
|
if (r.skipped) {
|
|
323
|
-
|
|
296
|
+
writeStdout(` ⬜ ${r.name.padEnd(22)} (not initialized)`);
|
|
324
297
|
continue;
|
|
325
298
|
}
|
|
326
299
|
if (r.error) {
|
|
327
|
-
|
|
300
|
+
writeStdout(` ❓ ${r.name.padEnd(22)} (${r.error})`);
|
|
328
301
|
continue;
|
|
329
302
|
}
|
|
330
303
|
const icon = r.tier3 > 0 ? '🔴' : r.tier2 > 0 ? '🟠' : r.tier1 > 0 ? '🟡' : '✅';
|
|
331
304
|
const counts = r.tier3 + r.tier2 + r.tier1 > 0
|
|
332
305
|
? ` T3:${r.tier3} T2:${r.tier2} T1:${r.tier1}`
|
|
333
306
|
: ' clean';
|
|
334
|
-
|
|
307
|
+
writeStdout(` ${icon} ${r.name.padEnd(22)}${counts}`);
|
|
335
308
|
}
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
309
|
+
writeStdout('\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
|
|
310
|
+
writeStdout(`📊 Total: 🔴 ${totals.tier3} BLOCK | 🟠 ${totals.tier2} PAUSE | 🟡 ${totals.tier1} WARN`);
|
|
311
|
+
writeStdout(` Repos scanned: ${results.length} | With conflicts: ${reposWithConflicts}`);
|
|
339
312
|
}
|
|
340
313
|
export const conflictsCommand = new Command('conflicts')
|
|
341
314
|
.description('Detect file conflicts between branches (with tiered blocking)')
|
|
342
315
|
.option('--branch <name>', 'Check conflicts for specific branch')
|
|
343
316
|
.option('--pr <number>', 'Check conflicts for a GitHub pull request by number')
|
|
344
317
|
.option('--json', 'Output as JSON')
|
|
318
|
+
.option('--verbose', 'Show detailed symbol overlap information in human output')
|
|
345
319
|
.option('--tier <level>', 'Filter by minimum tier (1, 2, or 3)', '1')
|
|
346
320
|
.option('--strict', 'Strict mode: exit with error if TIER 2+ conflicts found')
|
|
347
321
|
.option('--notify', 'Notify Hub of TIER 2+ conflicts')
|
|
@@ -364,26 +338,27 @@ export const conflictsCommand = new Command('conflicts')
|
|
|
364
338
|
const storage = await getStorage();
|
|
365
339
|
const config = await loadConfig();
|
|
366
340
|
if (!await storage.isInitialized()) {
|
|
367
|
-
|
|
368
|
-
process.exit(1);
|
|
341
|
+
fail('❌ Spidersan not initialized. Run: spidersan init');
|
|
369
342
|
}
|
|
370
343
|
const minTier = parseInt(options.tier, 10);
|
|
371
344
|
// Load custom patterns from config
|
|
372
345
|
const highSeverity = (config.conflicts?.highSeverityPatterns || []).map(p => new RegExp(p));
|
|
373
346
|
const mediumSeverity = (config.conflicts?.mediumSeverityPatterns || []).map(p => new RegExp(p));
|
|
347
|
+
const compiledHigh = compilePatterns(highSeverity);
|
|
348
|
+
const compiledMedium = compilePatterns(mediumSeverity);
|
|
374
349
|
let targetBranch;
|
|
375
350
|
let targetFiles;
|
|
376
351
|
if (options.pr) {
|
|
377
352
|
// --pr mode: fetch PR head branch + changed files from GitHub
|
|
378
353
|
const prNumber = parseInt(options.pr, 10);
|
|
379
354
|
if (isNaN(prNumber) || prNumber <= 0) {
|
|
380
|
-
|
|
381
|
-
process.exit(1);
|
|
355
|
+
fail('❌ Invalid PR number. Provide a positive integer, e.g. --pr 42');
|
|
382
356
|
}
|
|
383
357
|
if (!isGhAvailable()) {
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
358
|
+
fail([
|
|
359
|
+
'❌ GitHub CLI (gh) is not available or not authenticated.',
|
|
360
|
+
' Install: https://cli.github.com → gh auth login',
|
|
361
|
+
]);
|
|
387
362
|
}
|
|
388
363
|
let prDetails;
|
|
389
364
|
try {
|
|
@@ -391,58 +366,47 @@ export const conflictsCommand = new Command('conflicts')
|
|
|
391
366
|
}
|
|
392
367
|
catch (err) {
|
|
393
368
|
const msg = err instanceof Error ? err.message : String(err);
|
|
394
|
-
|
|
395
|
-
process.exit(1);
|
|
369
|
+
fail(`❌ Failed to fetch PR #${prNumber}: ${msg}`);
|
|
396
370
|
}
|
|
397
371
|
targetBranch = prDetails.headBranch;
|
|
398
372
|
targetFiles = prDetails.files;
|
|
399
|
-
|
|
400
|
-
|
|
373
|
+
writeStdout(`🕷️ Checking conflicts for PR #${prNumber}: "${prDetails.title}"`);
|
|
374
|
+
writeStdout(` Branch: ${targetBranch} (${targetFiles.length} file(s) changed)`);
|
|
401
375
|
}
|
|
402
376
|
else {
|
|
403
377
|
// Normal --branch or current branch mode
|
|
404
378
|
targetBranch = options.branch || getCurrentBranch();
|
|
405
379
|
const target = await storage.get(targetBranch);
|
|
406
380
|
if (!target) {
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
381
|
+
fail([
|
|
382
|
+
`❌ Branch "${targetBranch}" is not registered.`,
|
|
383
|
+
' Run: spidersan register --files "..."',
|
|
384
|
+
]);
|
|
410
385
|
}
|
|
411
386
|
targetFiles = target.files;
|
|
412
387
|
}
|
|
413
388
|
const allBranches = await storage.list();
|
|
414
|
-
const
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
}
|
|
432
|
-
if (maxTier.tier >= minTier) {
|
|
433
|
-
conflicts.push({
|
|
434
|
-
branch: branch.name,
|
|
435
|
-
files: overlappingFiles,
|
|
436
|
-
tier: maxTier.tier,
|
|
437
|
-
tierInfo: maxTier
|
|
438
|
-
});
|
|
439
|
-
}
|
|
440
|
-
}
|
|
441
|
-
}
|
|
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);
|
|
442
406
|
// SEMANTIC ANALYSIS with AST parser
|
|
443
407
|
const semanticConflicts = [];
|
|
444
408
|
if (options.semantic && conflicts.length > 0) {
|
|
445
|
-
|
|
409
|
+
writeStdout('\n🔬 Running semantic (AST) analysis...');
|
|
446
410
|
const astParser = new ASTParser();
|
|
447
411
|
for (const conflict of conflicts) {
|
|
448
412
|
for (const file of conflict.files) {
|
|
@@ -450,10 +414,11 @@ export const conflictsCommand = new Command('conflicts')
|
|
|
450
414
|
if (!/\.(ts|js|tsx|jsx)$/.test(file))
|
|
451
415
|
continue;
|
|
452
416
|
try {
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
417
|
+
const currentContent = getFileAtRef('HEAD', file);
|
|
418
|
+
const otherContent = getFileAtRef(conflict.branch, file);
|
|
419
|
+
if (currentContent === null || otherContent === null) {
|
|
420
|
+
continue;
|
|
421
|
+
}
|
|
457
422
|
const symbolConflicts = astParser.findSymbolConflicts(currentContent, `${targetBranch}:${file}`, otherContent, `${conflict.branch}:${file}`);
|
|
458
423
|
semanticConflicts.push(...symbolConflicts);
|
|
459
424
|
}
|
|
@@ -463,95 +428,75 @@ export const conflictsCommand = new Command('conflicts')
|
|
|
463
428
|
}
|
|
464
429
|
}
|
|
465
430
|
}
|
|
466
|
-
// Sort by tier (highest first)
|
|
467
|
-
conflicts.sort((a, b) => b.tier - a.tier);
|
|
468
431
|
// Log conflict detection to activity log
|
|
469
432
|
if (conflicts.length > 0) {
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
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
|
+
}
|
|
480
449
|
}
|
|
481
450
|
// Check for blocking conditions
|
|
482
|
-
const
|
|
483
|
-
const tier2Count = conflicts.filter(c => c.tier === 2).length;
|
|
451
|
+
const { tier2: tier2Count, tier3: tier3Count } = countByTier(report);
|
|
484
452
|
const shouldBlock = options.strict && (tier3Count > 0 || tier2Count > 0);
|
|
485
453
|
// Notify Hub if requested
|
|
486
454
|
if (options.notify && (tier3Count > 0 || tier2Count > 0)) {
|
|
487
|
-
await
|
|
455
|
+
await hub.notifyConflict(targetBranch, conflicts);
|
|
488
456
|
}
|
|
489
457
|
if (options.json) {
|
|
490
|
-
console.log(JSON.stringify(
|
|
491
|
-
branch: targetBranch,
|
|
492
|
-
conflicts,
|
|
493
|
-
summary: {
|
|
494
|
-
tier3: tier3Count,
|
|
495
|
-
tier2: tier2Count,
|
|
496
|
-
tier1: conflicts.filter(c => c.tier === 1).length,
|
|
497
|
-
blocked: shouldBlock
|
|
498
|
-
}
|
|
499
|
-
}, null, 2));
|
|
458
|
+
console.log(JSON.stringify(createJsonReport(report, targetBranch, shouldBlock), null, 2));
|
|
500
459
|
if (shouldBlock)
|
|
501
460
|
process.exit(1);
|
|
502
461
|
return;
|
|
503
462
|
}
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
}
|
|
509
|
-
console.log(`
|
|
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
|
+
: `
|
|
510
467
|
🕷️ CONFLICT ANALYSIS: "${targetBranch}"
|
|
511
468
|
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
for (const file of conflict.files) {
|
|
516
|
-
const fileTier = getConflictTier(file, highSeverity, mediumSeverity);
|
|
517
|
-
console.log(` ${fileTier.icon} ${file}`);
|
|
518
|
-
}
|
|
519
|
-
console.log(` → ${conflict.tierInfo.action}`);
|
|
520
|
-
console.log('');
|
|
521
|
-
}
|
|
522
|
-
console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
|
|
523
|
-
console.log(`📊 Summary: 🔴 ${tier3Count} BLOCK | 🟠 ${tier2Count} PAUSE | 🟡 ${conflicts.filter(c => c.tier === 1).length} WARN`);
|
|
524
|
-
// Offer add/add resolution suggestions for likely add/add files
|
|
525
|
-
suggestAddAddResolution(conflicts);
|
|
469
|
+
|
|
470
|
+
${renderedReport}`;
|
|
471
|
+
console.log(humanOutput);
|
|
526
472
|
// Show semantic analysis results
|
|
527
473
|
if (options.semantic && semanticConflicts.length > 0) {
|
|
528
|
-
|
|
529
|
-
|
|
474
|
+
writeStdout(`\n🔬 SEMANTIC CONFLICTS DETECTED (${semanticConflicts.length} symbols):`);
|
|
475
|
+
writeStdout('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
|
|
530
476
|
for (const sc of semanticConflicts) {
|
|
531
|
-
|
|
532
|
-
|
|
477
|
+
writeStdout(` ⚡ ${sc.symbolType} '${sc.symbolName}'`);
|
|
478
|
+
writeStdout(' Modified in BOTH branches (different content)');
|
|
533
479
|
}
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
480
|
+
writeStdout('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
|
|
481
|
+
writeStdout('\n💡 TIP: Coordinate on these specific functions/classes,');
|
|
482
|
+
writeStdout(' not just the files. One of you should rebase.');
|
|
537
483
|
}
|
|
538
484
|
else if (options.semantic && semanticConflicts.length === 0) {
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
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).');
|
|
542
488
|
}
|
|
543
489
|
if (tier3Count > 0) {
|
|
544
|
-
|
|
490
|
+
writeStdout(`
|
|
545
491
|
🔴 TIER 3 CONFLICTS FOUND
|
|
546
492
|
These files are security-critical and MUST be resolved.
|
|
547
493
|
Merge is BLOCKED until conflicts are cleared.
|
|
548
494
|
`);
|
|
549
495
|
}
|
|
550
496
|
else if (tier2Count > 0) {
|
|
551
|
-
|
|
497
|
+
writeStdout(`
|
|
552
498
|
🟠 TIER 2 CONFLICTS FOUND
|
|
553
499
|
Coordinate with the other agent before proceeding.
|
|
554
|
-
Use: mycmail send <agent> "Need to sync on <file>"
|
|
555
500
|
`);
|
|
556
501
|
}
|
|
557
502
|
// Wake conflicting agents if --wake flag is set
|
|
@@ -569,61 +514,67 @@ export const conflictsCommand = new Command('conflicts')
|
|
|
569
514
|
}
|
|
570
515
|
}
|
|
571
516
|
if (agentsToWake.size === 0) {
|
|
572
|
-
|
|
517
|
+
writeStdout('\n⚠️ No agents registered on conflicting branches.');
|
|
573
518
|
}
|
|
574
519
|
else {
|
|
575
520
|
// Show what will happen and ask for confirmation
|
|
576
|
-
|
|
577
|
-
|
|
521
|
+
writeStdout('\n🔔 WAKE AGENTS?');
|
|
522
|
+
writeStdout('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
|
|
578
523
|
for (const [agent, info] of agentsToWake) {
|
|
579
|
-
|
|
524
|
+
writeStdout(` • ${agent} (${info.branch})`);
|
|
580
525
|
}
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
console.log(' 2. Send mycmail with conflict details\n');
|
|
526
|
+
writeStdout('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
|
|
527
|
+
writeStdout('This will:');
|
|
528
|
+
writeStdout(' 1. Send wake signal via Hub\n');
|
|
585
529
|
const confirmed = await confirmAction('Wake these agents?', options.auto);
|
|
586
530
|
if (confirmed) {
|
|
587
|
-
|
|
531
|
+
writeStdout('\n🔔 WAKING AGENTS...\n');
|
|
588
532
|
for (const [agentId, info] of agentsToWake) {
|
|
589
|
-
await
|
|
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
|
+
}
|
|
590
540
|
}
|
|
591
|
-
|
|
541
|
+
writeStdout(`\n✅ Woke ${agentsToWake.size} agent(s)`);
|
|
592
542
|
// If --retry is set, ask before waiting
|
|
593
543
|
if (options.retry) {
|
|
594
544
|
const waitSeconds = parseInt(options.retry, 10);
|
|
595
545
|
const retryConfirmed = await confirmAction(`\nWait ${waitSeconds}s and re-check conflicts?`, options.auto);
|
|
596
546
|
if (retryConfirmed) {
|
|
597
|
-
|
|
547
|
+
writeStdout(`\n⏳ Waiting ${waitSeconds}s for agents to resolve conflicts...`);
|
|
598
548
|
await sleep(waitSeconds * 1000);
|
|
599
|
-
|
|
549
|
+
writeStdout('\n🔄 RE-CHECKING CONFLICTS...\n');
|
|
600
550
|
const { execFileSync } = await import('child_process');
|
|
601
551
|
try {
|
|
552
|
+
const { getCLIPath } = await import('../lib/security.js');
|
|
602
553
|
// Security: Use execFileSync with argument array
|
|
603
554
|
const safeBranch = validateBranchName(targetBranch);
|
|
604
555
|
const tierArg = String(parseInt(options.tier, 10) || 1);
|
|
605
|
-
execFileSync(
|
|
556
|
+
execFileSync(process.execPath, [getCLIPath(), 'conflicts', '--branch', safeBranch, '--tier', tierArg], {
|
|
606
557
|
encoding: 'utf-8',
|
|
607
558
|
stdio: 'inherit'
|
|
608
559
|
});
|
|
609
560
|
return;
|
|
610
561
|
}
|
|
611
562
|
catch {
|
|
612
|
-
|
|
563
|
+
writeStdout('❌ Conflicts still exist after retry.');
|
|
613
564
|
}
|
|
614
565
|
}
|
|
615
566
|
else {
|
|
616
|
-
|
|
567
|
+
writeStdout('⏭️ Skipping retry.');
|
|
617
568
|
}
|
|
618
569
|
}
|
|
619
570
|
}
|
|
620
571
|
else {
|
|
621
|
-
|
|
572
|
+
writeStdout('⏭️ Skipping wake.');
|
|
622
573
|
}
|
|
623
574
|
}
|
|
624
575
|
}
|
|
625
576
|
if (shouldBlock) {
|
|
626
|
-
|
|
577
|
+
writeStdout('❌ Strict mode: Exiting with error due to TIER 2+ conflicts.');
|
|
627
578
|
process.exit(1);
|
|
628
579
|
}
|
|
629
580
|
});
|