swarmdo 1.58.1 → 1.58.12

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 (25) hide show
  1. package/.claude/helpers/statusline.cjs +146 -11
  2. package/README.md +3 -2
  3. package/package.json +1 -1
  4. package/v3/@swarmdo/cli/.claude/commands/sDo/agents/README.md +1 -1
  5. package/v3/@swarmdo/cli/.claude/commands/sDo/agents/agent-capabilities.md +1 -1
  6. package/v3/@swarmdo/cli/.claude/commands/sDo/agents/agent-types.md +2 -2
  7. package/v3/@swarmdo/cli/.claude/commands/sDo/swarm/swarm.md +1 -1
  8. package/v3/@swarmdo/cli/.claude/helpers/statusline.cjs +770 -498
  9. package/v3/@swarmdo/cli/.claude/skills/sdo-agentic-jujutsu/SKILL.md +645 -0
  10. package/v3/@swarmdo/cli/.claude/skills/sdo-hive-mind-advanced/SKILL.md +709 -0
  11. package/v3/@swarmdo/cli/.claude/skills/sdo-performance-analysis/SKILL.md +560 -0
  12. package/v3/@swarmdo/cli/dist/src/commands/hooks.d.ts +26 -0
  13. package/v3/@swarmdo/cli/dist/src/commands/hooks.js +96 -24
  14. package/v3/@swarmdo/cli/dist/src/commands/integrations.js +185 -2
  15. package/v3/@swarmdo/cli/dist/src/commands/statusline.d.ts +22 -1
  16. package/v3/@swarmdo/cli/dist/src/commands/statusline.js +84 -3
  17. package/v3/@swarmdo/cli/dist/src/commands/swarm.js +5 -0
  18. package/v3/@swarmdo/cli/dist/src/init/statusline-generator.d.ts +1 -1
  19. package/v3/@swarmdo/cli/dist/src/init/statusline-generator.js +146 -11
  20. package/v3/@swarmdo/cli/dist/src/integrations/skills-sync.d.ts +120 -0
  21. package/v3/@swarmdo/cli/dist/src/integrations/skills-sync.js +180 -0
  22. package/v3/@swarmdo/cli/dist/src/mcp-tools/swarm-tools.js +2 -0
  23. package/v3/@swarmdo/cli/dist/src/usage/account-plan.d.ts +48 -0
  24. package/v3/@swarmdo/cli/dist/src/usage/account-plan.js +71 -0
  25. package/v3/@swarmdo/cli/package.json +1 -1
@@ -1,77 +1,378 @@
1
1
  #!/usr/bin/env node
2
2
  /**
3
- * Swarmdo V3.5 Statusline Generator
4
- * Displays real-time V3 implementation progress and system status
3
+ * Swarmdo V3 Statusline — delegation build (#2195)
5
4
  *
6
- * Usage: node statusline.cjs [options]
5
+ * Fix for upstream/swarmdo#2195: the previous version re-implemented all data
6
+ * readers locally using fragile file probes that missed AgentDB patterns,
7
+ * the v3/docs/adr/ ADR directory, and the real vector count.
7
8
  *
8
- * Options:
9
- * (default) Safe multi-line output with collision zone avoidance
10
- * --single Single-line output (completely avoids collision)
11
- * --unsafe Legacy multi-line without collision avoidance
12
- * --legacy Alias for --unsafe
13
- * --json JSON output with pretty printing
14
- * --compact JSON output without formatting
9
+ * This version delegates to 'npx @swarmdo/cli hooks statusline --json'
10
+ * as the single source of truth. That command queries AgentDB directly,
11
+ * counts ADRs in both directories, and reports the real intelligence pct.
15
12
  *
16
- * Collision Zone Fix (Issue #985):
17
- * Claude Code writes its internal status (e.g., "7s • 1p") at absolute
18
- * terminal coordinates (columns 15-25 on second-to-last line). The safe
19
- * mode pads the collision line with spaces to push content past column 25.
13
+ * ADR counting falls back to local file reads so the display still works
14
+ * without network access (counts both v3/docs/adr/ and v3/implementation/adrs/).
20
15
  *
21
- * IMPORTANT: This file uses .cjs extension to work in ES module projects.
22
- * The require() syntax is intentional for CommonJS compatibility.
16
+ * Cache: JSON result is cached in /tmp for 10s so rapid prompt triggers
17
+ * (every keystroke in some shells) don't hammer the CLI on every call.
18
+ *
19
+ * Usage: node statusline.cjs [--json] [--compact] [--dashboard]
23
20
  */
24
21
 
25
22
  /* eslint-disable @typescript-eslint/no-var-requires */
26
23
  const fs = require('fs');
27
24
  const path = require('path');
25
+ const { execSync } = require('child_process');
28
26
  const os = require('os');
29
- const { execSync, execFileSync } = require('child_process');
30
-
31
- // Read the installed plugin version once at startup. Probe the plugin's own
32
- // install location first (`~/.claude/plugins/marketplaces/swarmdo/package.json`),
33
- // then npm-style installs, then the source-checkout location. The previous
34
- // code hardcoded `Swarmdo V3.5` in the header — so users on alpha.27+ still
35
- // saw V3.5 in the statusline even though `swarmdo doctor` reported the real
36
- // version (#1951).
37
- let SWARMDO_VERSION = '3.6';
38
- try {
39
- const home = os.homedir();
40
- const cwd = process.cwd();
41
- const pkgPaths = [
42
- path.join(home, '.claude', 'plugins', 'marketplaces', 'swarmdo', 'package.json'),
43
- path.join(cwd, 'node_modules', '@swarmdo', 'cli', 'package.json'),
44
- path.join(cwd, 'node_modules', 'swarmdo', 'package.json'),
45
- path.join(cwd, 'v3', '@swarmdo', 'cli', 'package.json'),
46
- ];
47
- for (const p of pkgPaths) {
48
- if (!fs.existsSync(p)) continue;
49
- try {
50
- const pkg = JSON.parse(fs.readFileSync(p, 'utf-8'));
51
- if (pkg && typeof pkg.version === 'string' && pkg.version.length > 0) {
52
- SWARMDO_VERSION = pkg.version;
53
- break;
54
- }
55
- } catch { /* malformed package.json — try next */ }
56
- }
57
- } catch { /* fall through to the hardcoded default */ }
58
27
 
59
28
  // Configuration
60
29
  const CONFIG = {
61
- enabled: true,
62
- showProgress: true,
63
- showSecurity: true,
64
- showSwarm: true,
65
- showHooks: true,
66
- showPerformance: true,
67
- refreshInterval: 5000,
68
30
  maxAgents: 15,
69
- topology: 'hierarchical-mesh',
31
+ // Session-cost display. Claude Code's cost.total_cost_usd is a client-side
32
+ // estimate that "may differ from your actual bill" and reads as misleading on
33
+ // subscription plans, where token usage is not billed per dollar. These let
34
+ // each user pick what the segment means to them without changing the default.
35
+ // SWARMDO_STATUSLINE_COST_SYMBOL override the leading '$' (e.g. ⚡, €, 🌱);
36
+ // set to an empty string for the number alone.
37
+ // SWARMDO_STATUSLINE_HIDE_COST 1/true/yes/on removes the segment entirely.
38
+ costSymbol: process.env.SWARMDO_STATUSLINE_COST_SYMBOL ?? '$',
39
+ hideCost: /^(1|true|yes|on)$/i.test(process.env.SWARMDO_STATUSLINE_HIDE_COST || ''),
40
+ };
41
+
42
+ const CWD = process.cwd();
43
+
44
+ // ─── Delegation cache ───────────────────────────────────────────
45
+ // Cache the CLI JSON result for 60s so rapid prompt re-renders
46
+ // (Claude Code refreshes the statusline several times a second while
47
+ // streaming) don't re-invoke the CLI each time. #2337: bumped 10s→60s
48
+ // because 10s was far too short for how often Claude Code re-renders.
49
+ const CACHE_FILE = path.join(os.tmpdir(), 'swarmdo-statusline-cache-' + require('crypto').createHash('md5').update(CWD).digest('hex').slice(0, 8) + '.json');
50
+ const CACHE_TTL_MS = 60000;
51
+
52
+ // ─── User-selectable segments (SWARMDO_STATUSLINE) ───────────────────────────
53
+ // The full statusline is busy by design; users pick what they want to see.
54
+ // Resolution order: SWARMDO_STATUSLINE env var → .swarmdo/statusline.json in
55
+ // the project → ~/.swarmdo/statusline.json → 'full'.
56
+ // Value is a preset name ('full' | 'compact' | 'minimal') or a comma list of
57
+ // segment names: version,project,branch,model,duration,context,cost,
58
+ // domains,swarm,architecture,agentdb
59
+ const SEGMENT_PRESETS = {
60
+ full: ['version','project','branch','model','duration','context','cost','domains','swarm','architecture','agentdb'],
61
+ compact: ['version','project','branch','model','context','cost','swarm'],
62
+ minimal: ['project','branch','model','context'],
70
63
  };
64
+ function resolveSegments() {
65
+ let spec = (process.env.SWARMDO_STATUSLINE || '').trim();
66
+ if (!spec) {
67
+ const candidates = [
68
+ path.join(CWD, '.swarmdo', 'statusline.json'),
69
+ path.join(os.homedir(), '.swarmdo', 'statusline.json'),
70
+ ];
71
+ for (const cand of candidates) {
72
+ try {
73
+ const j = JSON.parse(fs.readFileSync(cand, 'utf8'));
74
+ if (typeof j.preset === 'string' && j.preset) { spec = j.preset; break; }
75
+ if (Array.isArray(j.segments) && j.segments.length) { spec = j.segments.join(','); break; }
76
+ } catch { /* absent or invalid — keep looking */ }
77
+ }
78
+ }
79
+ if (!spec) spec = 'full';
80
+ const preset = SEGMENT_PRESETS[spec.toLowerCase()];
81
+ const list = preset || spec.split(',').map(function (x) { return x.trim().toLowerCase(); }).filter(Boolean);
82
+ return new Set(list.length ? list : SEGMENT_PRESETS.full);
83
+ }
84
+ const SEGMENTS = resolveSegments();
85
+ function seg(name) { return SEGMENTS.has(name); }
86
+
87
+ // ─── Cost slot: plan-aware mode + account display ────────────────────────────
88
+ // Resolution mirrors resolveSegments: env → project config → global config →
89
+ // default. mode: auto|dollars|limits|off (auto → limits for a subscription
90
+ // account, dollars otherwise); showAccount prefixes the account label.
91
+ function resolveCostOptions() {
92
+ let mode = null;
93
+ let showAccount = null;
94
+ const envMode = (process.env.SWARMDO_STATUSLINE_COST_MODE || '').trim().toLowerCase();
95
+ if (envMode === 'auto' || envMode === 'dollars' || envMode === 'limits' || envMode === 'off') mode = envMode;
96
+ const envShow = (process.env.SWARMDO_STATUSLINE_SHOW_ACCOUNT || '').trim();
97
+ if (/^(1|true|yes|on)$/i.test(envShow)) showAccount = true;
98
+ else if (/^(0|false|no|off)$/i.test(envShow)) showAccount = false;
99
+ if (mode === null || showAccount === null) {
100
+ const files = [
101
+ path.join(CWD, '.swarmdo', 'statusline.json'),
102
+ path.join(os.homedir(), '.swarmdo', 'statusline.json'),
103
+ ];
104
+ for (const f of files) {
105
+ try {
106
+ const j = JSON.parse(fs.readFileSync(f, 'utf8'));
107
+ const cost = j && j.cost;
108
+ if (cost && typeof cost === 'object') {
109
+ if (mode === null && typeof cost.mode === 'string') {
110
+ const m = cost.mode.trim().toLowerCase();
111
+ if (m === 'auto' || m === 'dollars' || m === 'limits' || m === 'off') mode = m;
112
+ }
113
+ if (showAccount === null && typeof cost.showAccount === 'boolean') showAccount = cost.showAccount;
114
+ }
115
+ } catch (e) { /* absent/invalid — keep looking */ }
116
+ if (mode !== null && showAccount !== null) break;
117
+ }
118
+ }
119
+ if (CONFIG.hideCost) mode = 'off'; // legacy SWARMDO_STATUSLINE_HIDE_COST
120
+ return { mode: mode || 'auto', showAccount: showAccount === true };
121
+ }
122
+ const COST_OPTS = resolveCostOptions();
123
+
124
+ // #2337: resolve an already-installed @swarmdo/cli (or swarmdo) bin so we
125
+ // can invoke it directly via `node`. The previous version called
126
+ // `npx --yes @swarmdo/cli@latest` on every uncached render, which forces
127
+ // a registry resolution + cold-start of the entire CLI per render. With
128
+ // multiple concurrent Claude Code sessions this storms the host (reporter
129
+ // saw load average 40-65 on a 12-core box).
130
+ //
131
+ // Returns the absolute path to bin/cli.js or null. Mirrors getPkgVersion()'s
132
+ // path probing (project, monorepo, plugin marketplace, global node_modules
133
+ // including custom-prefix layouts like ~/.npm-global).
134
+ function resolveCliBin() {
135
+ try {
136
+ const home = os.homedir();
137
+ const candidates = [
138
+ path.join(home, '.claude', 'plugins', 'marketplaces', 'swarmdo', 'bin', 'cli.js'),
139
+ path.join(CWD, 'node_modules', '@swarmdo', 'cli', 'bin', 'cli.js'),
140
+ path.join(CWD, 'node_modules', 'swarmdo', 'bin', 'cli.js'),
141
+ path.join(CWD, 'v3', '@swarmdo', 'cli', 'bin', 'cli.js'),
142
+ ];
143
+ try {
144
+ const binDir = path.dirname(process.execPath);
145
+ const globalModuleDirs = [path.join(binDir, '..', 'lib', 'node_modules'), path.join(binDir, 'node_modules'), '/opt/homebrew/lib/node_modules', '/usr/local/lib/node_modules'];
146
+ for (const prefix of [process.env.npm_config_prefix, process.env.PREFIX, path.join(home, '.npm-global')]) {
147
+ if (prefix) globalModuleDirs.push(path.join(prefix, 'lib', 'node_modules'));
148
+ }
149
+ for (const gm of globalModuleDirs) {
150
+ candidates.push(
151
+ path.join(gm, 'swarmdo', 'bin', 'cli.js'),
152
+ path.join(gm, '@swarmdo', 'cli', 'bin', 'cli.js'),
153
+ );
154
+ }
155
+ } catch { /* ignore */ }
156
+ for (const p of candidates) {
157
+ if (fs.existsSync(p)) return p;
158
+ }
159
+ } catch { /* ignore */ }
160
+ return null;
161
+ }
162
+
163
+ function readCache() {
164
+ try {
165
+ if (fs.existsSync(CACHE_FILE)) {
166
+ const raw = JSON.parse(fs.readFileSync(CACHE_FILE, 'utf-8'));
167
+ if (raw && raw._ts && (Date.now() - raw._ts) < CACHE_TTL_MS) {
168
+ return raw.data;
169
+ }
170
+ }
171
+ } catch { /* ignore */ }
172
+ return null;
173
+ }
174
+
175
+ function writeCache(data) {
176
+ try { fs.writeFileSync(CACHE_FILE, JSON.stringify({ _ts: Date.now(), data }), 'utf-8'); } catch { /* ignore */ }
177
+ }
178
+
179
+ /**
180
+ * Single source of truth: delegate to the CLI hooks statusline --json command.
181
+ * Falls back to a minimal static object on failure so the statusline still renders.
182
+ *
183
+ * Fix for swarmdo#2195: the previous local readers returned 0 for AgentDB patterns
184
+ * (missed the .swarm/memory.db → AgentDB path), computed dddProgress wrong,
185
+ * and only counted ADRs in v3/implementation/adrs/ (missed v3/docs/adr/).
186
+ */
187
+ function getStatuslineData() {
188
+ // SWARMDO_STATUSLINE_NO_CLI: skip the CLI-delegation fork and render from local
189
+ // reads + stdin only. A cold render otherwise forks the hooks-statusline CLI
190
+ // (now more reachable via the global-node_modules probes below), and that fork
191
+ // can disturb the stdin cost payload; skipping it keeps renders fast, hermetic,
192
+ // and subprocess-free. Set by the statusline tests; usable by anyone who wants
193
+ // a no-fork statusline.
194
+ if (process.env.SWARMDO_STATUSLINE_NO_CLI) return buildLocalFallback();
195
+ const cached = readCache();
196
+ if (cached) return cached;
197
+
198
+ try {
199
+ // #2337: prefer an already-installed CLI bin via direct `node` invocation
200
+ // — no npx, no registry round-trip, no @latest re-resolve per render.
201
+ // Fall back to `npx --prefer-offline @swarmdo/cli` (no @latest) only
202
+ // when nothing is installed locally, so a cold environment still works.
203
+ const cliBin = resolveCliBin();
204
+ const cmd = cliBin
205
+ ? '"' + process.execPath + '" "' + cliBin + '" hooks statusline --json 2>/dev/null'
206
+ : 'npx --prefer-offline @swarmdo/cli hooks statusline --json 2>/dev/null';
207
+ const raw = execSync(
208
+ cmd,
209
+ { encoding: 'utf-8', timeout: 8000, stdio: ['pipe', 'pipe', 'pipe'], cwd: CWD }
210
+ ).trim();
211
+ // The CLI may emit preamble lines before the JSON — find the first '{'.
212
+ const jsonStart = raw.indexOf('{');
213
+ if (jsonStart === -1) throw new Error('no JSON in CLI output');
214
+ const data = JSON.parse(raw.slice(jsonStart));
215
+ // Overlay every block the CLI JSON omits (adrs/agentdb/tests/hooks/integration)
216
+ // with real local reads, so those segments reflect actual state instead of 0.
217
+ applyLocalOverlays(data);
218
+ writeCache(data);
219
+ return data;
220
+ } catch { /* CLI unavailable or timed out */ }
221
+
222
+ // Fallback: use local file probes only (will be less accurate, but non-zero
223
+ // when CLI is available and accurate when it's not).
224
+ return buildLocalFallback();
225
+ }
226
+
227
+ // Count ADRs from BOTH known directories (fix for swarmdo#2195: old code missed
228
+ // v3/docs/adr/ which holds ADR-088..ADR-137, i.e. 41 of the 128 total ADRs).
229
+ function getLocalADRCount() {
230
+ const adrDirs = [
231
+ path.join(CWD, 'v3', 'implementation', 'adrs'),
232
+ path.join(CWD, 'v3', 'docs', 'adr'),
233
+ path.join(CWD, 'docs', 'adrs'),
234
+ path.join(CWD, '.swarmdo', 'adrs'),
235
+ ];
236
+ let total = 0;
237
+ for (const dir of adrDirs) {
238
+ try {
239
+ if (fs.existsSync(dir)) {
240
+ const files = fs.readdirSync(dir).filter(function(f) {
241
+ return f.endsWith('.md') && (f.startsWith('ADR-') || f.startsWith('adr-') || /^\d{4}-/.test(f));
242
+ });
243
+ total += files.length;
244
+ }
245
+ } catch { /* ignore */ }
246
+ }
247
+ return { count: total, implemented: total, compliance: 0 };
248
+ }
71
249
 
72
- // Cross-platform helpers
73
- const isWindows = process.platform === 'win32';
74
- const nullDevice = isWindows ? 'NUL' : '/dev/null';
250
+ // ─── Local overlays for segments the CLI JSON omits ──────────────
251
+ // 'hooks statusline --json' only returns user/v3Progress/security/swarm/system.
252
+ // agentdb/tests/hooks/integration are never populated, so without these overlays
253
+ // they render as a permanent 0. Each reader is cheap and degrades to zeros.
254
+
255
+ // Real AgentDB stats from the local memory DB. Vectors live in .swarm/memory.db
256
+ // (sql.js + HNSW); vector.db is an opaque redb store counted only toward size.
257
+ // One read-only sqlite3 query (mode=ro never takes a write lock the daemon owns).
258
+ function getLocalAgentDB() {
259
+ const result = { vectorCount: 0, dbSizeKB: 0, hasHnsw: false };
260
+ try {
261
+ let bytes = 0;
262
+ for (const f of ['.swarm/memory.db', 'vector.db']) {
263
+ try { bytes += fs.statSync(path.join(CWD, f)).size; } catch { /* missing */ }
264
+ }
265
+ result.dbSizeKB = Math.round(bytes / 1024);
266
+
267
+ const memDb = path.join(CWD, '.swarm', 'memory.db');
268
+ if (fs.existsSync(memDb)) {
269
+ const Q = String.fromCharCode(34);
270
+ const sql = Q + 'SELECT (SELECT COUNT(*) FROM memory_entries WHERE embedding IS NOT NULL)||' + "'|'" + '||(SELECT COUNT(*) FROM vector_indexes);' + Q;
271
+ const out = safeExec("sqlite3 'file:" + memDb + "?mode=ro' " + sql, 1500);
272
+ if (out && out.indexOf('|') !== -1) {
273
+ const parts = out.split('|');
274
+ result.vectorCount = parseInt(parts[0], 10) || 0;
275
+ result.hasHnsw = (parseInt(parts[1], 10) || 0) > 0;
276
+ }
277
+ }
278
+ } catch { /* ignore */ }
279
+ return result;
280
+ }
281
+
282
+ // Count test files via a bounded directory walk (no file reads).
283
+ function getLocalTests() {
284
+ let testFiles = 0;
285
+ function countTests(dir, depth) {
286
+ if ((depth || 0) > 4) return;
287
+ try {
288
+ if (!fs.existsSync(dir)) return;
289
+ for (const e of fs.readdirSync(dir, { withFileTypes: true })) {
290
+ if (e.isDirectory() && !e.name.startsWith('.') && e.name !== 'node_modules') {
291
+ countTests(path.join(dir, e.name), (depth || 0) + 1);
292
+ } else if (e.isFile() && (e.name.includes('.test.') || e.name.includes('.spec.') || e.name.startsWith('test_') || e.name.startsWith('spec_'))) {
293
+ testFiles++;
294
+ }
295
+ }
296
+ } catch { /* ignore */ }
297
+ }
298
+ for (const d of ['tests', 'test', '__tests__', 'src', 'v3']) countTests(path.join(CWD, d));
299
+ return { testFiles, testCases: testFiles * 4 };
300
+ }
301
+
302
+ // Count configured hooks from project .claude/settings.json. Claude Code hooks
303
+ // have no enabled/disabled flag, so every configured hook counts as enabled.
304
+ function getLocalHooks() {
305
+ const result = { enabled: 0, total: 0 };
306
+ try {
307
+ const settings = readJSON(path.join(CWD, '.claude', 'settings.json'));
308
+ const hooks = settings && settings.hooks;
309
+ if (hooks && typeof hooks === 'object') {
310
+ let n = 0;
311
+ for (const ev of Object.keys(hooks)) {
312
+ const groups = hooks[ev];
313
+ if (Array.isArray(groups)) {
314
+ for (const g of groups) {
315
+ if (g && Array.isArray(g.hooks)) n += g.hooks.length;
316
+ }
317
+ }
318
+ }
319
+ result.total = n;
320
+ result.enabled = n;
321
+ }
322
+ } catch { /* ignore */ }
323
+ return result;
324
+ }
325
+
326
+ // Best-effort integration block: DB presence + locally-configured stdio MCP
327
+ // servers (project .mcp.json + global ~/.claude.json). Remote connectors are
328
+ // account-managed and not present in local config, so they are not counted.
329
+ function getLocalIntegration() {
330
+ const integration = { mcpServers: { enabled: 0, total: 0 }, hasDatabase: false };
331
+ try {
332
+ for (const f of ['.swarm/memory.db', 'vector.db']) {
333
+ if (fs.existsSync(path.join(CWD, f))) { integration.hasDatabase = true; break; }
334
+ }
335
+ const names = new Set();
336
+ const projMcp = readJSON(path.join(CWD, '.mcp.json'));
337
+ if (projMcp && projMcp.mcpServers) for (const k of Object.keys(projMcp.mcpServers)) names.add(k);
338
+ const claudeJson = readJSON(path.join(os.homedir(), '.claude.json'));
339
+ if (claudeJson) {
340
+ if (claudeJson.mcpServers) for (const k of Object.keys(claudeJson.mcpServers)) names.add(k);
341
+ const proj = claudeJson.projects && claudeJson.projects[CWD];
342
+ if (proj && proj.mcpServers && !Array.isArray(proj.mcpServers)) {
343
+ for (const k of Object.keys(proj.mcpServers)) names.add(k);
344
+ }
345
+ }
346
+ integration.mcpServers.total = names.size;
347
+ integration.mcpServers.enabled = names.size;
348
+ } catch { /* ignore */ }
349
+ return integration;
350
+ }
351
+
352
+ // Overlay every locally-derived block onto the CLI data (mutates in place).
353
+ function applyLocalOverlays(data) {
354
+ data.adrs = getLocalADRCount();
355
+ data.agentdb = getLocalAgentDB();
356
+ data.tests = getLocalTests();
357
+ data.hooks = getLocalHooks();
358
+ data.integration = getLocalIntegration();
359
+ return data;
360
+ }
361
+
362
+ // Minimal local fallback when the CLI is not installed or times out.
363
+ // Returns a structure that matches the CLI JSON schema so the renderer works.
364
+ function buildLocalFallback() {
365
+ const memMB = Math.floor(process.memoryUsage().heapUsed / 1024 / 1024);
366
+
367
+ return applyLocalOverlays({
368
+ user: { name: 'user', gitBranch: '', modelName: 'Claude Code' },
369
+ v3Progress: { domainsCompleted: 0, totalDomains: 5, dddProgress: 0, patternsLearned: 0, sessionsCompleted: 0 },
370
+ security: { status: 'NONE', critical: 0, high: 0, total: 0 },
371
+ swarm: { activeSwarms: 0, activeAgents: 0, coordinationActive: false },
372
+ system: { memoryMB: memMB, contextPct: 0, intelligencePct: 0 },
373
+ lastUpdated: new Date().toISOString(),
374
+ });
375
+ }
75
376
 
76
377
  // ANSI colors
77
378
  const c = {
@@ -93,512 +394,483 @@ const c = {
93
394
  brightWhite: '\x1b[1;37m',
94
395
  };
95
396
 
96
- // Get user info
97
- function getUserInfo() {
98
- let name = 'user';
99
- let gitBranch = '';
100
- let modelName = 'Unknown';
101
-
102
- // audit_1776853149979: previously used execSync with a shell string. Switched
103
- // to execFileSync('git', argv) on both platforms so there is no shell
104
- // interpretation. Cross-platform behavior is preserved by relying on git's
105
- // own exit code instead of `|| echo` fallbacks: missing repo / no user.name
106
- // throws, caught below, defaults retained.
107
- try {
108
- name = execFileSync('git', ['config', 'user.name'], { encoding: 'utf-8', stdio: ['ignore', 'pipe', 'ignore'] }).trim() || 'user';
109
- } catch { /* keep default */ }
397
+ // Safe execSync with strict timeout (returns empty string on failure)
398
+ function safeExec(cmd, timeoutMs) {
110
399
  try {
111
- gitBranch = execFileSync('git', ['branch', '--show-current'], { encoding: 'utf-8', stdio: ['ignore', 'pipe', 'ignore'] }).trim();
112
- } catch { /* keep empty */ }
400
+ return execSync(cmd, {
401
+ encoding: 'utf-8',
402
+ timeout: timeoutMs || 2000,
403
+ stdio: ['pipe', 'pipe', 'pipe'],
404
+ }).trim();
405
+ } catch {
406
+ return '';
407
+ }
408
+ }
113
409
 
114
- // Auto-detect model from Claude Code's config
410
+ // Safe JSON file reader (returns null on failure)
411
+ function readJSON(filePath) {
115
412
  try {
116
- const homedir = require('os').homedir();
117
- const claudeConfigPath = path.join(homedir, '.claude.json');
118
- if (fs.existsSync(claudeConfigPath)) {
119
- const claudeConfig = JSON.parse(fs.readFileSync(claudeConfigPath, 'utf-8'));
120
- // Try to find lastModelUsage - check current dir and parent dirs
121
- let lastModelUsage = null;
122
- const cwd = process.cwd();
123
- if (claudeConfig.projects) {
124
- // Try exact match first, then check if cwd starts with any project path
125
- for (const [projectPath, projectConfig] of Object.entries(claudeConfig.projects)) {
126
- if (cwd === projectPath || cwd.startsWith(projectPath + '/')) {
127
- lastModelUsage = projectConfig.lastModelUsage;
128
- break;
129
- }
130
- }
131
- }
132
- if (lastModelUsage) {
133
- const modelIds = Object.keys(lastModelUsage);
134
- if (modelIds.length > 0) {
135
- // Take the last model (most recently added to the object)
136
- // Or find the one with most tokens (most actively used this session)
137
- let modelId = modelIds[modelIds.length - 1];
138
- if (modelIds.length > 1) {
139
- // If multiple models, pick the one with most total tokens
140
- let maxTokens = 0;
141
- for (const id of modelIds) {
142
- const usage = lastModelUsage[id];
143
- const total = (usage.inputTokens || 0) + (usage.outputTokens || 0);
144
- if (total > maxTokens) {
145
- maxTokens = total;
146
- modelId = id;
147
- }
148
- }
149
- }
150
- // Parse model ID to human-readable name
151
- if (modelId.includes('opus')) modelName = 'Opus 4.6 (1M context)';
152
- else if (modelId.includes('sonnet')) modelName = 'Sonnet 4.6';
153
- else if (modelId.includes('haiku')) modelName = 'Haiku 4.5';
154
- else modelName = modelId.split('-').slice(1, 3).join(' ');
155
- }
156
- }
413
+ if (fs.existsSync(filePath)) {
414
+ return JSON.parse(fs.readFileSync(filePath, 'utf-8'));
157
415
  }
158
- } catch (e) {
159
- // Fallback to Unknown if can't read config
160
- }
161
-
162
- return { name, gitBranch, modelName };
416
+ } catch { /* ignore */ }
417
+ return null;
163
418
  }
164
419
 
165
- // Get learning stats from intelligence loop data (ADR-050)
166
- function getLearningStats() {
167
- let patterns = 0;
168
- let sessions = 0;
169
- let trajectories = 0;
170
- let edges = 0;
171
- let confidenceMean = 0;
172
- let accessedCount = 0;
173
- let trend = 'STABLE';
174
-
175
- // PRIMARY: Read from intelligence loop data files
176
- const dataDir = path.join(process.cwd(), '.swarmdo', 'data');
420
+ // ─── Git info (pure-Node / single exec — needed for branch display) ──────────
177
421
 
178
- // 1. graph-state.json — authoritative node/edge counts
179
- const graphPath = path.join(dataDir, 'graph-state.json');
180
- if (fs.existsSync(graphPath)) {
181
- try {
182
- const graph = JSON.parse(fs.readFileSync(graphPath, 'utf-8'));
183
- patterns = graph.nodes ? Object.keys(graph.nodes).length : 0;
184
- edges = Array.isArray(graph.edges) ? graph.edges.length : 0;
185
- } catch (e) { /* ignore */ }
186
- }
422
+ function getGitInfo() {
423
+ const result = {
424
+ name: 'user', gitBranch: '', modified: 0, untracked: 0,
425
+ staged: 0, ahead: 0, behind: 0,
426
+ };
187
427
 
188
- // 2. ranked-context.json — confidence and access data
189
- const rankedPath = path.join(dataDir, 'ranked-context.json');
190
- if (fs.existsSync(rankedPath)) {
191
- try {
192
- const ranked = JSON.parse(fs.readFileSync(rankedPath, 'utf-8'));
193
- if (ranked.entries && ranked.entries.length > 0) {
194
- patterns = Math.max(patterns, ranked.entries.length);
195
- let confSum = 0;
196
- let accCount = 0;
197
- for (let i = 0; i < ranked.entries.length; i++) {
198
- confSum += (ranked.entries[i].confidence || 0);
199
- if ((ranked.entries[i].accessCount || 0) > 0) accCount++;
200
- }
201
- confidenceMean = confSum / ranked.entries.length;
202
- accessedCount = accCount;
428
+ const script = [
429
+ 'git config user.name 2>/dev/null || echo user',
430
+ 'echo "---SEP---"',
431
+ 'git branch --show-current 2>/dev/null',
432
+ 'echo "---SEP---"',
433
+ 'git status --porcelain 2>/dev/null',
434
+ 'echo "---SEP---"',
435
+ 'git rev-list --left-right --count HEAD...@{upstream} 2>/dev/null || echo "0 0"',
436
+ ].join('; ');
437
+
438
+ const raw = safeExec("sh -c '" + script + "'", 3000);
439
+ if (!raw) return result;
440
+
441
+ const parts = raw.split('---SEP---').map(function(s) { return s.trim(); });
442
+ if (parts.length >= 4) {
443
+ result.name = parts[0] || 'user';
444
+ result.gitBranch = parts[1] || '';
445
+
446
+ if (parts[2]) {
447
+ for (const line of parts[2].split('\n')) {
448
+ if (!line || line.length < 2) continue;
449
+ const x = line[0], y = line[1];
450
+ if (x === '?' && y === '?') { result.untracked++; continue; }
451
+ if (x !== ' ' && x !== '?') result.staged++;
452
+ if (y !== ' ' && y !== '?') result.modified++;
203
453
  }
204
- } catch (e) { /* ignore */ }
205
- }
454
+ }
206
455
 
207
- // 3. intelligence-snapshot.json trend history
208
- const snapshotPath = path.join(dataDir, 'intelligence-snapshot.json');
209
- if (fs.existsSync(snapshotPath)) {
210
- try {
211
- const snapshot = JSON.parse(fs.readFileSync(snapshotPath, 'utf-8'));
212
- if (snapshot.history && snapshot.history.length >= 2) {
213
- const first = snapshot.history[0];
214
- const last = snapshot.history[snapshot.history.length - 1];
215
- const confDrift = (last.confidenceMean || 0) - (first.confidenceMean || 0);
216
- trend = confDrift > 0.01 ? 'IMPROVING' : confDrift < -0.01 ? 'DECLINING' : 'STABLE';
217
- sessions = Math.max(sessions, snapshot.history.length);
218
- }
219
- } catch (e) { /* ignore */ }
456
+ const ab = (parts[3] || '0 0').split(/\s+/);
457
+ result.ahead = parseInt(ab[0]) || 0;
458
+ result.behind = parseInt(ab[1]) || 0;
220
459
  }
221
460
 
222
- // 4. auto-memory-store.json — fallback entry count
223
- if (patterns === 0) {
224
- const autoMemPath = path.join(dataDir, 'auto-memory-store.json');
225
- if (fs.existsSync(autoMemPath)) {
226
- try {
227
- const data = JSON.parse(fs.readFileSync(autoMemPath, 'utf-8'));
228
- patterns = Array.isArray(data) ? data.length : (data.entries ? data.entries.length : 0);
229
- } catch (e) { /* ignore */ }
230
- }
231
- }
461
+ return result;
462
+ }
232
463
 
233
- // FALLBACK: Legacy memory.db file-size estimation
234
- if (patterns === 0) {
235
- const memoryPaths = [
236
- path.join(process.cwd(), '.swarm', 'memory.db'),
237
- path.join(process.cwd(), '.claude', 'memory.db'),
238
- path.join(process.cwd(), 'data', 'memory.db'),
239
- ];
240
- for (let j = 0; j < memoryPaths.length; j++) {
241
- if (fs.existsSync(memoryPaths[j])) {
242
- try {
243
- const dbStats = fs.statSync(memoryPaths[j]);
244
- patterns = Math.floor(dbStats.size / 1024 / 2);
464
+ // Detect model name from Claude config (pure file reads, no exec)
465
+ function getModelName() {
466
+ try {
467
+ const claudeConfig = readJSON(path.join(os.homedir(), '.claude.json'));
468
+ if (claudeConfig && claudeConfig.projects) {
469
+ for (const [projectPath, projectConfig] of Object.entries(claudeConfig.projects)) {
470
+ if (CWD === projectPath || CWD.startsWith(projectPath + '/')) {
471
+ const usage = projectConfig.lastModelUsage;
472
+ if (usage) {
473
+ const ids = Object.keys(usage);
474
+ if (ids.length > 0) {
475
+ let modelId = ids[ids.length - 1];
476
+ let latest = 0;
477
+ for (const id of ids) {
478
+ const ts = usage[id] && usage[id].lastUsedAt ? new Date(usage[id].lastUsedAt).getTime() : 0;
479
+ if (ts > latest) { latest = ts; modelId = id; }
480
+ }
481
+ if (modelId.includes('opus')) return 'Opus 4.8';
482
+ if (modelId.includes('sonnet')) return 'Sonnet 4.6';
483
+ if (modelId.includes('haiku')) return 'Haiku 4.5';
484
+ return modelId.split('-').slice(1, 3).join(' ');
485
+ }
486
+ }
245
487
  break;
246
- } catch (e) { /* ignore */ }
488
+ }
247
489
  }
248
490
  }
491
+ } catch { /* ignore */ }
492
+
493
+ // Fallback: settings.json model field
494
+ const settings = getSettings();
495
+ if (settings && settings.model) {
496
+ const m = settings.model;
497
+ if (m.includes('opus')) return 'Opus 4.8';
498
+ if (m.includes('sonnet')) return 'Sonnet 4.6';
499
+ if (m.includes('haiku')) return 'Haiku 4.5';
249
500
  }
501
+ return 'Claude Code';
502
+ }
250
503
 
251
- // Session count from session files
252
- const sessionsPath = path.join(process.cwd(), '.claude', 'sessions');
253
- if (fs.existsSync(sessionsPath)) {
504
+ // ─── Stdin reader (Claude Code pipes session JSON) ──────────────
505
+ // Claude Code sends session JSON via stdin. Read synchronously so the
506
+ // script works both when invoked by Claude Code (stdin has JSON) and
507
+ // when run manually from terminal (stdin is empty/tty).
508
+ let _stdinData = null;
509
+ function getStdinData() {
510
+ if (_stdinData !== undefined && _stdinData !== null) return _stdinData;
511
+ try {
512
+ if (process.stdin.isTTY) { _stdinData = null; return null; }
513
+ const chunks = [];
514
+ const buf = Buffer.alloc(4096);
515
+ let bytesRead;
254
516
  try {
255
- const sessionFiles = fs.readdirSync(sessionsPath).filter(f => f.endsWith('.json'));
256
- sessions = Math.max(sessions, sessionFiles.length);
257
- } catch (e) { /* ignore */ }
517
+ while ((bytesRead = fs.readSync(0, buf, 0, buf.length, null)) > 0) {
518
+ chunks.push(buf.slice(0, bytesRead));
519
+ }
520
+ } catch { /* EOF or read error */ }
521
+ const raw = Buffer.concat(chunks).toString('utf-8').trim();
522
+ _stdinData = (raw && raw.startsWith('{')) ? JSON.parse(raw) : null;
523
+ } catch {
524
+ _stdinData = null;
258
525
  }
259
-
260
- trajectories = Math.floor(patterns / 5);
261
-
262
- return { patterns, sessions, trajectories, edges, confidenceMean, accessedCount, trend };
526
+ return _stdinData;
263
527
  }
264
528
 
265
- // Get V3 progress from learning state (grows as system learns)
266
- function getV3Progress() {
267
- const learning = getLearningStats();
268
-
269
- // DDD progress based on actual learned patterns
270
- // New install: 0 patterns = 0/5 domains, 0% DDD
271
- // As patterns grow: 10+ patterns = 1 domain, 50+ = 2, 100+ = 3, 200+ = 4, 500+ = 5
272
- let domainsCompleted = 0;
273
- if (learning.patterns >= 500) domainsCompleted = 5;
274
- else if (learning.patterns >= 200) domainsCompleted = 4;
275
- else if (learning.patterns >= 100) domainsCompleted = 3;
276
- else if (learning.patterns >= 50) domainsCompleted = 2;
277
- else if (learning.patterns >= 10) domainsCompleted = 1;
278
-
279
- const totalDomains = 5;
280
- const dddProgress = Math.min(100, Math.floor((domainsCompleted / totalDomains) * 100));
281
-
282
- return {
283
- domainsCompleted,
284
- totalDomains,
285
- dddProgress,
286
- patternsLearned: learning.patterns,
287
- sessionsCompleted: learning.sessions
288
- };
529
+ function getModelFromStdin() {
530
+ const data = getStdinData();
531
+ return (data && data.model && data.model.display_name) ? data.model.display_name : null;
289
532
  }
290
533
 
291
- // Get security status based on actual scans
292
- function getSecurityStatus() {
293
- // Check for security scan results in memory
294
- const scanResultsPath = path.join(process.cwd(), '.claude', 'security-scans');
295
- let cvesFixed = 0;
296
- const totalCves = 3;
297
-
298
- if (fs.existsSync(scanResultsPath)) {
299
- try {
300
- const scans = fs.readdirSync(scanResultsPath).filter(f => f.endsWith('.json'));
301
- // Each successful scan file = 1 CVE addressed
302
- cvesFixed = Math.min(totalCves, scans.length);
303
- } catch (e) {
304
- // Ignore
305
- }
306
- }
307
-
308
- // Also check .swarm/security for audit results
309
- const auditPath = path.join(process.cwd(), '.swarm', 'security');
310
- if (fs.existsSync(auditPath)) {
311
- try {
312
- const audits = fs.readdirSync(auditPath).filter(f => f.includes('audit'));
313
- cvesFixed = Math.min(totalCves, Math.max(cvesFixed, audits.length));
314
- } catch (e) {
315
- // Ignore
316
- }
534
+ function getContextFromStdin() {
535
+ const data = getStdinData();
536
+ if (data && data.context_window) {
537
+ return { usedPct: Math.floor(data.context_window.used_percentage || 0) };
317
538
  }
318
-
319
- const status = cvesFixed >= totalCves ? 'CLEAN' : cvesFixed > 0 ? 'IN_PROGRESS' : 'PENDING';
320
-
321
- return {
322
- status,
323
- cvesFixed,
324
- totalCves,
325
- };
539
+ return null;
326
540
  }
327
541
 
328
- // Get swarm status
329
- function getSwarmStatus() {
330
- let activeAgents = 0;
331
- let coordinationActive = false;
332
-
333
- try {
334
- if (isWindows) {
335
- // Windows: use tasklist and findstr
336
- const ps = execSync('tasklist 2>NUL | findstr /I "agentic-flow" 2>NUL | find /C /V "" 2>NUL || echo 0', { encoding: 'utf-8' });
337
- activeAgents = Math.max(0, parseInt(ps.trim()) || 0);
338
- } else {
339
- const ps = execSync('ps aux 2>/dev/null | grep -c agentic-flow || echo "0"', { encoding: 'utf-8' });
340
- activeAgents = Math.max(0, parseInt(ps.trim()) - 1);
341
- }
342
- coordinationActive = activeAgents > 0;
343
- } catch (e) {
344
- // Ignore errors - default to 0 agents
542
+ function getCostFromStdin() {
543
+ const data = getStdinData();
544
+ if (data && data.cost) {
545
+ const durationMs = data.cost.total_duration_ms || 0;
546
+ const mins = Math.floor(durationMs / 60000);
547
+ const secs = Math.floor((durationMs % 60000) / 1000);
548
+ return {
549
+ costUsd: data.cost.total_cost_usd || 0,
550
+ duration: mins > 0 ? mins + 'm' + secs + 's' : secs + 's',
551
+ };
345
552
  }
346
-
347
- return {
348
- activeAgents,
349
- maxAgents: CONFIG.maxAgents,
350
- coordinationActive,
351
- };
553
+ return null;
352
554
  }
353
555
 
354
- // Get system metrics (dynamic based on actual state)
355
- function getSystemMetrics() {
356
- let memoryMB = 0;
357
- let subAgents = 0;
358
-
556
+ // ── Account plan + rate-limit windows (for the plan-aware cost slot) ──────────
557
+ // ~/.claude.json holds only the CURRENTLY active account (overwritten on switch),
558
+ // so we read it live each render — that makes the slot correct across account
559
+ // switches without any stored state. We read a few plan fields, never tokens.
560
+ function readClaudeAccount() {
359
561
  try {
360
- if (isWindows) {
361
- // Windows: use tasklist for memory info, fallback to process.memoryUsage
362
- // tasklist memory column is complex to parse, use Node.js API instead
363
- memoryMB = Math.floor(process.memoryUsage().heapUsed / 1024 / 1024);
364
- } else {
365
- const mem = execSync('ps aux | grep -E "(node|agentic|claude)" | grep -v grep | awk \'{sum += $6} END {print int(sum/1024)}\'', { encoding: 'utf-8' });
366
- memoryMB = parseInt(mem.trim()) || 0;
562
+ const j = JSON.parse(fs.readFileSync(path.join(os.homedir(), '.claude.json'), 'utf8'));
563
+ const a = j && j.oauthAccount;
564
+ if (a && typeof a === 'object') {
565
+ return { billingType: a.billingType, organizationType: a.organizationType, displayName: a.displayName, organizationName: a.organizationName };
367
566
  }
368
- } catch (e) {
369
- // Fallback
370
- memoryMB = Math.floor(process.memoryUsage().heapUsed / 1024 / 1024);
371
- }
372
-
373
- // Get learning stats for intelligence %
374
- const learning = getLearningStats();
375
-
376
- // Intelligence % from REAL intelligence loop data (ADR-050)
377
- // Composite: 40% confidence mean + 30% access ratio + 30% pattern density
378
- let intelligencePct = 0;
379
- if (learning.confidenceMean > 0 || (learning.patterns > 0 && learning.accessedCount > 0)) {
380
- const confScore = Math.min(100, Math.floor(learning.confidenceMean * 100));
381
- const accessRatio = learning.patterns > 0 ? (learning.accessedCount / learning.patterns) : 0;
382
- const accessScore = Math.min(100, Math.floor(accessRatio * 100));
383
- const densityScore = Math.min(100, Math.floor(learning.patterns / 5));
384
- intelligencePct = Math.floor(confScore * 0.4 + accessScore * 0.3 + densityScore * 0.3);
567
+ } catch (e) { /* not logged in / unreadable */ }
568
+ return null;
569
+ }
570
+ function detectPlan(a) {
571
+ if (!a || typeof a !== 'object') return 'unknown';
572
+ const b = typeof a.billingType === 'string' ? a.billingType : '';
573
+ const o = typeof a.organizationType === 'string' ? a.organizationType : '';
574
+ if (/subscription/i.test(b) || /claude_(max|team|enterprise|pro)/i.test(o)) return 'subscription';
575
+ if (b) return 'payg';
576
+ return 'unknown';
577
+ }
578
+ function accountLabel(a) {
579
+ if (!a) return '';
580
+ let raw = (typeof a.displayName === 'string' && a.displayName) || (typeof a.organizationName === 'string' && a.organizationName) || '';
581
+ raw = String(raw).trim();
582
+ if (!raw) return '';
583
+ return raw.length > 16 ? raw.slice(0, 15) + '…' : raw;
584
+ }
585
+ function rlToEpochMs(v) {
586
+ if (typeof v === 'number' && isFinite(v)) return v < 1e12 ? v * 1000 : v;
587
+ if (typeof v === 'string') { const t = Date.parse(v); return isNaN(t) ? null : t; }
588
+ return null;
589
+ }
590
+ function rlPickWindow(obj, windowMs, label) {
591
+ if (!obj || typeof obj !== 'object') return null;
592
+ const used = (obj.used_percentage != null ? obj.used_percentage : obj.usedPercentage);
593
+ const resetsAtMs = rlToEpochMs(obj.resets_at != null ? obj.resets_at : obj.resetsAt);
594
+ if (typeof used !== 'number' || !isFinite(used) || resetsAtMs === null) return null;
595
+ return { label: label, used: Math.max(0, Math.min(100, used)), windowMs: windowMs, resetsAtMs: resetsAtMs };
596
+ }
597
+ function getRateLimitWindows() {
598
+ const data = getStdinData();
599
+ if (!data) return [];
600
+ const rl = data.rate_limits || data.rateLimits || data;
601
+ if (!rl || typeof rl !== 'object') return [];
602
+ const out = [];
603
+ const w5 = rlPickWindow(rl.five_hour || rl.fiveHour, 5 * 60 * 60 * 1000, '5h');
604
+ const w7 = rlPickWindow(rl.seven_day || rl.sevenDay, 7 * 24 * 60 * 60 * 1000, '7d');
605
+ if (w5) out.push(w5);
606
+ if (w7) out.push(w7);
607
+ return out;
608
+ }
609
+ function rlWindowStatus(w, nowMs) {
610
+ if (w.used >= 100) return 'over';
611
+ const windowStart = w.resetsAtMs - w.windowMs;
612
+ const elapsed = nowMs - windowStart;
613
+ let willExhaust = false;
614
+ if (w.used > 0 && elapsed > 0) {
615
+ const msTo100 = ((100 - w.used) * elapsed) / w.used;
616
+ willExhaust = (nowMs + msTo100) <= w.resetsAtMs;
385
617
  }
386
- // Fallback: legacy pattern count
387
- if (intelligencePct === 0 && learning.patterns > 0) {
388
- intelligencePct = Math.min(100, Math.floor(learning.patterns / 10));
618
+ return (willExhaust || w.used >= 80) ? 'warn' : 'ok';
619
+ }
620
+ // { text, color } for the rate-limit cost slot, or null when Claude Code sent
621
+ // no rate_limits payload (older CC / non-subscription session).
622
+ function renderRateLimitSlot() {
623
+ const wins = getRateLimitWindows();
624
+ if (wins.length === 0) return null;
625
+ const now = Date.now();
626
+ let worst = 'ok';
627
+ const parts = [];
628
+ for (const w of wins) {
629
+ const st = rlWindowStatus(w, now);
630
+ if (st === 'over') worst = 'over';
631
+ else if (st === 'warn' && worst !== 'over') worst = 'warn';
632
+ const mark = st === 'over' ? '!' : st === 'warn' ? '⚠' : '';
633
+ parts.push(w.label + ' ' + Math.round(w.used) + '%' + mark);
389
634
  }
635
+ const color = worst === 'over' ? c.brightRed : worst === 'warn' ? c.brightYellow : c.brightGreen;
636
+ return { text: parts.join(' · '), color: color };
637
+ }
390
638
 
391
- // Context % based on session history
392
- const contextPct = Math.min(100, Math.floor(learning.sessions * 5));
393
-
394
- // Count active sub-agents from process list
639
+ // Read package version from the first package.json we find. The fallback
640
+ // (`BAKED_CLI_VERSION`) is the version of @swarmdo/cli that generated THIS
641
+ // statusline.cjs at `swarmdo init` time — so even when every probe below
642
+ // misses, the displayed version is meaningful (matches what the user
643
+ // installed), not a stale hard-coded string.
644
+ function getPkgVersion() {
645
+ let ver = '1.58.9';
395
646
  try {
396
- if (isWindows) {
397
- // Windows: use tasklist and findstr for agent counting
398
- const agents = execSync('tasklist 2>NUL | findstr /I "swarmdo" 2>NUL | find /C /V "" 2>NUL || echo 0', { encoding: 'utf-8' });
399
- subAgents = Math.max(0, parseInt(agents.trim()) || 0);
400
- } else {
401
- const agents = execSync('ps aux 2>/dev/null | grep -c "swarmdo.*agent" || echo "0"', { encoding: 'utf-8' });
402
- subAgents = Math.max(0, parseInt(agents.trim()) - 1);
647
+ const home = os.homedir();
648
+ const pkgPaths = [
649
+ path.join(home, '.claude', 'plugins', 'marketplaces', 'swarmdo', 'package.json'),
650
+ path.join(CWD, 'node_modules', '@swarmdo', 'cli', 'package.json'),
651
+ path.join(CWD, 'node_modules', 'swarmdo', 'package.json'),
652
+ path.join(CWD, 'v3', '@swarmdo', 'cli', 'package.json'),
653
+ ];
654
+ // #2221: global installs (npm i -g swarmdo) live outside CWD/node_modules, so the
655
+ // probes above all miss and the version falls back to the hard-coded default.
656
+ // Derive the global node_modules dir from the running node binary (no npm spawn —
657
+ // statusline renders often). Covers nvm/mise (bin/../lib/node_modules) and Windows
658
+ // (bin/node_modules) layouts.
659
+ try {
660
+ const binDir = path.dirname(process.execPath);
661
+ const globalModuleDirs = [path.join(binDir, '..', 'lib', 'node_modules'), path.join(binDir, 'node_modules'), '/opt/homebrew/lib/node_modules', '/usr/local/lib/node_modules'];
662
+ // #2221 follow-up: a custom npm prefix (e.g. ~/.npm-global) is decoupled from
663
+ // the node binary location, so the binDir-derived probes above all miss. Also
664
+ // probe the npm prefix from the environment and the common ~/.npm-global default.
665
+ for (const prefix of [process.env.npm_config_prefix, process.env.PREFIX, path.join(home, '.npm-global')]) {
666
+ if (prefix) globalModuleDirs.push(path.join(prefix, 'lib', 'node_modules'));
667
+ }
668
+ for (const gm of globalModuleDirs) {
669
+ pkgPaths.push(
670
+ path.join(gm, 'swarmdo', 'package.json'),
671
+ path.join(gm, '@swarmdo', 'cli', 'package.json'),
672
+ );
673
+ }
674
+ } catch { /* ignore */ }
675
+ for (const p of pkgPaths) {
676
+ if (!fs.existsSync(p)) continue;
677
+ try {
678
+ const pkg = JSON.parse(fs.readFileSync(p, 'utf-8'));
679
+ if (pkg && typeof pkg.version === 'string' && pkg.version.length > 0) { ver = pkg.version; break; }
680
+ } catch { /* ignore */ }
403
681
  }
404
- } catch (e) {
405
- // Ignore - default to 0
406
- }
407
-
408
- return {
409
- memoryMB,
410
- contextPct,
411
- intelligencePct,
412
- subAgents,
413
- };
682
+ } catch { /* ignore */ }
683
+ return ver;
414
684
  }
415
685
 
416
- // Generate progress bar
686
+ // ─── Rendering ──────────────────────────────────────────────────
687
+
417
688
  function progressBar(current, total) {
418
689
  const width = 5;
419
690
  const filled = Math.round((current / total) * width);
420
- const empty = width - filled;
421
- return '[' + '\u25CF'.repeat(filled) + '\u25CB'.repeat(empty) + ']';
691
+ return '[' + '●'.repeat(filled) + '○'.repeat(width - filled) + ']';
422
692
  }
423
693
 
424
- // Generate full statusline
425
694
  function generateStatusline() {
426
- const user = getUserInfo();
427
- const progress = getV3Progress();
428
- const security = getSecurityStatus();
429
- const swarm = getSwarmStatus();
430
- const system = getSystemMetrics();
695
+ const d = getStatuslineData();
696
+ const git = getGitInfo();
697
+ const modelName = getModelFromStdin() || (d.user && d.user.modelName) || 'Claude Code';
698
+ const ctxInfo = getContextFromStdin();
699
+ const costInfo = getCostFromStdin();
700
+ const pkgVersion = getPkgVersion();
701
+
702
+ const progress = d.v3Progress || {};
703
+ const security = d.security || {};
704
+ const swarm = d.swarm || {};
705
+ const system = d.system || {};
706
+ const adrs = d.adrs || {};
707
+ const hooks = d.hooks || {};
708
+ const agentdb = d.agentdb || {};
709
+ const tests = d.tests || {};
710
+
711
+ const domainsCompleted = progress.domainsCompleted || 0;
712
+ const totalDomains = progress.totalDomains || 5;
713
+ const dddProgress = progress.dddProgress || 0;
714
+ const patternsLearned = progress.patternsLearned || 0;
715
+ const activeSwarms = swarm.activeSwarms || 0;
716
+ const activeAgents = swarm.activeAgents || 0;
717
+ const coordinationActive = swarm.coordinationActive || false;
718
+ const intelligencePct = system.intelligencePct || 0;
719
+ const memoryMB = system.memoryMB || 0;
720
+ const secCritHigh = (security.critical || 0) + (security.high || 0);
721
+ const secStatus = security.status || 'NONE';
722
+ const secLabel = secStatus === 'VULN' ? 'sec ' + secCritHigh + '!' : secStatus === 'CLEAN' ? 'sec ✓' : secStatus === 'STALE' ? 'sec stale' : 'sec —';
723
+ const adrCount = adrs.count || 0;
724
+ const adrImpl = adrs.implemented || 0;
725
+ const hooksEnabled = hooks.enabled || 0;
726
+ const hooksTotal = hooks.total || 0;
727
+ const vectorCount = agentdb.vectorCount || 0;
728
+ const hasHnsw = agentdb.hasHnsw || false;
729
+ const dbSizeKB = agentdb.dbSizeKB || 0;
730
+ const testFiles = tests.testFiles || 0;
731
+ const testCases = tests.testCases || testFiles * 4;
732
+
431
733
  const lines = [];
432
734
 
433
- // Header Line
434
- let header = `${c.bold}${c.brightPurple}▊ Swarmdo V${SWARMDO_VERSION} ${c.reset}`;
435
- header += `${swarm.coordinationActive ? c.brightCyan : c.dim}● ${c.brightCyan}${user.name}${c.reset}`;
436
- if (user.gitBranch) {
437
- header += ` ${c.dim}│${c.reset} ${c.brightBlue}⎇ ${user.gitBranch}${c.reset}`;
735
+ // Header
736
+ let header = '';
737
+ if (seg('version')) header += c.bold + c.brightPurple + '▊ Swarmdo V' + pkgVersion + ' ' + c.reset;
738
+ if (seg('project')) header += (coordinationActive ? c.brightCyan : c.dim) + '● ' + c.brightCyan + git.name + c.reset;
739
+ if (seg('branch') && git.gitBranch) {
740
+ header += ' ' + c.dim + '│' + c.reset + ' ' + c.brightBlue + '⏇ ' + git.gitBranch + c.reset;
741
+ const changes = git.modified + git.staged + git.untracked;
742
+ if (changes > 0) {
743
+ let ind = '';
744
+ if (git.staged > 0) ind += c.brightGreen + '+' + git.staged + c.reset;
745
+ if (git.modified > 0) ind += c.brightYellow + '~' + git.modified + c.reset;
746
+ if (git.untracked > 0) ind += c.dim + '?' + git.untracked + c.reset;
747
+ header += ' ' + ind;
748
+ }
749
+ if (git.ahead > 0) header += ' ' + c.brightGreen + '↑' + git.ahead + c.reset;
750
+ if (git.behind > 0) header += ' ' + c.brightRed + '↓' + git.behind + c.reset;
751
+ }
752
+ if (seg('model')) header += ' ' + c.dim + '│' + c.reset + ' ' + c.purple + modelName + c.reset;
753
+ const duration = costInfo ? costInfo.duration : '';
754
+ if (seg('duration') && duration) header += ' ' + c.dim + '│' + c.reset + ' ' + c.cyan + '⏱ ' + duration + c.reset;
755
+ if (seg('context') && ctxInfo && ctxInfo.usedPct > 0) {
756
+ const ctxColor = ctxInfo.usedPct >= 90 ? c.brightRed : ctxInfo.usedPct >= 70 ? c.brightYellow : c.brightGreen;
757
+ header += ' ' + c.dim + '│' + c.reset + ' ' + ctxColor + '● ' + ctxInfo.usedPct + '% ctx' + c.reset;
758
+ }
759
+ if (seg('cost') && COST_OPTS.mode !== 'off') {
760
+ const acct = readClaudeAccount();
761
+ let eff = COST_OPTS.mode;
762
+ if (eff === 'auto') eff = detectPlan(acct) === 'subscription' ? 'limits' : 'dollars';
763
+ let slotText = null;
764
+ let slotColor = c.brightYellow;
765
+ if (eff === 'limits') {
766
+ const rl = renderRateLimitSlot();
767
+ if (rl) { slotText = rl.text; slotColor = rl.color; }
768
+ else if (costInfo && costInfo.costUsd > 0) { slotText = CONFIG.costSymbol + costInfo.costUsd.toFixed(2); } // no payload → $ fallback
769
+ } else if (costInfo && costInfo.costUsd > 0) {
770
+ slotText = CONFIG.costSymbol + costInfo.costUsd.toFixed(2);
771
+ }
772
+ if (slotText) {
773
+ let prefix = '';
774
+ if (COST_OPTS.showAccount) { const lbl = accountLabel(acct); if (lbl) prefix = c.dim + lbl + ' · ' + c.reset; }
775
+ header += ' ' + c.dim + '│' + c.reset + ' ' + prefix + slotColor + slotText + c.reset;
776
+ }
438
777
  }
439
- header += ` ${c.dim}│${c.reset} ${c.purple}${user.modelName}${c.reset}`;
440
- lines.push(header);
441
-
442
- // Separator
443
- lines.push(`${c.dim}─────────────────────────────────────────────────────${c.reset}`);
444
-
445
- // Line 1: DDD Domain Progress
446
- const domainsColor = progress.domainsCompleted >= 3 ? c.brightGreen : progress.domainsCompleted > 0 ? c.yellow : c.red;
447
- lines.push(
448
- `${c.brightCyan}🏗️ DDD Domains${c.reset} ${progressBar(progress.domainsCompleted, progress.totalDomains)} ` +
449
- `${domainsColor}${progress.domainsCompleted}${c.reset}/${c.brightWhite}${progress.totalDomains}${c.reset} ` +
450
- `${c.brightYellow}1.0x${c.reset} ${c.dim}→${c.reset} ${c.brightYellow}2.49x-7.47x${c.reset}`
778
+ if (header) lines.push(header);
779
+
780
+ // Separator — only when a body section follows a rendered header
781
+ const anyBody = seg('domains') || seg('swarm') || seg('architecture') || seg('agentdb');
782
+ if (header && anyBody) lines.push(c.dim + '─'.repeat(53) + c.reset);
783
+
784
+ // Line 1: DDD Domains
785
+ const domainsColor = domainsCompleted >= 3 ? c.brightGreen : domainsCompleted > 0 ? c.yellow : c.red;
786
+ let perfIndicator;
787
+ if (hasHnsw && vectorCount > 0) {
788
+ const speedup = vectorCount > 10000 ? '12500x' : vectorCount > 1000 ? '150x' : '10x';
789
+ perfIndicator = c.brightGreen + 'HNSW ' + speedup + c.reset;
790
+ } else if (patternsLearned > 0) {
791
+ const pk = patternsLearned >= 1000 ? (patternsLearned / 1000).toFixed(1) + 'k' : String(patternsLearned);
792
+ perfIndicator = c.brightYellow + '📚 ' + pk + ' patterns' + c.reset;
793
+ } else {
794
+ perfIndicator = c.dim + '⚡ target: 150x-12500x' + c.reset;
795
+ }
796
+ if (seg('domains')) lines.push(
797
+ c.brightCyan + '🏗️ DDD Domains' + c.reset + ' ' + progressBar(domainsCompleted, totalDomains) + ' ' +
798
+ domainsColor + domainsCompleted + c.reset + '/' + c.brightWhite + totalDomains + c.reset + ' ' + perfIndicator
799
+ );
800
+
801
+ // Line 2: Swarm + Hooks + CVE + Memory + Intelligence
802
+ const swarmsColor = activeSwarms > 0 ? c.brightGreen : c.dim;
803
+ const agentsColor = activeAgents > 0 ? c.brightGreen : c.dim;
804
+ const secIcon = secStatus === 'CLEAN' ? '🟢' : secStatus === 'STALE' ? '🟡' : (secStatus === 'NONE' ? '⚪' : '🔴');
805
+ const secColor = secStatus === 'CLEAN' ? c.brightGreen : secStatus === 'STALE' ? c.brightYellow : (secStatus === 'NONE' ? c.dim : c.brightRed);
806
+ const hooksColor = hooksEnabled > 0 ? c.brightGreen : c.dim;
807
+ const intellColor = intelligencePct >= 80 ? c.brightGreen : intelligencePct >= 40 ? c.brightYellow : c.dim;
808
+
809
+ if (seg('swarm')) lines.push(
810
+ c.brightYellow + '🐝 Swarms' + c.reset + ' ' + swarmsColor + activeSwarms + c.reset + ' ' +
811
+ c.brightYellow + '🤖 Agents' + c.reset + ' ' + agentsColor + activeAgents + c.reset + ' ' +
812
+ c.brightBlue + '🪝 ' + hooksColor + hooksEnabled + c.reset + '/' + c.brightWhite + hooksTotal + c.reset + ' ' +
813
+ secIcon + ' ' + secColor + secLabel + c.reset + ' ' +
814
+ c.brightCyan + '💾 ' + memoryMB + 'MB' + c.reset + ' ' +
815
+ intellColor + '🧠 ' + String(intelligencePct).padStart(3) + '%' + c.reset
451
816
  );
452
817
 
453
- // Line 2: Swarm + CVE + Memory + Context + Intelligence
454
- const swarmIndicator = swarm.coordinationActive ? `${c.brightGreen}◉${c.reset}` : `${c.dim}○${c.reset}`;
455
- const agentsColor = swarm.activeAgents > 0 ? c.brightGreen : c.red;
456
- let securityIcon = security.status === 'CLEAN' ? '🟢' : security.status === 'IN_PROGRESS' ? '🟡' : '🔴';
457
- let securityColor = security.status === 'CLEAN' ? c.brightGreen : security.status === 'IN_PROGRESS' ? c.brightYellow : c.brightRed;
458
-
459
- lines.push(
460
- `${c.brightYellow}🤖 Swarm${c.reset} ${swarmIndicator} [${agentsColor}${String(swarm.activeAgents).padStart(2)}${c.reset}/${c.brightWhite}${swarm.maxAgents}${c.reset}] ` +
461
- `${c.brightPurple}👥 ${system.subAgents}${c.reset} ` +
462
- `${securityIcon} ${securityColor}CVE ${security.cvesFixed}${c.reset}/${c.brightWhite}${security.totalCves}${c.reset} ` +
463
- `${c.brightCyan}💾 ${system.memoryMB}MB${c.reset} ` +
464
- `${c.brightGreen}📂 ${String(system.contextPct).padStart(3)}%${c.reset} ` +
465
- `${c.dim}🧠 ${String(system.intelligencePct).padStart(3)}%${c.reset}`
818
+ // Line 3: Architecture
819
+ const dddColor = dddProgress >= 50 ? c.brightGreen : dddProgress > 0 ? c.yellow : c.red;
820
+ const adrColor = adrCount > 0 ? (adrImpl === adrCount ? c.brightGreen : c.yellow) : c.dim;
821
+ const adrDisplay = adrColor + '' + adrImpl + '/' + adrCount + c.reset;
822
+
823
+ if (seg('architecture')) lines.push(
824
+ c.brightPurple + '🔧 Architecture' + c.reset + ' ' +
825
+ c.cyan + 'ADRs' + c.reset + ' ' + adrDisplay + ' ' + c.dim + '│' + c.reset + ' ' +
826
+ c.cyan + 'DDD' + c.reset + ' ' + dddColor + '●' + String(dddProgress).padStart(3) + '%' + c.reset + ' ' + c.dim + '│' + c.reset + ' ' +
827
+ c.cyan + 'Security' + c.reset + ' ' + secColor + '●' + secStatus + c.reset
466
828
  );
467
829
 
468
- // Line 3: Architecture status
469
- const dddColor = progress.dddProgress >= 50 ? c.brightGreen : progress.dddProgress > 0 ? c.yellow : c.red;
470
- lines.push(
471
- `${c.brightPurple}🔧 Architecture${c.reset} ` +
472
- `${c.cyan}DDD${c.reset} ${dddColor}●${String(progress.dddProgress).padStart(3)}%${c.reset} ${c.dim}│${c.reset} ` +
473
- `${c.cyan}Security${c.reset} ${securityColor}●${security.status}${c.reset} ${c.dim}│${c.reset} ` +
474
- `${c.cyan}Memory${c.reset} ${c.brightGreen}●AgentDB${c.reset} ${c.dim}│${c.reset} ` +
475
- `${c.cyan}Integration${c.reset} ${swarm.coordinationActive ? c.brightCyan : c.dim}●${c.reset}`
830
+ // Line 4: AgentDB, Tests, Integration
831
+ const hnswInd = hasHnsw ? c.brightGreen + '⚡' + c.reset : '';
832
+ const sizeDisp = dbSizeKB >= 1024 ? (dbSizeKB / 1024).toFixed(1) + 'MB' : dbSizeKB + 'KB';
833
+ const vectorColor = vectorCount > 0 ? c.brightGreen : c.dim;
834
+ const testColor = testFiles > 0 ? c.brightGreen : c.dim;
835
+
836
+ // MCP / DB integration from data
837
+ const integration = d.integration || {};
838
+ const mcpServers = (integration.mcpServers) || {};
839
+ let integStr = '';
840
+ if (mcpServers.total > 0) {
841
+ const mcpCol = mcpServers.enabled === mcpServers.total ? c.brightGreen : mcpServers.enabled > 0 ? c.brightYellow : c.red;
842
+ integStr += c.cyan + 'MCP' + c.reset + ' ' + mcpCol + '●' + mcpServers.enabled + '/' + mcpServers.total + c.reset;
843
+ }
844
+ if (integration.hasDatabase) integStr += (integStr ? ' ' : '') + c.brightGreen + '◆' + c.reset + 'DB';
845
+ if (!integStr) integStr = c.dim + '● none' + c.reset;
846
+
847
+ if (seg('agentdb')) lines.push(
848
+ c.brightCyan + '📊 AgentDB' + c.reset + ' ' +
849
+ c.cyan + 'Vectors' + c.reset + ' ' + vectorColor + '●' + vectorCount + hnswInd + c.reset + ' ' + c.dim + '│' + c.reset + ' ' +
850
+ c.cyan + 'Size' + c.reset + ' ' + c.brightWhite + sizeDisp + c.reset + ' ' + c.dim + '│' + c.reset + ' ' +
851
+ c.cyan + 'Tests' + c.reset + ' ' + testColor + '●' + testFiles + c.reset + ' ' + c.dim + '(~' + testCases + ' cases)' + c.reset + ' ' + c.dim + '│' + c.reset + ' ' +
852
+ integStr
476
853
  );
477
854
 
478
855
  return lines.join('\n');
479
856
  }
480
857
 
481
- // Generate JSON data
858
+ // JSON output — delegates to CLI for accuracy; caller can use --json flag
482
859
  function generateJSON() {
483
- return {
484
- user: getUserInfo(),
485
- v3Progress: getV3Progress(),
486
- security: getSecurityStatus(),
487
- swarm: getSwarmStatus(),
488
- system: getSystemMetrics(),
489
- performance: {
490
- flashAttentionTarget: '2.49x-7.47x',
491
- searchImprovement: '150x-12,500x',
492
- memoryReduction: '50-75%',
493
- },
860
+ const d = getStatuslineData();
861
+ const git = getGitInfo();
862
+ return Object.assign({}, d, {
863
+ user: Object.assign({ name: git.name, gitBranch: git.gitBranch }, d.user || {}),
864
+ git: { modified: git.modified, untracked: git.untracked, staged: git.staged, ahead: git.ahead, behind: git.behind },
494
865
  lastUpdated: new Date().toISOString(),
495
- };
866
+ });
496
867
  }
497
868
 
498
- /**
499
- * Generate single-line output for Claude Code compatibility
500
- * This avoids the collision zone issue entirely by using one line
501
- * @see the upstream project (see NOTICE)
502
- */
503
- function generateSingleLine() {
504
- if (!CONFIG.enabled) return '';
505
-
506
- const user = getUserInfo();
507
- const progress = getV3Progress();
508
- const security = getSecurityStatus();
509
- const swarm = getSwarmStatus();
510
- const system = getSystemMetrics();
511
-
512
- const swarmIndicator = swarm.coordinationActive ? '●' : '○';
513
- const securityStatus = security.status === 'CLEAN' ? '✓' :
514
- security.cvesFixed > 0 ? '~' : '✗';
515
-
516
- return `${c.brightPurple}Swarmdo${c.reset} ${c.dim}|${c.reset} ` +
517
- `${c.cyan}D:${progress.domainsCompleted}/${progress.totalDomains}${c.reset} ${c.dim}|${c.reset} ` +
518
- `${c.yellow}S:${swarmIndicator}${swarm.activeAgents}/${swarm.maxAgents}${c.reset} ${c.dim}|${c.reset} ` +
519
- `${security.status === 'CLEAN' ? c.green : c.red}CVE:${securityStatus}${security.cvesFixed}/${security.totalCves}${c.reset} ${c.dim}|${c.reset} ` +
520
- `${c.dim}🧠${system.intelligencePct}%${c.reset}`;
521
- }
522
-
523
- /**
524
- * Generate safe multi-line statusline that avoids Claude Code collision zone
525
- * The collision zone is columns 15-25 on the second-to-last line.
526
- * We pad that line with spaces to push content past column 25.
527
- * @see the upstream project (see NOTICE)
528
- */
529
- function generateSafeStatusline() {
530
- if (!CONFIG.enabled) return '';
531
-
532
- const user = getUserInfo();
533
- const progress = getV3Progress();
534
- const security = getSecurityStatus();
535
- const swarm = getSwarmStatus();
536
- const system = getSystemMetrics();
537
- const lines = [];
538
-
539
- // Header Line
540
- let header = `${c.bold}${c.brightPurple}▊ Swarmdo V${SWARMDO_VERSION} ${c.reset}`;
541
- header += `${swarm.coordinationActive ? c.brightCyan : c.dim}● ${c.brightCyan}${user.name}${c.reset}`;
542
- if (user.gitBranch) {
543
- header += ` ${c.dim}│${c.reset} ${c.brightBlue}⎇ ${user.gitBranch}${c.reset}`;
544
- }
545
- header += ` ${c.dim}│${c.reset} ${c.purple}${user.modelName}${c.reset}`;
546
- lines.push(header);
547
-
548
- // Separator
549
- lines.push(`${c.dim}─────────────────────────────────────────────────────${c.reset}`);
550
-
551
- // Line 1: DDD Domain Progress
552
- const domainsColor = progress.domainsCompleted >= 3 ? c.brightGreen : progress.domainsCompleted > 0 ? c.yellow : c.red;
553
- lines.push(
554
- `${c.brightCyan}🏗️ DDD Domains${c.reset} ${progressBar(progress.domainsCompleted, progress.totalDomains)} ` +
555
- `${domainsColor}${progress.domainsCompleted}${c.reset}/${c.brightWhite}${progress.totalDomains}${c.reset} ` +
556
- `${c.brightYellow}⚡ 1.0x${c.reset} ${c.dim}→${c.reset} ${c.brightYellow}2.49x-7.47x${c.reset}`
557
- );
558
-
559
- // Line 2 (COLLISION LINE): Swarm status with padding after label
560
- // The emoji+label is ~10 columns. Padding pushes content past collision zone (cols 15-25)
561
- const swarmIndicator = swarm.coordinationActive ? `${c.brightGreen}◉${c.reset}` : `${c.dim}○${c.reset}`;
562
- const agentsColor = swarm.activeAgents > 0 ? c.brightGreen : c.red;
563
- let securityIcon = security.status === 'CLEAN' ? '🟢' : security.status === 'IN_PROGRESS' ? '🟡' : '🔴';
564
- let securityColor = security.status === 'CLEAN' ? c.brightGreen : security.status === 'IN_PROGRESS' ? c.brightYellow : c.brightRed;
565
-
566
- // Padding after "🤖 Swarm" (emoji 2 cols + " Swarm" 6 cols = 8, pad 18 to reach col 26)
567
- lines.push(
568
- `${c.brightYellow}🤖 Swarm${c.reset} ` + // 18 spaces padding
569
- `${swarmIndicator} [${agentsColor}${String(swarm.activeAgents).padStart(2)}${c.reset}/${c.brightWhite}${swarm.maxAgents}${c.reset}] ` +
570
- `${c.brightPurple}👥 ${system.subAgents}${c.reset} ` +
571
- `${securityIcon} ${securityColor}CVE ${security.cvesFixed}${c.reset}/${c.brightWhite}${security.totalCves}${c.reset} ` +
572
- `${c.brightCyan}💾 ${system.memoryMB}MB${c.reset} ` +
573
- `${c.dim}🧠 ${system.intelligencePct}%${c.reset}`
574
- );
575
-
576
- // Line 3: Architecture status (this is the last line, not in collision zone)
577
- const dddColor = progress.dddProgress >= 50 ? c.brightGreen : progress.dddProgress > 0 ? c.yellow : c.red;
578
- lines.push(
579
- `${c.brightPurple}🔧 Architecture${c.reset} ` +
580
- `${c.cyan}DDD${c.reset} ${dddColor}●${String(progress.dddProgress).padStart(3)}%${c.reset} ${c.dim}│${c.reset} ` +
581
- `${c.cyan}Security${c.reset} ${securityColor}●${security.status}${c.reset} ${c.dim}│${c.reset} ` +
582
- `${c.cyan}Memory${c.reset} ${c.brightGreen}●AgentDB${c.reset} ${c.dim}│${c.reset} ` +
583
- `${c.cyan}Integration${c.reset} ${swarm.coordinationActive ? c.brightCyan : c.dim}●${c.reset}`
584
- );
585
-
586
- return lines.join('\n');
587
- }
588
-
589
- // Main
869
+ // ─── Main ───────────────────────────────────────────────────────
590
870
  if (process.argv.includes('--json')) {
591
871
  console.log(JSON.stringify(generateJSON(), null, 2));
592
872
  } else if (process.argv.includes('--compact')) {
593
873
  console.log(JSON.stringify(generateJSON()));
594
- } else if (process.argv.includes('--single')) {
595
- // Single-line mode - completely avoids collision zone
596
- console.log(generateSingleLine());
597
- } else if (process.argv.includes('--unsafe') || process.argv.includes('--legacy')) {
598
- // Legacy mode - original multi-line without collision avoidance
599
- console.log(generateStatusline());
600
874
  } else {
601
- // Default: Safe multi-line mode with collision zone avoidance
602
- // Use --unsafe or --legacy to get the original behavior
603
- console.log(generateSafeStatusline());
875
+ console.log(generateStatusline());
604
876
  }