trellis 1.0.8 → 2.0.5

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 (107) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +533 -82
  3. package/bin/trellis.mjs +2 -0
  4. package/dist/cli/index.js +4718 -0
  5. package/dist/core/index.js +12 -0
  6. package/dist/decisions/index.js +19 -0
  7. package/dist/embeddings/index.js +43 -0
  8. package/dist/index-1j1anhmr.js +4038 -0
  9. package/dist/index-3s0eak0p.js +1556 -0
  10. package/dist/index-8pce39mh.js +272 -0
  11. package/dist/index-a76rekgs.js +67 -0
  12. package/dist/index-cy9k1g6v.js +684 -0
  13. package/dist/index-fd4e26s4.js +69 -0
  14. package/dist/{store/eav-store.js → index-gkvhzm9f.js} +4 -6
  15. package/dist/index-gnw8d7d6.js +51 -0
  16. package/dist/index-vkpkfwhq.js +817 -0
  17. package/dist/index.js +118 -2876
  18. package/dist/links/index.js +55 -0
  19. package/dist/transformers-m9je15kg.js +32491 -0
  20. package/dist/vcs/index.js +110 -0
  21. package/logo.png +0 -0
  22. package/logo.svg +9 -0
  23. package/package.json +79 -76
  24. package/src/cli/index.ts +2340 -0
  25. package/src/core/index.ts +35 -0
  26. package/src/core/kernel/middleware.ts +44 -0
  27. package/src/core/persist/backend.ts +64 -0
  28. package/src/core/store/eav-store.ts +467 -0
  29. package/src/decisions/auto-capture.ts +136 -0
  30. package/src/decisions/hooks.ts +163 -0
  31. package/src/decisions/index.ts +261 -0
  32. package/src/decisions/types.ts +103 -0
  33. package/src/embeddings/chunker.ts +327 -0
  34. package/src/embeddings/index.ts +41 -0
  35. package/src/embeddings/model.ts +95 -0
  36. package/src/embeddings/search.ts +305 -0
  37. package/src/embeddings/store.ts +313 -0
  38. package/src/embeddings/types.ts +85 -0
  39. package/src/engine.ts +1083 -0
  40. package/src/garden/cluster.ts +330 -0
  41. package/src/garden/garden.ts +306 -0
  42. package/src/garden/index.ts +29 -0
  43. package/src/git/git-exporter.ts +286 -0
  44. package/src/git/git-importer.ts +329 -0
  45. package/src/git/git-reader.ts +189 -0
  46. package/src/git/index.ts +22 -0
  47. package/src/identity/governance.ts +211 -0
  48. package/src/identity/identity.ts +224 -0
  49. package/src/identity/index.ts +30 -0
  50. package/src/identity/signing-middleware.ts +97 -0
  51. package/src/index.ts +20 -0
  52. package/src/links/index.ts +49 -0
  53. package/src/links/lifecycle.ts +400 -0
  54. package/src/links/parser.ts +484 -0
  55. package/src/links/ref-index.ts +186 -0
  56. package/src/links/resolver.ts +314 -0
  57. package/src/links/types.ts +108 -0
  58. package/src/mcp/index.ts +22 -0
  59. package/src/mcp/server.ts +1278 -0
  60. package/src/semantic/csharp-parser.ts +493 -0
  61. package/src/semantic/go-parser.ts +585 -0
  62. package/src/semantic/index.ts +34 -0
  63. package/src/semantic/java-parser.ts +456 -0
  64. package/src/semantic/python-parser.ts +659 -0
  65. package/src/semantic/ruby-parser.ts +446 -0
  66. package/src/semantic/rust-parser.ts +784 -0
  67. package/src/semantic/semantic-merge.ts +210 -0
  68. package/src/semantic/ts-parser.ts +681 -0
  69. package/src/semantic/types.ts +175 -0
  70. package/src/sync/index.ts +32 -0
  71. package/src/sync/memory-transport.ts +66 -0
  72. package/src/sync/reconciler.ts +237 -0
  73. package/src/sync/sync-engine.ts +258 -0
  74. package/src/sync/types.ts +104 -0
  75. package/src/vcs/blob-store.ts +124 -0
  76. package/src/vcs/branch.ts +150 -0
  77. package/src/vcs/checkpoint.ts +64 -0
  78. package/src/vcs/decompose.ts +469 -0
  79. package/src/vcs/diff.ts +409 -0
  80. package/src/vcs/engine-context.ts +26 -0
  81. package/src/vcs/index.ts +23 -0
  82. package/src/vcs/issue.ts +800 -0
  83. package/src/vcs/merge.ts +425 -0
  84. package/src/vcs/milestone.ts +124 -0
  85. package/src/vcs/ops.ts +59 -0
  86. package/src/vcs/types.ts +213 -0
  87. package/src/vcs/vcs-middleware.ts +81 -0
  88. package/src/watcher/fs-watcher.ts +217 -0
  89. package/src/watcher/index.ts +9 -0
  90. package/src/watcher/ingestion.ts +116 -0
  91. package/dist/ai/index.js +0 -688
  92. package/dist/cli/server.js +0 -3321
  93. package/dist/cli/tql.js +0 -5282
  94. package/dist/client/tql-client.js +0 -108
  95. package/dist/graph/index.js +0 -2248
  96. package/dist/kernel/logic-middleware.js +0 -179
  97. package/dist/kernel/middleware.js +0 -0
  98. package/dist/kernel/operations.js +0 -32
  99. package/dist/kernel/schema-middleware.js +0 -34
  100. package/dist/kernel/security-middleware.js +0 -53
  101. package/dist/kernel/trellis-kernel.js +0 -2239
  102. package/dist/kernel/workspace.js +0 -91
  103. package/dist/persist/backend.js +0 -0
  104. package/dist/persist/sqlite-backend.js +0 -123
  105. package/dist/query/index.js +0 -1643
  106. package/dist/server/index.js +0 -3309
  107. package/dist/workflows/index.js +0 -3160
@@ -0,0 +1,2340 @@
1
+ #!/usr/bin/env bun
2
+
3
+ /**
4
+ * TrellisVCS CLI
5
+ *
6
+ * Commands:
7
+ * init Initialize a new TrellisVCS repository
8
+ * status Show current repository status
9
+ * log Show operation history
10
+ * watch Start file watcher (foreground)
11
+ * files List tracked files
12
+ * import Import from a Git repository
13
+ */
14
+
15
+ import { Command } from 'commander';
16
+ import chalk from 'chalk';
17
+ import { resolve, join } from 'path';
18
+ import { TrellisVcsEngine } from '../engine.js';
19
+ import { importFromGit } from '../git/git-importer.js';
20
+ import { exportToGit } from '../git/git-exporter.js';
21
+ import {
22
+ createIdentity,
23
+ saveIdentity,
24
+ loadIdentity,
25
+ hasIdentity,
26
+ toPublicIdentity,
27
+ } from '../identity/index.js';
28
+
29
+ const program = new Command();
30
+
31
+ program
32
+ .name('trellis')
33
+ .description('TrellisVCS — graph-native, code-first version control')
34
+ .version('0.1.0');
35
+
36
+ function requireRepo(rootPath: string): void {
37
+ if (!TrellisVcsEngine.isRepo(rootPath)) {
38
+ console.error(
39
+ chalk.red('Not a TrellisVCS repository. Run `trellis init` first.'),
40
+ );
41
+ process.exit(1);
42
+ }
43
+ }
44
+
45
+ // ---------------------------------------------------------------------------
46
+ // trellis init
47
+ // ---------------------------------------------------------------------------
48
+
49
+ program
50
+ .command('init')
51
+ .description(
52
+ 'Initialize a new TrellisVCS repository in the current directory',
53
+ )
54
+ .option('-p, --path <path>', 'Path to initialize', '.')
55
+ .action(async (opts) => {
56
+ const rootPath = resolve(opts.path);
57
+
58
+ if (TrellisVcsEngine.isRepo(rootPath)) {
59
+ console.log(chalk.yellow('Already a TrellisVCS repository.'));
60
+ return;
61
+ }
62
+
63
+ const engine = new TrellisVcsEngine({ rootPath });
64
+ const result = await engine.initRepo();
65
+
66
+ console.log(chalk.green('✓ Initialized TrellisVCS repository'));
67
+ console.log(` ${chalk.dim('Path:')} ${rootPath}`);
68
+ console.log(
69
+ ` ${chalk.dim('Ops:')} ${result.opsCreated} initial operations recorded`,
70
+ );
71
+ console.log(` ${chalk.dim('Config:')} .trellis/config.json`);
72
+ console.log(` ${chalk.dim('Op log:')} .trellis/ops.json`);
73
+ console.log();
74
+ console.log(
75
+ chalk.dim(
76
+ 'The causal stream is now recording. Every file change will be tracked.',
77
+ ),
78
+ );
79
+ });
80
+
81
+ // ---------------------------------------------------------------------------
82
+ // trellis repair
83
+ // ---------------------------------------------------------------------------
84
+
85
+ program
86
+ .command('repair')
87
+ .description('Attempt to repair a corrupted .trellis/ops.json file')
88
+ .option('-p, --path <path>', 'Repository path', '.')
89
+ .action((opts) => {
90
+ const rootPath = resolve(opts.path);
91
+ requireRepo(rootPath);
92
+
93
+ console.log(chalk.yellow('Attempting to repair ops.json...'));
94
+ const result = TrellisVcsEngine.repair(rootPath);
95
+
96
+ if (result.lost === -1) {
97
+ console.log(
98
+ chalk.red(
99
+ 'Could not recover any ops. A corrupted backup was saved as ops.json.corrupted',
100
+ ),
101
+ );
102
+ } else if (result.recovered > 0) {
103
+ console.log(chalk.green(`✓ Recovered ${result.recovered} ops.`));
104
+ } else {
105
+ console.log(chalk.green('ops.json is already valid. No repair needed.'));
106
+ }
107
+ });
108
+
109
+ // ---------------------------------------------------------------------------
110
+ // trellis status
111
+ // ---------------------------------------------------------------------------
112
+
113
+ program
114
+ .command('status')
115
+ .description('Show current repository status')
116
+ .option('-p, --path <path>', 'Repository path', '.')
117
+ .action(async (opts) => {
118
+ const rootPath = resolve(opts.path);
119
+
120
+ if (!TrellisVcsEngine.isRepo(rootPath)) {
121
+ console.log(
122
+ chalk.red('Not a TrellisVCS repository. Run `trellis init` first.'),
123
+ );
124
+ process.exit(1);
125
+ }
126
+
127
+ const engine = new TrellisVcsEngine({ rootPath });
128
+ engine.open();
129
+ const st = engine.status();
130
+
131
+ console.log(chalk.bold('TrellisVCS Status'));
132
+ console.log();
133
+ console.log(` ${chalk.dim('Branch:')} ${chalk.cyan(st.branch)}`);
134
+ console.log(` ${chalk.dim('Total ops:')} ${st.totalOps}`);
135
+ console.log(` ${chalk.dim('Tracked files:')} ${st.trackedFiles}`);
136
+
137
+ if (st.lastOp) {
138
+ console.log();
139
+ console.log(
140
+ ` ${chalk.dim('Last op:')} ${chalk.yellow(st.lastOp.kind)}`,
141
+ );
142
+ console.log(` ${chalk.dim(' at:')} ${st.lastOp.timestamp}`);
143
+ if (st.lastOp.vcs?.filePath) {
144
+ console.log(
145
+ ` ${chalk.dim(' file:')} ${st.lastOp.vcs.filePath}`,
146
+ );
147
+ }
148
+ }
149
+
150
+ if (st.recentOps.length > 0) {
151
+ console.log();
152
+ console.log(chalk.dim(' Recent activity:'));
153
+ // Show last 5 ops (excluding branch create for readability)
154
+ const display = st.recentOps
155
+ .filter((op) => op.kind !== 'vcs:branchCreate')
156
+ .slice(-5);
157
+ for (const op of display) {
158
+ const kind = formatOpKind(op.kind);
159
+ const file = op.vcs?.filePath ?? '';
160
+ const time = formatRelativeTime(op.timestamp);
161
+ console.log(` ${kind} ${chalk.white(file)} ${chalk.dim(time)}`);
162
+ }
163
+ }
164
+ });
165
+
166
+ // ---------------------------------------------------------------------------
167
+ // trellis log
168
+ // ---------------------------------------------------------------------------
169
+
170
+ program
171
+ .command('log')
172
+ .description('Show operation history')
173
+ .option('-p, --path <path>', 'Repository path', '.')
174
+ .option('-n, --limit <n>', 'Number of ops to show', '20')
175
+ .option('-f, --file <file>', 'Filter by file path')
176
+ .action(async (opts) => {
177
+ const rootPath = resolve(opts.path);
178
+
179
+ if (!TrellisVcsEngine.isRepo(rootPath)) {
180
+ console.log(
181
+ chalk.red('Not a TrellisVCS repository. Run `trellis init` first.'),
182
+ );
183
+ process.exit(1);
184
+ }
185
+
186
+ const engine = new TrellisVcsEngine({ rootPath });
187
+ engine.open();
188
+ const ops = engine.log({
189
+ limit: parseInt(opts.limit, 10),
190
+ filePath: opts.file,
191
+ });
192
+
193
+ if (ops.length === 0) {
194
+ console.log(chalk.dim('No operations found.'));
195
+ return;
196
+ }
197
+
198
+ console.log(chalk.bold(`Causal Stream — ${ops.length} ops`));
199
+ console.log();
200
+
201
+ for (const op of ops.reverse()) {
202
+ const kind = formatOpKind(op.kind);
203
+ const hash = chalk.dim(op.hash.slice(0, 28) + '…');
204
+ const time = formatRelativeTime(op.timestamp);
205
+ const file = op.vcs?.filePath ? chalk.white(op.vcs.filePath) : '';
206
+ const rename = op.vcs?.oldFilePath
207
+ ? chalk.dim(` (from ${op.vcs.oldFilePath})`)
208
+ : '';
209
+
210
+ console.log(` ${hash} ${kind} ${file}${rename} ${chalk.dim(time)}`);
211
+ }
212
+ });
213
+
214
+ // ---------------------------------------------------------------------------
215
+ // trellis files
216
+ // ---------------------------------------------------------------------------
217
+
218
+ program
219
+ .command('files')
220
+ .description('List all tracked files')
221
+ .option('-p, --path <path>', 'Repository path', '.')
222
+ .action(async (opts) => {
223
+ const rootPath = resolve(opts.path);
224
+
225
+ if (!TrellisVcsEngine.isRepo(rootPath)) {
226
+ console.log(
227
+ chalk.red('Not a TrellisVCS repository. Run `trellis init` first.'),
228
+ );
229
+ process.exit(1);
230
+ }
231
+
232
+ const engine = new TrellisVcsEngine({ rootPath });
233
+ engine.open();
234
+ const files = engine.trackedFiles();
235
+
236
+ if (files.length === 0) {
237
+ console.log(chalk.dim('No tracked files.'));
238
+ return;
239
+ }
240
+
241
+ console.log(chalk.bold(`Tracked Files — ${files.length}`));
242
+ console.log();
243
+
244
+ for (const f of files.sort((a, b) => a.path.localeCompare(b.path))) {
245
+ const hash = f.contentHash
246
+ ? chalk.dim(f.contentHash.slice(0, 12))
247
+ : chalk.dim('(no hash)');
248
+ console.log(` ${hash} ${f.path}`);
249
+ }
250
+ });
251
+
252
+ // ---------------------------------------------------------------------------
253
+ // trellis watch
254
+ // ---------------------------------------------------------------------------
255
+
256
+ program
257
+ .command('watch')
258
+ .description('Start file watcher (foreground, Ctrl+C to stop)')
259
+ .option('-p, --path <path>', 'Repository path', '.')
260
+ .action(async (opts) => {
261
+ const rootPath = resolve(opts.path);
262
+
263
+ if (!TrellisVcsEngine.isRepo(rootPath)) {
264
+ console.log(
265
+ chalk.red('Not a TrellisVCS repository. Run `trellis init` first.'),
266
+ );
267
+ process.exit(1);
268
+ }
269
+
270
+ const engine = new TrellisVcsEngine({ rootPath });
271
+ engine.open();
272
+
273
+ console.log(
274
+ chalk.green('✓ Watching for changes…') + chalk.dim(' (Ctrl+C to stop)'),
275
+ );
276
+ console.log();
277
+
278
+ // Override engine's watch to add logging
279
+ const originalWatch = engine.watch.bind(engine);
280
+ engine.watch();
281
+
282
+ // Keep process alive
283
+ process.on('SIGINT', () => {
284
+ engine.stop();
285
+ console.log();
286
+ console.log(chalk.dim('Watcher stopped.'));
287
+ process.exit(0);
288
+ });
289
+ });
290
+
291
+ // ---------------------------------------------------------------------------
292
+ // trellis import
293
+ // ---------------------------------------------------------------------------
294
+
295
+ program
296
+ .command('import')
297
+ .description('Import from an existing Git repository')
298
+ .requiredOption('--from <path>', 'Path to the Git repository to import from')
299
+ .option('-p, --path <path>', 'Target TrellisVCS repository path', '.')
300
+ .action(async (opts) => {
301
+ const from = resolve(opts.from);
302
+ const to = resolve(opts.path);
303
+
304
+ console.log(chalk.dim(`Importing from Git: ${from}`));
305
+ console.log(chalk.dim(`Target: ${to}`));
306
+ console.log();
307
+
308
+ try {
309
+ const result = await importFromGit({
310
+ from,
311
+ to,
312
+ onProgress: (p) => {
313
+ if (p.phase === 'reading') {
314
+ process.stdout.write(`\r ${chalk.dim('Reading…')} ${p.message}`);
315
+ } else if (p.phase === 'importing') {
316
+ process.stdout.write(
317
+ `\r ${chalk.dim('Importing…')} ${p.current}/${p.total} commits`,
318
+ );
319
+ } else {
320
+ process.stdout.write('\n');
321
+ }
322
+ },
323
+ });
324
+
325
+ console.log();
326
+ console.log(chalk.green('✓ Git import complete'));
327
+ console.log(` ${chalk.dim('Commits:')} ${result.commitsImported}`);
328
+ console.log(` ${chalk.dim('Ops:')} ${result.opsCreated}`);
329
+ console.log(` ${chalk.dim('Files:')} ${result.filesTracked}`);
330
+ console.log(
331
+ ` ${chalk.dim('Branches:')} ${result.branches.join(', ')}`,
332
+ );
333
+ console.log(
334
+ ` ${chalk.dim('Duration:')} ${(result.duration / 1000).toFixed(1)}s`,
335
+ );
336
+ console.log();
337
+ console.log(
338
+ chalk.dim(
339
+ 'Run `trellis status` or `trellis log` to explore the imported history.',
340
+ ),
341
+ );
342
+ } catch (err: any) {
343
+ console.error(chalk.red(`\nImport failed: ${err.message}`));
344
+ process.exit(1);
345
+ }
346
+ });
347
+
348
+ // ---------------------------------------------------------------------------
349
+ // trellis export
350
+ // ---------------------------------------------------------------------------
351
+
352
+ program
353
+ .command('export')
354
+ .description('Export milestones to a Git repository')
355
+ .requiredOption('--to <path>', 'Path to the target Git repository')
356
+ .option('-p, --path <path>', 'Source TrellisVCS repository path', '.')
357
+ .option('--author-name <name>', 'Author name for Git commits')
358
+ .option('--author-email <email>', 'Author email for Git commits')
359
+ .action(async (opts) => {
360
+ const from = resolve(opts.path);
361
+ const to = resolve(opts.to);
362
+
363
+ if (!TrellisVcsEngine.isRepo(from)) {
364
+ console.error(
365
+ chalk.red('Not a TrellisVCS repository. Run `trellis init` first.'),
366
+ );
367
+ process.exit(1);
368
+ }
369
+
370
+ console.log(chalk.dim(`Exporting from: ${from}`));
371
+ console.log(chalk.dim(`Target Git repo: ${to}`));
372
+ console.log();
373
+
374
+ try {
375
+ const result = await exportToGit({
376
+ from,
377
+ to,
378
+ authorName: opts.authorName,
379
+ authorEmail: opts.authorEmail,
380
+ onProgress: (p) => {
381
+ if (p.phase === 'preparing') {
382
+ console.log(` ${chalk.dim(p.message)}`);
383
+ } else if (p.phase === 'exporting') {
384
+ process.stdout.write(
385
+ `\r ${chalk.dim('Exporting…')} ${p.current}/${p.total} milestones`,
386
+ );
387
+ } else {
388
+ process.stdout.write('\n');
389
+ }
390
+ },
391
+ });
392
+
393
+ console.log();
394
+ console.log(chalk.green('✓ Git export complete'));
395
+ console.log(` ${chalk.dim('Milestones:')} ${result.milestonesExported}`);
396
+ console.log(` ${chalk.dim('Commits:')} ${result.commitsCreated}`);
397
+ console.log(
398
+ ` ${chalk.dim('Duration:')} ${(result.duration / 1000).toFixed(1)}s`,
399
+ );
400
+ } catch (err: any) {
401
+ console.error(chalk.red(`\nExport failed: ${err.message}`));
402
+ process.exit(1);
403
+ }
404
+ });
405
+
406
+ // ---------------------------------------------------------------------------
407
+ // trellis branch
408
+ // ---------------------------------------------------------------------------
409
+
410
+ program
411
+ .command('branch')
412
+ .description('Manage branches')
413
+ .argument('[name]', 'Branch name to create or switch to')
414
+ .option('-d, --delete <name>', 'Delete a branch')
415
+ .option('-l, --list', 'List all branches')
416
+ .option('-p, --path <path>', 'Repository path', '.')
417
+ .action(async (name, opts) => {
418
+ const rootPath = resolve(opts.path);
419
+ if (!TrellisVcsEngine.isRepo(rootPath)) {
420
+ console.error(
421
+ chalk.red('Not a TrellisVCS repository. Run `trellis init` first.'),
422
+ );
423
+ process.exit(1);
424
+ }
425
+
426
+ const engine = new TrellisVcsEngine({ rootPath });
427
+ engine.open();
428
+
429
+ // Delete
430
+ if (opts.delete) {
431
+ try {
432
+ await engine.deleteBranch(opts.delete);
433
+ console.log(chalk.green(`✓ Deleted branch '${opts.delete}'`));
434
+ } catch (err: any) {
435
+ console.error(chalk.red(err.message));
436
+ process.exit(1);
437
+ }
438
+ return;
439
+ }
440
+
441
+ // Create or switch
442
+ if (name) {
443
+ const branches = engine.listBranches();
444
+ const exists = branches.find((b) => b.name === name);
445
+
446
+ if (exists) {
447
+ // Switch to existing branch
448
+ try {
449
+ engine.switchBranch(name);
450
+ console.log(chalk.green(`✓ Switched to branch '${name}'`));
451
+ } catch (err: any) {
452
+ console.error(chalk.red(err.message));
453
+ process.exit(1);
454
+ }
455
+ } else {
456
+ // Create new branch
457
+ try {
458
+ await engine.createBranch(name);
459
+ engine.switchBranch(name);
460
+ console.log(
461
+ chalk.green(`✓ Created and switched to branch '${name}'`),
462
+ );
463
+ } catch (err: any) {
464
+ console.error(chalk.red(err.message));
465
+ process.exit(1);
466
+ }
467
+ }
468
+ return;
469
+ }
470
+
471
+ // List (default)
472
+ const branches = engine.listBranches();
473
+ if (branches.length === 0) {
474
+ console.log(chalk.dim('No branches'));
475
+ return;
476
+ }
477
+
478
+ console.log(chalk.bold('Branches\n'));
479
+ for (const b of branches) {
480
+ const marker = b.isCurrent ? chalk.green('* ') : ' ';
481
+ const name = b.isCurrent ? chalk.green(b.name) : b.name;
482
+ const age = b.createdAt ? chalk.dim(formatRelativeTime(b.createdAt)) : '';
483
+ console.log(`${marker}${name} ${age}`);
484
+ }
485
+ });
486
+
487
+ // ---------------------------------------------------------------------------
488
+ // trellis milestone
489
+ // ---------------------------------------------------------------------------
490
+
491
+ program
492
+ .command('milestone')
493
+ .description('Create or list milestones')
494
+ .argument('[action]', '"create" or "list" (default: list)')
495
+ .option('-m, --message <message>', 'Milestone message')
496
+ .option('--from <hash>', 'Start op hash for the milestone range')
497
+ .option('--to <hash>', 'End op hash for the milestone range')
498
+ .option('-p, --path <path>', 'Repository path', '.')
499
+ .action(async (action, opts) => {
500
+ const rootPath = resolve(opts.path);
501
+ if (!TrellisVcsEngine.isRepo(rootPath)) {
502
+ console.error(
503
+ chalk.red('Not a TrellisVCS repository. Run `trellis init` first.'),
504
+ );
505
+ process.exit(1);
506
+ }
507
+
508
+ const engine = new TrellisVcsEngine({ rootPath });
509
+ engine.open();
510
+
511
+ if (action === 'create') {
512
+ if (!opts.message) {
513
+ console.error(
514
+ chalk.red('Milestone message is required: --message "..."'),
515
+ );
516
+ process.exit(1);
517
+ }
518
+
519
+ try {
520
+ const op = await engine.createMilestone(opts.message, {
521
+ fromOpHash: opts.from,
522
+ toOpHash: opts.to,
523
+ });
524
+ console.log(chalk.green(`✓ Milestone created`));
525
+ console.log(` ${chalk.dim('ID:')} ${op.vcs?.milestoneId}`);
526
+ console.log(` ${chalk.dim('Message:')} ${opts.message}`);
527
+ console.log(` ${chalk.dim('Hash:')} ${op.hash.slice(0, 32)}…`);
528
+ } catch (err: any) {
529
+ console.error(chalk.red(`Failed: ${err.message}`));
530
+ process.exit(1);
531
+ }
532
+ return;
533
+ }
534
+
535
+ // List (default)
536
+ const milestones = engine.listMilestones();
537
+ if (milestones.length === 0) {
538
+ console.log(chalk.dim('No milestones'));
539
+ return;
540
+ }
541
+
542
+ console.log(chalk.bold(`Milestones (${milestones.length})\n`));
543
+ for (const m of milestones) {
544
+ const age = m.createdAt ? formatRelativeTime(m.createdAt) : '';
545
+ console.log(
546
+ ` ${chalk.cyan('★')} ${chalk.bold(m.message ?? '(no message)')}`,
547
+ );
548
+ console.log(` ${chalk.dim('ID:')} ${m.id} ${chalk.dim(age)}`);
549
+ if (m.affectedFiles.length > 0) {
550
+ console.log(
551
+ ` ${chalk.dim('Files:')} ${m.affectedFiles.slice(0, 5).join(', ')}${m.affectedFiles.length > 5 ? ` +${m.affectedFiles.length - 5} more` : ''}`,
552
+ );
553
+ }
554
+ console.log();
555
+ }
556
+ });
557
+
558
+ // ---------------------------------------------------------------------------
559
+ // trellis checkpoint
560
+ // ---------------------------------------------------------------------------
561
+
562
+ program
563
+ .command('checkpoint')
564
+ .description('Create or list checkpoints')
565
+ .argument('[action]', '"create" or "list" (default: list)')
566
+ .option('-p, --path <path>', 'Repository path', '.')
567
+ .action(async (action, opts) => {
568
+ const rootPath = resolve(opts.path);
569
+ if (!TrellisVcsEngine.isRepo(rootPath)) {
570
+ console.error(
571
+ chalk.red('Not a TrellisVCS repository. Run `trellis init` first.'),
572
+ );
573
+ process.exit(1);
574
+ }
575
+
576
+ const engine = new TrellisVcsEngine({ rootPath });
577
+ engine.open();
578
+
579
+ if (action === 'create') {
580
+ try {
581
+ const op = await engine.createCheckpoint('manual');
582
+ console.log(chalk.green(`✓ Checkpoint created`));
583
+ console.log(` ${chalk.dim('Hash:')} ${op.hash.slice(0, 32)}…`);
584
+ console.log(` ${chalk.dim('Trigger:')} manual`);
585
+ } catch (err: any) {
586
+ console.error(chalk.red(`Failed: ${err.message}`));
587
+ process.exit(1);
588
+ }
589
+ return;
590
+ }
591
+
592
+ // List (default)
593
+ const checkpoints = engine.listCheckpoints();
594
+ if (checkpoints.length === 0) {
595
+ console.log(chalk.dim('No checkpoints'));
596
+ return;
597
+ }
598
+
599
+ console.log(chalk.bold(`Checkpoints (${checkpoints.length})\n`));
600
+ for (const cp of checkpoints) {
601
+ const age = cp.createdAt ? formatRelativeTime(cp.createdAt) : '';
602
+ console.log(
603
+ ` ${chalk.dim('●')} ${cp.id.slice(0, 32)} ${chalk.dim(cp.trigger ?? '')} ${chalk.dim(age)}`,
604
+ );
605
+ }
606
+ });
607
+
608
+ // ---------------------------------------------------------------------------
609
+ // trellis diff
610
+ // ---------------------------------------------------------------------------
611
+
612
+ program
613
+ .command('diff')
614
+ .description('Show file-level diff between two points in history')
615
+ .argument('[from]', 'Starting op hash or milestone ID')
616
+ .argument('[to]', 'Ending op hash (default: current head)')
617
+ .option('-p, --path <path>', 'Repository path', '.')
618
+ .option('--stat', 'Show only summary stats')
619
+ .action((from, to, opts) => {
620
+ const rootPath = resolve(opts.path);
621
+ if (!TrellisVcsEngine.isRepo(rootPath)) {
622
+ console.error(
623
+ chalk.red('Not a TrellisVCS repository. Run `trellis init` first.'),
624
+ );
625
+ process.exit(1);
626
+ }
627
+
628
+ const engine = new TrellisVcsEngine({ rootPath });
629
+ engine.open();
630
+
631
+ let result;
632
+ if (from && to) {
633
+ result = engine.diffOps(from, to);
634
+ } else if (from) {
635
+ result = engine.diffFromOp(from);
636
+ } else {
637
+ // Diff from the first op to HEAD
638
+ const ops = engine.getOps();
639
+ if (ops.length < 2) {
640
+ console.log(chalk.dim('Not enough history to diff.'));
641
+ return;
642
+ }
643
+ result = engine.diffOps(ops[0].hash, ops[ops.length - 1].hash);
644
+ }
645
+
646
+ if (result.diffs.length === 0) {
647
+ console.log(chalk.dim('No differences.'));
648
+ return;
649
+ }
650
+
651
+ // Stats
652
+ const s = result.stats;
653
+ console.log(
654
+ `${chalk.green(`+${s.added} added`)} ${chalk.yellow(`~${s.modified} modified`)} ${chalk.red(`-${s.removed} removed`)}${s.renamed ? ` ${chalk.blue(`→${s.renamed} renamed`)}` : ''}`,
655
+ );
656
+ console.log();
657
+
658
+ if (opts.stat) return;
659
+
660
+ // Detailed output
661
+ for (const diff of result.diffs) {
662
+ switch (diff.kind) {
663
+ case 'fileAdded':
664
+ console.log(`${chalk.green('+ ' + diff.path)}`);
665
+ break;
666
+ case 'fileDeleted':
667
+ console.log(`${chalk.red('- ' + diff.path)}`);
668
+ break;
669
+ case 'fileRenamed':
670
+ console.log(`${chalk.blue(`→ ${diff.oldPath} → ${diff.path}`)}`);
671
+ break;
672
+ case 'fileModified':
673
+ console.log(`${chalk.yellow('~ ' + diff.path)}`);
674
+ if (diff.unifiedDiff) {
675
+ for (const line of diff.unifiedDiff.split('\n')) {
676
+ if (line.startsWith('+')) {
677
+ console.log(chalk.green(line));
678
+ } else if (line.startsWith('-')) {
679
+ console.log(chalk.red(line));
680
+ } else if (line.startsWith('@@')) {
681
+ console.log(chalk.cyan(line));
682
+ } else {
683
+ console.log(chalk.dim(line));
684
+ }
685
+ }
686
+ }
687
+ break;
688
+ }
689
+ console.log();
690
+ }
691
+ });
692
+
693
+ // ---------------------------------------------------------------------------
694
+ // trellis merge
695
+ // ---------------------------------------------------------------------------
696
+
697
+ program
698
+ .command('merge')
699
+ .description('Merge a branch into the current branch')
700
+ .argument('<branch>', 'Source branch to merge')
701
+ .option('-p, --path <path>', 'Repository path', '.')
702
+ .option('--dry-run', 'Preview merge without applying changes')
703
+ .action((branch, opts) => {
704
+ const rootPath = resolve(opts.path);
705
+ if (!TrellisVcsEngine.isRepo(rootPath)) {
706
+ console.error(
707
+ chalk.red('Not a TrellisVCS repository. Run `trellis init` first.'),
708
+ );
709
+ process.exit(1);
710
+ }
711
+
712
+ const engine = new TrellisVcsEngine({ rootPath });
713
+ engine.open();
714
+
715
+ const result = engine.mergeBranch(branch);
716
+
717
+ if (result.clean) {
718
+ console.log(chalk.green('✓ Merge completed cleanly'));
719
+ } else {
720
+ console.log(
721
+ chalk.yellow(`⚠ Merge has ${result.conflicts.length} conflict(s)`),
722
+ );
723
+ }
724
+
725
+ const s = result.stats;
726
+ console.log(` ${chalk.dim('Modified:')} ${s.modified}`);
727
+ console.log(` ${chalk.dim('Deleted:')} ${s.deleted}`);
728
+ console.log(` ${chalk.dim('Conflicted:')} ${s.conflicted}`);
729
+
730
+ if (result.conflicts.length > 0) {
731
+ console.log();
732
+ console.log(chalk.bold('Conflicts:'));
733
+ for (const c of result.conflicts) {
734
+ console.log(` ${chalk.red('✗')} ${c.path} (${c.kind})`);
735
+ }
736
+ }
737
+
738
+ if (opts.dryRun) {
739
+ console.log();
740
+ console.log(chalk.dim('(dry run — no changes applied)'));
741
+ }
742
+ });
743
+
744
+ // ---------------------------------------------------------------------------
745
+ // trellis parse
746
+ // ---------------------------------------------------------------------------
747
+
748
+ program
749
+ .command('parse')
750
+ .description('Parse a file into AST-level semantic entities')
751
+ .argument('<file>', 'File to parse')
752
+ .option('-p, --path <path>', 'Repository path', '.')
753
+ .action((file, opts) => {
754
+ const rootPath = resolve(opts.path);
755
+ const engine = new TrellisVcsEngine({ rootPath });
756
+ if (TrellisVcsEngine.isRepo(rootPath)) engine.open();
757
+
758
+ const { readFileSync } = require('fs');
759
+ const filePath = resolve(file);
760
+ const content = readFileSync(filePath, 'utf-8');
761
+ const result = engine.parseFile(content, file);
762
+
763
+ if (!result) {
764
+ console.log(chalk.dim(`No parser available for: ${file}`));
765
+ return;
766
+ }
767
+
768
+ console.log(chalk.bold(`Parse: ${file}\n`));
769
+ console.log(` ${chalk.dim('Language:')} ${result.language}`);
770
+ console.log(
771
+ ` ${chalk.dim('Declarations:')} ${result.declarations.length}`,
772
+ );
773
+ console.log(` ${chalk.dim('Imports:')} ${result.imports.length}`);
774
+ console.log(` ${chalk.dim('Exports:')} ${result.exports.length}`);
775
+
776
+ if (result.declarations.length > 0) {
777
+ console.log();
778
+ console.log(chalk.bold('Declarations:'));
779
+ for (const d of result.declarations) {
780
+ console.log(
781
+ ` ${chalk.cyan(d.kind.padEnd(14))} ${chalk.bold(d.name)}${d.children.length ? ` (${d.children.length} members)` : ''}`,
782
+ );
783
+ for (const child of d.children) {
784
+ console.log(` ${chalk.dim(child.kind.padEnd(14))} ${child.name}`);
785
+ }
786
+ }
787
+ }
788
+
789
+ if (result.imports.length > 0) {
790
+ console.log();
791
+ console.log(chalk.bold('Imports:'));
792
+ for (const imp of result.imports) {
793
+ const specs =
794
+ imp.specifiers.length > 0 ? ` { ${imp.specifiers.join(', ')} }` : '';
795
+ console.log(
796
+ ` ${chalk.dim('from')} ${chalk.yellow(imp.source)}${specs}`,
797
+ );
798
+ }
799
+ }
800
+ });
801
+
802
+ // ---------------------------------------------------------------------------
803
+ // trellis sdiff (semantic diff)
804
+ // ---------------------------------------------------------------------------
805
+
806
+ program
807
+ .command('sdiff')
808
+ .description('Show semantic diff between two versions of a file')
809
+ .argument('<fileA>', 'Old version of the file')
810
+ .argument('<fileB>', 'New version of the file')
811
+ .option('-p, --path <path>', 'Repository path', '.')
812
+ .action((fileA, fileB, opts) => {
813
+ const rootPath = resolve(opts.path);
814
+ const engine = new TrellisVcsEngine({ rootPath });
815
+ if (TrellisVcsEngine.isRepo(rootPath)) engine.open();
816
+
817
+ const { readFileSync } = require('fs');
818
+ const oldContent = readFileSync(resolve(fileA), 'utf-8');
819
+ const newContent = readFileSync(resolve(fileB), 'utf-8');
820
+ const patches = engine.semanticDiff(oldContent, newContent, fileA);
821
+
822
+ if (patches.length === 0) {
823
+ console.log(chalk.dim('No semantic differences.'));
824
+ return;
825
+ }
826
+
827
+ console.log(chalk.bold(`Semantic diff: ${fileA} → ${fileB}\n`));
828
+ console.log(` ${chalk.dim('Patches:')} ${patches.length}\n`);
829
+
830
+ for (const patch of patches) {
831
+ switch (patch.kind) {
832
+ case 'symbolAdd':
833
+ console.log(
834
+ ` ${chalk.green('+')} ${chalk.green(`${patch.entity.kind}: ${patch.entity.name}`)}`,
835
+ );
836
+ break;
837
+ case 'symbolRemove':
838
+ console.log(
839
+ ` ${chalk.red('-')} ${chalk.red(`${patch.entityName}`)} (removed)`,
840
+ );
841
+ break;
842
+ case 'symbolModify':
843
+ console.log(
844
+ ` ${chalk.yellow('~')} ${chalk.yellow(patch.entityName)} (modified)`,
845
+ );
846
+ break;
847
+ case 'symbolRename':
848
+ console.log(
849
+ ` ${chalk.blue('\u2192')} ${chalk.blue(`${patch.oldName} \u2192 ${patch.newName}`)} (renamed)`,
850
+ );
851
+ break;
852
+ case 'importAdd':
853
+ console.log(
854
+ ` ${chalk.green('+')} import from ${chalk.yellow(patch.source)}`,
855
+ );
856
+ break;
857
+ case 'importRemove':
858
+ console.log(
859
+ ` ${chalk.red('-')} import from ${chalk.yellow(patch.source)}`,
860
+ );
861
+ break;
862
+ case 'importModify':
863
+ console.log(
864
+ ` ${chalk.yellow('~')} import from ${chalk.yellow(patch.source)} (specifiers changed)`,
865
+ );
866
+ break;
867
+ case 'exportAdd':
868
+ console.log(` ${chalk.green('+')} export ${chalk.bold(patch.name)}`);
869
+ break;
870
+ case 'exportRemove':
871
+ console.log(` ${chalk.red('-')} export ${chalk.bold(patch.name)}`);
872
+ break;
873
+ case 'symbolMove':
874
+ console.log(
875
+ ` ${chalk.blue('\u2192')} ${chalk.blue(patch.entityName)} moved ${patch.oldFile} \u2192 ${patch.newFile}`,
876
+ );
877
+ break;
878
+ }
879
+ }
880
+ });
881
+
882
+ // ---------------------------------------------------------------------------
883
+ // trellis sync
884
+ // ---------------------------------------------------------------------------
885
+
886
+ program
887
+ .command('sync')
888
+ .description('Sync operations with another TrellisVCS repository')
889
+ .argument(
890
+ '[action]',
891
+ '"push", "pull", "status", or "reconcile" (default: status)',
892
+ )
893
+ .option('-p, --path <path>', 'Local repository path', '.')
894
+ .option('--remote <remote>', 'Remote repository path')
895
+ .action((action, opts) => {
896
+ const rootPath = resolve(opts.path);
897
+ if (!TrellisVcsEngine.isRepo(rootPath)) {
898
+ console.error(
899
+ chalk.red('Not a TrellisVCS repository. Run `trellis init` first.'),
900
+ );
901
+ process.exit(1);
902
+ }
903
+
904
+ const engine = new TrellisVcsEngine({ rootPath });
905
+ engine.open();
906
+ const ops = engine.getOps();
907
+
908
+ if (action === 'status' || !action) {
909
+ console.log(chalk.bold('Sync Status\n'));
910
+ console.log(` ${chalk.dim('Local ops:')} ${ops.length}`);
911
+ console.log(
912
+ ` ${chalk.dim('Head:')} ${ops.length > 0 ? ops[ops.length - 1].hash.slice(0, 16) + '\u2026' : '(none)'}`,
913
+ );
914
+ console.log(` ${chalk.dim('Branch:')} ${engine.getCurrentBranch()}`);
915
+ return;
916
+ }
917
+
918
+ if (action === 'reconcile' && opts.remote) {
919
+ const remotePath = resolve(opts.remote);
920
+ if (!TrellisVcsEngine.isRepo(remotePath)) {
921
+ console.error(chalk.red(`Not a TrellisVCS repository: ${remotePath}`));
922
+ process.exit(1);
923
+ }
924
+
925
+ const remoteEngine = new TrellisVcsEngine({ rootPath: remotePath });
926
+ remoteEngine.open();
927
+ const remoteOps = remoteEngine.getOps();
928
+
929
+ const { reconcile } = require('../sync/reconciler.js');
930
+ const result = reconcile(ops, remoteOps);
931
+
932
+ console.log(chalk.bold('Reconcile Result\n'));
933
+ console.log(` ${chalk.dim('Merged ops:')} ${result.merged.length}`);
934
+ console.log(` ${chalk.dim('Unique local:')} ${result.uniqueToA.length}`);
935
+ console.log(
936
+ ` ${chalk.dim('Unique remote:')} ${result.uniqueToB.length}`,
937
+ );
938
+ console.log(
939
+ ` ${chalk.dim('Fork point:')} ${result.forkPoint?.slice(0, 16) ?? '(none)'}`,
940
+ );
941
+ console.log(
942
+ ` ${chalk.dim('Clean:')} ${result.clean ? chalk.green('yes') : chalk.red('no')}`,
943
+ );
944
+
945
+ if (result.conflicts.length > 0) {
946
+ console.log();
947
+ console.log(chalk.bold('Conflicts:'));
948
+ for (const c of result.conflicts) {
949
+ console.log(` ${chalk.red('\u2717')} ${c.filePath}: ${c.reason}`);
950
+ }
951
+ }
952
+ return;
953
+ }
954
+
955
+ console.log(
956
+ chalk.dim('Use --remote <path> with reconcile to compare repositories.'),
957
+ );
958
+ console.log(
959
+ chalk.dim('Full peer sync requires a transport layer (coming soon).'),
960
+ );
961
+ });
962
+
963
+ // ---------------------------------------------------------------------------
964
+ // trellis garden
965
+ // ---------------------------------------------------------------------------
966
+
967
+ program
968
+ .command('garden')
969
+ .description('Explore the Idea Garden — abandoned work clusters')
970
+ .argument(
971
+ '[action]',
972
+ '"list", "show <id>", "search", "revive <id>", or "stats" (default: list)',
973
+ )
974
+ .argument('[id]', 'Cluster ID (for show/revive)')
975
+ .option('-p, --path <path>', 'Repository path', '.')
976
+ .option('-f, --file <file>', 'Filter by file path')
977
+ .option('-k, --keyword <keyword>', 'Filter by keyword')
978
+ .option('-s, --status <status>', 'Filter by status (abandoned|draft|revived)')
979
+ .option('-n, --limit <n>', 'Max results', parseInt as any)
980
+ .action((action, id, opts) => {
981
+ const rootPath = resolve(opts.path);
982
+ if (!TrellisVcsEngine.isRepo(rootPath)) {
983
+ console.error(
984
+ chalk.red('Not a TrellisVCS repository. Run `trellis init` first.'),
985
+ );
986
+ process.exit(1);
987
+ }
988
+
989
+ const engine = new TrellisVcsEngine({ rootPath });
990
+ engine.open();
991
+ const garden = engine.garden();
992
+
993
+ if (action === 'stats') {
994
+ const s = garden.stats();
995
+ console.log(chalk.bold('Idea Garden Stats\n'));
996
+ console.log(` ${chalk.dim('Total clusters:')} ${s.total}`);
997
+ console.log(` ${chalk.dim('Abandoned:')} ${s.abandoned}`);
998
+ console.log(` ${chalk.dim('Draft:')} ${s.draft}`);
999
+ console.log(` ${chalk.dim('Revived:')} ${s.revived}`);
1000
+ console.log(` ${chalk.dim('Total ops:')} ${s.totalOps}`);
1001
+ console.log(` ${chalk.dim('Total files:')} ${s.totalFiles}`);
1002
+ return;
1003
+ }
1004
+
1005
+ if (action === 'show') {
1006
+ if (!id) {
1007
+ console.error(chalk.red('Usage: trellis garden show <cluster-id>'));
1008
+ process.exit(1);
1009
+ }
1010
+ const cluster = garden.getCluster(id);
1011
+ if (!cluster) {
1012
+ console.error(chalk.red(`Cluster not found: ${id}`));
1013
+ process.exit(1);
1014
+ }
1015
+
1016
+ console.log(chalk.bold(`Cluster: ${cluster.id}\n`));
1017
+ console.log(
1018
+ ` ${chalk.dim('Status:')} ${formatClusterStatus(cluster.status)}`,
1019
+ );
1020
+ console.log(` ${chalk.dim('Detected by:')} ${cluster.detectedBy}`);
1021
+ console.log(` ${chalk.dim('Created:')} ${cluster.createdAt}`);
1022
+ console.log(` ${chalk.dim('Abandoned:')} ${cluster.abandonedAt}`);
1023
+ console.log(` ${chalk.dim('Ops:')} ${cluster.ops.length}`);
1024
+ console.log(
1025
+ ` ${chalk.dim('Files:')} ${cluster.affectedFiles.join(', ')}`,
1026
+ );
1027
+ if (cluster.estimatedIntent) {
1028
+ console.log(` ${chalk.dim('Intent:')} ${cluster.estimatedIntent}`);
1029
+ }
1030
+ console.log();
1031
+ console.log(chalk.bold('Operations:'));
1032
+ for (const op of cluster.ops.slice(0, 20)) {
1033
+ console.log(
1034
+ ` ${formatOpKind(op.kind)} ${chalk.dim(op.hash.slice(0, 12))} ${op.vcs?.filePath ?? ''}`,
1035
+ );
1036
+ }
1037
+ if (cluster.ops.length > 20) {
1038
+ console.log(chalk.dim(` ... +${cluster.ops.length - 20} more`));
1039
+ }
1040
+ return;
1041
+ }
1042
+
1043
+ if (action === 'revive') {
1044
+ if (!id) {
1045
+ console.error(chalk.red('Usage: trellis garden revive <cluster-id>'));
1046
+ process.exit(1);
1047
+ }
1048
+ const ops = garden.revive(id);
1049
+ if (!ops) {
1050
+ console.error(chalk.red(`Cluster not found: ${id}`));
1051
+ process.exit(1);
1052
+ }
1053
+ console.log(chalk.green(`\u2713 Cluster revived: ${id}`));
1054
+ console.log(` ${chalk.dim('Ops to replay:')} ${ops.length}`);
1055
+ console.log(
1056
+ ` ${chalk.dim('Files:')} ${[...new Set(ops.filter((o) => o.vcs?.filePath).map((o) => o.vcs!.filePath!))].join(', ')}`,
1057
+ );
1058
+ return;
1059
+ }
1060
+
1061
+ if (action === 'search') {
1062
+ const results = garden.search({
1063
+ file: opts.file,
1064
+ keyword: opts.keyword,
1065
+ status: opts.status as any,
1066
+ limit: opts.limit,
1067
+ });
1068
+
1069
+ if (results.length === 0) {
1070
+ console.log(chalk.dim('No matching clusters found.'));
1071
+ return;
1072
+ }
1073
+
1074
+ console.log(chalk.bold(`Search results (${results.length})\n`));
1075
+ for (const c of results) {
1076
+ printClusterSummary(c);
1077
+ }
1078
+ return;
1079
+ }
1080
+
1081
+ // List (default)
1082
+ const clusters = garden.search({
1083
+ file: opts.file,
1084
+ keyword: opts.keyword,
1085
+ status: opts.status as any,
1086
+ limit: opts.limit,
1087
+ });
1088
+
1089
+ if (clusters.length === 0) {
1090
+ console.log(chalk.dim('No idea clusters found. The garden is empty.'));
1091
+ return;
1092
+ }
1093
+
1094
+ console.log(chalk.bold(`Idea Garden (${clusters.length} clusters)\n`));
1095
+ for (const c of clusters) {
1096
+ printClusterSummary(c);
1097
+ }
1098
+ });
1099
+
1100
+ function formatClusterStatus(status: string): string {
1101
+ switch (status) {
1102
+ case 'abandoned':
1103
+ return chalk.yellow('abandoned');
1104
+ case 'draft':
1105
+ return chalk.blue('draft');
1106
+ case 'revived':
1107
+ return chalk.green('revived');
1108
+ default:
1109
+ return chalk.dim(status);
1110
+ }
1111
+ }
1112
+
1113
+ function printClusterSummary(c: {
1114
+ id: string;
1115
+ status: string;
1116
+ detectedBy: string;
1117
+ ops: any[];
1118
+ affectedFiles: string[];
1119
+ createdAt: string;
1120
+ abandonedAt: string;
1121
+ }): void {
1122
+ console.log(
1123
+ ` ${chalk.cyan('\u2740')} ${chalk.bold(c.id)} ${formatClusterStatus(c.status)} ${chalk.dim(c.detectedBy)}`,
1124
+ );
1125
+ console.log(
1126
+ ` ${chalk.dim('Ops:')} ${c.ops.length} ${chalk.dim('Files:')} ${c.affectedFiles.slice(0, 3).join(', ')}${c.affectedFiles.length > 3 ? ` +${c.affectedFiles.length - 3}` : ''}`,
1127
+ );
1128
+ console.log(
1129
+ ` ${chalk.dim('Created:')} ${formatRelativeTime(c.createdAt)} ${chalk.dim('Abandoned:')} ${formatRelativeTime(c.abandonedAt)}`,
1130
+ );
1131
+ console.log();
1132
+ }
1133
+
1134
+ // ---------------------------------------------------------------------------
1135
+ // trellis issue
1136
+ // ---------------------------------------------------------------------------
1137
+
1138
+ function formatIssueStatus(status: string | undefined): string {
1139
+ switch (status) {
1140
+ case 'backlog':
1141
+ return chalk.gray('backlog');
1142
+ case 'queue':
1143
+ return chalk.blue('queue');
1144
+ case 'in_progress':
1145
+ return chalk.yellow('in_progress');
1146
+ case 'paused':
1147
+ return chalk.magenta('paused');
1148
+ case 'closed':
1149
+ return chalk.green('closed');
1150
+ default:
1151
+ return chalk.dim(status ?? 'unknown');
1152
+ }
1153
+ }
1154
+
1155
+ function formatPriority(p: string | undefined): string {
1156
+ switch (p) {
1157
+ case 'critical':
1158
+ return chalk.red('critical');
1159
+ case 'high':
1160
+ return chalk.yellow('high');
1161
+ case 'medium':
1162
+ return chalk.cyan('medium');
1163
+ case 'low':
1164
+ return chalk.dim('low');
1165
+ default:
1166
+ return chalk.dim(p ?? '');
1167
+ }
1168
+ }
1169
+
1170
+ function formatCriterionStatus(status: string | undefined): string {
1171
+ switch (status) {
1172
+ case 'passed':
1173
+ return chalk.green('✓ passed');
1174
+ case 'failed':
1175
+ return chalk.red('✗ failed');
1176
+ case 'pending':
1177
+ return chalk.dim('○ pending');
1178
+ default:
1179
+ return chalk.dim(status ?? 'pending');
1180
+ }
1181
+ }
1182
+
1183
+ const issueCmd = program
1184
+ .command('issue')
1185
+ .description('Manage issues (task tracking)');
1186
+
1187
+ issueCmd
1188
+ .command('create')
1189
+ .description('Create a new issue')
1190
+ .requiredOption('-t, --title <title>', 'Issue title')
1191
+ .option(
1192
+ '-P, --priority <priority>',
1193
+ 'Priority: critical, high, medium, low',
1194
+ 'medium',
1195
+ )
1196
+ .option('-l, --labels <labels>', 'Comma-separated labels')
1197
+ .option('--assignee <agentId>', 'Agent to assign')
1198
+ .option('--parent <id>', 'Parent issue ID (for sub-tasks)')
1199
+ .option('-d, --desc <description>', 'Short description')
1200
+ .option(
1201
+ '-S, --status <status>',
1202
+ 'Initial status: backlog (default) or queue',
1203
+ 'backlog',
1204
+ )
1205
+ .option(
1206
+ '--ac <criteria...>',
1207
+ 'Acceptance criteria. Prefix with "test:" for test commands',
1208
+ )
1209
+ .option('-p, --path <path>', 'Repository path', '.')
1210
+ .action(async (opts) => {
1211
+ const rootPath = resolve(opts.path);
1212
+ requireRepo(rootPath);
1213
+
1214
+ const engine = new TrellisVcsEngine({ rootPath });
1215
+ engine.open();
1216
+
1217
+ const labels = opts.labels
1218
+ ? opts.labels.split(',').map((l: string) => l.trim())
1219
+ : undefined;
1220
+
1221
+ const criteria = opts.ac
1222
+ ? opts.ac.map((ac: string) => {
1223
+ if (ac.startsWith('test:')) {
1224
+ return { description: ac.slice(5), command: ac.slice(5) };
1225
+ }
1226
+ return { description: ac };
1227
+ })
1228
+ : undefined;
1229
+
1230
+ const op = await engine.createIssue(opts.title, {
1231
+ priority: opts.priority,
1232
+ labels,
1233
+ assignee: opts.assignee,
1234
+ parentId: opts.parent,
1235
+ description: opts.desc,
1236
+ status: opts.status,
1237
+ criteria,
1238
+ });
1239
+
1240
+ const issueId = op.vcs?.issueId;
1241
+ console.log(chalk.green(`✓ Issue created: ${chalk.bold(issueId)}`));
1242
+ console.log(` ${chalk.dim('Title:')} ${opts.title}`);
1243
+ console.log(` ${chalk.dim('Priority:')} ${formatPriority(opts.priority)}`);
1244
+ if (labels) {
1245
+ console.log(` ${chalk.dim('Labels:')} ${labels.join(', ')}`);
1246
+ }
1247
+ if (opts.parent) {
1248
+ console.log(` ${chalk.dim('Parent:')} ${opts.parent}`);
1249
+ }
1250
+ if (criteria) {
1251
+ console.log(
1252
+ ` ${chalk.dim('Criteria:')} ${criteria.length} acceptance criteria`,
1253
+ );
1254
+ }
1255
+ });
1256
+
1257
+ issueCmd
1258
+ .command('list')
1259
+ .description('List issues')
1260
+ .option(
1261
+ '--status <status>',
1262
+ 'Filter by status: backlog, queue, in_progress, paused, closed',
1263
+ )
1264
+ .option('--label <label>', 'Filter by label')
1265
+ .option('--assignee <agentId>', 'Filter by assignee')
1266
+ .option('--parent <id>', 'Filter by parent issue')
1267
+ .option('-p, --path <path>', 'Repository path', '.')
1268
+ .action((opts) => {
1269
+ const rootPath = resolve(opts.path);
1270
+ requireRepo(rootPath);
1271
+
1272
+ const engine = new TrellisVcsEngine({ rootPath });
1273
+ engine.open();
1274
+
1275
+ const issues = engine.listIssues({
1276
+ status: opts.status,
1277
+ label: opts.label,
1278
+ assignee: opts.assignee,
1279
+ parentId: opts.parent,
1280
+ });
1281
+
1282
+ if (issues.length === 0) {
1283
+ console.log(chalk.dim('No issues found.'));
1284
+ return;
1285
+ }
1286
+
1287
+ console.log(chalk.bold(`Issues (${issues.length})\n`));
1288
+ for (const issue of issues) {
1289
+ const labels =
1290
+ issue.labels.length > 0
1291
+ ? chalk.dim(` [${issue.labels.join(',')}]`)
1292
+ : '';
1293
+ const assignee = issue.assignee ? chalk.dim(` → ${issue.assignee}`) : '';
1294
+ const parent = issue.parentId ? chalk.dim(` ← ${issue.parentId}`) : '';
1295
+ const blocked = issue.isBlocked ? chalk.yellow(' 🔒 blocked') : '';
1296
+ const criteria =
1297
+ issue.criteria.length > 0
1298
+ ? chalk.dim(
1299
+ ` (${issue.criteria.filter((c) => c.status === 'passed').length}/${issue.criteria.length} AC)`,
1300
+ )
1301
+ : '';
1302
+ console.log(
1303
+ ` ${formatPriority(issue.priority)} ${chalk.bold(issue.id)} ${formatIssueStatus(issue.status)} ${issue.title ?? ''}${labels}${assignee}${parent}${blocked}${criteria}`,
1304
+ );
1305
+ }
1306
+ });
1307
+
1308
+ issueCmd
1309
+ .command('show')
1310
+ .description('Show issue details')
1311
+ .argument('<id>', 'Issue ID (e.g. TRL-1)')
1312
+ .option('-p, --path <path>', 'Repository path', '.')
1313
+ .action((id, opts) => {
1314
+ const rootPath = resolve(opts.path);
1315
+ requireRepo(rootPath);
1316
+
1317
+ const engine = new TrellisVcsEngine({ rootPath });
1318
+ engine.open();
1319
+
1320
+ const issue = engine.getIssue(id);
1321
+ if (!issue) {
1322
+ console.error(chalk.red(`Issue not found: ${id}`));
1323
+ process.exit(1);
1324
+ }
1325
+
1326
+ console.log(chalk.bold(`${issue.id}: ${issue.title ?? '(untitled)'}\n`));
1327
+ if (issue.description) {
1328
+ console.log(` ${chalk.dim(issue.description)}\n`);
1329
+ }
1330
+ console.log(
1331
+ ` ${chalk.dim('Status:')} ${formatIssueStatus(issue.status)}`,
1332
+ );
1333
+ console.log(
1334
+ ` ${chalk.dim('Priority:')} ${formatPriority(issue.priority)}`,
1335
+ );
1336
+ if (issue.labels.length > 0) {
1337
+ console.log(` ${chalk.dim('Labels:')} ${issue.labels.join(', ')}`);
1338
+ }
1339
+ if (issue.assignee) {
1340
+ console.log(` ${chalk.dim('Assignee:')} ${issue.assignee}`);
1341
+ }
1342
+ if (issue.parentId) {
1343
+ console.log(` ${chalk.dim('Parent:')} ${issue.parentId}`);
1344
+ }
1345
+ if (issue.branchName) {
1346
+ console.log(` ${chalk.dim('Branch:')} ${issue.branchName}`);
1347
+ }
1348
+ if (issue.blockedBy.length > 0) {
1349
+ console.log(
1350
+ ` ${chalk.dim('Blocked by:')} ${issue.blockedBy.map((b) => chalk.yellow(b)).join(', ')}`,
1351
+ );
1352
+ }
1353
+ if (issue.blocking.length > 0) {
1354
+ console.log(
1355
+ ` ${chalk.dim('Blocking:')} ${issue.blocking.map((b) => chalk.cyan(b)).join(', ')}`,
1356
+ );
1357
+ }
1358
+ if (issue.createdAt) {
1359
+ console.log(
1360
+ ` ${chalk.dim('Created:')} ${formatRelativeTime(issue.createdAt)}`,
1361
+ );
1362
+ }
1363
+ if (issue.startedAt) {
1364
+ console.log(
1365
+ ` ${chalk.dim('Started:')} ${formatRelativeTime(issue.startedAt)}`,
1366
+ );
1367
+ }
1368
+ if (issue.closedAt) {
1369
+ console.log(
1370
+ ` ${chalk.dim('Closed:')} ${formatRelativeTime(issue.closedAt)}`,
1371
+ );
1372
+ }
1373
+
1374
+ if (issue.criteria.length > 0) {
1375
+ console.log(`\n ${chalk.bold('Acceptance Criteria:')}`);
1376
+ for (const c of issue.criteria) {
1377
+ const desc = c.description ?? c.id;
1378
+ const cmd = c.command ? chalk.dim(` (${c.command})`) : '';
1379
+ console.log(` ${formatCriterionStatus(c.status)} ${desc}${cmd}`);
1380
+ }
1381
+ }
1382
+ });
1383
+
1384
+ issueCmd
1385
+ .command('start')
1386
+ .description('Start working on an issue (creates branch, auto-assigns)')
1387
+ .argument('<id>', 'Issue ID')
1388
+ .option('-p, --path <path>', 'Repository path', '.')
1389
+ .action(async (id, opts) => {
1390
+ const rootPath = resolve(opts.path);
1391
+ requireRepo(rootPath);
1392
+
1393
+ const engine = new TrellisVcsEngine({ rootPath });
1394
+ engine.open();
1395
+
1396
+ const op = await engine.startIssue(id);
1397
+ const issue = engine.getIssue(id);
1398
+ console.log(chalk.green(`✓ Started issue ${chalk.bold(id)}`));
1399
+ if (issue?.branchName) {
1400
+ console.log(` ${chalk.dim('Branch:')} ${issue.branchName}`);
1401
+ }
1402
+ if (issue?.assignee) {
1403
+ console.log(` ${chalk.dim('Assignee:')} ${issue.assignee}`);
1404
+ }
1405
+ });
1406
+
1407
+ issueCmd
1408
+ .command('pause')
1409
+ .description('Pause an in-progress issue (switches to default branch)')
1410
+ .argument('<id>', 'Issue ID')
1411
+ .requiredOption(
1412
+ '-n, --note <note>',
1413
+ 'Why paused and what must happen before resuming',
1414
+ )
1415
+ .option('-p, --path <path>', 'Repository path', '.')
1416
+ .action(async (id, opts) => {
1417
+ const rootPath = resolve(opts.path);
1418
+ requireRepo(rootPath);
1419
+
1420
+ const engine = new TrellisVcsEngine({ rootPath });
1421
+ engine.open();
1422
+
1423
+ await engine.pauseIssue(id, opts.note);
1424
+ console.log(chalk.yellow(`⏸ Paused issue ${chalk.bold(id)}`));
1425
+ console.log(` ${chalk.dim('Note:')} ${opts.note}`);
1426
+ console.log(` ${chalk.dim('Switched to:')} ${engine.getCurrentBranch()}`);
1427
+ });
1428
+
1429
+ issueCmd
1430
+ .command('resume')
1431
+ .description('Resume a paused issue (switches to issue branch)')
1432
+ .argument('<id>', 'Issue ID')
1433
+ .option('-p, --path <path>', 'Repository path', '.')
1434
+ .action(async (id, opts) => {
1435
+ const rootPath = resolve(opts.path);
1436
+ requireRepo(rootPath);
1437
+
1438
+ const engine = new TrellisVcsEngine({ rootPath });
1439
+ engine.open();
1440
+
1441
+ await engine.resumeIssue(id);
1442
+ const issue = engine.getIssue(id);
1443
+ console.log(chalk.green(`▶ Resumed issue ${chalk.bold(id)}`));
1444
+ if (issue?.branchName) {
1445
+ console.log(` ${chalk.dim('Branch:')} ${issue.branchName}`);
1446
+ }
1447
+ });
1448
+
1449
+ issueCmd
1450
+ .command('triage')
1451
+ .description('Move a backlog issue to queue (ready to start)')
1452
+ .argument('<id>', 'Issue ID')
1453
+ .option('-p, --path <path>', 'Repository path', '.')
1454
+ .action(async (id, opts) => {
1455
+ const rootPath = resolve(opts.path);
1456
+ requireRepo(rootPath);
1457
+
1458
+ const engine = new TrellisVcsEngine({ rootPath });
1459
+ engine.open();
1460
+
1461
+ await engine.triageIssue(id);
1462
+ console.log(chalk.green(`✓ Triaged ${chalk.bold(id)} → queue`));
1463
+ });
1464
+
1465
+ issueCmd
1466
+ .command('update')
1467
+ .description('Update issue metadata')
1468
+ .argument('<id>', 'Issue ID')
1469
+ .option('--title <title>', 'New title')
1470
+ .option('-d, --desc <description>', 'Short description')
1471
+ .option(
1472
+ '--status <status>',
1473
+ 'New status: backlog, queue, in_progress, paused, closed',
1474
+ )
1475
+ .option('-P, --priority <priority>', 'Priority: critical, high, medium, low')
1476
+ .option('-l, --labels <labels>', 'Comma-separated labels')
1477
+ .option('--assignee <agentId>', 'Agent to assign')
1478
+ .option('-p, --path <path>', 'Repository path', '.')
1479
+ .action(async (id, opts) => {
1480
+ const rootPath = resolve(opts.path);
1481
+ requireRepo(rootPath);
1482
+
1483
+ const engine = new TrellisVcsEngine({ rootPath });
1484
+ engine.open();
1485
+
1486
+ const updates: Record<string, any> = {};
1487
+ if (opts.title !== undefined) updates.title = opts.title;
1488
+ if (opts.desc !== undefined) updates.description = opts.desc;
1489
+ if (opts.status !== undefined) updates.status = opts.status;
1490
+ if (opts.priority !== undefined) updates.priority = opts.priority;
1491
+ if (opts.labels !== undefined) {
1492
+ updates.labels = opts.labels.split(',').map((l: string) => l.trim());
1493
+ }
1494
+ if (opts.assignee !== undefined) updates.assignee = opts.assignee;
1495
+
1496
+ await engine.updateIssue(id, updates);
1497
+ console.log(chalk.green(`✓ Updated ${chalk.bold(id)}`));
1498
+ });
1499
+
1500
+ issueCmd
1501
+ .command('describe')
1502
+ .description('Set an issue description')
1503
+ .argument('<id>', 'Issue ID')
1504
+ .argument('<description>', 'Short description text')
1505
+ .option('-p, --path <path>', 'Repository path', '.')
1506
+ .action(async (id, description, opts) => {
1507
+ const rootPath = resolve(opts.path);
1508
+ requireRepo(rootPath);
1509
+
1510
+ const engine = new TrellisVcsEngine({ rootPath });
1511
+ engine.open();
1512
+
1513
+ await engine.updateIssue(id, { description });
1514
+ console.log(chalk.green(`✓ Description set for ${chalk.bold(id)}`));
1515
+ });
1516
+
1517
+ issueCmd
1518
+ .command('assign')
1519
+ .description('Assign an issue to an agent')
1520
+ .argument('<id>', 'Issue ID')
1521
+ .requiredOption('--to <agentId>', 'Agent ID to assign')
1522
+ .option('-p, --path <path>', 'Repository path', '.')
1523
+ .action(async (id, opts) => {
1524
+ const rootPath = resolve(opts.path);
1525
+ requireRepo(rootPath);
1526
+
1527
+ const engine = new TrellisVcsEngine({ rootPath });
1528
+ engine.open();
1529
+
1530
+ await engine.assignIssue(id, opts.to);
1531
+ console.log(chalk.green(`✓ Assigned ${chalk.bold(id)} → ${opts.to}`));
1532
+ });
1533
+
1534
+ issueCmd
1535
+ .command('ac')
1536
+ .description('Add acceptance criterion to an issue')
1537
+ .argument('<id>', 'Issue ID')
1538
+ .argument('<description>', 'Criterion description')
1539
+ .option('--test <command>', 'Shell command to validate (exit 0 = pass)')
1540
+ .option('-p, --path <path>', 'Repository path', '.')
1541
+ .action(async (id, description, opts) => {
1542
+ const rootPath = resolve(opts.path);
1543
+ requireRepo(rootPath);
1544
+
1545
+ const engine = new TrellisVcsEngine({ rootPath });
1546
+ engine.open();
1547
+
1548
+ await engine.addCriterion(id, description, opts.test);
1549
+ const cmdNote = opts.test ? chalk.dim(` (test: ${opts.test})`) : '';
1550
+ console.log(
1551
+ chalk.green(
1552
+ `✓ Added criterion to ${chalk.bold(id)}: ${description}${cmdNote}`,
1553
+ ),
1554
+ );
1555
+ });
1556
+
1557
+ issueCmd
1558
+ .command('ac-pass')
1559
+ .description('Manually mark an acceptance criterion as passed')
1560
+ .argument('<id>', 'Issue ID')
1561
+ .argument('<index>', 'Criterion number (1-based)')
1562
+ .option('-p, --path <path>', 'Repository path', '.')
1563
+ .action(async (id, index, opts) => {
1564
+ const rootPath = resolve(opts.path);
1565
+ requireRepo(rootPath);
1566
+
1567
+ const engine = new TrellisVcsEngine({ rootPath });
1568
+ engine.open();
1569
+
1570
+ await engine.setCriterionStatus(id, parseInt(index, 10), 'passed');
1571
+ console.log(
1572
+ chalk.green(
1573
+ `✓ Criterion #${index} on ${chalk.bold(id)} marked as passed`,
1574
+ ),
1575
+ );
1576
+ });
1577
+
1578
+ issueCmd
1579
+ .command('ac-fail')
1580
+ .description('Manually mark an acceptance criterion as failed')
1581
+ .argument('<id>', 'Issue ID')
1582
+ .argument('<index>', 'Criterion number (1-based)')
1583
+ .option('-p, --path <path>', 'Repository path', '.')
1584
+ .action(async (id, index, opts) => {
1585
+ const rootPath = resolve(opts.path);
1586
+ requireRepo(rootPath);
1587
+
1588
+ const engine = new TrellisVcsEngine({ rootPath });
1589
+ engine.open();
1590
+
1591
+ await engine.setCriterionStatus(id, parseInt(index, 10), 'failed');
1592
+ console.log(
1593
+ chalk.red(`✗ Criterion #${index} on ${chalk.bold(id)} marked as failed`),
1594
+ );
1595
+ });
1596
+
1597
+ issueCmd
1598
+ .command('check')
1599
+ .description('Run acceptance criteria for an issue')
1600
+ .argument('<id>', 'Issue ID')
1601
+ .option('-p, --path <path>', 'Repository path', '.')
1602
+ .action(async (id, opts) => {
1603
+ const rootPath = resolve(opts.path);
1604
+ requireRepo(rootPath);
1605
+
1606
+ const engine = new TrellisVcsEngine({ rootPath });
1607
+ engine.open();
1608
+
1609
+ console.log(chalk.bold(`Running criteria for ${id}...\n`));
1610
+ const results = await engine.runCriteria(id);
1611
+
1612
+ if (results.length === 0) {
1613
+ console.log(chalk.dim('No acceptance criteria defined.'));
1614
+ return;
1615
+ }
1616
+
1617
+ for (const r of results) {
1618
+ const desc = r.description ?? r.id;
1619
+ const statusStr =
1620
+ r.status === 'passed'
1621
+ ? chalk.green('✓ PASSED')
1622
+ : r.status === 'failed'
1623
+ ? chalk.red('✗ FAILED')
1624
+ : chalk.dim('○ SKIPPED');
1625
+ console.log(` ${statusStr} ${desc}`);
1626
+ if (r.command) {
1627
+ console.log(` ${chalk.dim('$')} ${r.command}`);
1628
+ }
1629
+ if (r.output && r.status === 'failed') {
1630
+ const lines = r.output.split('\n').slice(0, 5);
1631
+ for (const line of lines) {
1632
+ console.log(` ${chalk.dim(line)}`);
1633
+ }
1634
+ }
1635
+ }
1636
+
1637
+ const passed = results.filter((r) => r.status === 'passed').length;
1638
+ const total = results.length;
1639
+ console.log();
1640
+ if (passed === total) {
1641
+ console.log(
1642
+ chalk.green(
1643
+ `All ${total} criteria passed. Close with: trellis issue close ${id} --confirm`,
1644
+ ),
1645
+ );
1646
+ } else {
1647
+ console.log(chalk.yellow(`${passed}/${total} criteria passing.`));
1648
+ }
1649
+ });
1650
+
1651
+ issueCmd
1652
+ .command('close')
1653
+ .description('Close an issue (requires all criteria pass + --confirm)')
1654
+ .argument('<id>', 'Issue ID')
1655
+ .option('--confirm', 'Confirm closure after criteria pass')
1656
+ .option('-p, --path <path>', 'Repository path', '.')
1657
+ .action(async (id, opts) => {
1658
+ const rootPath = resolve(opts.path);
1659
+ requireRepo(rootPath);
1660
+
1661
+ const engine = new TrellisVcsEngine({ rootPath });
1662
+ engine.open();
1663
+
1664
+ try {
1665
+ const result = await engine.closeIssue(id, { confirm: opts.confirm });
1666
+
1667
+ if (!result.op) {
1668
+ // Criteria passed but no --confirm
1669
+ console.log(chalk.bold(`Criteria status for ${id}:\n`));
1670
+ for (const r of result.criteriaResults) {
1671
+ console.log(
1672
+ ` ${formatCriterionStatus(r.status)} ${r.description ?? r.id}`,
1673
+ );
1674
+ }
1675
+ console.log();
1676
+ console.log(
1677
+ chalk.yellow(
1678
+ `All criteria pass. Re-run with --confirm to close: trellis issue close ${id} --confirm`,
1679
+ ),
1680
+ );
1681
+ return;
1682
+ }
1683
+
1684
+ console.log(chalk.green(`✓ Issue ${chalk.bold(id)} closed`));
1685
+ } catch (err: any) {
1686
+ console.error(chalk.red(err.message));
1687
+ process.exit(1);
1688
+ }
1689
+ });
1690
+
1691
+ issueCmd
1692
+ .command('reopen')
1693
+ .description('Reopen a closed issue')
1694
+ .argument('<id>', 'Issue ID')
1695
+ .option('-p, --path <path>', 'Repository path', '.')
1696
+ .action(async (id, opts) => {
1697
+ const rootPath = resolve(opts.path);
1698
+ requireRepo(rootPath);
1699
+
1700
+ const engine = new TrellisVcsEngine({ rootPath });
1701
+ engine.open();
1702
+
1703
+ await engine.reopenIssue(id);
1704
+ console.log(chalk.green(`✓ Issue ${chalk.bold(id)} reopened`));
1705
+ });
1706
+
1707
+ issueCmd
1708
+ .command('block')
1709
+ .description('Mark an issue as blocked by another issue')
1710
+ .argument('<id>', 'Issue ID to block')
1711
+ .argument('<blockedBy>', 'Issue ID that blocks it')
1712
+ .option('-p, --path <path>', 'Repository path', '.')
1713
+ .action(async (id, blockedBy, opts) => {
1714
+ const rootPath = resolve(opts.path);
1715
+ requireRepo(rootPath);
1716
+
1717
+ const engine = new TrellisVcsEngine({ rootPath });
1718
+ engine.open();
1719
+
1720
+ await engine.blockIssue(id, blockedBy);
1721
+ console.log(
1722
+ chalk.yellow(
1723
+ `🔒 ${chalk.bold(id)} is now blocked by ${chalk.bold(blockedBy)}`,
1724
+ ),
1725
+ );
1726
+ });
1727
+
1728
+ issueCmd
1729
+ .command('unblock')
1730
+ .description('Remove a blocking relationship')
1731
+ .argument('<id>', 'Blocked issue ID')
1732
+ .argument('<blockedBy>', 'Blocking issue ID to remove')
1733
+ .option('-p, --path <path>', 'Repository path', '.')
1734
+ .action(async (id, blockedBy, opts) => {
1735
+ const rootPath = resolve(opts.path);
1736
+ requireRepo(rootPath);
1737
+
1738
+ const engine = new TrellisVcsEngine({ rootPath });
1739
+ engine.open();
1740
+
1741
+ await engine.unblockIssue(id, blockedBy);
1742
+ console.log(
1743
+ chalk.green(
1744
+ `🔓 ${chalk.bold(id)} is no longer blocked by ${chalk.bold(blockedBy)}`,
1745
+ ),
1746
+ );
1747
+ });
1748
+
1749
+ issueCmd
1750
+ .command('active')
1751
+ .description('Show all active (in-progress) issues')
1752
+ .option('-p, --path <path>', 'Repository path', '.')
1753
+ .action((opts) => {
1754
+ const rootPath = resolve(opts.path);
1755
+ requireRepo(rootPath);
1756
+
1757
+ const engine = new TrellisVcsEngine({ rootPath });
1758
+ engine.open();
1759
+
1760
+ const active = engine.getActiveIssues();
1761
+ if (active.length === 0) {
1762
+ console.log(chalk.dim('No active issues.'));
1763
+ return;
1764
+ }
1765
+
1766
+ console.log(chalk.bold(`Active Issues (${active.length})\n`));
1767
+ for (const issue of active) {
1768
+ const branch = issue.branchName
1769
+ ? chalk.dim(` on ${issue.branchName}`)
1770
+ : '';
1771
+ const assignee = issue.assignee ? chalk.dim(` → ${issue.assignee}`) : '';
1772
+ console.log(
1773
+ ` ${formatPriority(issue.priority)} ${chalk.bold(issue.id)} ${issue.title ?? ''}${branch}${assignee}`,
1774
+ );
1775
+ }
1776
+ });
1777
+
1778
+ issueCmd
1779
+ .command('readiness')
1780
+ .description(
1781
+ 'Check if all issues are complete (no queue, paused, or in-progress)',
1782
+ )
1783
+ .option('-p, --path <path>', 'Repository path', '.')
1784
+ .action((opts) => {
1785
+ const rootPath = resolve(opts.path);
1786
+ requireRepo(rootPath);
1787
+
1788
+ const engine = new TrellisVcsEngine({ rootPath });
1789
+ engine.open();
1790
+
1791
+ const result = engine.checkCompletionReadiness();
1792
+ console.log(result.summary);
1793
+
1794
+ if (!result.ready) {
1795
+ process.exit(1);
1796
+ }
1797
+ });
1798
+
1799
+ // ---------------------------------------------------------------------------
1800
+ // trellis decision
1801
+ // ---------------------------------------------------------------------------
1802
+
1803
+ const decisionCmd = program
1804
+ .command('decision')
1805
+ .description('Manage decision traces');
1806
+
1807
+ decisionCmd
1808
+ .command('list')
1809
+ .description('List decision traces')
1810
+ .option('-p, --path <path>', 'Repository path', '.')
1811
+ .option(
1812
+ '-t, --tool <pattern>',
1813
+ 'Filter by tool name pattern (e.g. "trellis_issue_*")',
1814
+ )
1815
+ .option('-e, --entity <id>', 'Filter by related entity ID')
1816
+ .option('-n, --limit <n>', 'Max results', '20')
1817
+ .action((opts) => {
1818
+ const rootPath = resolve(opts.path);
1819
+ requireRepo(rootPath);
1820
+
1821
+ const engine = new TrellisVcsEngine({ rootPath });
1822
+ engine.open();
1823
+
1824
+ const decisions = engine.queryDecisions({
1825
+ toolPattern: opts.tool,
1826
+ entityId: opts.entity,
1827
+ limit: parseInt(opts.limit, 10),
1828
+ });
1829
+
1830
+ if (decisions.length === 0) {
1831
+ console.log(chalk.dim('No decision traces found.'));
1832
+ return;
1833
+ }
1834
+
1835
+ for (const d of decisions) {
1836
+ const ts = d.createdAt ? chalk.dim(d.createdAt) : '';
1837
+ console.log(`${chalk.cyan(d.id)} ${chalk.white(d.toolName)} ${ts}`);
1838
+ if (d.rationale) {
1839
+ console.log(` ${chalk.dim('→')} ${d.rationale}`);
1840
+ }
1841
+ }
1842
+ });
1843
+
1844
+ decisionCmd
1845
+ .command('show')
1846
+ .description('Show full details of a decision trace')
1847
+ .argument('<id>', 'Decision ID (e.g. DEC-1)')
1848
+ .option('-p, --path <path>', 'Repository path', '.')
1849
+ .action((id, opts) => {
1850
+ const rootPath = resolve(opts.path);
1851
+ requireRepo(rootPath);
1852
+
1853
+ const engine = new TrellisVcsEngine({ rootPath });
1854
+ engine.open();
1855
+
1856
+ const d = engine.getDecision(id);
1857
+ if (!d) {
1858
+ console.error(chalk.red(`Decision ${id} not found.`));
1859
+ process.exit(1);
1860
+ }
1861
+
1862
+ console.log(`${chalk.bold('ID:')} ${d.id}`);
1863
+ console.log(`${chalk.bold('Tool:')} ${d.toolName}`);
1864
+ console.log(`${chalk.bold('Created:')} ${d.createdAt ?? 'unknown'}`);
1865
+ console.log(`${chalk.bold('Agent:')} ${d.createdBy ?? 'unknown'}`);
1866
+ if (d.context) console.log(`${chalk.bold('Context:')} ${d.context}`);
1867
+ if (d.rationale) console.log(`${chalk.bold('Rationale:')} ${d.rationale}`);
1868
+ if (d.alternatives && d.alternatives.length > 0) {
1869
+ console.log(
1870
+ `${chalk.bold('Alternatives:')} ${d.alternatives.join(', ')}`,
1871
+ );
1872
+ }
1873
+ if (d.outputSummary) {
1874
+ console.log(`${chalk.bold('Output:')} ${d.outputSummary}`);
1875
+ }
1876
+ if (d.relatedEntities.length > 0) {
1877
+ console.log(
1878
+ `${chalk.bold('Related:')} ${d.relatedEntities.join(', ')}`,
1879
+ );
1880
+ }
1881
+ });
1882
+
1883
+ decisionCmd
1884
+ .command('chain')
1885
+ .description('Trace all decisions that affected a given entity')
1886
+ .argument(
1887
+ '<entityId>',
1888
+ 'Entity ID (e.g. "issue:TRL-5", "file:src/engine.ts")',
1889
+ )
1890
+ .option('-p, --path <path>', 'Repository path', '.')
1891
+ .action((entityId, opts) => {
1892
+ const rootPath = resolve(opts.path);
1893
+ requireRepo(rootPath);
1894
+
1895
+ const engine = new TrellisVcsEngine({ rootPath });
1896
+ engine.open();
1897
+
1898
+ const chain = engine.getDecisionChain(entityId);
1899
+ if (chain.length === 0) {
1900
+ console.log(chalk.dim(`No decision traces found for ${entityId}.`));
1901
+ return;
1902
+ }
1903
+
1904
+ console.log(
1905
+ chalk.bold(`Decision chain for ${entityId} (${chain.length} decisions):`),
1906
+ );
1907
+ for (const d of chain) {
1908
+ const ts = d.createdAt ? chalk.dim(d.createdAt) : '';
1909
+ console.log(` ${chalk.cyan(d.id)} ${chalk.white(d.toolName)} ${ts}`);
1910
+ if (d.rationale) {
1911
+ console.log(` ${chalk.dim('→')} ${d.rationale}`);
1912
+ }
1913
+ }
1914
+ });
1915
+
1916
+ // ---------------------------------------------------------------------------
1917
+ // trellis identity
1918
+ // ---------------------------------------------------------------------------
1919
+
1920
+ program
1921
+ .command('identity')
1922
+ .description('Manage local identity (Ed25519 key pair)')
1923
+ .argument('[action]', '"init" or "show" (default: show)')
1924
+ .option('-p, --path <path>', 'Repository path', '.')
1925
+ .option('--name <name>', 'Display name for new identity')
1926
+ .option('--email <email>', 'Email for new identity')
1927
+ .action((action, opts) => {
1928
+ const rootPath = resolve(opts.path);
1929
+ const trellisDir = join(rootPath, '.trellis');
1930
+
1931
+ if (action === 'init') {
1932
+ if (hasIdentity(trellisDir)) {
1933
+ console.error(
1934
+ chalk.yellow(
1935
+ 'Identity already exists. Use `trellis identity` to view it.',
1936
+ ),
1937
+ );
1938
+ process.exit(1);
1939
+ }
1940
+
1941
+ const name = opts.name ?? 'Anonymous';
1942
+ const email = opts.email;
1943
+
1944
+ const identity = createIdentity({ displayName: name, email });
1945
+ saveIdentity(trellisDir, identity);
1946
+
1947
+ console.log(chalk.green('✓ Identity created'));
1948
+ console.log(` ${chalk.dim('Name:')} ${identity.displayName}`);
1949
+ if (identity.email) {
1950
+ console.log(` ${chalk.dim('Email:')} ${identity.email}`);
1951
+ }
1952
+ console.log(` ${chalk.dim('DID:')} ${identity.did}`);
1953
+ console.log(` ${chalk.dim('ID:')} ${identity.entityId}`);
1954
+ return;
1955
+ }
1956
+
1957
+ // Show (default)
1958
+ const identity = loadIdentity(trellisDir);
1959
+ if (!identity) {
1960
+ console.log(
1961
+ chalk.dim(
1962
+ 'No identity configured. Run `trellis identity init --name "Your Name"`.',
1963
+ ),
1964
+ );
1965
+ return;
1966
+ }
1967
+
1968
+ const pub = toPublicIdentity(identity);
1969
+ console.log(chalk.bold('Identity\n'));
1970
+ console.log(` ${chalk.dim('Name:')} ${pub.displayName}`);
1971
+ if (pub.email) {
1972
+ console.log(` ${chalk.dim('Email:')} ${pub.email}`);
1973
+ }
1974
+ console.log(` ${chalk.dim('DID:')} ${pub.did}`);
1975
+ console.log(` ${chalk.dim('Entity ID:')} ${pub.entityId}`);
1976
+ console.log(` ${chalk.dim('Public Key:')} ${pub.publicKey.slice(0, 32)}…`);
1977
+ console.log(` ${chalk.dim('Created:')} ${pub.createdAt}`);
1978
+ });
1979
+
1980
+ // ---------------------------------------------------------------------------
1981
+ // trellis refs
1982
+ // ---------------------------------------------------------------------------
1983
+
1984
+ program
1985
+ .command('refs')
1986
+ .description('List wiki-link references in files or find backlinks')
1987
+ .argument('[file]', 'File to list outgoing refs for')
1988
+ .option('-p, --path <path>', 'Repository path', '.')
1989
+ .option(
1990
+ '--backlinks <entity>',
1991
+ 'Show all files referencing an entity (e.g. TRL-5)',
1992
+ )
1993
+ .option('--broken', 'List all broken and stale references')
1994
+ .option('--stats', 'Show reference index statistics')
1995
+ .action((file, opts) => {
1996
+ const rootPath = resolve(opts.path);
1997
+ if (!TrellisVcsEngine.isRepo(rootPath)) {
1998
+ console.error(
1999
+ chalk.red('Not a TrellisVCS repository. Run `trellis init` first.'),
2000
+ );
2001
+ process.exit(1);
2002
+ }
2003
+
2004
+ const engine = new TrellisVcsEngine({ rootPath });
2005
+ engine.open();
2006
+
2007
+ const { readFileSync } = require('fs');
2008
+ const {
2009
+ parseFileRefs,
2010
+ buildRefIndex,
2011
+ getOutgoingRefs,
2012
+ getBacklinks,
2013
+ getIndexStats,
2014
+ } = require('../links/index.js');
2015
+ const {
2016
+ resolveRef,
2017
+ resolveRefs,
2018
+ createResolverContext,
2019
+ } = require('../links/index.js');
2020
+ const { StaleRefRegistry, getDiagnostics } = require('../links/index.js');
2021
+
2022
+ // Build resolver context from engine
2023
+ const resolverCtx = createResolverContext(engine);
2024
+
2025
+ // Scan all tracked .md files and source files to build the index
2026
+ const trackedFiles = engine.trackedFiles();
2027
+ const fileContents: Array<{ path: string; content: string }> = [];
2028
+ for (const f of trackedFiles) {
2029
+ try {
2030
+ const absPath = join(rootPath, f.path);
2031
+ const content = readFileSync(absPath, 'utf-8');
2032
+ fileContents.push({ path: f.path, content });
2033
+ } catch {
2034
+ // File may not exist on disk
2035
+ }
2036
+ }
2037
+
2038
+ const index = buildRefIndex(fileContents, resolverCtx);
2039
+
2040
+ // --stats: show index statistics
2041
+ if (opts.stats) {
2042
+ const stats = getIndexStats(index);
2043
+ console.log(chalk.bold('Reference Index Stats\n'));
2044
+ console.log(` ${chalk.dim('Files with refs:')} ${stats.totalFiles}`);
2045
+ console.log(` ${chalk.dim('Total refs:')} ${stats.totalRefs}`);
2046
+ console.log(` ${chalk.dim('Unique entities:')} ${stats.totalEntities}`);
2047
+ return;
2048
+ }
2049
+
2050
+ // --backlinks <entity>: show all sources referencing an entity
2051
+ if (opts.backlinks) {
2052
+ const entity = opts.backlinks;
2053
+ // Try common entity ID formats
2054
+ const candidates = [
2055
+ `issue:${entity}`,
2056
+ `file:${entity}`,
2057
+ `symbol:${entity}`,
2058
+ `identity:${entity}`,
2059
+ `milestone:${entity}`,
2060
+ `decision:${entity}`,
2061
+ entity, // raw entity ID
2062
+ ];
2063
+
2064
+ let found = false;
2065
+ for (const eid of candidates) {
2066
+ const sources = getBacklinks(index, eid);
2067
+ if (sources.length > 0) {
2068
+ console.log(
2069
+ chalk.bold(
2070
+ `Backlinks for ${chalk.cyan(eid)} (${sources.length})\n`,
2071
+ ),
2072
+ );
2073
+ for (const s of sources) {
2074
+ console.log(
2075
+ ` ${chalk.dim(s.filePath)}:${s.line} ${chalk.dim(`(${s.context})`)}`,
2076
+ );
2077
+ }
2078
+ found = true;
2079
+ break;
2080
+ }
2081
+ }
2082
+
2083
+ if (!found) {
2084
+ console.log(chalk.dim(`No references found for: ${entity}`));
2085
+ }
2086
+ return;
2087
+ }
2088
+
2089
+ // --broken: list all broken and stale refs
2090
+ if (opts.broken) {
2091
+ const registry = new StaleRefRegistry();
2092
+ const resolvedIds = new Set<string>();
2093
+
2094
+ // Resolve all refs to build the resolved set
2095
+ for (const [, refs] of index.outgoing) {
2096
+ for (const ref of refs) {
2097
+ const resolved = resolveRef(ref, resolverCtx);
2098
+ if (resolved.state === 'resolved' && resolved.entityId) {
2099
+ resolvedIds.add(resolved.entityId);
2100
+ }
2101
+ }
2102
+ }
2103
+
2104
+ const diags = getDiagnostics(index, registry, resolvedIds);
2105
+
2106
+ if (diags.length === 0) {
2107
+ console.log(chalk.green('✓ No broken or stale references found.'));
2108
+ return;
2109
+ }
2110
+
2111
+ const stale = diags.filter((d: any) => d.state === 'stale');
2112
+ const broken = diags.filter((d: any) => d.state === 'broken');
2113
+
2114
+ if (broken.length > 0) {
2115
+ console.log(
2116
+ chalk.bold(chalk.red(`Broken references (${broken.length})\n`)),
2117
+ );
2118
+ for (const d of broken) {
2119
+ console.log(
2120
+ ` ${chalk.red('✗')} ${d.source.filePath}:${d.source.line} ${d.message}`,
2121
+ );
2122
+ }
2123
+ console.log();
2124
+ }
2125
+
2126
+ if (stale.length > 0) {
2127
+ console.log(
2128
+ chalk.bold(chalk.yellow(`Stale references (${stale.length})\n`)),
2129
+ );
2130
+ for (const d of stale) {
2131
+ console.log(
2132
+ ` ${chalk.yellow('⚠')} ${d.source.filePath}:${d.source.line} ${d.message}`,
2133
+ );
2134
+ }
2135
+ }
2136
+ return;
2137
+ }
2138
+
2139
+ // Default: list outgoing refs for a specific file (or all files)
2140
+ if (file) {
2141
+ const refs = getOutgoingRefs(index, file);
2142
+ if (refs.length === 0) {
2143
+ console.log(chalk.dim(`No [[...]] references found in: ${file}`));
2144
+ return;
2145
+ }
2146
+
2147
+ console.log(
2148
+ chalk.bold(`References in ${chalk.cyan(file)} (${refs.length})\n`),
2149
+ );
2150
+ for (const ref of refs) {
2151
+ const resolved = resolveRef(ref, resolverCtx);
2152
+ const stateIcon =
2153
+ resolved.state === 'resolved'
2154
+ ? chalk.green('✓')
2155
+ : resolved.state === 'stale'
2156
+ ? chalk.yellow('⚠')
2157
+ : chalk.red('✗');
2158
+ const display = ref.alias ?? ref.raw;
2159
+ const entityId = resolved.entityId ?? chalk.dim('unresolved');
2160
+ console.log(
2161
+ ` ${stateIcon} [[${display}]] → ${entityId} ${chalk.dim(`L${ref.source.line}`)}`,
2162
+ );
2163
+ }
2164
+ } else {
2165
+ // List all files with refs
2166
+ const stats = getIndexStats(index);
2167
+ if (stats.totalRefs === 0) {
2168
+ console.log(
2169
+ chalk.dim('No [[...]] references found in any tracked files.'),
2170
+ );
2171
+ return;
2172
+ }
2173
+
2174
+ console.log(
2175
+ chalk.bold(
2176
+ `References (${stats.totalRefs} across ${stats.totalFiles} files)\n`,
2177
+ ),
2178
+ );
2179
+ for (const [filePath, refs] of index.outgoing) {
2180
+ console.log(` ${chalk.cyan(filePath)} (${refs.length} refs)`);
2181
+ for (const ref of refs) {
2182
+ const resolved = resolveRef(ref, resolverCtx);
2183
+ const stateIcon =
2184
+ resolved.state === 'resolved'
2185
+ ? chalk.green('✓')
2186
+ : resolved.state === 'stale'
2187
+ ? chalk.yellow('⚠')
2188
+ : chalk.red('✗');
2189
+ console.log(
2190
+ ` ${stateIcon} [[${ref.raw}]] ${chalk.dim(`L${ref.source.line}`)}`,
2191
+ );
2192
+ }
2193
+ }
2194
+ }
2195
+ });
2196
+
2197
+ // ---------------------------------------------------------------------------
2198
+ // trellis search
2199
+ // ---------------------------------------------------------------------------
2200
+
2201
+ program
2202
+ .command('search')
2203
+ .description('Semantic search across all embedded content')
2204
+ .argument('<query>', 'Natural language search query')
2205
+ .option('-p, --path <path>', 'Repository path', '.')
2206
+ .option('-l, --limit <n>', 'Max results', '10')
2207
+ .option(
2208
+ '-t, --type <types>',
2209
+ 'Filter by chunk type(s), comma-separated (issue_title,issue_desc,milestone_msg,markdown,code_entity,doc_comment,summary_md)',
2210
+ )
2211
+ .action(async (query, opts) => {
2212
+ const rootPath = resolve(opts.path);
2213
+ if (!TrellisVcsEngine.isRepo(rootPath)) {
2214
+ console.error(
2215
+ chalk.red('Not a TrellisVCS repository. Run `trellis init` first.'),
2216
+ );
2217
+ process.exit(1);
2218
+ }
2219
+
2220
+ const engine = new TrellisVcsEngine({ rootPath });
2221
+ engine.open();
2222
+
2223
+ const { EmbeddingManager } = require('../embeddings/index.js');
2224
+
2225
+ const dbPath = join(rootPath, '.trellis', 'embeddings.db');
2226
+ const manager = new EmbeddingManager(dbPath);
2227
+
2228
+ try {
2229
+ const searchOpts: any = {
2230
+ limit: parseInt(opts.limit, 10) || 10,
2231
+ };
2232
+ if (opts.type) {
2233
+ searchOpts.types = opts.type.split(',').map((t: string) => t.trim());
2234
+ }
2235
+
2236
+ const results = await manager.search(query, searchOpts);
2237
+
2238
+ if (results.length === 0) {
2239
+ console.log(
2240
+ chalk.dim(
2241
+ 'No results found. Try `trellis reindex` to build the index.',
2242
+ ),
2243
+ );
2244
+ return;
2245
+ }
2246
+
2247
+ console.log(
2248
+ chalk.bold(
2249
+ `Search results for ${chalk.cyan(`"${query}"`)} (${results.length})\n`,
2250
+ ),
2251
+ );
2252
+
2253
+ for (const r of results) {
2254
+ const score = (r.score * 100).toFixed(1);
2255
+ const typeTag = chalk.dim(`[${r.chunk.chunkType}]`);
2256
+ const filePart = r.chunk.filePath
2257
+ ? chalk.dim(` ${r.chunk.filePath}`)
2258
+ : '';
2259
+ const preview = r.chunk.content.slice(0, 120).replace(/\n/g, ' ');
2260
+
2261
+ console.log(` ${chalk.green(`${score}%`)} ${typeTag}${filePart}`);
2262
+ console.log(` ${chalk.dim(preview)}`);
2263
+ console.log();
2264
+ }
2265
+ } finally {
2266
+ manager.close();
2267
+ }
2268
+ });
2269
+
2270
+ // ---------------------------------------------------------------------------
2271
+ // trellis reindex
2272
+ // ---------------------------------------------------------------------------
2273
+
2274
+ program
2275
+ .command('reindex')
2276
+ .description('Rebuild the semantic embedding index')
2277
+ .option('-p, --path <path>', 'Repository path', '.')
2278
+ .action(async (opts) => {
2279
+ const rootPath = resolve(opts.path);
2280
+ if (!TrellisVcsEngine.isRepo(rootPath)) {
2281
+ console.error(
2282
+ chalk.red('Not a TrellisVCS repository. Run `trellis init` first.'),
2283
+ );
2284
+ process.exit(1);
2285
+ }
2286
+
2287
+ const engine = new TrellisVcsEngine({ rootPath });
2288
+ engine.open();
2289
+
2290
+ const { EmbeddingManager } = require('../embeddings/index.js');
2291
+
2292
+ const dbPath = join(rootPath, '.trellis', 'embeddings.db');
2293
+ const manager = new EmbeddingManager(dbPath);
2294
+
2295
+ try {
2296
+ console.log(chalk.dim('Loading embedding model…'));
2297
+ const result = await manager.reindex(engine);
2298
+ console.log(chalk.green(`✓ Indexed ${result.chunks} chunks`));
2299
+
2300
+ const stats = manager.stats();
2301
+ console.log(chalk.dim(` Types: ${JSON.stringify(stats.byType)}`));
2302
+ } finally {
2303
+ manager.close();
2304
+ }
2305
+ });
2306
+
2307
+ // ---------------------------------------------------------------------------
2308
+ // Helpers
2309
+ // ---------------------------------------------------------------------------
2310
+
2311
+ function formatOpKind(kind: string): string {
2312
+ const kindMap: Record<string, string> = {
2313
+ 'vcs:fileAdd': chalk.green('+add'),
2314
+ 'vcs:fileModify': chalk.yellow('~mod'),
2315
+ 'vcs:fileDelete': chalk.red('-del'),
2316
+ 'vcs:fileRename': chalk.blue('→ren'),
2317
+ 'vcs:branchCreate': chalk.magenta('⊕branch'),
2318
+ 'vcs:branchAdvance': chalk.magenta('→branch'),
2319
+ 'vcs:milestoneCreate': chalk.cyan('★milestone'),
2320
+ 'vcs:checkpointCreate': chalk.dim('●checkpoint'),
2321
+ };
2322
+ return kindMap[kind] ?? chalk.dim(kind);
2323
+ }
2324
+
2325
+ function formatRelativeTime(iso: string): string {
2326
+ const now = Date.now();
2327
+ const then = new Date(iso).getTime();
2328
+ const diff = now - then;
2329
+
2330
+ if (diff < 60_000) return 'just now';
2331
+ if (diff < 3_600_000) return `${Math.floor(diff / 60_000)}m ago`;
2332
+ if (diff < 86_400_000) return `${Math.floor(diff / 3_600_000)}h ago`;
2333
+ return `${Math.floor(diff / 86_400_000)}d ago`;
2334
+ }
2335
+
2336
+ // ---------------------------------------------------------------------------
2337
+ // Run
2338
+ // ---------------------------------------------------------------------------
2339
+
2340
+ program.parse();