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