tribunal-kit 4.4.4 → 4.5.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.
@@ -1,1121 +1,1154 @@
1
1
  #!/usr/bin/env node
2
- /**
3
- * tribunal-kit CLI (alias: tk)
4
- *
5
- * Commands:
6
- * init — Install .agent/ into target project
7
- * update — Re-install to get latest changes
8
- * status — Check if .agent/ is installed
9
- * learn — Evolve project idioms based on git diffs
10
- * case — Manage Case Law precedents
11
- * hook — Install pre-push git hook
12
- * uninstall — Remove .agent/ from project
13
- *
14
- * Usage:
15
- * npx tribunal-kit init
16
- * npx tribunal-kit init --force
17
- * npx tribunal-kit init --path ./myapp
18
- * npx tribunal-kit init --quiet
19
- * npx tribunal-kit init --dry-run
20
- * tribunal-kit update
21
- * tribunal-kit status
22
- * tribunal-kit uninstall
23
- */
24
-
25
- const fs = require('fs');
26
- const path = require('path');
27
- const https = require('https');
28
- const { execSync } = require('child_process');
29
-
30
- const PKG = require(path.resolve(__dirname, '..', 'package.json'));
31
- const CURRENT_VERSION = PKG.version;
32
-
33
- // ── Colors ───────────────────────────────────────────────
34
- const C = {
35
- reset: '\x1b[0m',
36
- bold: '\x1b[1m',
37
- dim: '\x1b[2m',
38
- red: '\x1b[91m',
39
- green: '\x1b[92m',
40
- yellow: '\x1b[93m',
41
- blue: '\x1b[94m',
42
- magenta: '\x1b[95m',
43
- cyan: '\x1b[96m',
44
- white: '\x1b[97m',
45
- gray: '\x1b[90m',
46
- bgCyan: '\x1b[46m',
47
- };
48
-
49
- function colorize(color, text) {
50
- return `${C[color]}${text}${C.reset}`;
51
- }
52
-
53
- function c(color, text) { return `${C[color]}${text}${C.reset}`; }
54
- function bold(text) { return `${C.bold}${text}${C.reset}`; }
55
-
56
- // ── Logging ──────────────────────────────────────────────
57
- let quiet = false;
58
- let verbose = false;
59
-
60
- function log(msg) { if (!quiet) console.log(msg); }
61
- function ok(msg) { if (!quiet) console.log(` ${c('green', '✔')} ${msg}`); }
62
- function warn(msg) { if (!quiet) console.log(` ${c('yellow', '⚠')} ${msg}`); }
63
- function err(msg) { console.error(` ${c('red', '✖')} ${msg}`); }
64
- function dim(msg) { if (!quiet) console.log(` ${c('gray', msg)}`); }
65
- function dbg(msg) { if (verbose) console.log(` ${c('gray', '⊡')} ${c('gray', msg)}`); }
66
-
67
- // ── Arg Parser ───────────────────────────────────────────
68
- function parseArgs(argv) {
69
- const args = { command: null, flags: {} };
70
- const raw = argv.slice(2);
71
-
72
- // First non-flag arg is the command
73
- for (const arg of raw) {
74
- if (!arg.startsWith('--') && !args.command) {
75
- args.command = arg;
76
- continue;
77
- }
78
- if (arg === '--force') { args.flags.force = true; continue; }
79
- if (arg === '--quiet') { args.flags.quiet = true; continue; }
80
- if (arg === '--verbose') { args.flags.verbose = true; continue; }
81
- if (arg === '--dry-run') { args.flags.dryRun = true; continue; }
82
- if (arg === '--minimal') { args.flags.minimal = true; continue; }
83
- if (arg === '--skip-update-check') { args.flags.skipUpdateCheck = true; continue; }
84
- if (arg === '--head') { args.flags.head = true; continue; }
85
- if (arg.startsWith('--path=')) {
86
- args.flags.path = arg.split('=').slice(1).join('=');
87
- }
88
- if (arg === '--path') {
89
- const idx = raw.indexOf('--path');
90
- const nextVal = raw[idx + 1];
91
- if (!nextVal || nextVal.startsWith('--')) {
92
- console.error(` \x1b[91m✖ --path requires a directory argument\x1b[0m`);
93
- process.exit(1);
94
- }
95
- args.flags.path = nextVal;
96
- }
97
- if (arg.startsWith('--branch=')) {
98
- args.flags.branch = arg.split('=').slice(1).join('=');
99
- }
100
- }
101
-
102
- return args;
103
- }
104
-
105
- // ── File Utilities ────────────────────────────────────────
106
-
107
- // Core agents to install in --minimal mode
108
- const CORE_AGENTS = new Set([
109
- 'backend-specialist.md',
110
- 'frontend-specialist.md',
111
- 'database-architect.md',
112
- 'debugger.md',
113
- 'security-auditor.md',
114
- 'logic-reviewer.md',
115
- 'dependency-reviewer.md',
116
- 'type-safety-reviewer.md',
117
- 'performance-reviewer.md',
118
- 'orchestrator.md',
119
- 'explorer-agent.md',
120
- 'project-planner.md',
121
- 'test-engineer.md',
122
- ]);
123
-
124
- // Core skills to install in --minimal mode
125
- const CORE_SKILLS = new Set([
126
- 'clean-code', 'architecture', 'testing-patterns', 'systematic-debugging',
127
- 'frontend-design', 'database-design', 'api-patterns', 'nodejs-best-practices',
128
- 'vulnerability-scanner', 'typescript-advanced', 'python-pro', 'nextjs-react-expert',
129
- 'react-specialist', 'performance-profiling', 'lint-and-validate',
130
- ]);
131
-
132
- function copyDir(src, dest, dryRun = false, filter = null) {
133
- if (!dryRun) {
134
- fs.mkdirSync(dest, { recursive: true });
135
- }
136
-
137
- const entries = fs.readdirSync(src, { withFileTypes: true });
138
- let count = 0;
139
-
140
- for (const entry of entries) {
141
- // Apply filter if provided (for --minimal mode)
142
- if (filter && !filter(entry.name, src)) {
143
- dbg(` skip: ${entry.name}`);
144
- continue;
145
- }
146
-
147
- const srcPath = path.join(src, entry.name);
148
- const destPath = path.join(dest, entry.name);
149
-
150
- if (entry.isDirectory()) {
151
- count += copyDir(srcPath, destPath, dryRun, filter);
152
- } else {
153
- if (!dryRun) {
154
- fs.cpSync(srcPath, destPath, { force: true });
155
- }
156
- dbg(` copy: ${entry.name}`);
157
- count++;
158
- }
159
- }
160
-
161
- return count;
162
- }
163
-
164
- function countDir(dir) {
165
- let count = 0;
166
- const entries = fs.readdirSync(dir, { withFileTypes: true });
167
- for (const e of entries) {
168
- if (e.isDirectory()) count += countDir(path.join(dir, e.name));
169
- else count++;
170
- }
171
- return count;
172
- }
173
-
174
- // ── Version Check & Auto-Update ──────────────────────────
175
-
176
- /**
177
- * Compare two semver strings. Returns:
178
- * 1 if a > b, -1 if a < b, 0 if equal.
179
- */
180
- function compareSemver(a, b) {
181
- const pa = a.replace(/^v/, '').split('.').map(Number);
182
- const pb = b.replace(/^v/, '').split('.').map(Number);
183
- for (let i = 0; i < 3; i++) {
184
- const na = pa[i] || 0;
185
- const nb = pb[i] || 0;
186
- if (na > nb) return 1;
187
- if (na < nb) return -1;
188
- }
189
- return 0;
190
- }
191
-
192
- /**
193
- * Fetch the latest version from npm registry.
194
- * Returns the version string (e.g. '4.0.0') or null on failure.
195
- */
196
- function fetchLatestVersion() {
197
- return new Promise((resolve) => {
198
- const req = https.get(
199
- 'https://registry.npmjs.org/tribunal-kit/latest',
200
- {
201
- headers: {
202
- 'Accept': 'application/json',
203
- 'User-Agent': `tribunal-kit/${CURRENT_VERSION}`
204
- },
205
- timeout: 5000
206
- },
207
- (res) => {
208
- let data = '';
209
- res.on('data', (chunk) => { data += chunk; });
210
- res.on('end', () => {
211
- try {
212
- const json = JSON.parse(data);
213
- const version = json.version || null;
214
- resolve(version);
215
- } catch {
216
- resolve(null);
217
- }
218
- });
219
- }
220
- );
221
- req.on('error', () => resolve(null));
222
- req.on('timeout', () => { req.destroy(); resolve(null); });
223
- });
224
- }
225
-
226
- /**
227
- * Check for a newer version and re-invoke with @latest if found.
228
- * Uses TK_SKIP_UPDATE_CHECK env var as recursion guard.
229
- * Returns true if a re-invoke happened (caller should exit), false otherwise.
230
- */
231
- async function autoUpdateCheck(originalArgs) {
232
- // Recursion guard: if we're already a re-invoked process, skip
233
- if (process.env.TK_SKIP_UPDATE_CHECK === '1') {
234
- return false;
235
- }
236
-
237
- const latestVersion = await fetchLatestVersion();
238
-
239
- if (!latestVersion) {
240
- // Network fail proceed silently with current version
241
- return false;
242
- }
243
-
244
- if (compareSemver(latestVersion, CURRENT_VERSION) <= 0) {
245
- // Already up to date
246
- dim(`Version ${CURRENT_VERSION} is up to date.`);
247
- return false;
248
- }
249
-
250
- // Newer version available — re-invoke
251
- log('');
252
- log(colorize('cyan', ` ⬆ New version available: ${colorize('bold', CURRENT_VERSION)} → ${colorize('bold', latestVersion)}`));
253
- log(colorize('gray', ' Re-invoking with latest version...'));
254
- log('');
255
-
256
- try {
257
- // Build the command pulling from npm registry
258
- const args = originalArgs.join(' ');
259
- const cmd = `npx -y tribunal-kit@${latestVersion} ${args}`;
260
-
261
- execSync(cmd, {
262
- stdio: 'inherit',
263
- env: { ...process.env, TK_SKIP_UPDATE_CHECK: '1' },
264
- });
265
- return true; // Re-invoke succeeded, caller should exit
266
- } catch (e) {
267
- warn(`Auto-update failed: ${e.message}`);
268
- warn('Continuing with current version...');
269
- return false; // Fall through to current version
270
- }
271
- }
272
-
273
- // ── Kit Source Location ───────────────────────────────────
274
- function getKitAgent() {
275
- // When installed via npm, the .agent/ folder is next to this script's package
276
- const kitRoot = path.resolve(__dirname, '..');
277
- const agentDir = path.join(kitRoot, '.agent');
278
-
279
- if (!fs.existsSync(agentDir)) {
280
- err(`Kit .agent/ folder not found at: ${agentDir}`);
281
- err('The package may be corrupted. Try: npm install -g tribunal-kit');
282
- process.exit(1);
283
- }
284
-
285
- return agentDir;
286
- }
287
-
288
- // ── Self-Install Guard ────────────────────────────────────
289
- /**
290
- * Returns true if the target directory IS the tribunal-kit package itself.
291
- * This prevents `init --force` / `update` from deleting the package's own files
292
- * when run from inside the project directory.
293
- */
294
- function isSelfInstall(targetDir) {
295
- const kitRoot = path.resolve(__dirname, '..');
296
- const resolvedTarget = path.resolve(targetDir);
297
-
298
- // Direct path match
299
- if (resolvedTarget === kitRoot) return true;
300
-
301
- // Check if the target's package.json is this package
302
- const targetPkg = path.join(resolvedTarget, 'package.json');
303
- if (fs.existsSync(targetPkg)) {
304
- try {
305
- const targetName = JSON.parse(fs.readFileSync(targetPkg, 'utf8')).name;
306
- if (targetName === PKG.name) return true;
307
- } catch {
308
- // Unreadable package.json — not a match
309
- }
310
- }
311
-
312
- return false;
313
- }
314
-
315
- // ── Banner ────────────────────────────────────────────────
316
- function banner() {
317
- if (quiet) return;
318
- // Big ASCII art (TRIBUNAL-KIT)
319
- const art = String.raw`
320
- ████████╗██████╗ ██╗██████╗ ██╗ ██╗███╗ ██╗ █████╗ ██╗ ██╗ ██╗██╗████████╗
321
- ╚══██╔══╝██╔══██╗██║██╔══██╗██║ ██║████╗ ██║██╔══██╗██║ ██║ ██╔╝██║╚══██╔══╝
322
- ██║ ██████╔╝██║██████╔╝██║ ██║██╔██╗ ██║███████║██║█████╗█████╔╝ ██║ ██║
323
- ██║ ██╔══██╗██║██╔══██╗██║ ██║██║╚██╗██║██╔══██║██║╚════╝██╔═██╗ ██║ ██║
324
- ██║ ██║ ██║██║██████╔╝╚██████╔╝██║ ╚████║██║ ██║███████╗ ██║ ██╗██║ ██║
325
- ╚═╝ ╚═╝ ╚═╝╚═╝╚═════╝ ╚═════╝ ╚═╝ ╚═══╝╚═╝ ╚═╝╚══════╝ ╚═╝ ╚═╝╚═╝ ╚═╝ `.split('\n').filter(Boolean);
326
- console.log();
327
- const _maxLen = Math.max(...art.map(line => line.length));
328
- for (const line of art) {
329
- let gradientLine = ' ' + C.bold;
330
- for (let i = 0; i < line.length; i++) {
331
- gradientLine += `\x1b[38;2;255;22;55m${line[i]}`;
332
- }
333
- gradientLine += C.reset;
334
- log(gradientLine);
335
- }
336
- console.log();
337
- // Subtitle strip
338
- const W = 84;
339
- const sub = 'Anti-Hallucination Agent System';
340
- const sp = Math.max(0, W - sub.length);
341
- const centred = ' '.repeat(Math.floor(sp / 2)) + sub + ' '.repeat(Math.ceil(sp / 2));
342
- const RED_ANSI = '\x1b[38;2;255;22;55m';
343
- console.log(` ${RED_ANSI}╔${'═'.repeat(W)}╗${C.reset}`);
344
- console.log(` ${RED_ANSI}║${C.reset}${c('gray', centred)}${RED_ANSI}║${C.reset}`);
345
- console.log(` ${RED_ANSI}╚${'═'.repeat(W)}╝${C.reset}`);
346
- console.log();
347
- }
348
-
349
- // ── Commands ──────────────────────────────────────────────
350
- function cmdInit(flags) {
351
- const agentSrc = getKitAgent();
352
- const targetDir = flags.path ? path.resolve(flags.path) : process.cwd();
353
- const agentDest = path.join(targetDir, '.agent');
354
- const dryRun = flags.dryRun || false;
355
-
356
- // ── Self-install guard ──────────────────────────────────
357
- if (isSelfInstall(targetDir)) {
358
- err('Cannot run init/update inside the tribunal-kit package itself.');
359
- err(`Target: ${targetDir}`);
360
- err(`Package: ${path.resolve(__dirname, '..')}`);
361
- console.log();
362
- dim('This command is designed to install .agent/ into OTHER projects.');
363
- dim('Run it from the root of the project you want to set up:');
364
- dim(' cd /path/to/your-project');
365
- dim(' npx tribunal-kit init');
366
- console.log();
367
- process.exit(1);
368
- }
369
- // ────────────────────────────────────────────────────────
370
-
371
- // ── Backup / Cleanup ────────────────────────────────────
372
- if (!dryRun && fs.existsSync(agentDest) && flags.force) {
373
- // Backup the existing subdirectories before overwriting
374
- const backupDir = path.join(agentDest, '.backups', `backup-${Date.now()}`);
375
- fs.mkdirSync(backupDir, { recursive: true });
376
-
377
- // PRESERVE_DIRS: user-generated content that must survive updates
378
- const _PRESERVE_DIRS = ['history', 'patterns', 'mcp_config.json'];
379
- const subdirs = ['agents', 'workflows', 'skills', 'scripts', '.shared', 'rules'];
380
- for (const sub of subdirs) {
381
- const subPath = path.join(agentDest, sub);
382
- if (fs.existsSync(subPath)) {
383
- // Copy to backup dir
384
- copyDir(subPath, path.join(backupDir, sub), false);
385
- fs.rmSync(subPath, { recursive: true, force: true });
386
- }
387
- }
388
- log(` ${c('gray', '✦ Backed up existing configurations to .agent/.backups/')}`);
389
-
390
-
391
- }
392
- // ────────────────────────────────────────────────────────
393
-
394
- banner();
395
-
396
- if (dryRun) {
397
- log(colorize('yellow', ' DRY RUN — no files will be written'));
398
- console.log();
399
- }
400
-
401
- // Check target exists
402
- if (!fs.existsSync(targetDir)) {
403
- err(`Target directory not found: ${targetDir}`);
404
- process.exit(1);
405
- }
406
-
407
- // Check if .agent already exists
408
- if (fs.existsSync(agentDest) && !flags.force) {
409
- warn('.agent/ already exists in this project.');
410
- log(` ${c('gray', '▸')} To refresh or update it, run: ${colorize('white', 'tribunal-kit init --force')}`);
411
- log(` ${c('gray', '▸')} Or check status with: ${colorize('cyan', 'tribunal-kit status')}`);
412
- console.log();
413
- process.exit(0);
414
- }
415
-
416
- // Ensure history dirs exist (Case Law + Skill Evolution)
417
- if (!dryRun) {
418
- const caseDir = path.join(agentDest, 'history', 'case-law', 'cases');
419
- const evoDir = path.join(agentDest, 'history', 'skill-evolution');
420
- fs.mkdirSync(caseDir, { recursive: true });
421
- fs.mkdirSync(evoDir, { recursive: true });
422
- const gkCase = path.join(caseDir, '.gitkeep');
423
- const gkEvo = path.join(evoDir, '.gitkeep');
424
- if (!fs.existsSync(gkCase)) fs.writeFileSync(gkCase, '');
425
- if (!fs.existsSync(gkEvo)) fs.writeFileSync(gkEvo, '');
426
- }
427
-
428
- // Count what we're installing
429
- const isMinimal = flags.minimal || false;
430
- if (isMinimal) {
431
- log(` ${c('yellow','⚡')} ${bold('Minimal mode')} installing core agents and skills only`);
432
- console.log();
433
- }
434
- const totalFiles = countDir(agentSrc);
435
- dbg(`Source: ${agentSrc}`);
436
- dbg(`Target: ${agentDest}`);
437
- dbg(`Total source files: ${totalFiles}`);
438
- log(` ${c('gray','▸')} Scanning ${c('white', String(totalFiles))} files ${c('gray','→')} ${c('gray', agentDest)}`);
439
-
440
- try {
441
- // Build filter for --minimal mode
442
- const minimalFilter = isMinimal ? (name, parentDir) => {
443
- const parentName = path.basename(parentDir);
444
- if (parentName === 'agents') return CORE_AGENTS.has(name);
445
- if (parentName === 'skills') return CORE_SKILLS.has(name);
446
- return true; // everything else passes
447
- } : null;
448
-
449
- const copied = copyDir(agentSrc, agentDest, dryRun, minimalFilter);
450
-
451
- console.log();
452
- if (dryRun) {
453
- ok(`${bold('DRY RUN')} complete would install ${c('cyan', String(copied))} files`);
454
- dim(`Target: ${agentDest}`);
455
- } else {
456
- // ── Success card W=62, rows padded by plain-text length ──
457
- const W = 62;
458
- const agentsCount = fs.readdirSync(path.join(agentDest, 'agents')).length;
459
- const workflowsCount = fs.readdirSync(path.join(agentDest, 'workflows')).length;
460
- const skillsCount = fs.readdirSync(path.join(agentDest, 'skills')).length;
461
- const scriptsCount = fs.readdirSync(path.join(agentDest, 'scripts')).length;
462
-
463
- // Stat rows: compute trailing spaces from plain text so right ║ aligns
464
- const statRow = (icon, label, val, col) => {
465
- // emoji JS .length===2 == terminal display width 2 ✓
466
- const plain = ` ${icon} ${label.padEnd(10)}${String(val).padStart(3)} installed`;
467
- const trail = ' '.repeat(Math.max(0, W - plain.length));
468
- return ` ${c('cyan','║')} ${icon} ${c('white',label.padEnd(10))}${c(col,String(val).padStart(3))} ${c('gray','installed')}${trail}${c('cyan','║')}`;
469
- };
470
- // Plain-text rows (header / blank)
471
- const plainRow = (text, wrapFn) => {
472
- const trail = ' '.repeat(Math.max(0, W - text.length));
473
- return ` ${c('cyan','║')}${wrapFn(text)}${trail}${c('cyan','║')}`;
474
- };
475
- // Next-step rows: fixed cmd column + description
476
- const stepRow = (cmd, desc) => {
477
- const plain = ` ${cmd.padEnd(16)}${desc}`;
478
- const trail = ' '.repeat(Math.max(0, W - plain.length));
479
- return ` ${c('cyan','║')} ${c('white',cmd.padEnd(16))}${c('gray',desc)}${trail}${c('cyan','║')}`;
480
- };
481
-
482
- console.log(` ${c('green','✔')} ${bold(c('green','Installation complete'))} ${c('gray','—')} ${c('white',String(copied))} files`);
483
- console.log(` ${c('gray',' ╰─')} ${c('gray', agentDest)}`);
484
- console.log();
485
- console.log(` ${c('cyan', '╔' + '═'.repeat(W) + '╗')}`);
486
- console.log(plainRow(` What's inside:`, s => c('bold', c('white', s))));
487
- console.log(` ${c('cyan', '╠' + '═'.repeat(W) + '╣')}`);
488
- console.log(statRow('🤖', 'Agents', agentsCount, 'magenta'));
489
- console.log(statRow('⚡', 'Workflows', workflowsCount, 'yellow'));
490
- console.log(statRow('🧠', 'Skills', skillsCount, 'blue'));
491
- console.log(statRow('🔧', 'Scripts', scriptsCount, 'green'));
492
- console.log(` ${c('cyan', '╠' + '═'.repeat(W) + '╣')}`);
493
- console.log(plainRow('', () => ''));
494
- console.log(plainRow(` Next steps:`, s => c('gray', s)));
495
- console.log(stepRow('/generate', 'Generate code with anti-hallucination'));
496
- console.log(stepRow('/review', 'Audit existing code for issues'));
497
- console.log(stepRow('/tribunal-full', 'Run all 16 reviewers in parallel'));
498
- console.log(plainRow('', () => ''));
499
- console.log(` ${c('cyan', '' + ''.repeat(W) + '╝')}`);
500
- console.log();
501
- log(` ${c('gray', '✦ Generating IDE bridge files...')}`);
502
- generateIDEBridges(targetDir, agentDest, dryRun);
503
- }
504
-
505
- console.log();
506
- } catch (e) {
507
- err(`Failed to install: ${e.message}`);
508
- process.exit(1);
509
- }
510
- }
511
-
512
- // ── IDE Bridge Files ──────────────────────────────────────
513
- // Each AI IDE reads rules from a different location.
514
- // We generate bridge files that point each IDE at .agent/
515
- function generateIDEBridges(targetDir, agentDest, dryRun = false) {
516
- const rulesFile = path.join(agentDest, 'rules', 'GEMINI.md');
517
- let rulesContent = '';
518
- if (fs.existsSync(rulesFile)) {
519
- rulesContent = fs.readFileSync(rulesFile, 'utf8');
520
- }
521
-
522
- // Helper: write a bridge file only if it doesn't already exist
523
- const writeBridge = (filePath, content, label) => {
524
- if (dryRun) {
525
- dbg(` would create: ${filePath}`);
526
- return;
527
- }
528
- const dir = path.dirname(filePath);
529
- if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
530
- if (fs.existsSync(filePath)) {
531
- dbg(` skip (exists): ${path.basename(filePath)}`);
532
- return;
533
- }
534
- fs.writeFileSync(filePath, content, 'utf8');
535
- ok(`${label} ${c('gray', path.relative(targetDir, filePath))}`);
536
- };
537
-
538
- // ── 1. Cursor (.cursorrules) ──────────────────────────
539
- const cursorRules = `# Tribunal Kit — Cursor Bridge
540
- # Auto-generated by tribunal-kit init. Do not edit manually.
541
- # Source: .agent/rules/GEMINI.md
542
-
543
- ${rulesContent}
544
- `;
545
- writeBridge(
546
- path.join(targetDir, '.cursorrules'),
547
- cursorRules,
548
- 'Cursor'
549
- );
550
-
551
- // ── 2. Windsurf (.windsurfrules) ─────────────────────
552
- const windsurfRules = `# Tribunal Kit — Windsurf Bridge
553
- # Auto-generated by tribunal-kit init. Do not edit manually.
554
- # Source: .agent/rules/GEMINI.md
555
-
556
- ${rulesContent}
557
- `;
558
- writeBridge(
559
- path.join(targetDir, '.windsurfrules'),
560
- windsurfRules,
561
- 'Windsurf'
562
- );
563
-
564
- // ── 3. Gemini / Antigravity (.gemini/settings.json) ──
565
- const geminiSettings = JSON.stringify({
566
- "$schema": "https://raw.githubusercontent.com/anthropics/anthropic-cookbook/main/.gemini/settings.schema.json",
567
- "rules": [
568
- { "path": "../.agent/rules/GEMINI.md", "trigger": "always_on" }
569
- ],
570
- "agents": { "directory": "../.agent/agents" },
571
- "skills": { "directory": "../.agent/skills" },
572
- "workflows": { "directory": "../.agent/workflows" }
573
- }, null, 2) + '\n';
574
- writeBridge(
575
- path.join(targetDir, '.gemini', 'settings.json'),
576
- geminiSettings,
577
- 'Gemini/Antigravity'
578
- );
579
-
580
- // ── Also create .gemini/GEMINI.md as a direct rules file ──
581
- const geminiRulesBridge = `---
582
- trigger: always_on
583
- ---
584
-
585
- # Tribunal Kit — Gemini Bridge
586
- # Auto-generated by tribunal-kit init.
587
- # Full rules: .agent/rules/GEMINI.md
588
-
589
- ${rulesContent}
590
- `;
591
- writeBridge(
592
- path.join(targetDir, '.gemini', 'GEMINI.md'),
593
- geminiRulesBridge,
594
- 'Gemini rules'
595
- );
596
-
597
- // ── 4. GitHub Copilot (.github/copilot-instructions.md) ──
598
- const copilotInstructions = `# Tribunal Kit — Copilot Bridge
599
- # Auto-generated by tribunal-kit init. Do not edit manually.
600
- # Source: .agent/rules/GEMINI.md
601
-
602
- ${rulesContent}
603
- `;
604
- writeBridge(
605
- path.join(targetDir, '.github', 'copilot-instructions.md'),
606
- copilotInstructions,
607
- 'GitHub Copilot'
608
- );
609
-
610
- // ── 5. Claude (.claude/CLAUDE.md) ─────────────────────
611
- const claudeRules = `# Tribunal Kit — Claude Bridge
612
- # Auto-generated by tribunal-kit init. Do not edit manually.
613
- # Source: .agent/rules/GEMINI.md
614
-
615
- ${rulesContent}
616
- `;
617
- writeBridge(
618
- path.join(targetDir, '.claude', 'CLAUDE.md'),
619
- claudeRules,
620
- 'Claude'
621
- );
622
-
623
- console.log();
624
- }
625
-
626
- function cmdUpdate(flags) {
627
- // ── Self-install guard (early, before banner) ───────────
628
- const targetDir = flags.path ? path.resolve(flags.path) : process.cwd();
629
- if (isSelfInstall(targetDir)) {
630
- err('Cannot run update inside the tribunal-kit package itself.');
631
- err(`Target: ${targetDir}`);
632
- console.log();
633
- dim('This command is designed to update .agent/ in OTHER projects.');
634
- dim('Run it from the root of the project you want to update:');
635
- dim(' cd /path/to/your-project');
636
- dim(' npx tribunal-kit update');
637
- console.log();
638
- process.exit(1);
639
- }
640
- // ────────────────────────────────────────────────────────
641
-
642
- // Update = init with --force
643
- flags.force = true;
644
- if (!quiet) {
645
- log(` ${c('cyan','↻')} ${bold('Updating')} ${c('white','.agent/')} to latest version...`);
646
- console.log();
647
- }
648
- cmdInit(flags);
649
- }
650
-
651
-
652
- function cmdLearn(flags) {
653
- const targetDir = flags.path ? path.resolve(flags.path) : process.cwd();
654
- const agentDest = path.join(targetDir, '.agent');
655
-
656
- if (!fs.existsSync(agentDest)) {
657
- err('.agent/ not found. Run: npx tribunal-kit init');
658
- process.exit(1);
659
- }
660
-
661
- banner();
662
-
663
- const W = 62;
664
- const title = ' Tribunal Learn — Supreme Court Mode';
665
- const trail = ' '.repeat(Math.max(0, W - title.length));
666
- console.log(` ${c('cyan', '\u2554' + '\u2550'.repeat(W) + '\u2557')}`);
667
- console.log(` ${c('cyan', '\u2551')}${c('bold', c('white', title))}${trail}${c('cyan', '\u2551')}`);
668
- console.log(` ${c('cyan', '\u255a' + '\u2550'.repeat(W) + '\u255d')}`);
669
- console.log();
670
-
671
- const dryRun = flags.dryRun ? '--dry-run' : '';
672
- const useHead = flags.head ? '--head' : '';
673
-
674
-
675
- // Phase 1: Skill Evolution
676
- log(` ${c('cyan', '\u229b')} ${bold('Phase 1')} \u2014 Skill Evolution Forge (auto-generating project idioms)`);
677
- const evoScript = path.join(agentDest, 'scripts', 'skill_evolution.js');
678
- if (!fs.existsSync(evoScript)) {
679
- warn('skill_evolution.js not found \u2014 run: npx tribunal-kit update');
680
- } else {
681
- try {
682
- const cmd = `node "${evoScript}" digest ${dryRun} ${useHead}`.trim();
683
- execSync(cmd, { stdio: 'inherit', cwd: targetDir });
684
- } catch (e) {
685
- warn(`Skill Evolution error: ${e.message}`);
686
- }
687
- }
688
-
689
- console.log();
690
-
691
- // Phase 2: Case Law prompt
692
- log(` ${c('cyan', '\u229b')} ${bold('Phase 2')} \u2014 Case Law Engine (building precedence record)`);
693
- console.log();
694
- log(` ${c('gray','\u25b8')} Record a new rejection precedent:`);
695
- log(` ${c('white', 'npx tribunal-kit case add')}`);
696
- console.log();
697
- log(` ${c('gray','\u25b8')} Search existing case law:`);
698
- log(` ${c('white', 'npx tribunal-kit case search "your query"')}`);
699
- console.log();
700
- log(` ${c('green', '\u2714')} ${bold('Learn cycle complete.')} Your Tribunal grows smarter with every commit.`);
701
- console.log();
702
- }
703
-
704
- // ── Async Main Wrapper ───────────────────────────────────
705
- async function runWithUpdateCheck(command, flags) {
706
- const shouldSkip = flags.skipUpdateCheck || process.env.TK_SKIP_UPDATE_CHECK === '1';
707
-
708
- if (!shouldSkip && (command === 'init' || command === 'update')) {
709
- // Pass through the original args (minus the node/script path)
710
- const originalArgs = process.argv.slice(2);
711
- const didReInvoke = await autoUpdateCheck(originalArgs);
712
- if (didReInvoke) {
713
- process.exit(0); // Latest version handled it
714
- }
715
- }
716
-
717
- // Proceed with current version
718
- switch (command) {
719
- case 'init':
720
- cmdInit(flags);
721
- break;
722
- case 'update':
723
- cmdUpdate(flags);
724
- break;
725
- case 'status':
726
- cmdStatus(flags);
727
- break;
728
- case 'learn':
729
- cmdLearn(flags);
730
- break;
731
- case 'case':
732
- cmdCase(flags);
733
- break;
734
- case 'hook':
735
- cmdHook(flags);
736
- break;
737
- case 'graph':
738
- cmdGraph(flags);
739
- break;
740
- case 'mutate':
741
- cmdMutate(flags);
742
- break;
743
- case 'context':
744
- cmdContext(flags);
745
- break;
746
- case 'marathon':
747
- cmdMarathon(flags);
748
- break;
749
- case 'uninstall':
750
- cmdUninstall(flags);
751
- break;
752
- case 'help':
753
- case '--help':
754
- case '-h':
755
- case null:
756
- cmdHelp();
757
- break;
758
- default:
759
- err(`Unknown command: "${command}"`);
760
- console.log();
761
- dim('Run tribunal-kit --help for usage');
762
- process.exit(1);
763
- }
764
- }
765
-
766
- function cmdCase(flags) {
767
- const targetDir = flags.path ? path.resolve(flags.path) : process.cwd();
768
- const agentDest = path.join(targetDir, '.agent');
769
-
770
- if (!fs.existsSync(agentDest)) {
771
- err('.agent/ not found. Run: npx tribunal-kit init');
772
- process.exit(1);
773
- }
774
-
775
- const args = process.argv.slice(3).join(' ');
776
- if (!args || args === 'help' || args === '--help' || args === '-h') {
777
- banner();
778
- log(` ${c('cyan', '\u2554' + '\u2550'.repeat(60) + '\u2557')}`);
779
- log(` ${c('cyan', '\u2551')}${c('bold', c('white', ' Tribunal Case Law Engine \u2014 Supreme Court '))}${c('cyan', '\u2551')}`);
780
- log(` ${c('cyan', '\u255a' + '\u2550'.repeat(60) + '\u255d')}`);
781
- console.log();
782
- log(` ${c('cyan', 'add'.padEnd(10))} ${c('gray', 'Record a new Case Law rejection pattern')}`);
783
- log(` ${c('cyan', 'search'.padEnd(10))} ${c('gray', 'Search existing cases (e.g., search "query")')}`);
784
- log(` ${c('cyan', 'list'.padEnd(10))} ${c('gray', 'List all recorded case law')}`);
785
- log(` ${c('cyan', 'show'.padEnd(10))} ${c('gray', 'Show full diff for a case (e.g., show --id 1)')}`);
786
- log(` ${c('cyan', 'stats'.padEnd(10))} ${c('gray', 'Show case law stats by domain/verdict')}`);
787
- log(` ${c('cyan', 'export'.padEnd(10))} ${c('gray', 'Export all cases to Markdown')}`);
788
- log(` ${c('cyan', 'overrule'.padEnd(10))} ${c('gray', 'Overrule a past precedent (e.g., overrule --id 1)')}`);
789
- console.log();
790
- process.exit(1);
791
- }
792
-
793
- const caseLawScript = path.join(agentDest, 'scripts', 'case_law_manager.js');
794
-
795
- // Make shorthand aliases
796
- let pyArgs = args;
797
- if (pyArgs.startsWith('add')) pyArgs = pyArgs.replace(/^add/, 'add-case');
798
- if (pyArgs.startsWith('search')) pyArgs = pyArgs.replace(/^search/, 'search-cases');
799
-
800
- try {
801
- const { execSync } = require('child_process');
802
- execSync(`node "${caseLawScript}" ${pyArgs}`, { stdio: 'inherit', cwd: targetDir });
803
- } catch {
804
- process.exit(1); // Script already prints errors
805
- }
806
- }
807
-
808
- function cmdGraph(flags) {
809
- const targetDir = flags.path ? path.resolve(flags.path) : process.cwd();
810
- const agentDest = path.join(targetDir, '.agent');
811
-
812
- if (!fs.existsSync(agentDest)) {
813
- err('.agent/ not found. Run: npx tribunal-kit init');
814
- process.exit(1);
815
- }
816
-
817
- banner();
818
- const { execSync } = require('child_process');
819
- const builderScript = path.join(agentDest, 'scripts', 'graph_builder.js');
820
- const visualizerScript = path.join(agentDest, 'scripts', 'graph_visualizer.js');
821
- const htmlFile = path.join(agentDest, 'history', 'architecture-explorer.html');
822
-
823
- try {
824
- execSync(`node "${builderScript}"`, { stdio: 'inherit', cwd: targetDir });
825
- execSync(`node "${visualizerScript}"`, { stdio: 'inherit', cwd: targetDir });
826
-
827
- log(` ${c('cyan', '▸')} Opening visualizer in browser...`);
828
- const opener = process.platform === 'win32' ? 'start' : process.platform === 'darwin' ? 'open' : 'xdg-open';
829
- execSync(`${opener} "${htmlFile}"`);
830
- } catch (e) {
831
- err(`Graph generation failed: ${e.message}`);
832
- process.exit(1);
833
- }
834
- }
835
-
836
- function cmdHook(flags) {
837
- const targetDir = flags.path ? path.resolve(flags.path) : process.cwd();
838
- const gitDir = path.join(targetDir, '.git');
839
-
840
- if (!fs.existsSync(gitDir)) {
841
- err('Not a git repository. Cannot install git hooks here.');
842
- process.exit(1);
843
- }
844
-
845
- const hooksDir = path.join(gitDir, 'hooks');
846
- if (!fs.existsSync(hooksDir)) {
847
- fs.mkdirSync(hooksDir, { recursive: true });
848
- }
849
-
850
- const prePushPath = path.join(hooksDir, 'pre-push');
851
- const hookScript = `#!/bin/sh\n# Supreme Court - Auto Learn on Push\necho "⚖️ Tribunal Supreme Court: Evolving Skills..."\nnpx tribunal-kit learn --head\n`;
852
-
853
- fs.writeFileSync(prePushPath, hookScript, { mode: 0o755 });
854
-
855
- console.log();
856
- log(` ${c('green', '✔')} Installed pre-push git hook.`);
857
- log(` ${c('gray', '▸')} Skill Evolution will now run automatically every time you git push.`);
858
- console.log();
859
- }
860
-
861
- function cmdMutate(flags) {
862
- const targetDir = flags.path ? path.resolve(flags.path) : process.cwd();
863
- const agentDest = path.join(targetDir, '.agent');
864
-
865
- if (!fs.existsSync(agentDest)) {
866
- err('.agent/ not found. Run: npx tribunal-kit init');
867
- process.exit(1);
868
- }
869
-
870
- const args = process.argv.slice(3);
871
- if (args.length < 2) {
872
- err('Usage: npx tribunal-kit mutate <target_file> <test_command>');
873
- process.exit(1);
874
- }
875
-
876
- const mutateScript = path.join(agentDest, 'scripts', 'mutation_runner.js');
877
- const { execSync } = require('child_process');
878
- try {
879
- execSync(`node "${mutateScript}" ${args.join(' ')}`, { stdio: 'inherit', cwd: targetDir });
880
- } catch {
881
- process.exit(1);
882
- }
883
- }
884
-
885
- function cmdUninstall(flags) {
886
- const targetDir = flags.path ? path.resolve(flags.path) : process.cwd();
887
- const agentDest = path.join(targetDir, '.agent');
888
-
889
- banner();
890
-
891
- if (!fs.existsSync(agentDest)) {
892
- log(` ${c('yellow','⚠')} ${bold('.agent/')} is not installed in this project.`);
893
- console.log();
894
- return;
895
- }
896
-
897
- if (flags.dryRun) {
898
- log(colorize('yellow', ' DRY RUN — would remove:'));
899
- log(` ${c('gray',' ╰─')} ${agentDest}`);
900
- console.log();
901
- return;
902
- }
903
-
904
- try {
905
- fs.rmSync(agentDest, { recursive: true, force: true });
906
- log(` ${c('green','✔')} ${bold('.agent/')} has been removed from this project.`);
907
- console.log();
908
- log(` ${c('gray','▸')} To reinstall: ${c('cyan','npx tribunal-kit init')}`);
909
- console.log();
910
- } catch (e) {
911
- err(`Failed to remove .agent/: ${e.message}`);
912
- process.exit(1);
913
- }
914
- }
915
-
916
- function cmdStatus(flags) {
917
- const targetDir = flags.path ? path.resolve(flags.path) : process.cwd();
918
- const agentDest = path.join(targetDir, '.agent');
919
-
920
- banner();
921
-
922
- if (!fs.existsSync(agentDest)) {
923
- log(` ${c('red','✖')} ${bold('Not installed')} in this project`);
924
- console.log();
925
- log(` ${c('gray','Run:')} ${c('cyan','npx tribunal-kit init')}`);
926
- console.log();
927
- return;
928
- }
929
-
930
- log(` ${c('green','✔')} ${bold(c('green','Installed'))} ${c('gray','→')} ${c('gray', agentDest)}`);
931
- console.log();
932
-
933
- const icons = { agents: '🤖', workflows: '⚡', skills: '🧠', scripts: '🔧' };
934
- const colors = { agents: 'magenta', workflows: 'yellow', skills: 'blue', scripts: 'green' };
935
- const subdirs = ['agents', 'workflows', 'skills', 'scripts'];
936
- for (const sub of subdirs) {
937
- const subPath = path.join(agentDest, sub);
938
- if (fs.existsSync(subPath)) {
939
- const count = fs.readdirSync(subPath).filter(f => !fs.statSync(path.join(subPath, f)).isDirectory()).length;
940
- log(` ${icons[sub]} ${c(colors[sub], sub.padEnd(12))}${c('white', String(count).padStart(3))} files`);
941
- }
942
- }
943
- console.log();
944
- }
945
-
946
- function cmdHelp() {
947
- banner();
948
- const cmd = (name, desc) => ` ${c('cyan', name.padEnd(10))} ${c('gray', desc)}`;
949
- const opt = (flag, desc) => ` ${c('yellow', flag.padEnd(22))} ${c('gray', desc)}`;
950
- const ex = (s) => ` ${c('gray', '')} ${c('white', s)}`;
951
-
952
- log(bold(' Commands'));
953
- log(` ${c('gray','─'.repeat(40))}`);
954
- log(cmd('init', 'Install .agent/ into current project'));
955
- log(cmd('update', 'Re-install to get latest version'));
956
- log(cmd('status', 'Check if .agent/ is installed'));
957
- log(cmd('learn', 'Evolve project idioms based on git diffs'));
958
- log(cmd('case', 'Manage Case Law precedents (add, search, list, show, stats, overrule)'));
959
- log(cmd('graph', 'Build and visualize the architecture graph'));
960
- log(cmd('mutate', 'Run the Mutation Engine to test test-suite reliability'));
961
- log(cmd('context', 'Retrieve a highly-optimized Context Snapshot for a file'));
962
- log(cmd('marathon', 'Long-running agent harness (init, status, next, mark)'));
963
- log(cmd('hook', 'Install pre-push git hook for auto-learning'));
964
- log(cmd('uninstall','Remove .agent/ folder from project'));
965
- console.log();
966
- log(bold(' Options'));
967
- log(` ${c('gray',''.repeat(40))}`);
968
- log(opt('--force', 'Overwrite existing .agent/ folder'));
969
- log(opt('--path <dir>', 'Install in specific directory'));
970
- log(opt('--quiet', 'Suppress all output'));
971
- log(opt('--verbose', 'Show detailed debug logging'));
972
- log(opt('--dry-run', 'Preview actions without executing'));
973
- log(opt('--minimal', 'Install core agents/skills only (~13 agents)'));
974
- log(opt('--skip-update-check', 'Skip auto-update version check'));
975
- log(opt('--head', '(learn) Diff against last commit instead of staged'));
976
- console.log();
977
- log(bold(' Aliases'));
978
- log(` ${c('gray','─'.repeat(40))}`);
979
- log(` ${c('cyan', 'tk')} ${c('gray', 'Shorthand for tribunal-kit (e.g., tk init, tk status)')}`);
980
- console.log();
981
- log(bold(' Examples'));
982
- log(` ${c('gray',''.repeat(40))}`);
983
- log(ex('npx tribunal-kit init'));
984
- log(ex('tk init --force'));
985
- log(ex('tk init --path ./my-app'));
986
- log(ex('npx tribunal-kit init --dry-run'));
987
- log(ex('tk update'));
988
- log(ex('tk status'));
989
- log(ex('tk learn'));
990
- log(ex('tk learn --dry-run'));
991
- log(ex('tk learn --head'));
992
- log(ex('tk case add'));
993
- log(ex('tk case search "useEffect"'));
994
- log(ex('tk case list'));
995
- log(ex('tk case show --id 1'));
996
- log(ex('tk case stats'));
997
- log(ex('tk case export'));
998
- log(ex('tk case overrule --id 1'));
999
- log(ex('tk graph'));
1000
- log(ex('tk mutate src/utils.js "npm test"'));
1001
- log(ex('tk marathon init "Build a todo app"'));
1002
- log(ex('tk marathon status'));
1003
- log(ex('tk marathon next'));
1004
- log(ex('tk marathon mark 5 pass'));
1005
- log(ex('tk hook'));
1006
- log(ex('tk uninstall'));
1007
- console.log();
1008
- }
1009
-
1010
-
1011
- function cmdMarathon(flags) {
1012
- const targetDir = flags.path ? path.resolve(flags.path) : process.cwd();
1013
- const agentDest = path.join(targetDir, '.agent');
1014
-
1015
- if (!fs.existsSync(agentDest)) {
1016
- err('.agent/ not found. Run: npx tribunal-kit init');
1017
- process.exit(1);
1018
- }
1019
-
1020
- const args = process.argv.slice(3);
1021
- const argsStr = args.join(' ');
1022
- if (args.length === 0 || args[0] === 'help' || args[0] === '--help' || args[0] === '-h') {
1023
- banner();
1024
- log(` ${c('cyan', '╔' + '═'.repeat(60) + '╗')}`);
1025
- log(` ${c('cyan', '║')}${c('bold', c('white', ' Marathon — Long-Running Agent Harness '))}${c('cyan', '║')}`);
1026
- log(` ${c('cyan', '╚' + '═'.repeat(60) + '╝')}`);
1027
- console.log();
1028
- log(` ${c('cyan', 'init'.padEnd(16))} ${c('gray', 'Start a new marathon (init "spec")')}`);
1029
- log(` ${c('cyan', 'status'.padEnd(16))} ${c('gray', 'Show progress dashboard')}`);
1030
- log(` ${c('cyan', 'next'.padEnd(16))} ${c('gray', 'Show next unfinished feature')}`);
1031
- log(` ${c('cyan', 'mark'.padEnd(16))} ${c('gray', 'Mark feature pass/fail (mark <id> pass)')}`);
1032
- log(` ${c('cyan', 'log'.padEnd(16))} ${c('gray', 'Add a progress note')}`);
1033
- log(` ${c('cyan', 'session-start'.padEnd(16))} ${c('gray', 'Begin a new work session')}`);
1034
- log(` ${c('cyan', 'session-end'.padEnd(16))} ${c('gray', 'End session with summary')}`);
1035
- log(` ${c('cyan', 'add-feature'.padEnd(16))} ${c('gray', 'Add feature: "category" "desc" "step1" ...')}`);
1036
- log(` ${c('cyan', 'reset'.padEnd(16))} ${c('gray', 'Archive and start fresh')}`);
1037
- console.log();
1038
- return;
1039
- }
1040
-
1041
- const marathonScript = path.join(agentDest, 'scripts', 'marathon_harness.js');
1042
- try {
1043
- execSync(`node "${marathonScript}" ${argsStr}`, { stdio: 'inherit', cwd: targetDir });
1044
- } catch {
1045
- process.exit(1);
1046
- }
1047
- }
1048
-
1049
- function cmdContext(flags) {
1050
- const targetDir = flags.path ? path.resolve(flags.path) : process.cwd();
1051
- const agentDest = path.join(targetDir, '.agent');
1052
-
1053
- if (!fs.existsSync(agentDest)) {
1054
- err('.agent/ not found. Run: npx tribunal-kit init');
1055
- process.exit(1);
1056
- }
1057
-
1058
- const args = process.argv.slice(3);
1059
- if (args.length === 0 || args[0] === 'help' || args[0] === '--help') {
1060
- console.error('Usage: npx tribunal-kit context <target_file>');
1061
- process.exit(1);
1062
- }
1063
-
1064
- const targetFile = args[0].replace(/\\/g, '/');
1065
- const snapshotName = targetFile.replace(/[\\\/]/g, '__') + '.json';
1066
- const snapshotPath = require('path').join(agentDest, 'history', 'snapshots', snapshotName);
1067
-
1068
- if (!require('fs').existsSync(snapshotPath)) {
1069
- console.error(' \x1b[91m✖\x1b[0m Context Snapshot not found for: ' + targetFile);
1070
- console.log(' Run: npx tribunal-kit graph (to generate snapshots)');
1071
- process.exit(1);
1072
- }
1073
-
1074
- try {
1075
- const snapshot = JSON.parse(require('fs').readFileSync(snapshotPath, 'utf8'));
1076
-
1077
- console.log('\n# Context Snapshot: ' + snapshot.file);
1078
- process.stdout.write('> Size Estimate: ' + (snapshot['estimatedTokens'] || 'Unknown') + '\n');
1079
- console.log('> Risk Score: ' + snapshot.riskScore + ' (Blast Radius: ' + snapshot.blastRadius + ')\n');
1080
-
1081
- if (Object.keys(snapshot.imports).length > 0) {
1082
- console.log('## Imports');
1083
- for (const [imp, exports] of Object.entries(snapshot.imports)) {
1084
- if (exports && exports.length > 0) {
1085
- console.log('- `' + imp + '` (exports: ' + exports.join(', ') + ')');
1086
- } else {
1087
- console.log('- `' + imp + '`');
1088
- }
1089
- }
1090
- console.log();
1091
- }
1092
-
1093
- if (snapshot.dependents && snapshot.dependents.length > 0) {
1094
- console.log('## Dependents');
1095
- for (const dep of snapshot.dependents) {
1096
- console.log('- `' + dep + '`');
1097
- }
1098
- console.log();
1099
- }
1100
-
1101
- console.log('## Source Code');
1102
- console.log('```javascript\n' + snapshot.content + '\n```\n');
1103
-
1104
- } catch (e) {
1105
- console.error('Failed to read snapshot: ' + e.message);
1106
- process.exit(1);
1107
- }
1108
- }
1109
-
1110
- // ── Main ──────────────────────────────────────────────────
1111
- const { command, flags } = parseArgs(process.argv);
1112
-
1113
- if (flags.quiet) quiet = true;
1114
- if (flags.verbose) verbose = true;
1115
-
1116
- runWithUpdateCheck(command, flags);
1117
-
1118
- // -- Exports (for testing) -- do not remove
1119
- if (require.main !== module) {
1120
- module.exports = { parseArgs, compareSemver, copyDir, countDir, isSelfInstall, CORE_AGENTS, CORE_SKILLS, generateIDEBridges, cmdMarathon };
2
+ /**
3
+ * tribunal-kit CLI (alias: tk)
4
+ *
5
+ * Commands:
6
+ * init — Install .agent/ into target project
7
+ * update — Re-install to get latest changes
8
+ * status — Check if .agent/ is installed
9
+ * learn — Evolve project idioms based on git diffs
10
+ * case — Manage Case Law precedents
11
+ * hook — Install pre-push git hook
12
+ * uninstall — Remove .agent/ from project
13
+ *
14
+ * Usage:
15
+ * npx tribunal-kit init
16
+ * npx tribunal-kit init --force
17
+ * npx tribunal-kit init --path ./myapp
18
+ * npx tribunal-kit init --quiet
19
+ * npx tribunal-kit init --dry-run
20
+ * tribunal-kit update
21
+ * tribunal-kit status
22
+ * tribunal-kit uninstall
23
+ */
24
+
25
+ const fs = require('fs');
26
+ const path = require('path');
27
+ const https = require('https');
28
+ const { execSync, spawn } = require('child_process');
29
+
30
+ function runShellAsync(command, options) {
31
+ return new Promise((resolve, reject) => {
32
+ const child = spawn(command, [], { ...options, shell: true });
33
+ child.on('close', code => {
34
+ if (code !== 0) reject(new Error(`Command failed with exit code ${code}`));
35
+ else resolve();
36
+ });
37
+ child.on('error', reject);
38
+ });
39
+ }
40
+
41
+ const PKG = require(path.resolve(__dirname, '..', 'package.json'));
42
+ const CURRENT_VERSION = PKG.version;
43
+
44
+ // ── Colors ───────────────────────────────────────────────
45
+ const C = {
46
+ reset: '\x1b[0m',
47
+ bold: '\x1b[1m',
48
+ dim: '\x1b[2m',
49
+ red: '\x1b[91m',
50
+ green: '\x1b[92m',
51
+ yellow: '\x1b[93m',
52
+ blue: '\x1b[94m',
53
+ magenta: '\x1b[95m',
54
+ cyan: '\x1b[96m',
55
+ white: '\x1b[97m',
56
+ gray: '\x1b[90m',
57
+ bgCyan: '\x1b[46m',
58
+ };
59
+
60
+ function colorize(color, text) {
61
+ return `${C[color]}${text}${C.reset}`;
62
+ }
63
+
64
+ function c(color, text) { return `${C[color]}${text}${C.reset}`; }
65
+ function bold(text) { return `${C.bold}${text}${C.reset}`; }
66
+
67
+ // ── Logging ──────────────────────────────────────────────
68
+ let quiet = false;
69
+ let verbose = false;
70
+
71
+ function log(msg) { if (!quiet) console.log(msg); }
72
+ function ok(msg) { if (!quiet) console.log(` ${c('green', '✔')} ${msg}`); }
73
+ function warn(msg) { if (!quiet) console.log(` ${c('yellow', '⚠')} ${msg}`); }
74
+ function err(msg) { console.error(` ${c('red', '✖')} ${msg}`); }
75
+ function dim(msg) { if (!quiet) console.log(` ${c('gray', msg)}`); }
76
+ function dbg(msg) { if (verbose) console.log(` ${c('gray', '⊡')} ${c('gray', msg)}`); }
77
+
78
+ // ── Arg Parser ───────────────────────────────────────────
79
+ function parseArgs(argv) {
80
+ const args = { command: null, flags: {} };
81
+ const raw = argv.slice(2);
82
+
83
+ // First non-flag arg is the command
84
+ for (const arg of raw) {
85
+ if (!arg.startsWith('--') && !args.command) {
86
+ args.command = arg;
87
+ continue;
88
+ }
89
+ if (arg === '--force') { args.flags.force = true; continue; }
90
+ if (arg === '--quiet') { args.flags.quiet = true; continue; }
91
+ if (arg === '--verbose') { args.flags.verbose = true; continue; }
92
+ if (arg === '--dry-run') { args.flags.dryRun = true; continue; }
93
+ if (arg === '--minimal') { args.flags.minimal = true; continue; }
94
+ if (arg === '--skip-update-check') { args.flags.skipUpdateCheck = true; continue; }
95
+ if (arg === '--head') { args.flags.head = true; continue; }
96
+ if (arg.startsWith('--path=')) {
97
+ args.flags.path = arg.split('=').slice(1).join('=');
98
+ }
99
+ if (arg === '--path') {
100
+ const idx = raw.indexOf('--path');
101
+ const nextVal = raw[idx + 1];
102
+ if (!nextVal || nextVal.startsWith('--')) {
103
+ console.error(` \x1b[91m✖ --path requires a directory argument\x1b[0m`);
104
+ process.exit(1);
105
+ }
106
+ args.flags.path = nextVal;
107
+ }
108
+ if (arg.startsWith('--branch=')) {
109
+ args.flags.branch = arg.split('=').slice(1).join('=');
110
+ }
111
+ }
112
+
113
+ return args;
114
+ }
115
+
116
+ // ── File Utilities ────────────────────────────────────────
117
+
118
+ // Core agents to install in --minimal mode
119
+ const CORE_AGENTS = new Set([
120
+ 'backend-specialist.md',
121
+ 'frontend-specialist.md',
122
+ 'database-architect.md',
123
+ 'debugger.md',
124
+ 'security-auditor.md',
125
+ 'logic-reviewer.md',
126
+ 'dependency-reviewer.md',
127
+ 'type-safety-reviewer.md',
128
+ 'performance-reviewer.md',
129
+ 'orchestrator.md',
130
+ 'explorer-agent.md',
131
+ 'project-planner.md',
132
+ 'test-engineer.md',
133
+ ]);
134
+
135
+ // Core skills to install in --minimal mode
136
+ const CORE_SKILLS = new Set([
137
+ 'clean-code', 'architecture', 'testing-patterns', 'systematic-debugging',
138
+ 'frontend-design', 'database-design', 'api-patterns', 'nodejs-best-practices',
139
+ 'vulnerability-scanner', 'typescript-advanced', 'python-pro', 'nextjs-react-expert',
140
+ 'react-specialist', 'performance-profiling', 'lint-and-validate',
141
+ ]);
142
+
143
+ async function copyDir(src, dest, dryRun = false, filter = null) {
144
+ if (!dryRun) {
145
+ await fs.promises.mkdir(dest, { recursive: true });
146
+ }
147
+
148
+ const entries = await fs.promises.readdir(src, { withFileTypes: true });
149
+ let count = 0;
150
+
151
+ for (const entry of entries) {
152
+ // Apply filter if provided (for --minimal mode)
153
+ if (filter && !filter(entry.name, src)) {
154
+ dbg(` skip: ${entry.name}`);
155
+ continue;
156
+ }
157
+
158
+ const srcPath = path.join(src, entry.name);
159
+ const destPath = path.join(dest, entry.name);
160
+
161
+ if (entry.isDirectory()) {
162
+ count += await copyDir(srcPath, destPath, dryRun, filter);
163
+ } else {
164
+ if (!dryRun) {
165
+ await fs.promises.copyFile(srcPath, destPath);
166
+ }
167
+ dbg(` copy: ${entry.name}`);
168
+ count++;
169
+ }
170
+ }
171
+
172
+ return count;
173
+ }
174
+
175
+ async function countDir(dir) {
176
+ let count = 0;
177
+ const entries = await fs.promises.readdir(dir, { withFileTypes: true });
178
+ for (const e of entries) {
179
+ if (e.isDirectory()) count += await countDir(path.join(dir, e.name));
180
+ else count++;
181
+ }
182
+ return count;
183
+ }
184
+
185
+ // ── Version Check & Auto-Update ──────────────────────────
186
+
187
+ /**
188
+ * Compare two semver strings. Returns:
189
+ * 1 if a > b, -1 if a < b, 0 if equal.
190
+ */
191
+ function compareSemver(a, b) {
192
+ const pa = a.replace(/^v/, '').split('.').map(Number);
193
+ const pb = b.replace(/^v/, '').split('.').map(Number);
194
+ for (let i = 0; i < 3; i++) {
195
+ const na = pa[i] || 0;
196
+ const nb = pb[i] || 0;
197
+ if (na > nb) return 1;
198
+ if (na < nb) return -1;
199
+ }
200
+ return 0;
201
+ }
202
+
203
+ /**
204
+ * Fetch the latest version from npm registry.
205
+ * Returns the version string (e.g. '4.0.0') or null on failure.
206
+ */
207
+ function fetchLatestVersion() {
208
+ return new Promise((resolve) => {
209
+ const req = https.get(
210
+ 'https://registry.npmjs.org/tribunal-kit/latest',
211
+ {
212
+ headers: {
213
+ 'Accept': 'application/json',
214
+ 'User-Agent': `tribunal-kit/${CURRENT_VERSION}`
215
+ },
216
+ timeout: 5000
217
+ },
218
+ (res) => {
219
+ let data = '';
220
+ res.on('data', (chunk) => { data += chunk; });
221
+ res.on('end', () => {
222
+ try {
223
+ const json = JSON.parse(data);
224
+ const version = json.version || null;
225
+ resolve(version);
226
+ } catch {
227
+ resolve(null);
228
+ }
229
+ });
230
+ }
231
+ );
232
+ req.on('error', () => resolve(null));
233
+ req.on('timeout', () => { req.destroy(); resolve(null); });
234
+ });
235
+ }
236
+
237
+ /**
238
+ * Check for a newer version and re-invoke with @latest if found.
239
+ * Uses TK_SKIP_UPDATE_CHECK env var as recursion guard.
240
+ * Returns true if a re-invoke happened (caller should exit), false otherwise.
241
+ */
242
+ async function autoUpdateCheck(originalArgs) {
243
+ // Recursion guard: if we're already a re-invoked process, skip
244
+ if (process.env.TK_SKIP_UPDATE_CHECK === '1') {
245
+ return false;
246
+ }
247
+
248
+ const latestVersion = await fetchLatestVersion();
249
+
250
+ if (!latestVersion) {
251
+ // Network fail — proceed silently with current version
252
+ return false;
253
+ }
254
+
255
+ if (compareSemver(latestVersion, CURRENT_VERSION) <= 0) {
256
+ // Already up to date
257
+ dim(`Version ${CURRENT_VERSION} is up to date.`);
258
+ return false;
259
+ }
260
+
261
+ // Newer version available — re-invoke
262
+ log('');
263
+ log(colorize('cyan', ` ⬆ New version available: ${colorize('bold', CURRENT_VERSION)} → ${colorize('bold', latestVersion)}`));
264
+ log(colorize('gray', ' Re-invoking with latest version...'));
265
+ log('');
266
+
267
+ try {
268
+ // Build the command pulling from npm registry
269
+ const args = originalArgs.join(' ');
270
+ const cmd = `npx -y tribunal-kit@${latestVersion} ${args}`;
271
+
272
+ execSync(cmd, {
273
+ stdio: 'inherit',
274
+ env: { ...process.env, TK_SKIP_UPDATE_CHECK: '1' },
275
+ });
276
+ return true; // Re-invoke succeeded, caller should exit
277
+ } catch (e) {
278
+ warn(`Auto-update failed: ${e.message}`);
279
+ warn('Continuing with current version...');
280
+ return false; // Fall through to current version
281
+ }
282
+ }
283
+
284
+ // ── Kit Source Location ───────────────────────────────────
285
+ function getKitAgent() {
286
+ // When installed via npm, the .agent/ folder is next to this script's package
287
+ const kitRoot = path.resolve(__dirname, '..');
288
+ const agentDir = path.join(kitRoot, '.agent');
289
+
290
+ if (!fs.existsSync(agentDir)) {
291
+ err(`Kit .agent/ folder not found at: ${agentDir}`);
292
+ err('The package may be corrupted. Try: npm install -g tribunal-kit');
293
+ process.exit(1);
294
+ }
295
+
296
+ return agentDir;
297
+ }
298
+
299
+ // ── Self-Install Guard ────────────────────────────────────
300
+ /**
301
+ * Returns true if the target directory IS the tribunal-kit package itself.
302
+ * This prevents `init --force` / `update` from deleting the package's own files
303
+ * when run from inside the project directory.
304
+ */
305
+ function isSelfInstall(targetDir) {
306
+ const kitRoot = path.resolve(__dirname, '..');
307
+ const resolvedTarget = path.resolve(targetDir);
308
+
309
+ // Direct path match
310
+ if (resolvedTarget === kitRoot) return true;
311
+
312
+ // Check if the target's package.json is this package
313
+ const targetPkg = path.join(resolvedTarget, 'package.json');
314
+ if (fs.existsSync(targetPkg)) {
315
+ try {
316
+ const targetName = JSON.parse(fs.readFileSync(targetPkg, 'utf8')).name;
317
+ if (targetName === PKG.name) return true;
318
+ } catch {
319
+ // Unreadable package.json — not a match
320
+ }
321
+ }
322
+
323
+ return false;
324
+ }
325
+
326
+ // ── Banner ────────────────────────────────────────────────
327
+ function banner() {
328
+ if (quiet) return;
329
+ // Big ASCII art (TRIBUNAL-KIT)
330
+ const art = String.raw`
331
+ ████████╗██████╗ ██╗██████╗ ██╗ ██╗███╗ ██╗ █████╗ ██╗ ██╗ ██╗██╗████████╗
332
+ ╚══██╔══╝██╔══██╗██║██╔══██╗██║ ██║████╗ ██║██╔══██╗██║ ██║ ██╔╝██║╚══██╔══╝
333
+ ██║ ██████╔╝██║██████╔╝██║ ██║██╔██╗ ██║███████║██║█████╗█████╔╝ ██║ ██║
334
+ ██║ ██╔══██╗██║██╔══██╗██║ ██║██║╚██╗██║██╔══██║██║╚════╝██╔═██╗ ██║ ██║
335
+ ██║ ██║ ██║██║██████╔╝╚██████╔╝██║ ╚████║██║ ██║███████╗ ██║ ██╗██║ ██║
336
+ ╚═╝ ╚═╝ ╚═╝╚═╝╚═════╝ ╚═════╝ ╚═╝ ╚═══╝╚═╝ ╚═╝╚══════╝ ╚═╝ ╚═╝╚═╝ ╚═╝ `.split('\n').filter(Boolean);
337
+ console.log();
338
+ const _maxLen = Math.max(...art.map(line => line.length));
339
+ for (const line of art) {
340
+ let gradientLine = ' ' + C.bold;
341
+ for (let i = 0; i < line.length; i++) {
342
+ gradientLine += `\x1b[38;2;255;22;55m${line[i]}`;
343
+ }
344
+ gradientLine += C.reset;
345
+ log(gradientLine);
346
+ }
347
+ console.log();
348
+ // Subtitle strip
349
+ const W = 84;
350
+ const sub = 'Anti-Hallucination Agent System';
351
+ const sp = Math.max(0, W - sub.length);
352
+ const centred = ' '.repeat(Math.floor(sp / 2)) + sub + ' '.repeat(Math.ceil(sp / 2));
353
+ const RED_ANSI = '\x1b[38;2;255;22;55m';
354
+ console.log(` ${RED_ANSI}╔${'═'.repeat(W)}╗${C.reset}`);
355
+ console.log(` ${RED_ANSI}║${C.reset}${c('gray', centred)}${RED_ANSI}║${C.reset}`);
356
+ console.log(` ${RED_ANSI}╚${'═'.repeat(W)}╝${C.reset}`);
357
+ console.log();
358
+ }
359
+
360
+ // ── Commands ──────────────────────────────────────────────
361
+ async function cmdInit(flags) {
362
+ const agentSrc = getKitAgent();
363
+ const targetDir = flags.path ? path.resolve(flags.path) : process.cwd();
364
+ const agentDest = path.join(targetDir, '.agent');
365
+ const dryRun = flags.dryRun || false;
366
+
367
+ // ── Self-install guard ──────────────────────────────────
368
+ if (isSelfInstall(targetDir)) {
369
+ err('Cannot run init/update inside the tribunal-kit package itself.');
370
+ err(`Target: ${targetDir}`);
371
+ err(`Package: ${path.resolve(__dirname, '..')}`);
372
+ console.log();
373
+ dim('This command is designed to install .agent/ into OTHER projects.');
374
+ dim('Run it from the root of the project you want to set up:');
375
+ dim(' cd /path/to/your-project');
376
+ dim(' npx tribunal-kit init');
377
+ console.log();
378
+ process.exit(1);
379
+ }
380
+ // ────────────────────────────────────────────────────────
381
+
382
+ // ── Backup / Cleanup ────────────────────────────────────
383
+ if (!dryRun && fs.existsSync(agentDest) && flags.force) {
384
+ // Backup the existing subdirectories before overwriting
385
+ const backupDir = path.join(agentDest, '.backups', `backup-${Date.now()}`);
386
+ fs.mkdirSync(backupDir, { recursive: true });
387
+
388
+ // PRESERVE_DIRS: user-generated content that must survive updates
389
+ const _PRESERVE_DIRS = ['history', 'patterns', 'mcp_config.json'];
390
+ const subdirs = ['agents', 'workflows', 'skills', 'scripts', '.shared', 'rules'];
391
+ for (const sub of subdirs) {
392
+ const subPath = path.join(agentDest, sub);
393
+ if (fs.existsSync(subPath)) {
394
+ // Copy to backup dir
395
+ await copyDir(subPath, path.join(backupDir, sub), false);
396
+ await fs.promises.rm(subPath, { recursive: true, force: true });
397
+ }
398
+ }
399
+ log(` ${c('gray', '✦ Backed up existing configurations to .agent/.backups/')}`);
400
+
401
+
402
+ }
403
+ // ────────────────────────────────────────────────────────
404
+
405
+ banner();
406
+
407
+ if (dryRun) {
408
+ log(colorize('yellow', ' DRY RUN — no files will be written'));
409
+ console.log();
410
+ }
411
+
412
+ // Check target exists
413
+ if (!fs.existsSync(targetDir)) {
414
+ err(`Target directory not found: ${targetDir}`);
415
+ process.exit(1);
416
+ }
417
+
418
+ // Check if .agent already exists
419
+ if (fs.existsSync(agentDest) && !flags.force) {
420
+ warn('.agent/ already exists in this project.');
421
+ log(` ${c('gray', '▸')} To refresh or update it, run: ${colorize('white', 'tribunal-kit init --force')}`);
422
+ log(` ${c('gray', '▸')} Or check status with: ${colorize('cyan', 'tribunal-kit status')}`);
423
+ console.log();
424
+ process.exit(0);
425
+ }
426
+
427
+ // Ensure history dirs exist (Case Law + Skill Evolution)
428
+ if (!dryRun) {
429
+ const caseDir = path.join(agentDest, 'history', 'case-law', 'cases');
430
+ const evoDir = path.join(agentDest, 'history', 'skill-evolution');
431
+ fs.mkdirSync(caseDir, { recursive: true });
432
+ fs.mkdirSync(evoDir, { recursive: true });
433
+ const gkCase = path.join(caseDir, '.gitkeep');
434
+ const gkEvo = path.join(evoDir, '.gitkeep');
435
+ if (!fs.existsSync(gkCase)) fs.writeFileSync(gkCase, '');
436
+ if (!fs.existsSync(gkEvo)) fs.writeFileSync(gkEvo, '');
437
+ }
438
+
439
+ // Count what we're installing
440
+ const isMinimal = flags.minimal || false;
441
+ if (isMinimal) {
442
+ log(` ${c('yellow','⚡')} ${bold('Minimal mode')} installing core agents and skills only`);
443
+ console.log();
444
+ }
445
+ const totalFiles = await countDir(agentSrc);
446
+ dbg(`Source: ${agentSrc}`);
447
+ dbg(`Target: ${agentDest}`);
448
+ dbg(`Total source files: ${totalFiles}`);
449
+ log(` ${c('gray','▸')} Scanning ${c('white', String(totalFiles))} files ${c('gray','→')} ${c('gray', agentDest)}`);
450
+
451
+ try {
452
+ // Build filter for --minimal mode
453
+ const minimalFilter = isMinimal ? (name, parentDir) => {
454
+ const parentName = path.basename(parentDir);
455
+ if (parentName === 'agents') return CORE_AGENTS.has(name);
456
+ if (parentName === 'skills') return CORE_SKILLS.has(name);
457
+ return true; // everything else passes
458
+ } : null;
459
+
460
+ const copied = await copyDir(agentSrc, agentDest, dryRun, minimalFilter);
461
+
462
+ console.log();
463
+ if (dryRun) {
464
+ ok(`${bold('DRY RUN')} complete would install ${c('cyan', String(copied))} files`);
465
+ dim(`Target: ${agentDest}`);
466
+ } else {
467
+ // ── Success card W=62, rows padded by plain-text length ──
468
+ const W = 62;
469
+ const agentsCount = fs.readdirSync(path.join(agentDest, 'agents')).length;
470
+ const workflowsCount = fs.readdirSync(path.join(agentDest, 'workflows')).length;
471
+ const skillsCount = fs.readdirSync(path.join(agentDest, 'skills')).length;
472
+ const scriptsCount = fs.readdirSync(path.join(agentDest, 'scripts')).length;
473
+
474
+ // Stat rows: compute trailing spaces from plain text so right ║ aligns
475
+ const statRow = (icon, label, val, col) => {
476
+ // emoji JS .length===2 == terminal display width 2 ✓
477
+ const plain = ` ${icon} ${label.padEnd(10)}${String(val).padStart(3)} installed`;
478
+ const trail = ' '.repeat(Math.max(0, W - plain.length));
479
+ return ` ${c('cyan','║')} ${icon} ${c('white',label.padEnd(10))}${c(col,String(val).padStart(3))} ${c('gray','installed')}${trail}${c('cyan','║')}`;
480
+ };
481
+ // Plain-text rows (header / blank)
482
+ const plainRow = (text, wrapFn) => {
483
+ const trail = ' '.repeat(Math.max(0, W - text.length));
484
+ return ` ${c('cyan','║')}${wrapFn(text)}${trail}${c('cyan','║')}`;
485
+ };
486
+ // Next-step rows: fixed cmd column + description
487
+ const stepRow = (cmd, desc) => {
488
+ const plain = ` ${cmd.padEnd(16)}${desc}`;
489
+ const trail = ' '.repeat(Math.max(0, W - plain.length));
490
+ return ` ${c('cyan','║')} ${c('white',cmd.padEnd(16))}${c('gray',desc)}${trail}${c('cyan','')}`;
491
+ };
492
+
493
+ console.log(` ${c('green','✔')} ${bold(c('green','Installation complete'))} ${c('gray','—')} ${c('white',String(copied))} files`);
494
+ console.log(` ${c('gray',' ╰─')} ${c('gray', agentDest)}`);
495
+ console.log();
496
+ console.log(` ${c('cyan', '╔' + '═'.repeat(W) + '╗')}`);
497
+ console.log(plainRow(` What's inside:`, s => c('bold', c('white', s))));
498
+ console.log(` ${c('cyan', '╠' + '═'.repeat(W) + '')}`);
499
+ console.log(statRow('🤖', 'Agents', agentsCount, 'magenta'));
500
+ console.log(statRow('⚡', 'Workflows', workflowsCount, 'yellow'));
501
+ console.log(statRow('🧠', 'Skills', skillsCount, 'blue'));
502
+ console.log(statRow('🔧', 'Scripts', scriptsCount, 'green'));
503
+ console.log(` ${c('cyan', '╠' + '═'.repeat(W) + '╣')}`);
504
+ console.log(plainRow('', () => ''));
505
+ console.log(plainRow(` Next steps:`, s => c('gray', s)));
506
+ console.log(stepRow('/generate', 'Generate code with anti-hallucination'));
507
+ console.log(stepRow('/review', 'Audit existing code for issues'));
508
+ console.log(stepRow('/tribunal-full', 'Run all 16 reviewers in parallel'));
509
+ console.log(plainRow('', () => ''));
510
+ console.log(` ${c('cyan', '╚' + '═'.repeat(W) + '╝')}`);
511
+ console.log();
512
+ log(` ${c('gray', '✦ Generating IDE bridge files...')}`);
513
+ await generateIDEBridges(targetDir, agentDest, dryRun);
514
+ }
515
+
516
+ console.log();
517
+ } catch (e) {
518
+ err(`Failed to install: ${e.message}`);
519
+ process.exit(1);
520
+ }
521
+ }
522
+
523
+ // ── IDE Bridge Files ──────────────────────────────────────
524
+ // Each AI IDE reads rules from a different location.
525
+ // We generate bridge files that point each IDE at .agent/
526
+ async function generateIDEBridges(targetDir, agentDest, dryRun = false) {
527
+ const rulesFile = path.join(agentDest, 'rules', 'GEMINI.md');
528
+ let rulesContent = '';
529
+ try {
530
+ rulesContent = await fs.promises.readFile(rulesFile, 'utf8');
531
+ } catch {
532
+ // rules file doesn't exist
533
+ }
534
+
535
+ // Helper: write a bridge file only if it doesn't already exist
536
+ const writeBridge = async (filePath, content, label) => {
537
+ if (dryRun) {
538
+ dbg(` would create: ${filePath}`);
539
+ return;
540
+ }
541
+ const dir = path.dirname(filePath);
542
+ try {
543
+ await fs.promises.mkdir(dir, { recursive: true });
544
+ await fs.promises.stat(filePath);
545
+ dbg(` skip (exists): ${path.basename(filePath)}`);
546
+ } catch (err) {
547
+ if (err.code === 'ENOENT') {
548
+ await fs.promises.writeFile(filePath, content, 'utf8');
549
+ ok(`${label} → ${c('gray', path.relative(targetDir, filePath))}`);
550
+ }
551
+ }
552
+ };
553
+
554
+ // ── 1. Cursor (.cursorrules) ──────────────────────────
555
+ const cursorRules = `# Tribunal Kit — Cursor Bridge
556
+ # Auto-generated by tribunal-kit init. Do not edit manually.
557
+ # Source: .agent/rules/GEMINI.md
558
+
559
+ ${rulesContent}
560
+ `;
561
+ await writeBridge(
562
+ path.join(targetDir, '.cursorrules'),
563
+ cursorRules,
564
+ 'Cursor'
565
+ );
566
+
567
+ // ── 2. Windsurf (.windsurfrules) ─────────────────────
568
+ const windsurfRules = `# Tribunal Kit — Windsurf Bridge
569
+ # Auto-generated by tribunal-kit init. Do not edit manually.
570
+ # Source: .agent/rules/GEMINI.md
571
+
572
+ ${rulesContent}
573
+ `;
574
+ await writeBridge(
575
+ path.join(targetDir, '.windsurfrules'),
576
+ windsurfRules,
577
+ 'Windsurf'
578
+ );
579
+
580
+ // ── 3. Gemini / Antigravity (.gemini/settings.json) ──
581
+ const geminiSettings = JSON.stringify({
582
+ "$schema": "https://raw.githubusercontent.com/anthropics/anthropic-cookbook/main/.gemini/settings.schema.json",
583
+ "rules": [
584
+ { "path": "../.agent/rules/GEMINI.md", "trigger": "always_on" }
585
+ ],
586
+ "agents": { "directory": "../.agent/agents" },
587
+ "skills": { "directory": "../.agent/skills" },
588
+ "workflows": { "directory": "../.agent/workflows" }
589
+ }, null, 2) + '\n';
590
+ await writeBridge(
591
+ path.join(targetDir, '.gemini', 'settings.json'),
592
+ geminiSettings,
593
+ 'Gemini/Antigravity'
594
+ );
595
+
596
+ // ── Also create .gemini/GEMINI.md as a direct rules file ──
597
+ const geminiRulesBridge = `---
598
+ trigger: always_on
599
+ ---
600
+
601
+ # Tribunal Kit — Gemini Bridge
602
+ # Auto-generated by tribunal-kit init.
603
+ # Full rules: .agent/rules/GEMINI.md
604
+
605
+ ${rulesContent}
606
+ `;
607
+ await writeBridge(
608
+ path.join(targetDir, '.gemini', 'GEMINI.md'),
609
+ geminiRulesBridge,
610
+ 'Gemini rules'
611
+ );
612
+
613
+ // ── 4. GitHub Copilot (.github/copilot-instructions.md) ──
614
+ const copilotInstructions = `# Tribunal Kit — Copilot Bridge
615
+ # Auto-generated by tribunal-kit init. Do not edit manually.
616
+ # Source: .agent/rules/GEMINI.md
617
+
618
+ ${rulesContent}
619
+ `;
620
+ await writeBridge(
621
+ path.join(targetDir, '.github', 'copilot-instructions.md'),
622
+ copilotInstructions,
623
+ 'GitHub Copilot'
624
+ );
625
+
626
+ // ── 5. Claude (.claude/CLAUDE.md) ─────────────────────
627
+ const claudeRules = `# Tribunal Kit Claude Bridge
628
+ # Auto-generated by tribunal-kit init. Do not edit manually.
629
+ # Source: .agent/rules/GEMINI.md
630
+
631
+ ${rulesContent}
632
+ `;
633
+ await writeBridge(
634
+ path.join(targetDir, '.claude', 'CLAUDE.md'),
635
+ claudeRules,
636
+ 'Claude'
637
+ );
638
+
639
+ console.log();
640
+ }
641
+
642
+ async function cmdSync(args) {
643
+ console.log(`\n╭─ ${c('bold', 'Tribunal IDE Sync')} ──────────────────`);
644
+ console.log('│');
645
+ console.log(`│ ${c('gray', ' Regenerating IDE bridge files...')}`);
646
+ const cwd = process.cwd();
647
+ const agentDest = path.join(cwd, '.agent');
648
+ if (!fs.existsSync(agentDest)) {
649
+ console.error(`│ ${c('red', '✖ Error: .agent/ directory not found.')}`);
650
+ console.error(`│ ${c('gray', 'Run `tk init` first.')}`);
651
+ process.exit(1);
652
+ }
653
+ await generateIDEBridges(cwd, agentDest, false);
654
+ console.log(`│ ${c('green', '✔ Sync complete.')}`);
655
+ console.log('╰────────────────────────────────────────\n');
656
+ }
657
+
658
+ async function cmdUpdate(flags) {
659
+ // ── Self-install guard (early, before banner) ───────────
660
+ const targetDir = flags.path ? path.resolve(flags.path) : process.cwd();
661
+ if (isSelfInstall(targetDir)) {
662
+ err('Cannot run update inside the tribunal-kit package itself.');
663
+ err(`Target: ${targetDir}`);
664
+ console.log();
665
+ dim('This command is designed to update .agent/ in OTHER projects.');
666
+ dim('Run it from the root of the project you want to update:');
667
+ dim(' cd /path/to/your-project');
668
+ dim(' npx tribunal-kit update');
669
+ console.log();
670
+ process.exit(1);
671
+ }
672
+ // ────────────────────────────────────────────────────────
673
+
674
+ // Update = init with --force
675
+ flags.force = true;
676
+ if (!quiet) {
677
+ log(` ${c('cyan','↻')} ${bold('Updating')} ${c('white','.agent/')} to latest version...`);
678
+ console.log();
679
+ }
680
+ await cmdInit(flags);
681
+ }
682
+
683
+
684
+ async function cmdLearn(flags) {
685
+ const targetDir = flags.path ? path.resolve(flags.path) : process.cwd();
686
+ const agentDest = path.join(targetDir, '.agent');
687
+
688
+ if (!fs.existsSync(agentDest)) {
689
+ err('.agent/ not found. Run: npx tribunal-kit init');
690
+ process.exit(1);
691
+ }
692
+
693
+ banner();
694
+
695
+ const W = 62;
696
+ const title = ' Tribunal Learn — Supreme Court Mode';
697
+ const trail = ' '.repeat(Math.max(0, W - title.length));
698
+ console.log(` ${c('cyan', '\u2554' + '\u2550'.repeat(W) + '\u2557')}`);
699
+ console.log(` ${c('cyan', '\u2551')}${c('bold', c('white', title))}${trail}${c('cyan', '\u2551')}`);
700
+ console.log(` ${c('cyan', '\u255a' + '\u2550'.repeat(W) + '\u255d')}`);
701
+ console.log();
702
+
703
+ const dryRun = flags.dryRun ? '--dry-run' : '';
704
+ const useHead = flags.head ? '--head' : '';
705
+
706
+
707
+ // Phase 1: Skill Evolution
708
+ log(` ${c('cyan', '\u229b')} ${bold('Phase 1')} \u2014 Skill Evolution Forge (auto-generating project idioms)`);
709
+ const evoScript = path.join(agentDest, 'scripts', 'skill_evolution.js');
710
+ if (!fs.existsSync(evoScript)) {
711
+ warn('skill_evolution.js not found \u2014 run: npx tribunal-kit update');
712
+ } else {
713
+ try {
714
+ const cmd = `node "${evoScript}" digest ${dryRun} ${useHead}`.trim();
715
+ await runShellAsync(cmd, { stdio: 'inherit', cwd: targetDir });
716
+ } catch (e) {
717
+ warn(`Skill Evolution error: ${e.message}`);
718
+ }
719
+ }
720
+
721
+ console.log();
722
+
723
+ // Phase 2: Case Law prompt
724
+ log(` ${c('cyan', '\u229b')} ${bold('Phase 2')} \u2014 Case Law Engine (building precedence record)`);
725
+ console.log();
726
+ log(` ${c('gray','\u25b8')} Record a new rejection precedent:`);
727
+ log(` ${c('white', 'npx tribunal-kit case add')}`);
728
+ console.log();
729
+ log(` ${c('gray','\u25b8')} Search existing case law:`);
730
+ log(` ${c('white', 'npx tribunal-kit case search "your query"')}`);
731
+ console.log();
732
+ log(` ${c('green', '\u2714')} ${bold('Learn cycle complete.')} Your Tribunal grows smarter with every commit.`);
733
+ console.log();
734
+ }
735
+
736
+ // ── Async Main Wrapper ───────────────────────────────────
737
+ async function runWithUpdateCheck(command, flags) {
738
+ const shouldSkip = flags.skipUpdateCheck || process.env.TK_SKIP_UPDATE_CHECK === '1';
739
+
740
+ if (!shouldSkip && (command === 'init' || command === 'update')) {
741
+ // Pass through the original args (minus the node/script path)
742
+ const originalArgs = process.argv.slice(2);
743
+ const didReInvoke = await autoUpdateCheck(originalArgs);
744
+ if (didReInvoke) {
745
+ process.exit(0); // Latest version handled it
746
+ }
747
+ }
748
+
749
+ // Proceed with current version
750
+ switch (command) {
751
+ case 'init':
752
+ await cmdInit(flags);
753
+ break;
754
+ case 'update':
755
+ await cmdUpdate(flags);
756
+ break;
757
+ case 'status':
758
+ cmdStatus(flags);
759
+ break;
760
+ case 'learn':
761
+ await cmdLearn(flags);
762
+ break;
763
+ case 'case':
764
+ await cmdCase(flags);
765
+ break;
766
+ case 'hook':
767
+ cmdHook(flags);
768
+ break;
769
+ case 'graph':
770
+ await cmdGraph(flags);
771
+ break;
772
+ case 'mutate':
773
+ await cmdMutate(flags);
774
+ break;
775
+ case 'context':
776
+ cmdContext(flags);
777
+ break;
778
+ case 'sync':
779
+ await cmdSync();
780
+ break;
781
+ case 'marathon':
782
+ await cmdMarathon(flags);
783
+ break;
784
+ case 'uninstall':
785
+ cmdUninstall(flags);
786
+ break;
787
+ case 'help':
788
+ case '--help':
789
+ case '-h':
790
+ case null:
791
+ cmdHelp();
792
+ break;
793
+ default:
794
+ err(`Unknown command: "${command}"`);
795
+ console.log();
796
+ dim('Run tribunal-kit --help for usage');
797
+ process.exit(1);
798
+ }
799
+ }
800
+
801
+ async function cmdCase(flags) {
802
+ const targetDir = flags.path ? path.resolve(flags.path) : process.cwd();
803
+ const agentDest = path.join(targetDir, '.agent');
804
+
805
+ if (!fs.existsSync(agentDest)) {
806
+ err('.agent/ not found. Run: npx tribunal-kit init');
807
+ process.exit(1);
808
+ }
809
+
810
+ const args = process.argv.slice(3).join(' ');
811
+ if (!args || args === 'help' || args === '--help' || args === '-h') {
812
+ banner();
813
+ log(` ${c('cyan', '\u2554' + '\u2550'.repeat(60) + '\u2557')}`);
814
+ log(` ${c('cyan', '\u2551')}${c('bold', c('white', ' Tribunal Case Law Engine \u2014 Supreme Court '))}${c('cyan', '\u2551')}`);
815
+ log(` ${c('cyan', '\u255a' + '\u2550'.repeat(60) + '\u255d')}`);
816
+ console.log();
817
+ log(` ${c('cyan', 'add'.padEnd(10))} ${c('gray', 'Record a new Case Law rejection pattern')}`);
818
+ log(` ${c('cyan', 'search'.padEnd(10))} ${c('gray', 'Search existing cases (e.g., search "query")')}`);
819
+ log(` ${c('cyan', 'list'.padEnd(10))} ${c('gray', 'List all recorded case law')}`);
820
+ log(` ${c('cyan', 'show'.padEnd(10))} ${c('gray', 'Show full diff for a case (e.g., show --id 1)')}`);
821
+ log(` ${c('cyan', 'stats'.padEnd(10))} ${c('gray', 'Show case law stats by domain/verdict')}`);
822
+ log(` ${c('cyan', 'export'.padEnd(10))} ${c('gray', 'Export all cases to Markdown')}`);
823
+ log(` ${c('cyan', 'overrule'.padEnd(10))} ${c('gray', 'Overrule a past precedent (e.g., overrule --id 1)')}`);
824
+ console.log();
825
+ process.exit(1);
826
+ }
827
+
828
+ const caseLawScript = path.join(agentDest, 'scripts', 'case_law_manager.js');
829
+
830
+ // Make shorthand aliases
831
+ let pyArgs = args;
832
+ if (pyArgs.startsWith('add')) pyArgs = pyArgs.replace(/^add/, 'add-case');
833
+ if (pyArgs.startsWith('search')) pyArgs = pyArgs.replace(/^search/, 'search-cases');
834
+
835
+ try {
836
+ await runShellAsync(`node "${caseLawScript}" ${pyArgs}`, { stdio: 'inherit', cwd: targetDir });
837
+ } catch {
838
+ process.exit(1); // Script already prints errors
839
+ }
840
+ }
841
+
842
+ async function cmdGraph(flags) {
843
+ const targetDir = flags.path ? path.resolve(flags.path) : process.cwd();
844
+ const agentDest = path.join(targetDir, '.agent');
845
+
846
+ if (!fs.existsSync(agentDest)) {
847
+ err('.agent/ not found. Run: npx tribunal-kit init');
848
+ process.exit(1);
849
+ }
850
+
851
+ banner();
852
+ const builderScript = path.join(agentDest, 'scripts', 'graph_builder.js');
853
+ const visualizerScript = path.join(agentDest, 'scripts', 'graph_visualizer.js');
854
+ const htmlFile = path.join(agentDest, 'history', 'architecture-explorer.html');
855
+
856
+ try {
857
+ await runShellAsync(`node "${builderScript}"`, { stdio: 'inherit', cwd: targetDir });
858
+ await runShellAsync(`node "${visualizerScript}"`, { stdio: 'inherit', cwd: targetDir });
859
+
860
+ log(` ${c('cyan', '▸')} Opening visualizer in browser...`);
861
+ const opener = process.platform === 'win32' ? 'start' : process.platform === 'darwin' ? 'open' : 'xdg-open';
862
+ await runShellAsync(`${opener} "${htmlFile}"`, { stdio: 'ignore' });
863
+ } catch (e) {
864
+ err(`Graph generation failed: ${e.message}`);
865
+ process.exit(1);
866
+ }
867
+ }
868
+
869
+ function cmdHook(flags) {
870
+ const targetDir = flags.path ? path.resolve(flags.path) : process.cwd();
871
+ const gitDir = path.join(targetDir, '.git');
872
+
873
+ if (!fs.existsSync(gitDir)) {
874
+ err('Not a git repository. Cannot install git hooks here.');
875
+ process.exit(1);
876
+ }
877
+
878
+ const hooksDir = path.join(gitDir, 'hooks');
879
+ if (!fs.existsSync(hooksDir)) {
880
+ fs.mkdirSync(hooksDir, { recursive: true });
881
+ }
882
+
883
+ const prePushPath = path.join(hooksDir, 'pre-push');
884
+ const hookScript = `#!/bin/sh\n# Supreme Court - Auto Learn on Push\necho "⚖️ Tribunal Supreme Court: Evolving Skills..."\nnpx tribunal-kit learn --head\necho "✦ Synchronizing IDE bridges..."\nnpx tribunal-kit sync\n`;
885
+
886
+ fs.writeFileSync(prePushPath, hookScript, { mode: 0o755 });
887
+
888
+ console.log();
889
+ log(` ${c('green', '✔')} Installed pre-push git hook.`);
890
+ log(` ${c('gray', '▸')} Skill Evolution and IDE Sync will now run automatically every time you git push.`);
891
+ console.log();
892
+ }
893
+
894
+ async function cmdMutate(flags) {
895
+ const targetDir = flags.path ? path.resolve(flags.path) : process.cwd();
896
+ const agentDest = path.join(targetDir, '.agent');
897
+
898
+ if (!fs.existsSync(agentDest)) {
899
+ err('.agent/ not found. Run: npx tribunal-kit init');
900
+ process.exit(1);
901
+ }
902
+
903
+ const args = process.argv.slice(3);
904
+ if (args.length < 2) {
905
+ err('Usage: npx tribunal-kit mutate <target_file> <test_command>');
906
+ process.exit(1);
907
+ }
908
+
909
+ const mutateScript = path.join(agentDest, 'scripts', 'mutation_runner.js');
910
+ try {
911
+ await runShellAsync(`node "${mutateScript}" ${args.join(' ')}`, { stdio: 'inherit', cwd: targetDir });
912
+ } catch {
913
+ process.exit(1);
914
+ }
915
+ }
916
+
917
+ function cmdUninstall(flags) {
918
+ const targetDir = flags.path ? path.resolve(flags.path) : process.cwd();
919
+ const agentDest = path.join(targetDir, '.agent');
920
+
921
+ banner();
922
+
923
+ if (!fs.existsSync(agentDest)) {
924
+ log(` ${c('yellow','⚠')} ${bold('.agent/')} is not installed in this project.`);
925
+ console.log();
926
+ return;
927
+ }
928
+
929
+ if (flags.dryRun) {
930
+ log(colorize('yellow', ' DRY RUN — would remove:'));
931
+ log(` ${c('gray',' ╰─')} ${agentDest}`);
932
+ console.log();
933
+ return;
934
+ }
935
+
936
+ try {
937
+ fs.rmSync(agentDest, { recursive: true, force: true });
938
+ log(` ${c('green','✔')} ${bold('.agent/')} has been removed from this project.`);
939
+ console.log();
940
+ log(` ${c('gray','▸')} To reinstall: ${c('cyan','npx tribunal-kit init')}`);
941
+ console.log();
942
+ } catch (e) {
943
+ err(`Failed to remove .agent/: ${e.message}`);
944
+ process.exit(1);
945
+ }
946
+ }
947
+
948
+ function cmdStatus(flags) {
949
+ const targetDir = flags.path ? path.resolve(flags.path) : process.cwd();
950
+ const agentDest = path.join(targetDir, '.agent');
951
+
952
+ banner();
953
+
954
+ if (!fs.existsSync(agentDest)) {
955
+ log(` ${c('red','✖')} ${bold('Not installed')} in this project`);
956
+ console.log();
957
+ log(` ${c('gray','Run:')} ${c('cyan','npx tribunal-kit init')}`);
958
+ console.log();
959
+ return;
960
+ }
961
+
962
+ log(` ${c('green','✔')} ${bold(c('green','Installed'))} ${c('gray','→')} ${c('gray', agentDest)}`);
963
+ console.log();
964
+
965
+ const icons = { agents: '🤖', workflows: '⚡', skills: '🧠', scripts: '🔧' };
966
+ const colors = { agents: 'magenta', workflows: 'yellow', skills: 'blue', scripts: 'green' };
967
+ const subdirs = ['agents', 'workflows', 'skills', 'scripts'];
968
+ for (const sub of subdirs) {
969
+ const subPath = path.join(agentDest, sub);
970
+ if (fs.existsSync(subPath)) {
971
+ const count = fs.readdirSync(subPath).filter(f => !fs.statSync(path.join(subPath, f)).isDirectory()).length;
972
+ log(` ${icons[sub]} ${c(colors[sub], sub.padEnd(12))}${c('white', String(count).padStart(3))} files`);
973
+ }
974
+ }
975
+ console.log();
976
+ }
977
+
978
+ function cmdHelp() {
979
+ banner();
980
+ const cmd = (name, desc) => ` ${c('cyan', name.padEnd(10))} ${c('gray', desc)}`;
981
+ const opt = (flag, desc) => ` ${c('yellow', flag.padEnd(22))} ${c('gray', desc)}`;
982
+ const ex = (s) => ` ${c('gray', '')} ${c('white', s)}`;
983
+
984
+ log(bold(' Commands'));
985
+ log(` ${c('gray','─'.repeat(40))}`);
986
+ log(cmd('init', 'Install .agent/ into current project'));
987
+ log(cmd('update', 'Re-install to get latest version'));
988
+ log(cmd('status', 'Check if .agent/ is installed'));
989
+ log(cmd('learn', 'Evolve project idioms based on git diffs'));
990
+ log(cmd('case', 'Manage Case Law precedents (add, search, list, show, stats, overrule)'));
991
+ log(cmd('graph', 'Build and visualize the architecture graph'));
992
+ log(cmd('mutate', 'Run the Mutation Engine to test test-suite reliability'));
993
+ log(cmd('context', 'Retrieve a highly-optimized Context Snapshot for a file'));
994
+ log(cmd('sync', 'Synchronize IDE bridge files with current rules'));
995
+ log(cmd('marathon', 'Long-running agent harness (init, status, next, mark)'));
996
+ log(cmd('hook', 'Install pre-push git hook for auto-learning'));
997
+ log(cmd('uninstall','Remove .agent/ folder from project'));
998
+ console.log();
999
+ log(bold(' Options'));
1000
+ log(` ${c('gray','─'.repeat(40))}`);
1001
+ log(opt('--force', 'Overwrite existing .agent/ folder'));
1002
+ log(opt('--path <dir>', 'Install in specific directory'));
1003
+ log(opt('--quiet', 'Suppress all output'));
1004
+ log(opt('--verbose', 'Show detailed debug logging'));
1005
+ log(opt('--dry-run', 'Preview actions without executing'));
1006
+ log(opt('--minimal', 'Install core agents/skills only (~13 agents)'));
1007
+ log(opt('--skip-update-check', 'Skip auto-update version check'));
1008
+ log(opt('--head', '(learn) Diff against last commit instead of staged'));
1009
+ console.log();
1010
+ log(bold(' Aliases'));
1011
+ log(` ${c('gray','─'.repeat(40))}`);
1012
+ log(` ${c('cyan', 'tk')} ${c('gray', 'Shorthand for tribunal-kit (e.g., tk init, tk status)')}`);
1013
+ console.log();
1014
+ log(bold(' Examples'));
1015
+ log(` ${c('gray','─'.repeat(40))}`);
1016
+ log(ex('npx tribunal-kit init'));
1017
+ log(ex('tk init --force'));
1018
+ log(ex('tk init --path ./my-app'));
1019
+ log(ex('npx tribunal-kit init --dry-run'));
1020
+ log(ex('tk update'));
1021
+ log(ex('tk status'));
1022
+ log(ex('tk learn'));
1023
+ log(ex('tk learn --dry-run'));
1024
+ log(ex('tk learn --head'));
1025
+ log(ex('tk case add'));
1026
+ log(ex('tk case search "useEffect"'));
1027
+ log(ex('tk case list'));
1028
+ log(ex('tk case show --id 1'));
1029
+ log(ex('tk case stats'));
1030
+ log(ex('tk case export'));
1031
+ log(ex('tk case overrule --id 1'));
1032
+ log(ex('tk graph'));
1033
+ log(ex('tk mutate src/utils.js "npm test"'));
1034
+ log(ex('tk marathon init "Build a todo app"'));
1035
+ log(ex('tk marathon status'));
1036
+ log(ex('tk marathon next'));
1037
+ log(ex('tk marathon mark 5 pass'));
1038
+ log(ex('tk hook'));
1039
+ log(ex('tk uninstall'));
1040
+ console.log();
1041
+ }
1042
+
1043
+
1044
+ async function cmdMarathon(flags) {
1045
+ const targetDir = flags.path ? path.resolve(flags.path) : process.cwd();
1046
+ const agentDest = path.join(targetDir, '.agent');
1047
+
1048
+ if (!fs.existsSync(agentDest)) {
1049
+ err('.agent/ not found. Run: npx tribunal-kit init');
1050
+ process.exit(1);
1051
+ }
1052
+
1053
+ const args = process.argv.slice(3);
1054
+ const argsStr = args.join(' ');
1055
+ if (args.length === 0 || args[0] === 'help' || args[0] === '--help' || args[0] === '-h') {
1056
+ banner();
1057
+ log(` ${c('cyan', '╔' + '═'.repeat(60) + '╗')}`);
1058
+ log(` ${c('cyan', '║')}${c('bold', c('white', ' Marathon — Long-Running Agent Harness '))}${c('cyan', '║')}`);
1059
+ log(` ${c('cyan', '╚' + ''.repeat(60) + '')}`);
1060
+ console.log();
1061
+ log(` ${c('cyan', 'init'.padEnd(16))} ${c('gray', 'Start a new marathon (init "spec")')}`);
1062
+ log(` ${c('cyan', 'status'.padEnd(16))} ${c('gray', 'Show progress dashboard')}`);
1063
+ log(` ${c('cyan', 'next'.padEnd(16))} ${c('gray', 'Show next unfinished feature')}`);
1064
+ log(` ${c('cyan', 'mark'.padEnd(16))} ${c('gray', 'Mark feature pass/fail (mark <id> pass)')}`);
1065
+ log(` ${c('cyan', 'log'.padEnd(16))} ${c('gray', 'Add a progress note')}`);
1066
+ log(` ${c('cyan', 'session-start'.padEnd(16))} ${c('gray', 'Begin a new work session')}`);
1067
+ log(` ${c('cyan', 'session-end'.padEnd(16))} ${c('gray', 'End session with summary')}`);
1068
+ log(` ${c('cyan', 'add-feature'.padEnd(16))} ${c('gray', 'Add feature: "category" "desc" "step1" ...')}`);
1069
+ log(` ${c('cyan', 'reset'.padEnd(16))} ${c('gray', 'Archive and start fresh')}`);
1070
+ console.log();
1071
+ return;
1072
+ }
1073
+
1074
+ const marathonScript = path.join(agentDest, 'scripts', 'marathon_harness.js');
1075
+ try {
1076
+ await runShellAsync(`node "${marathonScript}" ${argsStr}`, { stdio: 'inherit', cwd: targetDir });
1077
+ } catch {
1078
+ process.exit(1);
1079
+ }
1080
+ }
1081
+
1082
+ function cmdContext(flags) {
1083
+ const targetDir = flags.path ? path.resolve(flags.path) : process.cwd();
1084
+ const agentDest = path.join(targetDir, '.agent');
1085
+
1086
+ if (!fs.existsSync(agentDest)) {
1087
+ err('.agent/ not found. Run: npx tribunal-kit init');
1088
+ process.exit(1);
1089
+ }
1090
+
1091
+ const args = process.argv.slice(3);
1092
+ if (args.length === 0 || args[0] === 'help' || args[0] === '--help') {
1093
+ console.error('Usage: npx tribunal-kit context <target_file>');
1094
+ process.exit(1);
1095
+ }
1096
+
1097
+ const targetFile = args[0].replace(/\\/g, '/');
1098
+ const snapshotName = targetFile.replace(/[\\\/]/g, '__') + '.json';
1099
+ const snapshotPath = require('path').join(agentDest, 'history', 'snapshots', snapshotName);
1100
+
1101
+ if (!require('fs').existsSync(snapshotPath)) {
1102
+ console.error(' \x1b[91m✖\x1b[0m Context Snapshot not found for: ' + targetFile);
1103
+ console.log(' Run: npx tribunal-kit graph (to generate snapshots)');
1104
+ process.exit(1);
1105
+ }
1106
+
1107
+ try {
1108
+ const snapshot = JSON.parse(require('fs').readFileSync(snapshotPath, 'utf8'));
1109
+
1110
+ console.log('\n# Context Snapshot: ' + snapshot.file);
1111
+ process.stdout.write('> Size Estimate: ' + (snapshot['estimatedTokens'] || 'Unknown') + '\n');
1112
+ console.log('> Risk Score: ' + snapshot.riskScore + ' (Blast Radius: ' + snapshot.blastRadius + ')\n');
1113
+
1114
+ if (Object.keys(snapshot.imports).length > 0) {
1115
+ console.log('## Imports');
1116
+ for (const [imp, exports] of Object.entries(snapshot.imports)) {
1117
+ if (exports && exports.length > 0) {
1118
+ console.log('- `' + imp + '` (exports: ' + exports.join(', ') + ')');
1119
+ } else {
1120
+ console.log('- `' + imp + '`');
1121
+ }
1122
+ }
1123
+ console.log();
1124
+ }
1125
+
1126
+ if (snapshot.dependents && snapshot.dependents.length > 0) {
1127
+ console.log('## Dependents');
1128
+ for (const dep of snapshot.dependents) {
1129
+ console.log('- `' + dep + '`');
1130
+ }
1131
+ console.log();
1132
+ }
1133
+
1134
+ console.log('## Source Code');
1135
+ console.log('```javascript\n' + snapshot.content + '\n```\n');
1136
+
1137
+ } catch (e) {
1138
+ console.error('Failed to read snapshot: ' + e.message);
1139
+ process.exit(1);
1140
+ }
1141
+ }
1142
+
1143
+ // ── Main ──────────────────────────────────────────────────
1144
+ const { command, flags } = parseArgs(process.argv);
1145
+
1146
+ if (flags.quiet) quiet = true;
1147
+ if (flags.verbose) verbose = true;
1148
+
1149
+ runWithUpdateCheck(command, flags);
1150
+
1151
+ // -- Exports (for testing) -- do not remove
1152
+ if (require.main !== module) {
1153
+ module.exports = { parseArgs, compareSemver, copyDir, countDir, isSelfInstall, CORE_AGENTS, CORE_SKILLS, generateIDEBridges, cmdMarathon };
1121
1154
  }