all-hands-cli 0.1.3 → 0.1.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.
@@ -7,6 +7,7 @@
7
7
  * - ah specs list - List all specs grouped by domain_name
8
8
  * - ah specs complete <name> - Mark spec completed, move spec out of roadmap
9
9
  * - ah specs create <path> - Create spec: validate, assign branch, commit and push
10
+ * - ah specs graph - Render dependency graph with availability markers
10
11
  */
11
12
 
12
13
  import { Command } from 'commander';
@@ -349,6 +350,195 @@ export function register(program: Command): void {
349
350
  }
350
351
  });
351
352
 
353
+ // ah specs graph
354
+ specs
355
+ .command('graph')
356
+ .description('Render dependency graph showing spec relationships and availability')
357
+ .option('--json', 'Output as JSON')
358
+ .option('--roadmap', 'Only show roadmap/in-progress specs')
359
+ .action((options: { json?: boolean; roadmap?: boolean }) => {
360
+ const allSpecs = loadAllSpecGroups().flatMap((g) => g.specs);
361
+
362
+ if (allSpecs.length === 0) {
363
+ if (options.json) {
364
+ console.log(JSON.stringify({ success: true, count: 0, available: [], tree: [], summary: { completed: 0, in_progress: 0, roadmap: 0, available: 0 } }, null, 2));
365
+ } else {
366
+ console.log('No specs found.');
367
+ }
368
+ return;
369
+ }
370
+
371
+ // Index all specs by id for lookups
372
+ const specById = new Map<string, typeof allSpecs[0]>();
373
+ for (const spec of allSpecs) {
374
+ specById.set(spec.id, spec);
375
+ }
376
+
377
+ // Compute availability: roadmap + all deps completed (against full unfiltered set)
378
+ // Dangling deps (unknown spec ids) are treated as satisfied
379
+ function isAvailable(spec: typeof allSpecs[0]): boolean {
380
+ if (spec.status !== 'roadmap') return false;
381
+ return spec.dependencies.every((depId) => {
382
+ const dep = specById.get(depId);
383
+ return !dep || dep.status === 'completed';
384
+ });
385
+ }
386
+
387
+ const availableIds = allSpecs.filter(isAvailable).map((s) => s.id).sort();
388
+
389
+ // Build display set (filtered or full)
390
+ let displaySpecs = allSpecs;
391
+ if (options.roadmap) {
392
+ displaySpecs = allSpecs.filter((s) => s.status !== 'completed');
393
+ if (displaySpecs.length === 0) {
394
+ if (options.json) {
395
+ console.log(JSON.stringify({ success: true, count: 0, available: availableIds, tree: [], summary: { completed: allSpecs.filter((s) => s.status === 'completed').length, in_progress: 0, roadmap: 0, available: availableIds.length } }, null, 2));
396
+ } else {
397
+ console.log('No roadmap specs found.');
398
+ }
399
+ return;
400
+ }
401
+ }
402
+
403
+ const displayIds = new Set(displaySpecs.map((s) => s.id));
404
+
405
+ // Build parent→children edges and find roots in one pass
406
+ const childrenOf = new Map<string, string[]>();
407
+ const roots: string[] = [];
408
+ for (const spec of displaySpecs) {
409
+ const visibleDeps = spec.dependencies.filter((depId) => depId !== spec.id && displayIds.has(depId));
410
+ if (visibleDeps.length === 0) {
411
+ roots.push(spec.id);
412
+ }
413
+ for (const depId of visibleDeps) {
414
+ if (!childrenOf.has(depId)) childrenOf.set(depId, []);
415
+ childrenOf.get(depId)!.push(spec.id);
416
+ }
417
+ }
418
+
419
+ // Detect orphaned cycles: specs not reachable from roots
420
+ const reachable = new Set<string>();
421
+ function markReachable(id: string): void {
422
+ if (reachable.has(id)) return;
423
+ reachable.add(id);
424
+ for (const childId of childrenOf.get(id) || []) {
425
+ markReachable(childId);
426
+ }
427
+ }
428
+ for (const rootId of roots) {
429
+ markReachable(rootId);
430
+ }
431
+ for (const spec of displaySpecs) {
432
+ if (!reachable.has(spec.id)) {
433
+ roots.push(spec.id);
434
+ markReachable(spec.id);
435
+ }
436
+ }
437
+
438
+ // Sort roots alphabetically
439
+ roots.sort();
440
+
441
+ // Summary counts
442
+ const summary = {
443
+ completed: displaySpecs.filter((s) => s.status === 'completed').length,
444
+ in_progress: displaySpecs.filter((s) => s.status === 'in_progress').length,
445
+ roadmap: displaySpecs.filter((s) => s.status === 'roadmap').length,
446
+ available: availableIds.length,
447
+ };
448
+
449
+ // JSON output
450
+ if (options.json) {
451
+ interface TreeNode {
452
+ id: string;
453
+ domain_name: string;
454
+ status: string;
455
+ available: boolean;
456
+ children: TreeNode[];
457
+ }
458
+
459
+ function buildJsonTree(id: string, path: Set<string>): TreeNode | null {
460
+ const spec = specById.get(id);
461
+ if (!spec) return null;
462
+ const node: TreeNode = {
463
+ id: spec.id,
464
+ domain_name: spec.domain_name,
465
+ status: spec.status,
466
+ available: availableIds.includes(spec.id),
467
+ children: [],
468
+ };
469
+ if (path.has(id)) return node; // cycle: return node without children
470
+ path.add(id);
471
+ const children = (childrenOf.get(id) || []).slice().sort();
472
+ for (const childId of children) {
473
+ const child = buildJsonTree(childId, path);
474
+ if (child) node.children.push(child);
475
+ }
476
+ path.delete(id);
477
+ return node;
478
+ }
479
+
480
+ const tree: TreeNode[] = [];
481
+ for (const rootId of roots) {
482
+ const node = buildJsonTree(rootId, new Set());
483
+ if (node) tree.push(node);
484
+ }
485
+
486
+ console.log(JSON.stringify({
487
+ success: true,
488
+ count: displaySpecs.length,
489
+ available: availableIds,
490
+ tree,
491
+ summary,
492
+ }, null, 2));
493
+ return;
494
+ }
495
+
496
+ // Human-readable tree output
497
+ const lines: string[] = [];
498
+ lines.push(`Dependency Tree (${displaySpecs.length} specs):\n`);
499
+
500
+ function statusIcon(status: string): string {
501
+ if (status === 'completed') return '[x]';
502
+ if (status === 'in_progress') return '[>]';
503
+ return '[ ]';
504
+ }
505
+
506
+ function renderNode(id: string, prefix: string, isLast: boolean, isRoot: boolean, pathSet: Set<string>): void {
507
+ const spec = specById.get(id);
508
+ if (!spec) return;
509
+
510
+ const connector = isRoot ? '' : isLast ? '└── ' : '├── ';
511
+ const icon = statusIcon(spec.status);
512
+ const avail = availableIds.includes(spec.id) ? ' ★' : '';
513
+
514
+ if (pathSet.has(id)) {
515
+ lines.push(`${prefix}${connector}${icon} ${spec.id} (${spec.domain_name}) [cycle]`);
516
+ return;
517
+ }
518
+
519
+ lines.push(`${prefix}${connector}${icon} ${spec.id} (${spec.domain_name})${avail}`);
520
+
521
+ const children = (childrenOf.get(id) || []).slice().sort();
522
+ if (children.length === 0) return;
523
+
524
+ pathSet.add(id);
525
+ const childPrefix = isRoot ? prefix : prefix + (isLast ? ' ' : '│ ');
526
+ for (let i = 0; i < children.length; i++) {
527
+ renderNode(children[i], childPrefix, i === children.length - 1, false, pathSet);
528
+ }
529
+ pathSet.delete(id);
530
+ }
531
+
532
+ for (const rootId of roots) {
533
+ renderNode(rootId, '', true, true, new Set());
534
+ }
535
+
536
+ lines.push('');
537
+ lines.push('Legend: [x] completed [>] in_progress [ ] roadmap ★ available');
538
+
539
+ console.log(lines.join('\n'));
540
+ });
541
+
352
542
  // ah specs create <path>
353
543
  specs
354
544
  .command('create <path>')
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "all-hands-cli",
3
- "version": "0.1.3",
3
+ "version": "0.1.5",
4
4
  "description": "Agentic harness for model-first software development",
5
5
  "type": "module",
6
6
  "bin": {
package/target-lines.json CHANGED
@@ -11,8 +11,8 @@
11
11
  ".allhands-*.backup/",
12
12
  ".claude-*.backup/",
13
13
  ".allhands/harness/.cache/",
14
- "test_plan.md",
15
- "problems.md"
14
+ ".allhands/harness/.knowledge/",
15
+ ".allhands/harness/.allhands/"
16
16
  ],
17
17
  ".tldrignore": [
18
18
  ".reposearch/",