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.
Files changed (217) hide show
  1. package/CHANGELOG.md +46 -48
  2. package/README.md +241 -4
  3. package/dist/bin/spidersan.d.ts.map +1 -1
  4. package/dist/bin/spidersan.js +13 -1
  5. package/dist/bin/spidersan.js.map +1 -1
  6. package/dist/commands/abandon.d.ts.map +1 -1
  7. package/dist/commands/abandon.js +1 -9
  8. package/dist/commands/abandon.js.map +1 -1
  9. package/dist/commands/ai.d.ts +15 -0
  10. package/dist/commands/ai.d.ts.map +1 -0
  11. package/dist/commands/ai.js +498 -0
  12. package/dist/commands/ai.js.map +1 -0
  13. package/dist/commands/auto.d.ts.map +1 -1
  14. package/dist/commands/auto.js +2 -1
  15. package/dist/commands/auto.js.map +1 -1
  16. package/dist/commands/bot.d.ts +16 -0
  17. package/dist/commands/bot.d.ts.map +1 -0
  18. package/dist/commands/bot.js +398 -0
  19. package/dist/commands/bot.js.map +1 -0
  20. package/dist/commands/conflicts.d.ts.map +1 -1
  21. package/dist/commands/conflicts.js +264 -313
  22. package/dist/commands/conflicts.js.map +1 -1
  23. package/dist/commands/context.d.ts +8 -0
  24. package/dist/commands/context.d.ts.map +1 -0
  25. package/dist/commands/context.js +104 -0
  26. package/dist/commands/context.js.map +1 -0
  27. package/dist/commands/cross-conflicts.d.ts.map +1 -1
  28. package/dist/commands/cross-conflicts.js +1 -38
  29. package/dist/commands/cross-conflicts.js.map +1 -1
  30. package/dist/commands/depends.d.ts.map +1 -1
  31. package/dist/commands/depends.js +1 -9
  32. package/dist/commands/depends.js.map +1 -1
  33. package/dist/commands/doctor.d.ts.map +1 -1
  34. package/dist/commands/doctor.js +117 -59
  35. package/dist/commands/doctor.js.map +1 -1
  36. package/dist/commands/fleet-status.d.ts +14 -0
  37. package/dist/commands/fleet-status.d.ts.map +1 -0
  38. package/dist/commands/fleet-status.js +127 -0
  39. package/dist/commands/fleet-status.js.map +1 -0
  40. package/dist/commands/git-watch.d.ts +24 -0
  41. package/dist/commands/git-watch.d.ts.map +1 -0
  42. package/dist/commands/git-watch.js +84 -0
  43. package/dist/commands/git-watch.js.map +1 -0
  44. package/dist/commands/index.d.ts +5 -0
  45. package/dist/commands/index.d.ts.map +1 -1
  46. package/dist/commands/index.js +7 -0
  47. package/dist/commands/index.js.map +1 -1
  48. package/dist/commands/merge-order.d.ts.map +1 -1
  49. package/dist/commands/merge-order.js +18 -67
  50. package/dist/commands/merge-order.js.map +1 -1
  51. package/dist/commands/merged.d.ts.map +1 -1
  52. package/dist/commands/merged.js +1 -9
  53. package/dist/commands/merged.js.map +1 -1
  54. package/dist/commands/pulse.d.ts.map +1 -1
  55. package/dist/commands/pulse.js +137 -74
  56. package/dist/commands/pulse.js.map +1 -1
  57. package/dist/commands/queen.d.ts.map +1 -1
  58. package/dist/commands/queen.js +36 -19
  59. package/dist/commands/queen.js.map +1 -1
  60. package/dist/commands/ready-check.d.ts +2 -1
  61. package/dist/commands/ready-check.d.ts.map +1 -1
  62. package/dist/commands/ready-check.js +6 -30
  63. package/dist/commands/ready-check.js.map +1 -1
  64. package/dist/commands/register.d.ts.map +1 -1
  65. package/dist/commands/register.js +7 -29
  66. package/dist/commands/register.js.map +1 -1
  67. package/dist/commands/stale.d.ts +1 -9
  68. package/dist/commands/stale.d.ts.map +1 -1
  69. package/dist/commands/stale.js +1 -42
  70. package/dist/commands/stale.js.map +1 -1
  71. package/dist/commands/torrent.d.ts.map +1 -1
  72. package/dist/commands/torrent.js +29 -18
  73. package/dist/commands/torrent.js.map +1 -1
  74. package/dist/commands/watch.d.ts.map +1 -1
  75. package/dist/commands/watch.js +13 -34
  76. package/dist/commands/watch.js.map +1 -1
  77. package/dist/lib/agent-errors.js +2 -2
  78. package/dist/lib/agent-errors.js.map +1 -1
  79. package/dist/lib/ai/context-builder.d.ts +16 -0
  80. package/dist/lib/ai/context-builder.d.ts.map +1 -0
  81. package/dist/lib/ai/context-builder.js +216 -0
  82. package/dist/lib/ai/context-builder.js.map +1 -0
  83. package/dist/lib/ai/event-handler.d.ts +21 -0
  84. package/dist/lib/ai/event-handler.d.ts.map +1 -0
  85. package/dist/lib/ai/event-handler.js +98 -0
  86. package/dist/lib/ai/event-handler.js.map +1 -0
  87. package/dist/lib/ai/index.d.ts +13 -0
  88. package/dist/lib/ai/index.d.ts.map +1 -0
  89. package/dist/lib/ai/index.js +11 -0
  90. package/dist/lib/ai/index.js.map +1 -0
  91. package/dist/lib/ai/llm-client.d.ts +37 -0
  92. package/dist/lib/ai/llm-client.d.ts.map +1 -0
  93. package/dist/lib/ai/llm-client.js +225 -0
  94. package/dist/lib/ai/llm-client.js.map +1 -0
  95. package/dist/lib/ai/reasoner.d.ts +11 -0
  96. package/dist/lib/ai/reasoner.d.ts.map +1 -0
  97. package/dist/lib/ai/reasoner.js +246 -0
  98. package/dist/lib/ai/reasoner.js.map +1 -0
  99. package/dist/lib/ai/setup.d.ts +40 -0
  100. package/dist/lib/ai/setup.d.ts.map +1 -0
  101. package/dist/lib/ai/setup.js +154 -0
  102. package/dist/lib/ai/setup.js.map +1 -0
  103. package/dist/lib/ai/types.d.ts +135 -0
  104. package/dist/lib/ai/types.d.ts.map +1 -0
  105. package/dist/lib/ai/types.js +39 -0
  106. package/dist/lib/ai/types.js.map +1 -0
  107. package/dist/lib/colony-subscriber.d.ts +15 -12
  108. package/dist/lib/colony-subscriber.d.ts.map +1 -1
  109. package/dist/lib/colony-subscriber.js +146 -65
  110. package/dist/lib/colony-subscriber.js.map +1 -1
  111. package/dist/lib/config.d.ts.map +1 -1
  112. package/dist/lib/config.js +18 -4
  113. package/dist/lib/config.js.map +1 -1
  114. package/dist/lib/conflict-analyzer.d.ts +33 -0
  115. package/dist/lib/conflict-analyzer.d.ts.map +1 -0
  116. package/dist/lib/conflict-analyzer.js +114 -0
  117. package/dist/lib/conflict-analyzer.js.map +1 -0
  118. package/dist/lib/conflict-renderer.d.ts +7 -0
  119. package/dist/lib/conflict-renderer.d.ts.map +1 -0
  120. package/dist/lib/conflict-renderer.js +162 -0
  121. package/dist/lib/conflict-renderer.js.map +1 -0
  122. package/dist/lib/conflict-tier.d.ts +20 -0
  123. package/dist/lib/conflict-tier.d.ts.map +1 -0
  124. package/dist/lib/conflict-tier.js +49 -0
  125. package/dist/lib/conflict-tier.js.map +1 -0
  126. package/dist/lib/crypto.js +1 -1
  127. package/dist/lib/crypto.js.map +1 -1
  128. package/dist/lib/daily-bridge.d.ts.map +1 -1
  129. package/dist/lib/daily-bridge.js +3 -2
  130. package/dist/lib/daily-bridge.js.map +1 -1
  131. package/dist/lib/git-events-subscriber.d.ts +59 -0
  132. package/dist/lib/git-events-subscriber.d.ts.map +1 -0
  133. package/dist/lib/git-events-subscriber.js +779 -0
  134. package/dist/lib/git-events-subscriber.js.map +1 -0
  135. package/dist/lib/git.d.ts +15 -0
  136. package/dist/lib/git.d.ts.map +1 -0
  137. package/dist/lib/git.js +180 -0
  138. package/dist/lib/git.js.map +1 -0
  139. package/dist/lib/github.d.ts.map +1 -1
  140. package/dist/lib/github.js +14 -9
  141. package/dist/lib/github.js.map +1 -1
  142. package/dist/lib/graph.d.ts +23 -0
  143. package/dist/lib/graph.d.ts.map +1 -0
  144. package/dist/lib/graph.js +134 -0
  145. package/dist/lib/graph.js.map +1 -0
  146. package/dist/lib/hub.d.ts +31 -0
  147. package/dist/lib/hub.d.ts.map +1 -0
  148. package/dist/lib/hub.js +92 -0
  149. package/dist/lib/hub.js.map +1 -0
  150. package/dist/lib/regex-utils.d.ts +6 -0
  151. package/dist/lib/regex-utils.d.ts.map +1 -0
  152. package/dist/lib/regex-utils.js +24 -0
  153. package/dist/lib/regex-utils.js.map +1 -0
  154. package/dist/lib/remote-drift.d.ts +60 -0
  155. package/dist/lib/remote-drift.d.ts.map +1 -0
  156. package/dist/lib/remote-drift.js +225 -0
  157. package/dist/lib/remote-drift.js.map +1 -0
  158. package/dist/lib/repo-scanner.d.ts +0 -4
  159. package/dist/lib/repo-scanner.d.ts.map +1 -1
  160. package/dist/lib/repo-scanner.js +2 -1
  161. package/dist/lib/repo-scanner.js.map +1 -1
  162. package/dist/lib/salvage-analyzer.d.ts.map +1 -1
  163. package/dist/lib/salvage-analyzer.js +2 -3
  164. package/dist/lib/salvage-analyzer.js.map +1 -1
  165. package/dist/lib/security.d.ts +17 -0
  166. package/dist/lib/security.d.ts.map +1 -1
  167. package/dist/lib/security.js +34 -1
  168. package/dist/lib/security.js.map +1 -1
  169. package/dist/lib/session-logger.d.ts +54 -0
  170. package/dist/lib/session-logger.d.ts.map +1 -0
  171. package/dist/lib/session-logger.js +136 -0
  172. package/dist/lib/session-logger.js.map +1 -0
  173. package/dist/storage/adapter.d.ts +4 -0
  174. package/dist/storage/adapter.d.ts.map +1 -1
  175. package/dist/storage/branch-registry-store.d.ts +13 -0
  176. package/dist/storage/branch-registry-store.d.ts.map +1 -0
  177. package/dist/storage/branch-registry-store.js +2 -0
  178. package/dist/storage/branch-registry-store.js.map +1 -0
  179. package/dist/storage/factory.d.ts +4 -0
  180. package/dist/storage/factory.d.ts.map +1 -1
  181. package/dist/storage/factory.js +25 -9
  182. package/dist/storage/factory.js.map +1 -1
  183. package/dist/storage/index.d.ts +5 -5
  184. package/dist/storage/index.d.ts.map +1 -1
  185. package/dist/storage/index.js +5 -6
  186. package/dist/storage/index.js.map +1 -1
  187. package/dist/storage/json-branch-registry-store.d.ts +19 -0
  188. package/dist/storage/json-branch-registry-store.d.ts.map +1 -0
  189. package/dist/storage/json-branch-registry-store.js +112 -0
  190. package/dist/storage/json-branch-registry-store.js.map +1 -0
  191. package/dist/storage/local.d.ts +2 -6
  192. package/dist/storage/local.d.ts.map +1 -1
  193. package/dist/storage/local.js +13 -76
  194. package/dist/storage/local.js.map +1 -1
  195. package/dist/storage/memory-branch-registry-store.d.ts +16 -0
  196. package/dist/storage/memory-branch-registry-store.d.ts.map +1 -0
  197. package/dist/storage/memory-branch-registry-store.js +65 -0
  198. package/dist/storage/memory-branch-registry-store.js.map +1 -0
  199. package/dist/storage/mycmail-adapter.d.ts.map +1 -1
  200. package/dist/storage/mycmail-adapter.js +4 -6
  201. package/dist/storage/mycmail-adapter.js.map +1 -1
  202. package/dist/storage/supabase-registry-sync-client-impl.d.ts +46 -0
  203. package/dist/storage/supabase-registry-sync-client-impl.d.ts.map +1 -0
  204. package/dist/storage/supabase-registry-sync-client-impl.js +322 -0
  205. package/dist/storage/supabase-registry-sync-client-impl.js.map +1 -0
  206. package/dist/storage/supabase-registry-sync-client.d.ts +9 -0
  207. package/dist/storage/supabase-registry-sync-client.d.ts.map +1 -0
  208. package/dist/storage/supabase-registry-sync-client.js +2 -0
  209. package/dist/storage/supabase-registry-sync-client.js.map +1 -0
  210. package/dist/storage/supabase.d.ts +8 -46
  211. package/dist/storage/supabase.d.ts.map +1 -1
  212. package/dist/storage/supabase.js +30 -342
  213. package/dist/storage/supabase.js.map +1 -1
  214. package/dist/tui/screen.d.ts.map +1 -1
  215. package/dist/tui/screen.js +5 -3
  216. package/dist/tui/screen.js.map +1 -1
  217. 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 { execFileSync, spawnSync } from 'child_process';
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 { validateAgentId, validateBranchName } from '../lib/security.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
+ 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
- // Config
22
- const HUB_URL = process.env.HUB_URL || 'https://hub.treebird.uk';
23
- // Critical files that trigger TIER 3 blocking
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
- * Suggest simple resolutions for add/add style conflicts where both branches added the same file
179
- * Typically occurs with docs or generated files. This prints guidance and a recommended sequence
180
- * to preserve both variants for auditability (e.g., create a .incoming copy).
181
- */
182
- function suggestAddAddResolution(conflicts) {
183
- const addAddFiles = new Set();
184
- for (const conflict of conflicts) {
185
- for (const f of conflict.files) {
186
- // Simple heuristic: docs and markdown files are common add/add candidates
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
- console.log(`${prompt} [Y/n]: Y (auto)`);
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
- console.log(JSON.stringify({
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
- console.log(`
290
+ writeStdout(`
318
291
  🕷️ ECOSYSTEM CONFLICT SCAN
319
292
  ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
320
293
  `);
321
294
  for (const r of results) {
322
295
  if (r.skipped) {
323
- console.log(` ⬜ ${r.name.padEnd(22)} (not initialized)`);
296
+ writeStdout(` ⬜ ${r.name.padEnd(22)} (not initialized)`);
324
297
  continue;
325
298
  }
326
299
  if (r.error) {
327
- console.log(` ❓ ${r.name.padEnd(22)} (${r.error})`);
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
- console.log(` ${icon} ${r.name.padEnd(22)}${counts}`);
307
+ writeStdout(` ${icon} ${r.name.padEnd(22)}${counts}`);
335
308
  }
336
- console.log(`\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━`);
337
- console.log(`📊 Total: 🔴 ${totals.tier3} BLOCK | 🟠 ${totals.tier2} PAUSE | 🟡 ${totals.tier1} WARN`);
338
- console.log(` Repos scanned: ${results.length} | With conflicts: ${reposWithConflicts}`);
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
- console.error('❌ Spidersan not initialized. Run: spidersan init');
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
- console.error('❌ Invalid PR number. Provide a positive integer, e.g. --pr 42');
381
- process.exit(1);
355
+ fail('❌ Invalid PR number. Provide a positive integer, e.g. --pr 42');
382
356
  }
383
357
  if (!isGhAvailable()) {
384
- console.error('❌ GitHub CLI (gh) is not available or not authenticated.');
385
- console.error(' Install: https://cli.github.com → gh auth login');
386
- process.exit(1);
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
- console.error(`❌ Failed to fetch PR #${prNumber}: ${msg}`);
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
- console.log(`🕷️ Checking conflicts for PR #${prNumber}: "${prDetails.title}"`);
400
- console.log(` Branch: ${targetBranch} (${targetFiles.length} file(s) changed)`);
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
- console.error(`❌ Branch "${targetBranch}" is not registered.`);
408
- console.error(' Run: spidersan register --files "..."');
409
- process.exit(1);
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 conflicts = [];
415
- // Performance Optimization: Convert target files to Set for O(1) lookup
416
- // Reduces complexity from O(N*M*K) to O(N*M) where N=branches, M=files/branch, K=target_files
417
- // Filter excluded paths (node_modules/, dist/, etc.) at query time to handle stale registrations
418
- const targetFilesSet = new Set(targetFiles.filter(f => !isExcludedPath(f)));
419
- for (const branch of allBranches) {
420
- if (branch.name === targetBranch || branch.status !== 'active')
421
- continue;
422
- const overlappingFiles = branch.files.filter(f => !isExcludedPath(f) && targetFilesSet.has(f));
423
- if (overlappingFiles.length > 0) {
424
- // Get highest tier for this conflict
425
- let maxTier = { tier: 1, label: 'WARN', icon: '🟡', action: '' };
426
- for (const file of overlappingFiles) {
427
- const fileTier = getConflictTier(file, highSeverity, mediumSeverity);
428
- if (fileTier.tier > maxTier.tier) {
429
- maxTier = fileTier;
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
- console.log('\n🔬 Running semantic (AST) analysis...');
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
- // Get file content from both branches
454
- // Security: Use execFileSync to prevent command injection via file names
455
- const currentContent = execFileSync('git', ['show', `HEAD:${file}`], { encoding: 'utf-8' });
456
- const otherContent = execFileSync('git', ['show', `${conflict.branch}:${file}`], { encoding: 'utf-8' });
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
- logActivity({
471
- event: 'conflict_detected',
472
- branch: targetBranch,
473
- details: {
474
- tier3: conflicts.filter(c => c.tier === 3).length,
475
- tier2: conflicts.filter(c => c.tier === 2).length,
476
- tier1: conflicts.filter(c => c.tier === 1).length,
477
- conflicting_branches: conflicts.map(c => c.branch),
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 tier3Count = conflicts.filter(c => c.tier === 3).length;
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 notifyHub(targetBranch, conflicts);
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
- if (conflicts.length === 0) {
505
- console.log(`🕷️ No conflicts detected for "${targetBranch}"`);
506
- console.log(' You\'re good to merge!');
507
- return;
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
- for (const conflict of conflicts) {
514
- console.log(`${conflict.tierInfo.icon} TIER ${conflict.tier} (${conflict.tierInfo.label}): ${conflict.branch}`);
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
- console.log(`\n🔬 SEMANTIC CONFLICTS DETECTED (${semanticConflicts.length} symbols):`);
529
- console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
474
+ writeStdout(`\n🔬 SEMANTIC CONFLICTS DETECTED (${semanticConflicts.length} symbols):`);
475
+ writeStdout('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
530
476
  for (const sc of semanticConflicts) {
531
- console.log(` ⚡ ${sc.symbolType} '${sc.symbolName}'`);
532
- console.log(` Modified in BOTH branches (different content)`);
477
+ writeStdout(` ⚡ ${sc.symbolType} '${sc.symbolName}'`);
478
+ writeStdout(' Modified in BOTH branches (different content)');
533
479
  }
534
- console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
535
- console.log('\n💡 TIP: Coordinate on these specific functions/classes,');
536
- console.log(' not just the files. One of you should rebase.');
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
- console.log('\n🔬 SEMANTIC ANALYSIS: No symbol-level conflicts!');
540
- console.log(' Files overlap, but different functions were modified.');
541
- console.log(' ✅ Likely safe to merge (git will auto-merge).');
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
- console.log(`
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
- console.log(`
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
- console.log('\n⚠️ No agents registered on conflicting branches.');
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
- console.log('\n🔔 WAKE AGENTS?');
577
- console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
521
+ writeStdout('\n🔔 WAKE AGENTS?');
522
+ writeStdout('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
578
523
  for (const [agent, info] of agentsToWake) {
579
- console.log(` • ${agent} (${info.branch})`);
524
+ writeStdout(` • ${agent} (${info.branch})`);
580
525
  }
581
- console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
582
- console.log('This will:');
583
- console.log(' 1. Send wake signal via Hub');
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
- console.log('\n🔔 WAKING AGENTS...\n');
531
+ writeStdout('\n🔔 WAKING AGENTS...\n');
588
532
  for (const [agentId, info] of agentsToWake) {
589
- await wakeConflictingAgent(agentId, targetBranch, info.branch, info.files);
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
- console.log(`\n✅ Woke ${agentsToWake.size} agent(s)`);
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
- console.log(`\n⏳ Waiting ${waitSeconds}s for agents to resolve conflicts...`);
547
+ writeStdout(`\n⏳ Waiting ${waitSeconds}s for agents to resolve conflicts...`);
598
548
  await sleep(waitSeconds * 1000);
599
- console.log('\n🔄 RE-CHECKING CONFLICTS...\n');
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('node', [process.argv[1], 'conflicts', '--branch', safeBranch, '--tier', tierArg], {
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
- console.log('❌ Conflicts still exist after retry.');
563
+ writeStdout('❌ Conflicts still exist after retry.');
613
564
  }
614
565
  }
615
566
  else {
616
- console.log('⏭️ Skipping retry.');
567
+ writeStdout('⏭️ Skipping retry.');
617
568
  }
618
569
  }
619
570
  }
620
571
  else {
621
- console.log('⏭️ Skipping wake.');
572
+ writeStdout('⏭️ Skipping wake.');
622
573
  }
623
574
  }
624
575
  }
625
576
  if (shouldBlock) {
626
- console.log('❌ Strict mode: Exiting with error due to TIER 2+ conflicts.');
577
+ writeStdout('❌ Strict mode: Exiting with error due to TIER 2+ conflicts.');
627
578
  process.exit(1);
628
579
  }
629
580
  });