@groundctl/cli 0.3.2 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/index.js +433 -214
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -5,9 +5,9 @@ import { Command } from "commander";
5
5
  import { createRequire } from "module";
6
6
 
7
7
  // src/commands/init.ts
8
- import { existsSync as existsSync3, mkdirSync as mkdirSync2, writeFileSync as writeFileSync2, chmodSync, readFileSync as readFileSync3, appendFileSync } from "fs";
8
+ import { existsSync as existsSync3, mkdirSync as mkdirSync2, writeFileSync as writeFileSync3, chmodSync, readFileSync as readFileSync3, appendFileSync } from "fs";
9
9
  import { join as join3 } from "path";
10
- import chalk from "chalk";
10
+ import chalk2 from "chalk";
11
11
 
12
12
  // src/storage/db.ts
13
13
  import initSqlJs from "sql.js";
@@ -352,8 +352,6 @@ function generateAgentsMd(db, projectName) {
352
352
 
353
353
  // src/ingest/git-import.ts
354
354
  import { execSync } from "child_process";
355
- import { existsSync as existsSync2, readFileSync as readFileSync2 } from "fs";
356
- import { join as join2 } from "path";
357
355
  function run(cmd, cwd) {
358
356
  try {
359
357
  return execSync(cmd, { cwd, encoding: "utf-8", stdio: ["ignore", "pipe", "ignore"] });
@@ -409,67 +407,10 @@ function parseGitLog(cwd) {
409
407
  }
410
408
  return commits.reverse();
411
409
  }
412
- function parseProjectStateMd(content) {
413
- const features = [];
414
- const lines = content.split("\n");
415
- let section = "";
416
- for (const line of lines) {
417
- const trimmed = line.trim();
418
- if (trimmed.startsWith("## ")) {
419
- section = trimmed.toLowerCase();
420
- continue;
421
- }
422
- if (section.includes("decision") || section.includes("session") || section.includes("debt") || section.includes("note")) continue;
423
- if (!trimmed.startsWith("- ") && !trimmed.startsWith("* ")) continue;
424
- const item = trimmed.slice(2).trim();
425
- if (!item || item.length < 3) continue;
426
- const name = item.split("(")[0].split("\u2192")[0].split("\u2014")[0].trim();
427
- if (!name || name.length < 3 || name.length > 80) continue;
428
- if (/^\d{4}-\d{2}-\d{2}/.test(name)) continue;
429
- if (name.split(" ").length > 8) continue;
430
- let status = "pending";
431
- let priority = "medium";
432
- if (section.includes("built") || section.includes("done") || section.includes("complete")) {
433
- status = "done";
434
- } else if (section.includes("claimed") || section.includes("in progress") || section.includes("current")) {
435
- status = "in_progress";
436
- } else if (section.includes("available") || section.includes("next")) {
437
- status = "pending";
438
- } else if (section.includes("blocked")) {
439
- status = "blocked";
440
- }
441
- if (/priority:\s*critical|critical\)/i.test(item)) priority = "critical";
442
- else if (/priority:\s*high|high\)/i.test(item)) priority = "high";
443
- else if (/priority:\s*low|low\)/i.test(item)) priority = "low";
444
- features.push({ name, status, priority });
445
- }
446
- return features;
447
- }
448
- function featureIdFromName(name) {
449
- return name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "").slice(0, 60);
450
- }
451
410
  function importFromGit(db, projectPath) {
452
411
  let sessionsCreated = 0;
453
- let featuresImported = 0;
454
- const psMdPath = join2(projectPath, "PROJECT_STATE.md");
455
- if (existsSync2(psMdPath)) {
456
- const content = readFileSync2(psMdPath, "utf-8");
457
- const features = parseProjectStateMd(content);
458
- for (const feat of features) {
459
- const id = featureIdFromName(feat.name);
460
- if (!id) continue;
461
- const exists = queryOne(db, "SELECT id FROM features WHERE id = ?", [id]);
462
- if (!exists) {
463
- db.run(
464
- "INSERT INTO features (id, name, status, priority) VALUES (?, ?, ?, ?)",
465
- [id, feat.name, feat.status, feat.priority]
466
- );
467
- featuresImported++;
468
- }
469
- }
470
- }
471
412
  const commits = parseGitLog(projectPath);
472
- if (commits.length === 0) return { sessionsCreated, featuresImported };
413
+ if (commits.length === 0) return { sessionsCreated };
473
414
  const SESSION_GAP_MS = 4 * 60 * 60 * 1e3;
474
415
  const sessions = [];
475
416
  let currentSession = [];
@@ -523,7 +464,286 @@ function importFromGit(db, projectPath) {
523
464
  }
524
465
  }
525
466
  saveDb();
526
- return { sessionsCreated, featuresImported };
467
+ return { sessionsCreated };
468
+ }
469
+
470
+ // src/ingest/feature-detector.ts
471
+ import { existsSync as existsSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "fs";
472
+ import { join as join2 } from "path";
473
+ import { tmpdir } from "os";
474
+ import { execSync as execSync2 } from "child_process";
475
+ import { request as httpsRequest } from "https";
476
+ import { createInterface } from "readline";
477
+ import chalk from "chalk";
478
+ function run2(cmd, cwd) {
479
+ try {
480
+ return execSync2(cmd, { cwd, encoding: "utf-8", stdio: ["ignore", "pipe", "ignore"] });
481
+ } catch {
482
+ return "";
483
+ }
484
+ }
485
+ function collectContext(projectPath) {
486
+ const parts = [];
487
+ const gitLog = run2("git log --oneline --no-merges", projectPath);
488
+ if (gitLog.trim()) {
489
+ const lines = gitLog.trim().split("\n").slice(0, 150).join("\n");
490
+ parts.push(`## Git history (${lines.split("\n").length} commits)
491
+ ${lines}`);
492
+ }
493
+ const diffStat = run2("git log --stat --no-merges --oneline -30", projectPath);
494
+ if (diffStat.trim()) {
495
+ parts.push(`## Recent commit file changes
496
+ ${diffStat.trim().slice(0, 3e3)}`);
497
+ }
498
+ const find = run2(
499
+ [
500
+ "find . -type f",
501
+ "-not -path '*/node_modules/*'",
502
+ "-not -path '*/.git/*'",
503
+ "-not -path '*/dist/*'",
504
+ "-not -path '*/.groundctl/*'",
505
+ "-not -path '*/build/*'",
506
+ "-not -path '*/coverage/*'",
507
+ "-not -path '*/.venv/*'",
508
+ "-not -path '*/__pycache__/*'",
509
+ "-not -path '*/.pytest_cache/*'",
510
+ "-not -path '*/vendor/*'",
511
+ "-not -path '*/.next/*'",
512
+ "-not -name '*.lock'",
513
+ "-not -name '*.log'",
514
+ "-not -name '*.pyc'",
515
+ "| sort | head -120"
516
+ ].join(" "),
517
+ projectPath
518
+ );
519
+ if (find.trim()) {
520
+ parts.push(`## Project file structure
521
+ ${find.trim()}`);
522
+ }
523
+ const readmePath = join2(projectPath, "README.md");
524
+ if (existsSync2(readmePath)) {
525
+ const readme = readFileSync2(readmePath, "utf-8").slice(0, 3e3);
526
+ parts.push(`## README.md
527
+ ${readme}`);
528
+ }
529
+ const psPath = join2(projectPath, "PROJECT_STATE.md");
530
+ if (existsSync2(psPath)) {
531
+ const ps = readFileSync2(psPath, "utf-8").slice(0, 2e3);
532
+ parts.push(`## Existing PROJECT_STATE.md
533
+ ${ps}`);
534
+ }
535
+ return parts.join("\n\n");
536
+ }
537
+ var SYSTEM_PROMPT = "You are a product analyst. Analyze this project and identify the main product features.";
538
+ var USER_TEMPLATE = (context) => `Based on this git history and project structure, identify the product features with their status and priority.
539
+
540
+ Rules:
541
+ - Features are functional capabilities, not technical tasks
542
+ - Maximum 12 features
543
+ - status: "done" if all related commits are old and nothing is open, otherwise "open"
544
+ - priority: critical/high/medium/low
545
+ - name: short, kebab-case, human-readable (e.g. "user-auth", "data-pipeline")
546
+ - description: one sentence, what the feature does for the user
547
+
548
+ Respond ONLY with valid JSON, no markdown, no explanation:
549
+ {"features":[{"name":"...","status":"done","priority":"high","description":"..."}]}
550
+
551
+ Project context:
552
+ ${context}`;
553
+ function httpsPost(opts) {
554
+ return new Promise((resolve3, reject) => {
555
+ const body = JSON.stringify({
556
+ model: opts.model,
557
+ max_tokens: opts.maxTokens ?? 1024,
558
+ system: opts.system,
559
+ messages: [{ role: "user", content: opts.userMessage }]
560
+ });
561
+ const req = httpsRequest(
562
+ {
563
+ hostname: "api.anthropic.com",
564
+ path: "/v1/messages",
565
+ method: "POST",
566
+ headers: {
567
+ "x-api-key": opts.apiKey,
568
+ "anthropic-version": "2023-06-01",
569
+ "content-type": "application/json",
570
+ "content-length": Buffer.byteLength(body)
571
+ }
572
+ },
573
+ (res) => {
574
+ let data = "";
575
+ res.on("data", (chunk) => {
576
+ data += chunk.toString();
577
+ });
578
+ res.on("end", () => {
579
+ resolve3(data);
580
+ });
581
+ }
582
+ );
583
+ req.on("error", reject);
584
+ req.write(body);
585
+ req.end();
586
+ });
587
+ }
588
+ function extractText(raw) {
589
+ const json = JSON.parse(raw);
590
+ if (json.error) throw new Error(`API error: ${json.error.message}`);
591
+ const block = (json.content ?? []).find((b) => b.type === "text");
592
+ if (!block) throw new Error("No text block in API response");
593
+ return block.text;
594
+ }
595
+ function parseFeatureJson(text) {
596
+ const stripped = text.replace(/^```[^\n]*\n?/, "").replace(/\n?```$/, "").trim();
597
+ let obj;
598
+ try {
599
+ obj = JSON.parse(stripped);
600
+ } catch {
601
+ const match = stripped.match(/\{[\s\S]*\}/);
602
+ if (!match) throw new Error("Could not parse JSON from model response");
603
+ obj = JSON.parse(match[0]);
604
+ }
605
+ if (!Array.isArray(obj.features)) throw new Error("Response missing 'features' array");
606
+ return obj.features.map((f) => ({
607
+ name: String(f.name ?? "").toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, ""),
608
+ status: f.status === "done" ? "done" : "open",
609
+ priority: ["critical", "high", "medium", "low"].includes(f.priority) ? f.priority : "medium",
610
+ description: String(f.description ?? "").slice(0, 120)
611
+ })).filter((f) => f.name.length >= 2);
612
+ }
613
+ async function callClaude(apiKey, context) {
614
+ const raw = await httpsPost({
615
+ apiKey,
616
+ model: "claude-haiku-4-5-20251001",
617
+ system: SYSTEM_PROMPT,
618
+ userMessage: USER_TEMPLATE(context),
619
+ maxTokens: 1024
620
+ });
621
+ const text = extractText(raw);
622
+ return parseFeatureJson(text);
623
+ }
624
+ function renderFeatureList(features) {
625
+ console.log(chalk.bold(`
626
+ Detected ${features.length} features:
627
+ `));
628
+ for (const f of features) {
629
+ const statusIcon = f.status === "done" ? chalk.green("\u2713") : chalk.gray("\u25CB");
630
+ const prioColor = f.priority === "critical" || f.priority === "high" ? chalk.red : chalk.gray;
631
+ console.log(
632
+ ` ${statusIcon} ${chalk.white(f.name.padEnd(28))}` + prioColor(`(${f.priority}, ${f.status})`.padEnd(18)) + chalk.gray(f.description)
633
+ );
634
+ }
635
+ console.log("");
636
+ }
637
+ function readLine(prompt) {
638
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
639
+ return new Promise((resolve3) => {
640
+ rl.question(prompt, (answer) => {
641
+ rl.close();
642
+ resolve3(answer.trim().toLowerCase());
643
+ });
644
+ });
645
+ }
646
+ async function editInEditor(features) {
647
+ const tmpPath = join2(tmpdir(), `groundctl-features-${Date.now()}.json`);
648
+ writeFileSync2(tmpPath, JSON.stringify({ features }, null, 2), "utf-8");
649
+ const editor = process.env.EDITOR ?? process.env.VISUAL ?? "vi";
650
+ try {
651
+ execSync2(`${editor} "${tmpPath}"`, { stdio: "inherit" });
652
+ } catch {
653
+ console.log(chalk.red(" Editor exited with error \u2014 using original features."));
654
+ return features;
655
+ }
656
+ try {
657
+ const edited = readFileSync2(tmpPath, "utf-8");
658
+ return parseFeatureJson(edited);
659
+ } catch (err) {
660
+ console.log(chalk.red(` Could not parse edited JSON: ${err.message}`));
661
+ return null;
662
+ }
663
+ }
664
+ function importFeatures(db, features) {
665
+ db.run(
666
+ `DELETE FROM features
667
+ WHERE id NOT IN (SELECT DISTINCT feature_id FROM claims)
668
+ AND status = 'pending'`
669
+ );
670
+ for (const f of features) {
671
+ const id = f.name;
672
+ const status = f.status === "done" ? "done" : "pending";
673
+ const exists = queryOne(db, "SELECT id FROM features WHERE id = ?", [id]);
674
+ if (!exists) {
675
+ db.run(
676
+ "INSERT INTO features (id, name, status, priority, description) VALUES (?, ?, ?, ?, ?)",
677
+ [id, f.name, status, f.priority, f.description]
678
+ );
679
+ } else {
680
+ db.run(
681
+ `UPDATE features
682
+ SET description = ?, priority = ?, updated_at = datetime('now')
683
+ WHERE id = ?`,
684
+ [f.description, f.priority, id]
685
+ );
686
+ }
687
+ }
688
+ saveDb();
689
+ }
690
+ async function detectAndImportFeatures(db, projectPath) {
691
+ const apiKey = process.env.ANTHROPIC_API_KEY;
692
+ if (!apiKey) {
693
+ console.log(chalk.yellow(
694
+ "\n Smart feature detection disabled \u2014 ANTHROPIC_API_KEY not set."
695
+ ));
696
+ console.log(chalk.gray(" To enable:"));
697
+ console.log(chalk.gray(" export ANTHROPIC_API_KEY=sk-ant-..."));
698
+ console.log(chalk.gray(
699
+ "\n Or add features manually:\n groundctl add feature -n 'my-feature'\n"
700
+ ));
701
+ return false;
702
+ }
703
+ console.log(chalk.gray(" Collecting project context..."));
704
+ const context = collectContext(projectPath);
705
+ console.log(chalk.gray(" Asking Claude to detect features..."));
706
+ let features;
707
+ try {
708
+ features = await callClaude(apiKey, context);
709
+ } catch (err) {
710
+ console.log(chalk.red(` \u2717 Feature detection failed: ${err.message}`));
711
+ console.log(chalk.gray(" Add features manually with: groundctl add feature -n 'my-feature'\n"));
712
+ return false;
713
+ }
714
+ if (features.length === 0) {
715
+ console.log(chalk.yellow(" No features detected \u2014 add them manually.\n"));
716
+ return false;
717
+ }
718
+ renderFeatureList(features);
719
+ let pending = features;
720
+ while (true) {
721
+ const answer = await readLine(
722
+ chalk.bold(" Import these features? ") + chalk.gray("[y/n/edit] ") + ""
723
+ );
724
+ if (answer === "y" || answer === "yes") {
725
+ importFeatures(db, pending);
726
+ console.log(chalk.green(`
727
+ \u2713 ${pending.length} features imported
728
+ `));
729
+ return true;
730
+ }
731
+ if (answer === "n" || answer === "no") {
732
+ console.log(chalk.gray(" Skipped \u2014 no features imported.\n"));
733
+ return false;
734
+ }
735
+ if (answer === "e" || answer === "edit") {
736
+ const edited = await editInEditor(pending);
737
+ if (edited && edited.length > 0) {
738
+ pending = edited;
739
+ renderFeatureList(pending);
740
+ } else {
741
+ console.log(chalk.yellow(" No valid features after edit \u2014 try again.\n"));
742
+ }
743
+ continue;
744
+ }
745
+ console.log(chalk.gray(" Please answer y, n, or edit."));
746
+ }
527
747
  }
528
748
 
529
749
  // src/commands/init.ts
@@ -563,53 +783,52 @@ echo "--- groundctl: Product state updated ---"
563
783
  async function initCommand(options) {
564
784
  const cwd = process.cwd();
565
785
  const projectName = cwd.split("/").pop() ?? "unknown";
566
- console.log(chalk.bold(`
786
+ console.log(chalk2.bold(`
567
787
  groundctl init \u2014 ${projectName}
568
788
  `));
569
- console.log(chalk.gray(" Creating SQLite database..."));
789
+ console.log(chalk2.gray(" Creating SQLite database..."));
570
790
  const db = await openDb();
571
791
  if (options.importFromGit) {
572
792
  const isGitRepo = existsSync3(join3(cwd, ".git"));
573
793
  if (!isGitRepo) {
574
- console.log(chalk.yellow(" \u26A0 Not a git repo \u2014 skipping --import-from-git"));
794
+ console.log(chalk2.yellow(" \u26A0 Not a git repo \u2014 skipping --import-from-git"));
575
795
  } else {
576
- console.log(chalk.gray(" Importing from git history..."));
796
+ console.log(chalk2.gray(" Importing sessions from git history..."));
577
797
  const result = importFromGit(db, cwd);
578
798
  console.log(
579
- chalk.green(
580
- ` \u2713 Git import: ${result.sessionsCreated} sessions, ${result.featuresImported} features`
581
- )
799
+ chalk2.green(` \u2713 Git import: ${result.sessionsCreated} sessions`)
582
800
  );
801
+ await detectAndImportFeatures(db, cwd);
583
802
  }
584
803
  }
585
804
  const projectState = generateProjectState(db, projectName);
586
805
  const agentsMd = generateAgentsMd(db, projectName);
587
806
  closeDb();
588
- console.log(chalk.green(" \u2713 Database ready"));
807
+ console.log(chalk2.green(" \u2713 Database ready"));
589
808
  const claudeHooksDir = join3(cwd, ".claude", "hooks");
590
809
  if (!existsSync3(claudeHooksDir)) {
591
810
  mkdirSync2(claudeHooksDir, { recursive: true });
592
811
  }
593
- writeFileSync2(join3(claudeHooksDir, "pre-session.sh"), PRE_SESSION_HOOK);
812
+ writeFileSync3(join3(claudeHooksDir, "pre-session.sh"), PRE_SESSION_HOOK);
594
813
  chmodSync(join3(claudeHooksDir, "pre-session.sh"), 493);
595
- writeFileSync2(join3(claudeHooksDir, "post-session.sh"), POST_SESSION_HOOK);
814
+ writeFileSync3(join3(claudeHooksDir, "post-session.sh"), POST_SESSION_HOOK);
596
815
  chmodSync(join3(claudeHooksDir, "post-session.sh"), 493);
597
- console.log(chalk.green(" \u2713 Claude Code hooks installed"));
816
+ console.log(chalk2.green(" \u2713 Claude Code hooks installed"));
598
817
  const codexHooksDir = join3(cwd, ".codex", "hooks");
599
818
  if (!existsSync3(codexHooksDir)) {
600
819
  mkdirSync2(codexHooksDir, { recursive: true });
601
820
  }
602
821
  const codexPre = PRE_SESSION_HOOK.replace("Claude Code", "Codex");
603
822
  const codexPost = POST_SESSION_HOOK.replace("Claude Code", "Codex").replace("claude-code", "codex");
604
- writeFileSync2(join3(codexHooksDir, "pre-session.sh"), codexPre);
823
+ writeFileSync3(join3(codexHooksDir, "pre-session.sh"), codexPre);
605
824
  chmodSync(join3(codexHooksDir, "pre-session.sh"), 493);
606
- writeFileSync2(join3(codexHooksDir, "post-session.sh"), codexPost);
825
+ writeFileSync3(join3(codexHooksDir, "post-session.sh"), codexPost);
607
826
  chmodSync(join3(codexHooksDir, "post-session.sh"), 493);
608
- console.log(chalk.green(" \u2713 Codex hooks installed"));
609
- writeFileSync2(join3(cwd, "PROJECT_STATE.md"), projectState);
610
- writeFileSync2(join3(cwd, "AGENTS.md"), agentsMd);
611
- console.log(chalk.green(" \u2713 PROJECT_STATE.md generated"));
612
- console.log(chalk.green(" \u2713 AGENTS.md generated"));
827
+ console.log(chalk2.green(" \u2713 Codex hooks installed"));
828
+ writeFileSync3(join3(cwd, "PROJECT_STATE.md"), projectState);
829
+ writeFileSync3(join3(cwd, "AGENTS.md"), agentsMd);
830
+ console.log(chalk2.green(" \u2713 PROJECT_STATE.md generated"));
831
+ console.log(chalk2.green(" \u2713 AGENTS.md generated"));
613
832
  const gitignorePath = join3(cwd, ".gitignore");
614
833
  const gitignoreEntry = "\n# groundctl local state\n.groundctl/\n";
615
834
  if (existsSync3(gitignorePath)) {
@@ -618,32 +837,32 @@ groundctl init \u2014 ${projectName}
618
837
  appendFileSync(gitignorePath, gitignoreEntry);
619
838
  }
620
839
  }
621
- console.log(chalk.bold.green(`
840
+ console.log(chalk2.bold.green(`
622
841
  \u2713 groundctl initialized for ${projectName}
623
842
  `));
624
843
  if (!options.importFromGit) {
625
- console.log(chalk.gray(" Next steps:"));
626
- console.log(chalk.gray(" groundctl add feature -n 'my-feature' -p high"));
627
- console.log(chalk.gray(" groundctl status"));
628
- console.log(chalk.gray(" groundctl claim my-feature"));
629
- console.log(chalk.gray("\n Or bootstrap from git history:"));
630
- console.log(chalk.gray(" groundctl init --import-from-git\n"));
844
+ console.log(chalk2.gray(" Next steps:"));
845
+ console.log(chalk2.gray(" groundctl add feature -n 'my-feature' -p high"));
846
+ console.log(chalk2.gray(" groundctl status"));
847
+ console.log(chalk2.gray(" groundctl claim my-feature"));
848
+ console.log(chalk2.gray("\n Or bootstrap from git history:"));
849
+ console.log(chalk2.gray(" groundctl init --import-from-git\n"));
631
850
  } else {
632
- console.log(chalk.gray(" Next steps:"));
633
- console.log(chalk.gray(" groundctl status"));
634
- console.log(chalk.gray(" groundctl next\n"));
851
+ console.log(chalk2.gray(" Next steps:"));
852
+ console.log(chalk2.gray(" groundctl status"));
853
+ console.log(chalk2.gray(" groundctl next\n"));
635
854
  }
636
855
  }
637
856
 
638
857
  // src/commands/status.ts
639
- import chalk2 from "chalk";
858
+ import chalk3 from "chalk";
640
859
  var BAR_W = 14;
641
860
  var NAME_W = 22;
642
861
  var PROG_W = 6;
643
862
  function progressBar(done, total, width) {
644
- if (total <= 0) return chalk2.gray("\u2591".repeat(width));
863
+ if (total <= 0) return chalk3.gray("\u2591".repeat(width));
645
864
  const filled = Math.min(width, Math.round(done / total * width));
646
- return chalk2.green("\u2588".repeat(filled)) + chalk2.gray("\u2591".repeat(width - filled));
865
+ return chalk3.green("\u2588".repeat(filled)) + chalk3.gray("\u2591".repeat(width - filled));
647
866
  }
648
867
  function featureBar(status, progressDone, progressTotal) {
649
868
  if (progressTotal != null && progressTotal > 0) {
@@ -655,9 +874,9 @@ function featureBar(status, progressDone, progressTotal) {
655
874
  case "in_progress":
656
875
  return progressBar(1, 2, BAR_W);
657
876
  case "blocked":
658
- return chalk2.red("\u2591".repeat(BAR_W));
877
+ return chalk3.red("\u2591".repeat(BAR_W));
659
878
  default:
660
- return chalk2.gray("\u2591".repeat(BAR_W));
879
+ return chalk3.gray("\u2591".repeat(BAR_W));
661
880
  }
662
881
  }
663
882
  function featureProgress(progressDone, progressTotal) {
@@ -726,10 +945,10 @@ async function statusCommand() {
726
945
  closeDb();
727
946
  console.log("");
728
947
  if (features.length === 0) {
729
- console.log(chalk2.bold(` ${projectName} \u2014 no features tracked yet
948
+ console.log(chalk3.bold(` ${projectName} \u2014 no features tracked yet
730
949
  `));
731
- console.log(chalk2.gray(" Add features with: groundctl add feature -n 'my-feature'"));
732
- console.log(chalk2.gray(" Then run: groundctl status\n"));
950
+ console.log(chalk3.gray(" Add features with: groundctl add feature -n 'my-feature'"));
951
+ console.log(chalk3.gray(" Then run: groundctl status\n"));
733
952
  return;
734
953
  }
735
954
  const total = features.length;
@@ -738,13 +957,13 @@ async function statusCommand() {
738
957
  const blocked = features.filter((f) => f.status === "blocked").length;
739
958
  const pct = Math.round(done / total * 100);
740
959
  console.log(
741
- chalk2.bold(` ${projectName} \u2014 ${pct}% implemented`) + chalk2.gray(` (${sessionCount} session${sessionCount !== 1 ? "s" : ""})`)
960
+ chalk3.bold(` ${projectName} \u2014 ${pct}% implemented`) + chalk3.gray(` (${sessionCount} session${sessionCount !== 1 ? "s" : ""})`)
742
961
  );
743
962
  console.log("");
744
963
  const aggBar = progressBar(done, total, 20);
745
- let aggSuffix = chalk2.white(` ${done}/${total} done`);
746
- if (inProg > 0) aggSuffix += chalk2.yellow(` ${inProg} in progress`);
747
- if (blocked > 0) aggSuffix += chalk2.red(` ${blocked} blocked`);
964
+ let aggSuffix = chalk3.white(` ${done}/${total} done`);
965
+ if (inProg > 0) aggSuffix += chalk3.yellow(` ${inProg} in progress`);
966
+ if (blocked > 0) aggSuffix += chalk3.red(` ${blocked} blocked`);
748
967
  console.log(` Features ${aggBar}${aggSuffix}`);
749
968
  console.log("");
750
969
  const maxNameLen = Math.min(NAME_W, Math.max(...features.map((f) => f.name.length)));
@@ -756,20 +975,20 @@ async function statusCommand() {
756
975
  const isActive = f.status === "in_progress";
757
976
  const isBlocked = f.status === "blocked";
758
977
  const icon = isDone ? "\u2713" : isActive ? "\u25CF" : isBlocked ? "\u2717" : "\u25CB";
759
- const iconChalk = isDone ? chalk2.green : isActive ? chalk2.yellow : isBlocked ? chalk2.red : chalk2.gray;
978
+ const iconChalk = isDone ? chalk3.green : isActive ? chalk3.yellow : isBlocked ? chalk3.red : chalk3.gray;
760
979
  const nameRaw = f.name.slice(0, nameW).padEnd(nameW);
761
- const nameChalk = isDone ? chalk2.dim : isActive ? chalk2.white : isBlocked ? chalk2.red : chalk2.gray;
980
+ const nameChalk = isDone ? chalk3.dim : isActive ? chalk3.white : isBlocked ? chalk3.red : chalk3.gray;
762
981
  const pd = f.progress_done ?? null;
763
982
  const pt = f.progress_total ?? null;
764
983
  const bar2 = featureBar(f.status, pd, pt);
765
984
  const prog = featureProgress(pd, pt).padEnd(PROG_W);
766
985
  const descRaw = f.description ?? "";
767
986
  const descTrunc = descRaw.length > 38 ? descRaw.slice(0, 36) + "\u2026" : descRaw;
768
- const descStr = descTrunc ? chalk2.gray(` ${descTrunc}`) : "";
987
+ const descStr = descTrunc ? chalk3.gray(` ${descTrunc}`) : "";
769
988
  let claimedStr = "";
770
989
  if (isActive && f.claimed_session) {
771
990
  const elapsed = f.claimed_at ? timeSince(f.claimed_at) : "";
772
- claimedStr = chalk2.yellow(` \u2192 ${f.claimed_session}${elapsed ? ` (${elapsed})` : ""}`);
991
+ claimedStr = chalk3.yellow(` \u2192 ${f.claimed_session}${elapsed ? ` (${elapsed})` : ""}`);
773
992
  }
774
993
  console.log(
775
994
  ` ${iconChalk(icon)} ${nameChalk(nameRaw)} ${bar2} ${prog}${descStr}${claimedStr}`
@@ -777,7 +996,7 @@ async function statusCommand() {
777
996
  if (f.items) {
778
997
  const lines = wrapItems(f.items, itemsMaxW);
779
998
  for (const line of lines) {
780
- console.log(chalk2.dim(`${contIndent}${line}`));
999
+ console.log(chalk3.dim(`${contIndent}${line}`));
781
1000
  }
782
1001
  }
783
1002
  }
@@ -785,7 +1004,7 @@ async function statusCommand() {
785
1004
  }
786
1005
 
787
1006
  // src/commands/claim.ts
788
- import chalk3 from "chalk";
1007
+ import chalk4 from "chalk";
789
1008
  import { randomUUID } from "crypto";
790
1009
  function findFeature(db, term) {
791
1010
  return queryOne(
@@ -822,15 +1041,15 @@ async function claimCommand(featureIdOrName, options) {
822
1041
  const db = await openDb();
823
1042
  const feature = findFeature(db, featureIdOrName);
824
1043
  if (!feature) {
825
- console.log(chalk3.red(`
1044
+ console.log(chalk4.red(`
826
1045
  Feature "${featureIdOrName}" not found.
827
1046
  `));
828
- console.log(chalk3.gray(" Add it with: groundctl add feature -n '" + featureIdOrName + "'"));
1047
+ console.log(chalk4.gray(" Add it with: groundctl add feature -n '" + featureIdOrName + "'"));
829
1048
  closeDb();
830
1049
  process.exit(1);
831
1050
  }
832
1051
  if (feature.status === "done") {
833
- console.log(chalk3.yellow(`
1052
+ console.log(chalk4.yellow(`
834
1053
  Feature "${feature.name}" is already done.
835
1054
  `));
836
1055
  closeDb();
@@ -844,7 +1063,7 @@ async function claimCommand(featureIdOrName, options) {
844
1063
  );
845
1064
  if (existingClaim) {
846
1065
  console.log(
847
- chalk3.red(`
1066
+ chalk4.red(`
848
1067
  Feature "${feature.name}" is already claimed by session ${existingClaim.session_id}`)
849
1068
  );
850
1069
  const alternatives = query(
@@ -859,9 +1078,9 @@ async function claimCommand(featureIdOrName, options) {
859
1078
  LIMIT 3`
860
1079
  );
861
1080
  if (alternatives.length > 0) {
862
- console.log(chalk3.gray("\n Available instead:"));
1081
+ console.log(chalk4.gray("\n Available instead:"));
863
1082
  for (const alt of alternatives) {
864
- console.log(chalk3.gray(` \u25CB ${alt.name}`));
1083
+ console.log(chalk4.gray(` \u25CB ${alt.name}`));
865
1084
  }
866
1085
  }
867
1086
  console.log("");
@@ -891,7 +1110,7 @@ async function claimCommand(featureIdOrName, options) {
891
1110
  saveDb();
892
1111
  closeDb();
893
1112
  console.log(
894
- chalk3.green(`
1113
+ chalk4.green(`
895
1114
  \u2713 Claimed "${feature.name}" \u2192 session ${sessionId}
896
1115
  `)
897
1116
  );
@@ -900,7 +1119,7 @@ async function completeCommand(featureIdOrName) {
900
1119
  const db = await openDb();
901
1120
  const feature = findFeature(db, featureIdOrName);
902
1121
  if (!feature) {
903
- console.log(chalk3.red(`
1122
+ console.log(chalk4.red(`
904
1123
  Feature "${featureIdOrName}" not found.
905
1124
  `));
906
1125
  closeDb();
@@ -916,15 +1135,15 @@ async function completeCommand(featureIdOrName) {
916
1135
  );
917
1136
  saveDb();
918
1137
  closeDb();
919
- console.log(chalk3.green(`
1138
+ console.log(chalk4.green(`
920
1139
  \u2713 Completed "${feature.name}"
921
1140
  `));
922
1141
  }
923
1142
 
924
1143
  // src/commands/sync.ts
925
- import { writeFileSync as writeFileSync3 } from "fs";
1144
+ import { writeFileSync as writeFileSync4 } from "fs";
926
1145
  import { join as join4 } from "path";
927
- import chalk4 from "chalk";
1146
+ import chalk5 from "chalk";
928
1147
  async function syncCommand(opts) {
929
1148
  const db = await openDb();
930
1149
  const projectName = process.cwd().split("/").pop() ?? "unknown";
@@ -932,16 +1151,16 @@ async function syncCommand(opts) {
932
1151
  const agentsMd = generateAgentsMd(db, projectName);
933
1152
  closeDb();
934
1153
  const cwd = process.cwd();
935
- writeFileSync3(join4(cwd, "PROJECT_STATE.md"), projectState);
936
- writeFileSync3(join4(cwd, "AGENTS.md"), agentsMd);
1154
+ writeFileSync4(join4(cwd, "PROJECT_STATE.md"), projectState);
1155
+ writeFileSync4(join4(cwd, "AGENTS.md"), agentsMd);
937
1156
  if (!opts?.silent) {
938
- console.log(chalk4.green("\n \u2713 PROJECT_STATE.md regenerated"));
939
- console.log(chalk4.green(" \u2713 AGENTS.md regenerated\n"));
1157
+ console.log(chalk5.green("\n \u2713 PROJECT_STATE.md regenerated"));
1158
+ console.log(chalk5.green(" \u2713 AGENTS.md regenerated\n"));
940
1159
  }
941
1160
  }
942
1161
 
943
1162
  // src/commands/next.ts
944
- import chalk5 from "chalk";
1163
+ import chalk6 from "chalk";
945
1164
  async function nextCommand() {
946
1165
  const db = await openDb();
947
1166
  const available = query(
@@ -961,26 +1180,26 @@ async function nextCommand() {
961
1180
  );
962
1181
  closeDb();
963
1182
  if (available.length === 0) {
964
- console.log(chalk5.yellow("\n No available features to claim.\n"));
1183
+ console.log(chalk6.yellow("\n No available features to claim.\n"));
965
1184
  return;
966
1185
  }
967
- console.log(chalk5.bold("\n Next available features:\n"));
1186
+ console.log(chalk6.bold("\n Next available features:\n"));
968
1187
  for (let i = 0; i < available.length; i++) {
969
1188
  const feat = available[i];
970
- const pColor = feat.priority === "critical" || feat.priority === "high" ? chalk5.red : chalk5.gray;
971
- const marker = i === 0 ? chalk5.green("\u2192") : " ";
1189
+ const pColor = feat.priority === "critical" || feat.priority === "high" ? chalk6.red : chalk6.gray;
1190
+ const marker = i === 0 ? chalk6.green("\u2192") : " ";
972
1191
  console.log(` ${marker} ${feat.name} ${pColor(`(${feat.priority})`)}`);
973
1192
  if (feat.description) {
974
- console.log(chalk5.gray(` ${feat.description}`));
1193
+ console.log(chalk6.gray(` ${feat.description}`));
975
1194
  }
976
1195
  }
977
- console.log(chalk5.gray(`
1196
+ console.log(chalk6.gray(`
978
1197
  Claim with: groundctl claim "${available[0].name}"
979
1198
  `));
980
1199
  }
981
1200
 
982
1201
  // src/commands/log.ts
983
- import chalk6 from "chalk";
1202
+ import chalk7 from "chalk";
984
1203
  async function logCommand(options) {
985
1204
  const db = await openDb();
986
1205
  if (options.session) {
@@ -989,18 +1208,18 @@ async function logCommand(options) {
989
1208
  `%${options.session}%`
990
1209
  ]);
991
1210
  if (!session) {
992
- console.log(chalk6.red(`
1211
+ console.log(chalk7.red(`
993
1212
  Session "${options.session}" not found.
994
1213
  `));
995
1214
  closeDb();
996
1215
  return;
997
1216
  }
998
- console.log(chalk6.bold(`
1217
+ console.log(chalk7.bold(`
999
1218
  Session ${session.id}`));
1000
- console.log(chalk6.gray(` Agent: ${session.agent}`));
1001
- console.log(chalk6.gray(` Started: ${session.started_at}`));
1219
+ console.log(chalk7.gray(` Agent: ${session.agent}`));
1220
+ console.log(chalk7.gray(` Started: ${session.started_at}`));
1002
1221
  if (session.ended_at) {
1003
- console.log(chalk6.gray(` Ended: ${session.ended_at}`));
1222
+ console.log(chalk7.gray(` Ended: ${session.ended_at}`));
1004
1223
  }
1005
1224
  if (session.summary) {
1006
1225
  console.log(`
@@ -1012,11 +1231,11 @@ async function logCommand(options) {
1012
1231
  [session.id]
1013
1232
  );
1014
1233
  if (decisions.length > 0) {
1015
- console.log(chalk6.bold("\n Decisions:"));
1234
+ console.log(chalk7.bold("\n Decisions:"));
1016
1235
  for (const d of decisions) {
1017
1236
  console.log(` \u2022 ${d.description}`);
1018
1237
  if (d.rationale) {
1019
- console.log(chalk6.gray(` ${d.rationale}`));
1238
+ console.log(chalk7.gray(` ${d.rationale}`));
1020
1239
  }
1021
1240
  }
1022
1241
  }
@@ -1026,10 +1245,10 @@ async function logCommand(options) {
1026
1245
  [session.id]
1027
1246
  );
1028
1247
  if (files.length > 0) {
1029
- console.log(chalk6.bold(`
1248
+ console.log(chalk7.bold(`
1030
1249
  Files modified (${files.length}):`));
1031
1250
  for (const f of files) {
1032
- const op = f.operation === "created" ? chalk6.green("+") : f.operation === "deleted" ? chalk6.red("-") : chalk6.yellow("~");
1251
+ const op = f.operation === "created" ? chalk7.green("+") : f.operation === "deleted" ? chalk7.red("-") : chalk7.yellow("~");
1033
1252
  console.log(` ${op} ${f.path} (${f.lines_changed} lines)`);
1034
1253
  }
1035
1254
  }
@@ -1037,18 +1256,18 @@ async function logCommand(options) {
1037
1256
  } else {
1038
1257
  const sessions = query(db, "SELECT * FROM sessions ORDER BY started_at DESC LIMIT 20");
1039
1258
  if (sessions.length === 0) {
1040
- console.log(chalk6.yellow("\n No sessions recorded yet.\n"));
1259
+ console.log(chalk7.yellow("\n No sessions recorded yet.\n"));
1041
1260
  closeDb();
1042
1261
  return;
1043
1262
  }
1044
- console.log(chalk6.bold("\n Session timeline:\n"));
1263
+ console.log(chalk7.bold("\n Session timeline:\n"));
1045
1264
  for (const s of sessions) {
1046
- const status = s.ended_at ? chalk6.green("done") : chalk6.yellow("active");
1265
+ const status = s.ended_at ? chalk7.green("done") : chalk7.yellow("active");
1047
1266
  console.log(
1048
- ` ${chalk6.bold(s.id)} ${chalk6.gray(s.started_at)} ${status} ${chalk6.gray(s.agent)}`
1267
+ ` ${chalk7.bold(s.id)} ${chalk7.gray(s.started_at)} ${status} ${chalk7.gray(s.agent)}`
1049
1268
  );
1050
1269
  if (s.summary) {
1051
- console.log(chalk6.gray(` ${s.summary}`));
1270
+ console.log(chalk7.gray(` ${s.summary}`));
1052
1271
  }
1053
1272
  }
1054
1273
  console.log("");
@@ -1057,7 +1276,7 @@ async function logCommand(options) {
1057
1276
  }
1058
1277
 
1059
1278
  // src/commands/add.ts
1060
- import chalk7 from "chalk";
1279
+ import chalk8 from "chalk";
1061
1280
  import { randomUUID as randomUUID2 } from "crypto";
1062
1281
  function parseProgress(s) {
1063
1282
  const m = s.match(/^(\d+)\/(\d+)$/);
@@ -1068,7 +1287,7 @@ async function addCommand(type, options) {
1068
1287
  const db = await openDb();
1069
1288
  if (type === "feature") {
1070
1289
  if (!options.name) {
1071
- console.log(chalk7.red("\n --name is required for features.\n"));
1290
+ console.log(chalk8.red("\n --name is required for features.\n"));
1072
1291
  closeDb();
1073
1292
  process.exit(1);
1074
1293
  }
@@ -1082,7 +1301,7 @@ async function addCommand(type, options) {
1082
1301
  progressDone = p.done;
1083
1302
  progressTotal = p.total;
1084
1303
  } else {
1085
- console.log(chalk7.yellow(` \u26A0 --progress "${options.progress}" ignored (expected N/N format)`));
1304
+ console.log(chalk8.yellow(` \u26A0 --progress "${options.progress}" ignored (expected N/N format)`));
1086
1305
  }
1087
1306
  }
1088
1307
  const items = options.items ? options.items.split(",").map((s) => s.trim()).filter(Boolean).join(",") : null;
@@ -1105,8 +1324,8 @@ async function addCommand(type, options) {
1105
1324
  const extras = [];
1106
1325
  if (progressDone !== null) extras.push(`${progressDone}/${progressTotal}`);
1107
1326
  if (items) extras.push(`${items.split(",").length} items`);
1108
- const suffix = extras.length ? chalk7.gray(` \u2014 ${extras.join(", ")}`) : "";
1109
- console.log(chalk7.green(`
1327
+ const suffix = extras.length ? chalk8.gray(` \u2014 ${extras.join(", ")}`) : "";
1328
+ console.log(chalk8.green(`
1110
1329
  \u2713 Feature added: ${options.name} (${priority})${suffix}
1111
1330
  `));
1112
1331
  } else if (type === "session") {
@@ -1115,11 +1334,11 @@ async function addCommand(type, options) {
1115
1334
  db.run("INSERT INTO sessions (id, agent) VALUES (?, ?)", [id, agent]);
1116
1335
  saveDb();
1117
1336
  closeDb();
1118
- console.log(chalk7.green(`
1337
+ console.log(chalk8.green(`
1119
1338
  \u2713 Session created: ${id} (${agent})
1120
1339
  `));
1121
1340
  } else {
1122
- console.log(chalk7.red(`
1341
+ console.log(chalk8.red(`
1123
1342
  Unknown type "${type}". Use "feature" or "session".
1124
1343
  `));
1125
1344
  closeDb();
@@ -1131,7 +1350,7 @@ async function addCommand(type, options) {
1131
1350
  import { existsSync as existsSync4, readdirSync } from "fs";
1132
1351
  import { join as join5, resolve } from "path";
1133
1352
  import { homedir as homedir2 } from "os";
1134
- import chalk8 from "chalk";
1353
+ import chalk9 from "chalk";
1135
1354
 
1136
1355
  // src/ingest/claude-parser.ts
1137
1356
  import { readFileSync as readFileSync4 } from "fs";
@@ -1401,11 +1620,11 @@ async function ingestCommand(options) {
1401
1620
  transcriptPath = findLatestTranscript(projectPath) ?? void 0;
1402
1621
  }
1403
1622
  if (!transcriptPath || !existsSync4(transcriptPath)) {
1404
- console.log(chalk8.yellow("\n No transcript found. Skipping ingest.\n"));
1623
+ console.log(chalk9.yellow("\n No transcript found. Skipping ingest.\n"));
1405
1624
  if (!options.noSync) await syncCommand();
1406
1625
  return;
1407
1626
  }
1408
- console.log(chalk8.gray(`
1627
+ console.log(chalk9.gray(`
1409
1628
  Parsing transcript: ${transcriptPath.split("/").slice(-2).join("/")}`));
1410
1629
  const parsed = parseTranscript(transcriptPath, options.sessionId ?? "auto", projectPath);
1411
1630
  const db = await openDb();
@@ -1455,16 +1674,16 @@ async function ingestCommand(options) {
1455
1674
  saveDb();
1456
1675
  closeDb();
1457
1676
  console.log(
1458
- chalk8.green(
1677
+ chalk9.green(
1459
1678
  ` \u2713 Ingested session ${sessionId}: ${newFiles} files, ${parsed.commits.length} commits, ${newDecisions} decisions`
1460
1679
  )
1461
1680
  );
1462
1681
  if (parsed.decisions.length > 0 && newDecisions > 0) {
1463
- console.log(chalk8.gray(`
1682
+ console.log(chalk9.gray(`
1464
1683
  Decisions captured:`));
1465
1684
  for (const d of parsed.decisions.slice(0, 5)) {
1466
- const conf = d.confidence === "low" ? chalk8.gray(" (low confidence)") : "";
1467
- console.log(chalk8.gray(` \u2022 ${d.description.slice(0, 80)}${conf}`));
1685
+ const conf = d.confidence === "low" ? chalk9.gray(" (low confidence)") : "";
1686
+ console.log(chalk9.gray(` \u2022 ${d.description.slice(0, 80)}${conf}`));
1468
1687
  }
1469
1688
  }
1470
1689
  if (!options.noSync) {
@@ -1474,9 +1693,9 @@ async function ingestCommand(options) {
1474
1693
  }
1475
1694
 
1476
1695
  // src/commands/report.ts
1477
- import { writeFileSync as writeFileSync4 } from "fs";
1696
+ import { writeFileSync as writeFileSync5 } from "fs";
1478
1697
  import { join as join6 } from "path";
1479
- import chalk9 from "chalk";
1698
+ import chalk10 from "chalk";
1480
1699
  function formatDuration(start, end) {
1481
1700
  if (!end) return "ongoing";
1482
1701
  const startMs = new Date(start).getTime();
@@ -1571,7 +1790,7 @@ async function reportCommand(options) {
1571
1790
  [options.session, `%${options.session}%`]
1572
1791
  );
1573
1792
  if (!s) {
1574
- console.log(chalk9.red(`
1793
+ console.log(chalk10.red(`
1575
1794
  Session "${options.session}" not found.
1576
1795
  `));
1577
1796
  closeDb();
@@ -1584,7 +1803,7 @@ async function reportCommand(options) {
1584
1803
  "SELECT * FROM sessions ORDER BY started_at DESC LIMIT 1"
1585
1804
  );
1586
1805
  if (!s) {
1587
- console.log(chalk9.yellow("\n No sessions found. Run groundctl init first.\n"));
1806
+ console.log(chalk10.yellow("\n No sessions found. Run groundctl init first.\n"));
1588
1807
  closeDb();
1589
1808
  return;
1590
1809
  }
@@ -1626,8 +1845,8 @@ async function reportCommand(options) {
1626
1845
  }
1627
1846
  closeDb();
1628
1847
  const outPath2 = join6(cwd, "SESSION_HISTORY.md");
1629
- writeFileSync4(outPath2, fullReport);
1630
- console.log(chalk9.green(`
1848
+ writeFileSync5(outPath2, fullReport);
1849
+ console.log(chalk10.green(`
1631
1850
  \u2713 SESSION_HISTORY.md written (${sessions.length} sessions)
1632
1851
  `));
1633
1852
  return;
@@ -1653,16 +1872,16 @@ async function reportCommand(options) {
1653
1872
  activeClaims
1654
1873
  );
1655
1874
  const outPath = join6(cwd, "SESSION_REPORT.md");
1656
- writeFileSync4(outPath, report);
1657
- console.log(chalk9.green(`
1875
+ writeFileSync5(outPath, report);
1876
+ console.log(chalk10.green(`
1658
1877
  \u2713 SESSION_REPORT.md written (session ${session.id})
1659
1878
  `));
1660
- console.log(chalk9.gray(` ${files.length} files \xB7 ${decisions.length} arch log entries \xB7 ${completedFeatures.length} features completed`));
1879
+ console.log(chalk10.gray(` ${files.length} files \xB7 ${decisions.length} arch log entries \xB7 ${completedFeatures.length} features completed`));
1661
1880
  console.log("");
1662
1881
  }
1663
1882
 
1664
1883
  // src/commands/health.ts
1665
- import chalk10 from "chalk";
1884
+ import chalk11 from "chalk";
1666
1885
  async function healthCommand() {
1667
1886
  const db = await openDb();
1668
1887
  const projectName = process.cwd().split("/").pop() ?? "unknown";
@@ -1713,32 +1932,32 @@ async function healthCommand() {
1713
1932
  closeDb();
1714
1933
  const totalScore = featureScore + testScore + decisionScore + claimScore + deployScore;
1715
1934
  console.log("");
1716
- console.log(chalk10.bold(` ${projectName} \u2014 Health Score: ${totalScore}/100
1935
+ console.log(chalk11.bold(` ${projectName} \u2014 Health Score: ${totalScore}/100
1717
1936
  `));
1718
- const featureColor = featurePct >= 0.7 ? chalk10.green : featurePct >= 0.4 ? chalk10.yellow : chalk10.red;
1937
+ const featureColor = featurePct >= 0.7 ? chalk11.green : featurePct >= 0.4 ? chalk11.yellow : chalk11.red;
1719
1938
  const featureMark = featurePct >= 0.4 ? "\u2705" : "\u26A0\uFE0F ";
1720
1939
  console.log(
1721
- ` ${featureMark} Features ${String(counts.done).padStart(2)}/${total} complete` + featureColor(` (${Math.round(featurePct * 100)}%)`) + chalk10.gray(` +${featureScore}pts`)
1940
+ ` ${featureMark} Features ${String(counts.done).padStart(2)}/${total} complete` + featureColor(` (${Math.round(featurePct * 100)}%)`) + chalk11.gray(` +${featureScore}pts`)
1722
1941
  );
1723
1942
  const testMark = testFiles > 0 ? "\u2705" : "\u26A0\uFE0F ";
1724
- const testColor = testFiles > 0 ? chalk10.green : chalk10.red;
1943
+ const testColor = testFiles > 0 ? chalk11.green : chalk11.red;
1725
1944
  console.log(
1726
- ` ${testMark} Tests ${testColor(String(testFiles) + " test files")}` + (testFiles === 0 ? chalk10.red(" (-20pts)") : chalk10.gray(` +${testScore}pts`))
1945
+ ` ${testMark} Tests ${testColor(String(testFiles) + " test files")}` + (testFiles === 0 ? chalk11.red(" (-20pts)") : chalk11.gray(` +${testScore}pts`))
1727
1946
  );
1728
1947
  const decMark = decisionCount > 0 ? "\u2705" : "\u26A0\uFE0F ";
1729
- const decColor = decisionCount > 0 ? chalk10.green : chalk10.yellow;
1948
+ const decColor = decisionCount > 0 ? chalk11.green : chalk11.yellow;
1730
1949
  console.log(
1731
- ` ${decMark} Arch log ${decColor(decisionCount + " entries")}` + chalk10.gray(` +${decisionScore}pts`)
1950
+ ` ${decMark} Arch log ${decColor(decisionCount + " entries")}` + chalk11.gray(` +${decisionScore}pts`)
1732
1951
  );
1733
1952
  const claimMark = staleClaims === 0 ? "\u2705" : "\u26A0\uFE0F ";
1734
- const claimColor = staleClaims === 0 ? chalk10.green : chalk10.red;
1953
+ const claimColor = staleClaims === 0 ? chalk11.green : chalk11.red;
1735
1954
  console.log(
1736
- ` ${claimMark} Claims ${claimColor(staleClaims > 0 ? staleClaims + " stale (>24h)" : "0 stale")}` + chalk10.gray(` +${claimScore}pts`)
1955
+ ` ${claimMark} Claims ${claimColor(staleClaims > 0 ? staleClaims + " stale (>24h)" : "0 stale")}` + chalk11.gray(` +${claimScore}pts`)
1737
1956
  );
1738
1957
  const deployMark = deployScore > 0 ? "\u2705" : "\u26A0\uFE0F ";
1739
- const deployLabel = deployScore > 0 ? chalk10.green("detected") : chalk10.gray("not detected");
1958
+ const deployLabel = deployScore > 0 ? chalk11.green("detected") : chalk11.gray("not detected");
1740
1959
  console.log(
1741
- ` ${deployMark} Deploy ${deployLabel}` + (deployScore > 0 ? chalk10.gray(` +${deployScore}pts`) : chalk10.gray(" +0pts"))
1960
+ ` ${deployMark} Deploy ${deployLabel}` + (deployScore > 0 ? chalk11.gray(` +${deployScore}pts`) : chalk11.gray(" +0pts"))
1742
1961
  );
1743
1962
  console.log("");
1744
1963
  const recommendations = [];
@@ -1747,9 +1966,9 @@ async function healthCommand() {
1747
1966
  if (decisionCount === 0) recommendations.push("Log architecture decisions during sessions so agents understand the why.");
1748
1967
  if (featurePct < 0.5 && total > 0) recommendations.push(`${counts.pending} features pending \u2014 run groundctl next to pick one.`);
1749
1968
  if (recommendations.length > 0) {
1750
- console.log(chalk10.bold(" Recommendations:"));
1969
+ console.log(chalk11.bold(" Recommendations:"));
1751
1970
  for (const r of recommendations) {
1752
- console.log(chalk10.yellow(` \u2192 ${r}`));
1971
+ console.log(chalk11.yellow(` \u2192 ${r}`));
1753
1972
  }
1754
1973
  console.log("");
1755
1974
  }
@@ -1760,7 +1979,7 @@ import { createServer } from "http";
1760
1979
  import { existsSync as existsSync5, readFileSync as readFileSync5 } from "fs";
1761
1980
  import { join as join7, dirname as dirname2 } from "path";
1762
1981
  import { exec } from "child_process";
1763
- import chalk11 from "chalk";
1982
+ import chalk12 from "chalk";
1764
1983
  import initSqlJs2 from "sql.js";
1765
1984
  function findDbPath(startDir = process.cwd()) {
1766
1985
  let dir = startDir;
@@ -1923,9 +2142,9 @@ async function dashboardCommand(options) {
1923
2142
  }
1924
2143
  });
1925
2144
  server.listen(port, "127.0.0.1", () => {
1926
- console.log(chalk11.bold(`
1927
- groundctl dashboard \u2192 `) + chalk11.blue(`http://localhost:${port}`) + "\n");
1928
- console.log(chalk11.gray(" Auto-refreshes every 10s. Press Ctrl+C to stop.\n"));
2145
+ console.log(chalk12.bold(`
2146
+ groundctl dashboard \u2192 `) + chalk12.blue(`http://localhost:${port}`) + "\n");
2147
+ console.log(chalk12.gray(" Auto-refreshes every 10s. Press Ctrl+C to stop.\n"));
1929
2148
  exec(`open http://localhost:${port} 2>/dev/null || xdg-open http://localhost:${port} 2>/dev/null || true`);
1930
2149
  });
1931
2150
  await new Promise((_, reject) => {
@@ -1938,7 +2157,7 @@ import {
1938
2157
  existsSync as existsSync6,
1939
2158
  readdirSync as readdirSync2,
1940
2159
  statSync,
1941
- writeFileSync as writeFileSync5,
2160
+ writeFileSync as writeFileSync6,
1942
2161
  readFileSync as readFileSync6,
1943
2162
  mkdirSync as mkdirSync3,
1944
2163
  watch as fsWatch
@@ -1946,7 +2165,7 @@ import {
1946
2165
  import { join as join8, resolve as resolve2 } from "path";
1947
2166
  import { homedir as homedir3 } from "os";
1948
2167
  import { spawn } from "child_process";
1949
- import chalk12 from "chalk";
2168
+ import chalk13 from "chalk";
1950
2169
  var DEBOUNCE_MS = 8e3;
1951
2170
  var DIR_POLL_MS = 5e3;
1952
2171
  function claudeEncode2(p) {
@@ -1978,7 +2197,7 @@ function fileSize(p) {
1978
2197
  function writePidFile(groundctlDir, pid) {
1979
2198
  try {
1980
2199
  mkdirSync3(groundctlDir, { recursive: true });
1981
- writeFileSync5(join8(groundctlDir, "watch.pid"), String(pid), "utf8");
2200
+ writeFileSync6(join8(groundctlDir, "watch.pid"), String(pid), "utf8");
1982
2201
  } catch {
1983
2202
  }
1984
2203
  }
@@ -2001,8 +2220,8 @@ function processAlive(pid) {
2001
2220
  async function runIngest(transcriptPath, projectPath) {
2002
2221
  const filename = transcriptPath.split("/").slice(-2).join("/");
2003
2222
  console.log(
2004
- chalk12.gray(`
2005
- [${(/* @__PURE__ */ new Date()).toLocaleTimeString()}] `) + chalk12.cyan(`Transcript stable \u2192 ingesting ${filename}`)
2223
+ chalk13.gray(`
2224
+ [${(/* @__PURE__ */ new Date()).toLocaleTimeString()}] `) + chalk13.cyan(`Transcript stable \u2192 ingesting ${filename}`)
2006
2225
  );
2007
2226
  try {
2008
2227
  const parsed = parseTranscript(transcriptPath, "auto", projectPath);
@@ -2058,11 +2277,11 @@ async function runIngest(transcriptPath, projectPath) {
2058
2277
  if (parsed.commits.length > 0) parts.push(`${parsed.commits.length} commit${parsed.commits.length !== 1 ? "s" : ""}`);
2059
2278
  if (newDecisions > 0) parts.push(`${newDecisions} decision${newDecisions !== 1 ? "s" : ""} captured`);
2060
2279
  const summary = parts.length > 0 ? parts.join(", ") : "no new data";
2061
- console.log(chalk12.green(` \u2713 Session ingested \u2014 ${summary}`));
2280
+ console.log(chalk13.green(` \u2713 Session ingested \u2014 ${summary}`));
2062
2281
  await syncCommand({ silent: true });
2063
- console.log(chalk12.gray(" \u21B3 PROJECT_STATE.md + AGENTS.md updated"));
2282
+ console.log(chalk13.gray(" \u21B3 PROJECT_STATE.md + AGENTS.md updated"));
2064
2283
  } catch (err) {
2065
- console.log(chalk12.red(` \u2717 Ingest failed: ${err.message}`));
2284
+ console.log(chalk13.red(` \u2717 Ingest failed: ${err.message}`));
2066
2285
  }
2067
2286
  }
2068
2287
  function startWatcher(transcriptDir, projectPath) {
@@ -2110,12 +2329,12 @@ function startWatcher(transcriptDir, projectPath) {
2110
2329
  schedule(fp);
2111
2330
  }
2112
2331
  });
2113
- console.log(chalk12.bold("\n groundctl watch") + chalk12.gray(" \u2014 auto-ingest on session end\n"));
2332
+ console.log(chalk13.bold("\n groundctl watch") + chalk13.gray(" \u2014 auto-ingest on session end\n"));
2114
2333
  console.log(
2115
- chalk12.gray(" Watching: ") + chalk12.blue(transcriptDir.replace(homedir3(), "~"))
2334
+ chalk13.gray(" Watching: ") + chalk13.blue(transcriptDir.replace(homedir3(), "~"))
2116
2335
  );
2117
- console.log(chalk12.gray(" Stability threshold: ") + chalk12.white(`${DEBOUNCE_MS / 1e3}s`));
2118
- console.log(chalk12.gray(" Press Ctrl+C to stop.\n"));
2336
+ console.log(chalk13.gray(" Stability threshold: ") + chalk13.white(`${DEBOUNCE_MS / 1e3}s`));
2337
+ console.log(chalk13.gray(" Press Ctrl+C to stop.\n"));
2119
2338
  }
2120
2339
  async function watchCommand(options) {
2121
2340
  const projectPath = options.projectPath ? resolve2(options.projectPath) : process.cwd();
@@ -2128,29 +2347,29 @@ async function watchCommand(options) {
2128
2347
  child.unref();
2129
2348
  const groundctlDir2 = join8(projectPath, ".groundctl");
2130
2349
  writePidFile(groundctlDir2, child.pid);
2131
- console.log(chalk12.green(`
2350
+ console.log(chalk13.green(`
2132
2351
  \u2713 groundctl watch running in background (PID ${child.pid})`));
2133
- console.log(chalk12.gray(` PID saved to .groundctl/watch.pid`));
2134
- console.log(chalk12.gray(` To stop: kill ${child.pid}
2352
+ console.log(chalk13.gray(` PID saved to .groundctl/watch.pid`));
2353
+ console.log(chalk13.gray(` To stop: kill ${child.pid}
2135
2354
  `));
2136
2355
  process.exit(0);
2137
2356
  }
2138
2357
  const groundctlDir = join8(projectPath, ".groundctl");
2139
2358
  const existingPid = readPidFile(groundctlDir);
2140
2359
  if (existingPid && processAlive(existingPid)) {
2141
- console.log(chalk12.yellow(`
2360
+ console.log(chalk13.yellow(`
2142
2361
  \u26A0 A watcher is already running (PID ${existingPid}).`));
2143
- console.log(chalk12.gray(` To stop it: kill ${existingPid}
2362
+ console.log(chalk13.gray(` To stop it: kill ${existingPid}
2144
2363
  `));
2145
2364
  process.exit(1);
2146
2365
  }
2147
2366
  let transcriptDir = findTranscriptDir(projectPath);
2148
2367
  if (!transcriptDir) {
2149
- console.log(chalk12.bold("\n groundctl watch\n"));
2368
+ console.log(chalk13.bold("\n groundctl watch\n"));
2150
2369
  console.log(
2151
- chalk12.yellow(" No Claude Code transcript directory found for this project yet.")
2370
+ chalk13.yellow(" No Claude Code transcript directory found for this project yet.")
2152
2371
  );
2153
- console.log(chalk12.gray(" Waiting for first session to start...\n"));
2372
+ console.log(chalk13.gray(" Waiting for first session to start...\n"));
2154
2373
  await new Promise((resolve3) => {
2155
2374
  const interval = setInterval(() => {
2156
2375
  const dir = findTranscriptDir(projectPath);
@@ -2165,7 +2384,7 @@ async function watchCommand(options) {
2165
2384
  startWatcher(transcriptDir, projectPath);
2166
2385
  await new Promise(() => {
2167
2386
  process.on("SIGINT", () => {
2168
- console.log(chalk12.gray("\n Watcher stopped.\n"));
2387
+ console.log(chalk13.gray("\n Watcher stopped.\n"));
2169
2388
  process.exit(0);
2170
2389
  });
2171
2390
  process.on("SIGTERM", () => {
@@ -2175,7 +2394,7 @@ async function watchCommand(options) {
2175
2394
  }
2176
2395
 
2177
2396
  // src/commands/update.ts
2178
- import chalk13 from "chalk";
2397
+ import chalk14 from "chalk";
2179
2398
  function parseProgress2(s) {
2180
2399
  const m = s.match(/^(\d+)\/(\d+)$/);
2181
2400
  if (!m) return null;
@@ -2183,7 +2402,7 @@ function parseProgress2(s) {
2183
2402
  }
2184
2403
  async function updateCommand(type, nameOrId, options) {
2185
2404
  if (type !== "feature") {
2186
- console.log(chalk13.red(`
2405
+ console.log(chalk14.red(`
2187
2406
  Unknown type "${type}". Use "feature".
2188
2407
  `));
2189
2408
  process.exit(1);
@@ -2199,7 +2418,7 @@ async function updateCommand(type, nameOrId, options) {
2199
2418
  [nameOrId, `%${nameOrId}%`]
2200
2419
  );
2201
2420
  if (!feature) {
2202
- console.log(chalk13.red(`
2421
+ console.log(chalk14.red(`
2203
2422
  Feature "${nameOrId}" not found.
2204
2423
  `));
2205
2424
  closeDb();
@@ -2219,7 +2438,7 @@ async function updateCommand(type, nameOrId, options) {
2219
2438
  if (options.progress !== void 0) {
2220
2439
  const p = parseProgress2(options.progress);
2221
2440
  if (!p) {
2222
- console.log(chalk13.yellow(` \u26A0 --progress "${options.progress}" ignored (expected N/N format)
2441
+ console.log(chalk14.yellow(` \u26A0 --progress "${options.progress}" ignored (expected N/N format)
2223
2442
  `));
2224
2443
  } else {
2225
2444
  sets.push("progress_done = ?", "progress_total = ?");
@@ -2235,7 +2454,7 @@ async function updateCommand(type, nameOrId, options) {
2235
2454
  params.push(options.status);
2236
2455
  }
2237
2456
  if (sets.length === 0) {
2238
- console.log(chalk13.yellow("\n Nothing to update \u2014 pass at least one option.\n"));
2457
+ console.log(chalk14.yellow("\n Nothing to update \u2014 pass at least one option.\n"));
2239
2458
  closeDb();
2240
2459
  return;
2241
2460
  }
@@ -2247,7 +2466,7 @@ async function updateCommand(type, nameOrId, options) {
2247
2466
  );
2248
2467
  saveDb();
2249
2468
  closeDb();
2250
- console.log(chalk13.green(` \u2713 Updated: ${feature.name}`));
2469
+ console.log(chalk14.green(` \u2713 Updated: ${feature.name}`));
2251
2470
  }
2252
2471
 
2253
2472
  // src/index.ts
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@groundctl/cli",
3
- "version": "0.3.2",
3
+ "version": "0.4.0",
4
4
  "description": "Always know what to build next.",
5
5
  "license": "MIT",
6
6
  "bin": {