@groundctl/cli 0.3.1 → 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 +657 -271
  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";
@@ -98,6 +98,15 @@ function applySchema(db) {
98
98
  db.run("CREATE INDEX IF NOT EXISTS idx_claims_active ON claims(feature_id) WHERE released_at IS NULL");
99
99
  db.run("CREATE INDEX IF NOT EXISTS idx_files_session ON files_modified(session_id)");
100
100
  db.run("CREATE INDEX IF NOT EXISTS idx_decisions_session ON decisions(session_id)");
101
+ const tryAlter = (sql) => {
102
+ try {
103
+ db.run(sql);
104
+ } catch {
105
+ }
106
+ };
107
+ tryAlter("ALTER TABLE features ADD COLUMN progress_done INTEGER");
108
+ tryAlter("ALTER TABLE features ADD COLUMN progress_total INTEGER");
109
+ tryAlter("ALTER TABLE features ADD COLUMN items TEXT");
101
110
  db.run(
102
111
  "INSERT OR REPLACE INTO meta (key, value) VALUES ('schema_version', ?)",
103
112
  [String(SCHEMA_VERSION)]
@@ -343,8 +352,6 @@ function generateAgentsMd(db, projectName) {
343
352
 
344
353
  // src/ingest/git-import.ts
345
354
  import { execSync } from "child_process";
346
- import { existsSync as existsSync2, readFileSync as readFileSync2 } from "fs";
347
- import { join as join2 } from "path";
348
355
  function run(cmd, cwd) {
349
356
  try {
350
357
  return execSync(cmd, { cwd, encoding: "utf-8", stdio: ["ignore", "pipe", "ignore"] });
@@ -400,67 +407,10 @@ function parseGitLog(cwd) {
400
407
  }
401
408
  return commits.reverse();
402
409
  }
403
- function parseProjectStateMd(content) {
404
- const features = [];
405
- const lines = content.split("\n");
406
- let section = "";
407
- for (const line of lines) {
408
- const trimmed = line.trim();
409
- if (trimmed.startsWith("## ")) {
410
- section = trimmed.toLowerCase();
411
- continue;
412
- }
413
- if (section.includes("decision") || section.includes("session") || section.includes("debt") || section.includes("note")) continue;
414
- if (!trimmed.startsWith("- ") && !trimmed.startsWith("* ")) continue;
415
- const item = trimmed.slice(2).trim();
416
- if (!item || item.length < 3) continue;
417
- const name = item.split("(")[0].split("\u2192")[0].split("\u2014")[0].trim();
418
- if (!name || name.length < 3 || name.length > 80) continue;
419
- if (/^\d{4}-\d{2}-\d{2}/.test(name)) continue;
420
- if (name.split(" ").length > 8) continue;
421
- let status = "pending";
422
- let priority = "medium";
423
- if (section.includes("built") || section.includes("done") || section.includes("complete")) {
424
- status = "done";
425
- } else if (section.includes("claimed") || section.includes("in progress") || section.includes("current")) {
426
- status = "in_progress";
427
- } else if (section.includes("available") || section.includes("next")) {
428
- status = "pending";
429
- } else if (section.includes("blocked")) {
430
- status = "blocked";
431
- }
432
- if (/priority:\s*critical|critical\)/i.test(item)) priority = "critical";
433
- else if (/priority:\s*high|high\)/i.test(item)) priority = "high";
434
- else if (/priority:\s*low|low\)/i.test(item)) priority = "low";
435
- features.push({ name, status, priority });
436
- }
437
- return features;
438
- }
439
- function featureIdFromName(name) {
440
- return name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "").slice(0, 60);
441
- }
442
410
  function importFromGit(db, projectPath) {
443
411
  let sessionsCreated = 0;
444
- let featuresImported = 0;
445
- const psMdPath = join2(projectPath, "PROJECT_STATE.md");
446
- if (existsSync2(psMdPath)) {
447
- const content = readFileSync2(psMdPath, "utf-8");
448
- const features = parseProjectStateMd(content);
449
- for (const feat of features) {
450
- const id = featureIdFromName(feat.name);
451
- if (!id) continue;
452
- const exists = queryOne(db, "SELECT id FROM features WHERE id = ?", [id]);
453
- if (!exists) {
454
- db.run(
455
- "INSERT INTO features (id, name, status, priority) VALUES (?, ?, ?, ?)",
456
- [id, feat.name, feat.status, feat.priority]
457
- );
458
- featuresImported++;
459
- }
460
- }
461
- }
462
412
  const commits = parseGitLog(projectPath);
463
- if (commits.length === 0) return { sessionsCreated, featuresImported };
413
+ if (commits.length === 0) return { sessionsCreated };
464
414
  const SESSION_GAP_MS = 4 * 60 * 60 * 1e3;
465
415
  const sessions = [];
466
416
  let currentSession = [];
@@ -514,7 +464,286 @@ function importFromGit(db, projectPath) {
514
464
  }
515
465
  }
516
466
  saveDb();
517
- 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
+ }
518
747
  }
519
748
 
520
749
  // src/commands/init.ts
@@ -554,53 +783,52 @@ echo "--- groundctl: Product state updated ---"
554
783
  async function initCommand(options) {
555
784
  const cwd = process.cwd();
556
785
  const projectName = cwd.split("/").pop() ?? "unknown";
557
- console.log(chalk.bold(`
786
+ console.log(chalk2.bold(`
558
787
  groundctl init \u2014 ${projectName}
559
788
  `));
560
- console.log(chalk.gray(" Creating SQLite database..."));
789
+ console.log(chalk2.gray(" Creating SQLite database..."));
561
790
  const db = await openDb();
562
791
  if (options.importFromGit) {
563
792
  const isGitRepo = existsSync3(join3(cwd, ".git"));
564
793
  if (!isGitRepo) {
565
- 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"));
566
795
  } else {
567
- console.log(chalk.gray(" Importing from git history..."));
796
+ console.log(chalk2.gray(" Importing sessions from git history..."));
568
797
  const result = importFromGit(db, cwd);
569
798
  console.log(
570
- chalk.green(
571
- ` \u2713 Git import: ${result.sessionsCreated} sessions, ${result.featuresImported} features`
572
- )
799
+ chalk2.green(` \u2713 Git import: ${result.sessionsCreated} sessions`)
573
800
  );
801
+ await detectAndImportFeatures(db, cwd);
574
802
  }
575
803
  }
576
804
  const projectState = generateProjectState(db, projectName);
577
805
  const agentsMd = generateAgentsMd(db, projectName);
578
806
  closeDb();
579
- console.log(chalk.green(" \u2713 Database ready"));
807
+ console.log(chalk2.green(" \u2713 Database ready"));
580
808
  const claudeHooksDir = join3(cwd, ".claude", "hooks");
581
809
  if (!existsSync3(claudeHooksDir)) {
582
810
  mkdirSync2(claudeHooksDir, { recursive: true });
583
811
  }
584
- writeFileSync2(join3(claudeHooksDir, "pre-session.sh"), PRE_SESSION_HOOK);
812
+ writeFileSync3(join3(claudeHooksDir, "pre-session.sh"), PRE_SESSION_HOOK);
585
813
  chmodSync(join3(claudeHooksDir, "pre-session.sh"), 493);
586
- writeFileSync2(join3(claudeHooksDir, "post-session.sh"), POST_SESSION_HOOK);
814
+ writeFileSync3(join3(claudeHooksDir, "post-session.sh"), POST_SESSION_HOOK);
587
815
  chmodSync(join3(claudeHooksDir, "post-session.sh"), 493);
588
- console.log(chalk.green(" \u2713 Claude Code hooks installed"));
816
+ console.log(chalk2.green(" \u2713 Claude Code hooks installed"));
589
817
  const codexHooksDir = join3(cwd, ".codex", "hooks");
590
818
  if (!existsSync3(codexHooksDir)) {
591
819
  mkdirSync2(codexHooksDir, { recursive: true });
592
820
  }
593
821
  const codexPre = PRE_SESSION_HOOK.replace("Claude Code", "Codex");
594
822
  const codexPost = POST_SESSION_HOOK.replace("Claude Code", "Codex").replace("claude-code", "codex");
595
- writeFileSync2(join3(codexHooksDir, "pre-session.sh"), codexPre);
823
+ writeFileSync3(join3(codexHooksDir, "pre-session.sh"), codexPre);
596
824
  chmodSync(join3(codexHooksDir, "pre-session.sh"), 493);
597
- writeFileSync2(join3(codexHooksDir, "post-session.sh"), codexPost);
825
+ writeFileSync3(join3(codexHooksDir, "post-session.sh"), codexPost);
598
826
  chmodSync(join3(codexHooksDir, "post-session.sh"), 493);
599
- console.log(chalk.green(" \u2713 Codex hooks installed"));
600
- writeFileSync2(join3(cwd, "PROJECT_STATE.md"), projectState);
601
- writeFileSync2(join3(cwd, "AGENTS.md"), agentsMd);
602
- console.log(chalk.green(" \u2713 PROJECT_STATE.md generated"));
603
- 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"));
604
832
  const gitignorePath = join3(cwd, ".gitignore");
605
833
  const gitignoreEntry = "\n# groundctl local state\n.groundctl/\n";
606
834
  if (existsSync3(gitignorePath)) {
@@ -609,68 +837,106 @@ groundctl init \u2014 ${projectName}
609
837
  appendFileSync(gitignorePath, gitignoreEntry);
610
838
  }
611
839
  }
612
- console.log(chalk.bold.green(`
840
+ console.log(chalk2.bold.green(`
613
841
  \u2713 groundctl initialized for ${projectName}
614
842
  `));
615
843
  if (!options.importFromGit) {
616
- console.log(chalk.gray(" Next steps:"));
617
- console.log(chalk.gray(" groundctl add feature -n 'my-feature' -p high"));
618
- console.log(chalk.gray(" groundctl status"));
619
- console.log(chalk.gray(" groundctl claim my-feature"));
620
- console.log(chalk.gray("\n Or bootstrap from git history:"));
621
- 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"));
622
850
  } else {
623
- console.log(chalk.gray(" Next steps:"));
624
- console.log(chalk.gray(" groundctl status"));
625
- 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"));
626
854
  }
627
855
  }
628
856
 
629
857
  // src/commands/status.ts
630
- import chalk2 from "chalk";
631
- function progressBar(done, total, width = 20) {
632
- if (total === 0) return chalk2.gray("\u2591".repeat(width));
633
- const filled = Math.round(done / total * width);
634
- const empty = width - filled;
635
- return chalk2.green("\u2588".repeat(filled)) + chalk2.gray("\u2591".repeat(empty));
858
+ import chalk3 from "chalk";
859
+ var BAR_W = 14;
860
+ var NAME_W = 22;
861
+ var PROG_W = 6;
862
+ function progressBar(done, total, width) {
863
+ if (total <= 0) return chalk3.gray("\u2591".repeat(width));
864
+ const filled = Math.min(width, Math.round(done / total * width));
865
+ return chalk3.green("\u2588".repeat(filled)) + chalk3.gray("\u2591".repeat(width - filled));
866
+ }
867
+ function featureBar(status, progressDone, progressTotal) {
868
+ if (progressTotal != null && progressTotal > 0) {
869
+ return progressBar(progressDone ?? 0, progressTotal, BAR_W);
870
+ }
871
+ switch (status) {
872
+ case "done":
873
+ return progressBar(1, 1, BAR_W);
874
+ case "in_progress":
875
+ return progressBar(1, 2, BAR_W);
876
+ case "blocked":
877
+ return chalk3.red("\u2591".repeat(BAR_W));
878
+ default:
879
+ return chalk3.gray("\u2591".repeat(BAR_W));
880
+ }
881
+ }
882
+ function featureProgress(progressDone, progressTotal) {
883
+ if (progressDone != null && progressTotal != null) {
884
+ return `${progressDone}/${progressTotal}`;
885
+ }
886
+ return "";
887
+ }
888
+ function wrapItems(itemsCsv, maxWidth) {
889
+ const items = itemsCsv.split(",").map((s) => s.trim()).filter(Boolean);
890
+ const lines = [];
891
+ let current = "";
892
+ for (const item of items) {
893
+ const next = current ? `${current} \xB7 ${item}` : item;
894
+ if (next.length > maxWidth && current.length > 0) {
895
+ lines.push(current);
896
+ current = item;
897
+ } else {
898
+ current = next;
899
+ }
900
+ }
901
+ if (current) lines.push(current);
902
+ return lines;
903
+ }
904
+ function timeSince(isoDate) {
905
+ const then = (/* @__PURE__ */ new Date(isoDate + "Z")).getTime();
906
+ const ms = Date.now() - then;
907
+ const mins = Math.floor(ms / 6e4);
908
+ if (mins < 60) return `${mins}m`;
909
+ const h = Math.floor(mins / 60);
910
+ const m = mins % 60;
911
+ return `${h}h${m > 0 ? String(m).padStart(2, "0") : ""}`;
636
912
  }
637
913
  async function statusCommand() {
638
914
  const db = await openDb();
639
915
  const projectName = process.cwd().split("/").pop() ?? "unknown";
640
- const statusCounts = query(
641
- db,
642
- "SELECT status, COUNT(*) as count FROM features GROUP BY status"
643
- );
644
- const counts = {
645
- pending: 0,
646
- in_progress: 0,
647
- done: 0,
648
- blocked: 0
649
- };
650
- for (const row of statusCounts) {
651
- counts[row.status] = row.count;
652
- }
653
- const total = counts.pending + counts.in_progress + counts.done + counts.blocked;
654
- const activeClaims = query(
916
+ const features = query(
655
917
  db,
656
- `SELECT c.feature_id, f.name as feature_name, c.session_id, c.claimed_at
657
- FROM claims c
658
- JOIN features f ON c.feature_id = f.id
659
- WHERE c.released_at IS NULL`
660
- );
661
- const available = query(
662
- db,
663
- `SELECT f.id, f.name, f.priority
918
+ `SELECT
919
+ f.id, f.name, f.status, f.priority,
920
+ f.description, f.progress_done, f.progress_total, f.items,
921
+ c.session_id AS claimed_session,
922
+ c.claimed_at AS claimed_at
664
923
  FROM features f
665
- WHERE f.status = 'pending'
666
- AND f.id NOT IN (SELECT feature_id FROM claims WHERE released_at IS NULL)
924
+ LEFT JOIN claims c
925
+ ON c.feature_id = f.id AND c.released_at IS NULL
667
926
  ORDER BY
927
+ CASE f.status
928
+ WHEN 'in_progress' THEN 0
929
+ WHEN 'blocked' THEN 1
930
+ WHEN 'pending' THEN 2
931
+ WHEN 'done' THEN 3
932
+ END,
668
933
  CASE f.priority
669
934
  WHEN 'critical' THEN 0
670
- WHEN 'high' THEN 1
671
- WHEN 'medium' THEN 2
672
- WHEN 'low' THEN 3
673
- END`
935
+ WHEN 'high' THEN 1
936
+ WHEN 'medium' THEN 2
937
+ WHEN 'low' THEN 3
938
+ END,
939
+ f.created_at`
674
940
  );
675
941
  const sessionCount = queryOne(
676
942
  db,
@@ -678,63 +944,67 @@ async function statusCommand() {
678
944
  )?.count ?? 0;
679
945
  closeDb();
680
946
  console.log("");
681
- if (total === 0) {
682
- console.log(chalk2.bold(` ${projectName} \u2014 no features tracked yet
947
+ if (features.length === 0) {
948
+ console.log(chalk3.bold(` ${projectName} \u2014 no features tracked yet
683
949
  `));
684
- console.log(chalk2.gray(" Add features with: groundctl add feature -n 'my-feature'"));
685
- 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"));
686
952
  return;
687
953
  }
688
- const pct = total > 0 ? Math.round(counts.done / total * 100) : 0;
954
+ const total = features.length;
955
+ const done = features.filter((f) => f.status === "done").length;
956
+ const inProg = features.filter((f) => f.status === "in_progress").length;
957
+ const blocked = features.filter((f) => f.status === "blocked").length;
958
+ const pct = Math.round(done / total * 100);
689
959
  console.log(
690
- chalk2.bold(` ${projectName} \u2014 ${pct}% implemented`) + chalk2.gray(` (${sessionCount} sessions)`)
960
+ chalk3.bold(` ${projectName} \u2014 ${pct}% implemented`) + chalk3.gray(` (${sessionCount} session${sessionCount !== 1 ? "s" : ""})`)
691
961
  );
692
962
  console.log("");
693
- console.log(
694
- ` Features ${progressBar(counts.done, total)} ${counts.done}/${total} done`
695
- );
696
- if (counts.in_progress > 0) {
697
- console.log(chalk2.yellow(` ${counts.in_progress} in progress`));
698
- }
699
- if (counts.blocked > 0) {
700
- console.log(chalk2.red(` ${counts.blocked} blocked`));
701
- }
963
+ const aggBar = progressBar(done, total, 20);
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`);
967
+ console.log(` Features ${aggBar}${aggSuffix}`);
702
968
  console.log("");
703
- if (activeClaims.length > 0) {
704
- console.log(chalk2.bold(" Claimed:"));
705
- for (const claim of activeClaims) {
706
- const elapsed = timeSince(claim.claimed_at);
707
- console.log(
708
- chalk2.yellow(` \u25CF ${claim.feature_name} \u2192 session ${claim.session_id} (${elapsed})`)
709
- );
710
- }
711
- console.log("");
712
- }
713
- if (available.length > 0) {
714
- console.log(chalk2.bold(" Available:"));
715
- for (const feat of available.slice(0, 5)) {
716
- const pColor = feat.priority === "critical" || feat.priority === "high" ? chalk2.red : chalk2.gray;
717
- console.log(` \u25CB ${feat.name} ${pColor(`(${feat.priority})`)}`);
969
+ const maxNameLen = Math.min(NAME_W, Math.max(...features.map((f) => f.name.length)));
970
+ const nameW = Math.max(maxNameLen, 12);
971
+ const contIndent = " ".repeat(4 + nameW + 1);
972
+ const itemsMaxW = Math.max(40, 76 - contIndent.length);
973
+ for (const f of features) {
974
+ const isDone = f.status === "done";
975
+ const isActive = f.status === "in_progress";
976
+ const isBlocked = f.status === "blocked";
977
+ const icon = isDone ? "\u2713" : isActive ? "\u25CF" : isBlocked ? "\u2717" : "\u25CB";
978
+ const iconChalk = isDone ? chalk3.green : isActive ? chalk3.yellow : isBlocked ? chalk3.red : chalk3.gray;
979
+ const nameRaw = f.name.slice(0, nameW).padEnd(nameW);
980
+ const nameChalk = isDone ? chalk3.dim : isActive ? chalk3.white : isBlocked ? chalk3.red : chalk3.gray;
981
+ const pd = f.progress_done ?? null;
982
+ const pt = f.progress_total ?? null;
983
+ const bar2 = featureBar(f.status, pd, pt);
984
+ const prog = featureProgress(pd, pt).padEnd(PROG_W);
985
+ const descRaw = f.description ?? "";
986
+ const descTrunc = descRaw.length > 38 ? descRaw.slice(0, 36) + "\u2026" : descRaw;
987
+ const descStr = descTrunc ? chalk3.gray(` ${descTrunc}`) : "";
988
+ let claimedStr = "";
989
+ if (isActive && f.claimed_session) {
990
+ const elapsed = f.claimed_at ? timeSince(f.claimed_at) : "";
991
+ claimedStr = chalk3.yellow(` \u2192 ${f.claimed_session}${elapsed ? ` (${elapsed})` : ""}`);
718
992
  }
719
- if (available.length > 5) {
720
- console.log(chalk2.gray(` ... and ${available.length - 5} more`));
993
+ console.log(
994
+ ` ${iconChalk(icon)} ${nameChalk(nameRaw)} ${bar2} ${prog}${descStr}${claimedStr}`
995
+ );
996
+ if (f.items) {
997
+ const lines = wrapItems(f.items, itemsMaxW);
998
+ for (const line of lines) {
999
+ console.log(chalk3.dim(`${contIndent}${line}`));
1000
+ }
721
1001
  }
722
- console.log("");
723
1002
  }
724
- }
725
- function timeSince(isoDate) {
726
- const then = (/* @__PURE__ */ new Date(isoDate + "Z")).getTime();
727
- const now = Date.now();
728
- const diffMs = now - then;
729
- const mins = Math.floor(diffMs / 6e4);
730
- if (mins < 60) return `${mins}m`;
731
- const hours = Math.floor(mins / 60);
732
- const remainMins = mins % 60;
733
- return `${hours}h${remainMins > 0 ? String(remainMins).padStart(2, "0") : ""}`;
1003
+ console.log("");
734
1004
  }
735
1005
 
736
1006
  // src/commands/claim.ts
737
- import chalk3 from "chalk";
1007
+ import chalk4 from "chalk";
738
1008
  import { randomUUID } from "crypto";
739
1009
  function findFeature(db, term) {
740
1010
  return queryOne(
@@ -771,15 +1041,15 @@ async function claimCommand(featureIdOrName, options) {
771
1041
  const db = await openDb();
772
1042
  const feature = findFeature(db, featureIdOrName);
773
1043
  if (!feature) {
774
- console.log(chalk3.red(`
1044
+ console.log(chalk4.red(`
775
1045
  Feature "${featureIdOrName}" not found.
776
1046
  `));
777
- 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 + "'"));
778
1048
  closeDb();
779
1049
  process.exit(1);
780
1050
  }
781
1051
  if (feature.status === "done") {
782
- console.log(chalk3.yellow(`
1052
+ console.log(chalk4.yellow(`
783
1053
  Feature "${feature.name}" is already done.
784
1054
  `));
785
1055
  closeDb();
@@ -793,7 +1063,7 @@ async function claimCommand(featureIdOrName, options) {
793
1063
  );
794
1064
  if (existingClaim) {
795
1065
  console.log(
796
- chalk3.red(`
1066
+ chalk4.red(`
797
1067
  Feature "${feature.name}" is already claimed by session ${existingClaim.session_id}`)
798
1068
  );
799
1069
  const alternatives = query(
@@ -808,9 +1078,9 @@ async function claimCommand(featureIdOrName, options) {
808
1078
  LIMIT 3`
809
1079
  );
810
1080
  if (alternatives.length > 0) {
811
- console.log(chalk3.gray("\n Available instead:"));
1081
+ console.log(chalk4.gray("\n Available instead:"));
812
1082
  for (const alt of alternatives) {
813
- console.log(chalk3.gray(` \u25CB ${alt.name}`));
1083
+ console.log(chalk4.gray(` \u25CB ${alt.name}`));
814
1084
  }
815
1085
  }
816
1086
  console.log("");
@@ -840,7 +1110,7 @@ async function claimCommand(featureIdOrName, options) {
840
1110
  saveDb();
841
1111
  closeDb();
842
1112
  console.log(
843
- chalk3.green(`
1113
+ chalk4.green(`
844
1114
  \u2713 Claimed "${feature.name}" \u2192 session ${sessionId}
845
1115
  `)
846
1116
  );
@@ -849,7 +1119,7 @@ async function completeCommand(featureIdOrName) {
849
1119
  const db = await openDb();
850
1120
  const feature = findFeature(db, featureIdOrName);
851
1121
  if (!feature) {
852
- console.log(chalk3.red(`
1122
+ console.log(chalk4.red(`
853
1123
  Feature "${featureIdOrName}" not found.
854
1124
  `));
855
1125
  closeDb();
@@ -865,15 +1135,15 @@ async function completeCommand(featureIdOrName) {
865
1135
  );
866
1136
  saveDb();
867
1137
  closeDb();
868
- console.log(chalk3.green(`
1138
+ console.log(chalk4.green(`
869
1139
  \u2713 Completed "${feature.name}"
870
1140
  `));
871
1141
  }
872
1142
 
873
1143
  // src/commands/sync.ts
874
- import { writeFileSync as writeFileSync3 } from "fs";
1144
+ import { writeFileSync as writeFileSync4 } from "fs";
875
1145
  import { join as join4 } from "path";
876
- import chalk4 from "chalk";
1146
+ import chalk5 from "chalk";
877
1147
  async function syncCommand(opts) {
878
1148
  const db = await openDb();
879
1149
  const projectName = process.cwd().split("/").pop() ?? "unknown";
@@ -881,16 +1151,16 @@ async function syncCommand(opts) {
881
1151
  const agentsMd = generateAgentsMd(db, projectName);
882
1152
  closeDb();
883
1153
  const cwd = process.cwd();
884
- writeFileSync3(join4(cwd, "PROJECT_STATE.md"), projectState);
885
- writeFileSync3(join4(cwd, "AGENTS.md"), agentsMd);
1154
+ writeFileSync4(join4(cwd, "PROJECT_STATE.md"), projectState);
1155
+ writeFileSync4(join4(cwd, "AGENTS.md"), agentsMd);
886
1156
  if (!opts?.silent) {
887
- console.log(chalk4.green("\n \u2713 PROJECT_STATE.md regenerated"));
888
- 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"));
889
1159
  }
890
1160
  }
891
1161
 
892
1162
  // src/commands/next.ts
893
- import chalk5 from "chalk";
1163
+ import chalk6 from "chalk";
894
1164
  async function nextCommand() {
895
1165
  const db = await openDb();
896
1166
  const available = query(
@@ -910,26 +1180,26 @@ async function nextCommand() {
910
1180
  );
911
1181
  closeDb();
912
1182
  if (available.length === 0) {
913
- console.log(chalk5.yellow("\n No available features to claim.\n"));
1183
+ console.log(chalk6.yellow("\n No available features to claim.\n"));
914
1184
  return;
915
1185
  }
916
- console.log(chalk5.bold("\n Next available features:\n"));
1186
+ console.log(chalk6.bold("\n Next available features:\n"));
917
1187
  for (let i = 0; i < available.length; i++) {
918
1188
  const feat = available[i];
919
- const pColor = feat.priority === "critical" || feat.priority === "high" ? chalk5.red : chalk5.gray;
920
- 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") : " ";
921
1191
  console.log(` ${marker} ${feat.name} ${pColor(`(${feat.priority})`)}`);
922
1192
  if (feat.description) {
923
- console.log(chalk5.gray(` ${feat.description}`));
1193
+ console.log(chalk6.gray(` ${feat.description}`));
924
1194
  }
925
1195
  }
926
- console.log(chalk5.gray(`
1196
+ console.log(chalk6.gray(`
927
1197
  Claim with: groundctl claim "${available[0].name}"
928
1198
  `));
929
1199
  }
930
1200
 
931
1201
  // src/commands/log.ts
932
- import chalk6 from "chalk";
1202
+ import chalk7 from "chalk";
933
1203
  async function logCommand(options) {
934
1204
  const db = await openDb();
935
1205
  if (options.session) {
@@ -938,18 +1208,18 @@ async function logCommand(options) {
938
1208
  `%${options.session}%`
939
1209
  ]);
940
1210
  if (!session) {
941
- console.log(chalk6.red(`
1211
+ console.log(chalk7.red(`
942
1212
  Session "${options.session}" not found.
943
1213
  `));
944
1214
  closeDb();
945
1215
  return;
946
1216
  }
947
- console.log(chalk6.bold(`
1217
+ console.log(chalk7.bold(`
948
1218
  Session ${session.id}`));
949
- console.log(chalk6.gray(` Agent: ${session.agent}`));
950
- 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}`));
951
1221
  if (session.ended_at) {
952
- console.log(chalk6.gray(` Ended: ${session.ended_at}`));
1222
+ console.log(chalk7.gray(` Ended: ${session.ended_at}`));
953
1223
  }
954
1224
  if (session.summary) {
955
1225
  console.log(`
@@ -961,11 +1231,11 @@ async function logCommand(options) {
961
1231
  [session.id]
962
1232
  );
963
1233
  if (decisions.length > 0) {
964
- console.log(chalk6.bold("\n Decisions:"));
1234
+ console.log(chalk7.bold("\n Decisions:"));
965
1235
  for (const d of decisions) {
966
1236
  console.log(` \u2022 ${d.description}`);
967
1237
  if (d.rationale) {
968
- console.log(chalk6.gray(` ${d.rationale}`));
1238
+ console.log(chalk7.gray(` ${d.rationale}`));
969
1239
  }
970
1240
  }
971
1241
  }
@@ -975,10 +1245,10 @@ async function logCommand(options) {
975
1245
  [session.id]
976
1246
  );
977
1247
  if (files.length > 0) {
978
- console.log(chalk6.bold(`
1248
+ console.log(chalk7.bold(`
979
1249
  Files modified (${files.length}):`));
980
1250
  for (const f of files) {
981
- 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("~");
982
1252
  console.log(` ${op} ${f.path} (${f.lines_changed} lines)`);
983
1253
  }
984
1254
  }
@@ -986,18 +1256,18 @@ async function logCommand(options) {
986
1256
  } else {
987
1257
  const sessions = query(db, "SELECT * FROM sessions ORDER BY started_at DESC LIMIT 20");
988
1258
  if (sessions.length === 0) {
989
- console.log(chalk6.yellow("\n No sessions recorded yet.\n"));
1259
+ console.log(chalk7.yellow("\n No sessions recorded yet.\n"));
990
1260
  closeDb();
991
1261
  return;
992
1262
  }
993
- console.log(chalk6.bold("\n Session timeline:\n"));
1263
+ console.log(chalk7.bold("\n Session timeline:\n"));
994
1264
  for (const s of sessions) {
995
- const status = s.ended_at ? chalk6.green("done") : chalk6.yellow("active");
1265
+ const status = s.ended_at ? chalk7.green("done") : chalk7.yellow("active");
996
1266
  console.log(
997
- ` ${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)}`
998
1268
  );
999
1269
  if (s.summary) {
1000
- console.log(chalk6.gray(` ${s.summary}`));
1270
+ console.log(chalk7.gray(` ${s.summary}`));
1001
1271
  }
1002
1272
  }
1003
1273
  console.log("");
@@ -1006,26 +1276,57 @@ async function logCommand(options) {
1006
1276
  }
1007
1277
 
1008
1278
  // src/commands/add.ts
1009
- import chalk7 from "chalk";
1279
+ import chalk8 from "chalk";
1010
1280
  import { randomUUID as randomUUID2 } from "crypto";
1281
+ function parseProgress(s) {
1282
+ const m = s.match(/^(\d+)\/(\d+)$/);
1283
+ if (!m) return null;
1284
+ return { done: parseInt(m[1], 10), total: parseInt(m[2], 10) };
1285
+ }
1011
1286
  async function addCommand(type, options) {
1012
1287
  const db = await openDb();
1013
1288
  if (type === "feature") {
1014
1289
  if (!options.name) {
1015
- console.log(chalk7.red("\n --name is required for features.\n"));
1290
+ console.log(chalk8.red("\n --name is required for features.\n"));
1016
1291
  closeDb();
1017
1292
  process.exit(1);
1018
1293
  }
1019
1294
  const id = options.name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
1020
1295
  const priority = options.priority ?? "medium";
1296
+ let progressDone = null;
1297
+ let progressTotal = null;
1298
+ if (options.progress) {
1299
+ const p = parseProgress(options.progress);
1300
+ if (p) {
1301
+ progressDone = p.done;
1302
+ progressTotal = p.total;
1303
+ } else {
1304
+ console.log(chalk8.yellow(` \u26A0 --progress "${options.progress}" ignored (expected N/N format)`));
1305
+ }
1306
+ }
1307
+ const items = options.items ? options.items.split(",").map((s) => s.trim()).filter(Boolean).join(",") : null;
1021
1308
  db.run(
1022
- "INSERT INTO features (id, name, priority, description) VALUES (?, ?, ?, ?)",
1023
- [id, options.name, priority, options.description ?? null]
1309
+ `INSERT INTO features
1310
+ (id, name, priority, description, progress_done, progress_total, items)
1311
+ VALUES (?, ?, ?, ?, ?, ?, ?)`,
1312
+ [
1313
+ id,
1314
+ options.name,
1315
+ priority,
1316
+ options.description ?? null,
1317
+ progressDone,
1318
+ progressTotal,
1319
+ items
1320
+ ]
1024
1321
  );
1025
1322
  saveDb();
1026
1323
  closeDb();
1027
- console.log(chalk7.green(`
1028
- \u2713 Feature added: ${options.name} (${priority})
1324
+ const extras = [];
1325
+ if (progressDone !== null) extras.push(`${progressDone}/${progressTotal}`);
1326
+ if (items) extras.push(`${items.split(",").length} items`);
1327
+ const suffix = extras.length ? chalk8.gray(` \u2014 ${extras.join(", ")}`) : "";
1328
+ console.log(chalk8.green(`
1329
+ \u2713 Feature added: ${options.name} (${priority})${suffix}
1029
1330
  `));
1030
1331
  } else if (type === "session") {
1031
1332
  const id = options.name ?? randomUUID2().slice(0, 8);
@@ -1033,11 +1334,11 @@ async function addCommand(type, options) {
1033
1334
  db.run("INSERT INTO sessions (id, agent) VALUES (?, ?)", [id, agent]);
1034
1335
  saveDb();
1035
1336
  closeDb();
1036
- console.log(chalk7.green(`
1337
+ console.log(chalk8.green(`
1037
1338
  \u2713 Session created: ${id} (${agent})
1038
1339
  `));
1039
1340
  } else {
1040
- console.log(chalk7.red(`
1341
+ console.log(chalk8.red(`
1041
1342
  Unknown type "${type}". Use "feature" or "session".
1042
1343
  `));
1043
1344
  closeDb();
@@ -1049,7 +1350,7 @@ async function addCommand(type, options) {
1049
1350
  import { existsSync as existsSync4, readdirSync } from "fs";
1050
1351
  import { join as join5, resolve } from "path";
1051
1352
  import { homedir as homedir2 } from "os";
1052
- import chalk8 from "chalk";
1353
+ import chalk9 from "chalk";
1053
1354
 
1054
1355
  // src/ingest/claude-parser.ts
1055
1356
  import { readFileSync as readFileSync4 } from "fs";
@@ -1319,11 +1620,11 @@ async function ingestCommand(options) {
1319
1620
  transcriptPath = findLatestTranscript(projectPath) ?? void 0;
1320
1621
  }
1321
1622
  if (!transcriptPath || !existsSync4(transcriptPath)) {
1322
- console.log(chalk8.yellow("\n No transcript found. Skipping ingest.\n"));
1623
+ console.log(chalk9.yellow("\n No transcript found. Skipping ingest.\n"));
1323
1624
  if (!options.noSync) await syncCommand();
1324
1625
  return;
1325
1626
  }
1326
- console.log(chalk8.gray(`
1627
+ console.log(chalk9.gray(`
1327
1628
  Parsing transcript: ${transcriptPath.split("/").slice(-2).join("/")}`));
1328
1629
  const parsed = parseTranscript(transcriptPath, options.sessionId ?? "auto", projectPath);
1329
1630
  const db = await openDb();
@@ -1373,16 +1674,16 @@ async function ingestCommand(options) {
1373
1674
  saveDb();
1374
1675
  closeDb();
1375
1676
  console.log(
1376
- chalk8.green(
1677
+ chalk9.green(
1377
1678
  ` \u2713 Ingested session ${sessionId}: ${newFiles} files, ${parsed.commits.length} commits, ${newDecisions} decisions`
1378
1679
  )
1379
1680
  );
1380
1681
  if (parsed.decisions.length > 0 && newDecisions > 0) {
1381
- console.log(chalk8.gray(`
1682
+ console.log(chalk9.gray(`
1382
1683
  Decisions captured:`));
1383
1684
  for (const d of parsed.decisions.slice(0, 5)) {
1384
- const conf = d.confidence === "low" ? chalk8.gray(" (low confidence)") : "";
1385
- 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}`));
1386
1687
  }
1387
1688
  }
1388
1689
  if (!options.noSync) {
@@ -1392,9 +1693,9 @@ async function ingestCommand(options) {
1392
1693
  }
1393
1694
 
1394
1695
  // src/commands/report.ts
1395
- import { writeFileSync as writeFileSync4 } from "fs";
1696
+ import { writeFileSync as writeFileSync5 } from "fs";
1396
1697
  import { join as join6 } from "path";
1397
- import chalk9 from "chalk";
1698
+ import chalk10 from "chalk";
1398
1699
  function formatDuration(start, end) {
1399
1700
  if (!end) return "ongoing";
1400
1701
  const startMs = new Date(start).getTime();
@@ -1489,7 +1790,7 @@ async function reportCommand(options) {
1489
1790
  [options.session, `%${options.session}%`]
1490
1791
  );
1491
1792
  if (!s) {
1492
- console.log(chalk9.red(`
1793
+ console.log(chalk10.red(`
1493
1794
  Session "${options.session}" not found.
1494
1795
  `));
1495
1796
  closeDb();
@@ -1502,7 +1803,7 @@ async function reportCommand(options) {
1502
1803
  "SELECT * FROM sessions ORDER BY started_at DESC LIMIT 1"
1503
1804
  );
1504
1805
  if (!s) {
1505
- 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"));
1506
1807
  closeDb();
1507
1808
  return;
1508
1809
  }
@@ -1544,8 +1845,8 @@ async function reportCommand(options) {
1544
1845
  }
1545
1846
  closeDb();
1546
1847
  const outPath2 = join6(cwd, "SESSION_HISTORY.md");
1547
- writeFileSync4(outPath2, fullReport);
1548
- console.log(chalk9.green(`
1848
+ writeFileSync5(outPath2, fullReport);
1849
+ console.log(chalk10.green(`
1549
1850
  \u2713 SESSION_HISTORY.md written (${sessions.length} sessions)
1550
1851
  `));
1551
1852
  return;
@@ -1571,16 +1872,16 @@ async function reportCommand(options) {
1571
1872
  activeClaims
1572
1873
  );
1573
1874
  const outPath = join6(cwd, "SESSION_REPORT.md");
1574
- writeFileSync4(outPath, report);
1575
- console.log(chalk9.green(`
1875
+ writeFileSync5(outPath, report);
1876
+ console.log(chalk10.green(`
1576
1877
  \u2713 SESSION_REPORT.md written (session ${session.id})
1577
1878
  `));
1578
- 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`));
1579
1880
  console.log("");
1580
1881
  }
1581
1882
 
1582
1883
  // src/commands/health.ts
1583
- import chalk10 from "chalk";
1884
+ import chalk11 from "chalk";
1584
1885
  async function healthCommand() {
1585
1886
  const db = await openDb();
1586
1887
  const projectName = process.cwd().split("/").pop() ?? "unknown";
@@ -1631,32 +1932,32 @@ async function healthCommand() {
1631
1932
  closeDb();
1632
1933
  const totalScore = featureScore + testScore + decisionScore + claimScore + deployScore;
1633
1934
  console.log("");
1634
- console.log(chalk10.bold(` ${projectName} \u2014 Health Score: ${totalScore}/100
1935
+ console.log(chalk11.bold(` ${projectName} \u2014 Health Score: ${totalScore}/100
1635
1936
  `));
1636
- 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;
1637
1938
  const featureMark = featurePct >= 0.4 ? "\u2705" : "\u26A0\uFE0F ";
1638
1939
  console.log(
1639
- ` ${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`)
1640
1941
  );
1641
1942
  const testMark = testFiles > 0 ? "\u2705" : "\u26A0\uFE0F ";
1642
- const testColor = testFiles > 0 ? chalk10.green : chalk10.red;
1943
+ const testColor = testFiles > 0 ? chalk11.green : chalk11.red;
1643
1944
  console.log(
1644
- ` ${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`))
1645
1946
  );
1646
1947
  const decMark = decisionCount > 0 ? "\u2705" : "\u26A0\uFE0F ";
1647
- const decColor = decisionCount > 0 ? chalk10.green : chalk10.yellow;
1948
+ const decColor = decisionCount > 0 ? chalk11.green : chalk11.yellow;
1648
1949
  console.log(
1649
- ` ${decMark} Arch log ${decColor(decisionCount + " entries")}` + chalk10.gray(` +${decisionScore}pts`)
1950
+ ` ${decMark} Arch log ${decColor(decisionCount + " entries")}` + chalk11.gray(` +${decisionScore}pts`)
1650
1951
  );
1651
1952
  const claimMark = staleClaims === 0 ? "\u2705" : "\u26A0\uFE0F ";
1652
- const claimColor = staleClaims === 0 ? chalk10.green : chalk10.red;
1953
+ const claimColor = staleClaims === 0 ? chalk11.green : chalk11.red;
1653
1954
  console.log(
1654
- ` ${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`)
1655
1956
  );
1656
1957
  const deployMark = deployScore > 0 ? "\u2705" : "\u26A0\uFE0F ";
1657
- const deployLabel = deployScore > 0 ? chalk10.green("detected") : chalk10.gray("not detected");
1958
+ const deployLabel = deployScore > 0 ? chalk11.green("detected") : chalk11.gray("not detected");
1658
1959
  console.log(
1659
- ` ${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"))
1660
1961
  );
1661
1962
  console.log("");
1662
1963
  const recommendations = [];
@@ -1665,9 +1966,9 @@ async function healthCommand() {
1665
1966
  if (decisionCount === 0) recommendations.push("Log architecture decisions during sessions so agents understand the why.");
1666
1967
  if (featurePct < 0.5 && total > 0) recommendations.push(`${counts.pending} features pending \u2014 run groundctl next to pick one.`);
1667
1968
  if (recommendations.length > 0) {
1668
- console.log(chalk10.bold(" Recommendations:"));
1969
+ console.log(chalk11.bold(" Recommendations:"));
1669
1970
  for (const r of recommendations) {
1670
- console.log(chalk10.yellow(` \u2192 ${r}`));
1971
+ console.log(chalk11.yellow(` \u2192 ${r}`));
1671
1972
  }
1672
1973
  console.log("");
1673
1974
  }
@@ -1678,7 +1979,7 @@ import { createServer } from "http";
1678
1979
  import { existsSync as existsSync5, readFileSync as readFileSync5 } from "fs";
1679
1980
  import { join as join7, dirname as dirname2 } from "path";
1680
1981
  import { exec } from "child_process";
1681
- import chalk11 from "chalk";
1982
+ import chalk12 from "chalk";
1682
1983
  import initSqlJs2 from "sql.js";
1683
1984
  function findDbPath(startDir = process.cwd()) {
1684
1985
  let dir = startDir;
@@ -1841,9 +2142,9 @@ async function dashboardCommand(options) {
1841
2142
  }
1842
2143
  });
1843
2144
  server.listen(port, "127.0.0.1", () => {
1844
- console.log(chalk11.bold(`
1845
- groundctl dashboard \u2192 `) + chalk11.blue(`http://localhost:${port}`) + "\n");
1846
- 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"));
1847
2148
  exec(`open http://localhost:${port} 2>/dev/null || xdg-open http://localhost:${port} 2>/dev/null || true`);
1848
2149
  });
1849
2150
  await new Promise((_, reject) => {
@@ -1856,7 +2157,7 @@ import {
1856
2157
  existsSync as existsSync6,
1857
2158
  readdirSync as readdirSync2,
1858
2159
  statSync,
1859
- writeFileSync as writeFileSync5,
2160
+ writeFileSync as writeFileSync6,
1860
2161
  readFileSync as readFileSync6,
1861
2162
  mkdirSync as mkdirSync3,
1862
2163
  watch as fsWatch
@@ -1864,7 +2165,7 @@ import {
1864
2165
  import { join as join8, resolve as resolve2 } from "path";
1865
2166
  import { homedir as homedir3 } from "os";
1866
2167
  import { spawn } from "child_process";
1867
- import chalk12 from "chalk";
2168
+ import chalk13 from "chalk";
1868
2169
  var DEBOUNCE_MS = 8e3;
1869
2170
  var DIR_POLL_MS = 5e3;
1870
2171
  function claudeEncode2(p) {
@@ -1896,7 +2197,7 @@ function fileSize(p) {
1896
2197
  function writePidFile(groundctlDir, pid) {
1897
2198
  try {
1898
2199
  mkdirSync3(groundctlDir, { recursive: true });
1899
- writeFileSync5(join8(groundctlDir, "watch.pid"), String(pid), "utf8");
2200
+ writeFileSync6(join8(groundctlDir, "watch.pid"), String(pid), "utf8");
1900
2201
  } catch {
1901
2202
  }
1902
2203
  }
@@ -1919,8 +2220,8 @@ function processAlive(pid) {
1919
2220
  async function runIngest(transcriptPath, projectPath) {
1920
2221
  const filename = transcriptPath.split("/").slice(-2).join("/");
1921
2222
  console.log(
1922
- chalk12.gray(`
1923
- [${(/* @__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}`)
1924
2225
  );
1925
2226
  try {
1926
2227
  const parsed = parseTranscript(transcriptPath, "auto", projectPath);
@@ -1976,11 +2277,11 @@ async function runIngest(transcriptPath, projectPath) {
1976
2277
  if (parsed.commits.length > 0) parts.push(`${parsed.commits.length} commit${parsed.commits.length !== 1 ? "s" : ""}`);
1977
2278
  if (newDecisions > 0) parts.push(`${newDecisions} decision${newDecisions !== 1 ? "s" : ""} captured`);
1978
2279
  const summary = parts.length > 0 ? parts.join(", ") : "no new data";
1979
- console.log(chalk12.green(` \u2713 Session ingested \u2014 ${summary}`));
2280
+ console.log(chalk13.green(` \u2713 Session ingested \u2014 ${summary}`));
1980
2281
  await syncCommand({ silent: true });
1981
- console.log(chalk12.gray(" \u21B3 PROJECT_STATE.md + AGENTS.md updated"));
2282
+ console.log(chalk13.gray(" \u21B3 PROJECT_STATE.md + AGENTS.md updated"));
1982
2283
  } catch (err) {
1983
- console.log(chalk12.red(` \u2717 Ingest failed: ${err.message}`));
2284
+ console.log(chalk13.red(` \u2717 Ingest failed: ${err.message}`));
1984
2285
  }
1985
2286
  }
1986
2287
  function startWatcher(transcriptDir, projectPath) {
@@ -2028,12 +2329,12 @@ function startWatcher(transcriptDir, projectPath) {
2028
2329
  schedule(fp);
2029
2330
  }
2030
2331
  });
2031
- 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"));
2032
2333
  console.log(
2033
- chalk12.gray(" Watching: ") + chalk12.blue(transcriptDir.replace(homedir3(), "~"))
2334
+ chalk13.gray(" Watching: ") + chalk13.blue(transcriptDir.replace(homedir3(), "~"))
2034
2335
  );
2035
- console.log(chalk12.gray(" Stability threshold: ") + chalk12.white(`${DEBOUNCE_MS / 1e3}s`));
2036
- 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"));
2037
2338
  }
2038
2339
  async function watchCommand(options) {
2039
2340
  const projectPath = options.projectPath ? resolve2(options.projectPath) : process.cwd();
@@ -2046,29 +2347,29 @@ async function watchCommand(options) {
2046
2347
  child.unref();
2047
2348
  const groundctlDir2 = join8(projectPath, ".groundctl");
2048
2349
  writePidFile(groundctlDir2, child.pid);
2049
- console.log(chalk12.green(`
2350
+ console.log(chalk13.green(`
2050
2351
  \u2713 groundctl watch running in background (PID ${child.pid})`));
2051
- console.log(chalk12.gray(` PID saved to .groundctl/watch.pid`));
2052
- 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}
2053
2354
  `));
2054
2355
  process.exit(0);
2055
2356
  }
2056
2357
  const groundctlDir = join8(projectPath, ".groundctl");
2057
2358
  const existingPid = readPidFile(groundctlDir);
2058
2359
  if (existingPid && processAlive(existingPid)) {
2059
- console.log(chalk12.yellow(`
2360
+ console.log(chalk13.yellow(`
2060
2361
  \u26A0 A watcher is already running (PID ${existingPid}).`));
2061
- console.log(chalk12.gray(` To stop it: kill ${existingPid}
2362
+ console.log(chalk13.gray(` To stop it: kill ${existingPid}
2062
2363
  `));
2063
2364
  process.exit(1);
2064
2365
  }
2065
2366
  let transcriptDir = findTranscriptDir(projectPath);
2066
2367
  if (!transcriptDir) {
2067
- console.log(chalk12.bold("\n groundctl watch\n"));
2368
+ console.log(chalk13.bold("\n groundctl watch\n"));
2068
2369
  console.log(
2069
- 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.")
2070
2371
  );
2071
- console.log(chalk12.gray(" Waiting for first session to start...\n"));
2372
+ console.log(chalk13.gray(" Waiting for first session to start...\n"));
2072
2373
  await new Promise((resolve3) => {
2073
2374
  const interval = setInterval(() => {
2074
2375
  const dir = findTranscriptDir(projectPath);
@@ -2083,7 +2384,7 @@ async function watchCommand(options) {
2083
2384
  startWatcher(transcriptDir, projectPath);
2084
2385
  await new Promise(() => {
2085
2386
  process.on("SIGINT", () => {
2086
- console.log(chalk12.gray("\n Watcher stopped.\n"));
2387
+ console.log(chalk13.gray("\n Watcher stopped.\n"));
2087
2388
  process.exit(0);
2088
2389
  });
2089
2390
  process.on("SIGTERM", () => {
@@ -2092,6 +2393,82 @@ async function watchCommand(options) {
2092
2393
  });
2093
2394
  }
2094
2395
 
2396
+ // src/commands/update.ts
2397
+ import chalk14 from "chalk";
2398
+ function parseProgress2(s) {
2399
+ const m = s.match(/^(\d+)\/(\d+)$/);
2400
+ if (!m) return null;
2401
+ return { done: parseInt(m[1], 10), total: parseInt(m[2], 10) };
2402
+ }
2403
+ async function updateCommand(type, nameOrId, options) {
2404
+ if (type !== "feature") {
2405
+ console.log(chalk14.red(`
2406
+ Unknown type "${type}". Use "feature".
2407
+ `));
2408
+ process.exit(1);
2409
+ }
2410
+ const db = await openDb();
2411
+ const feature = queryOne(
2412
+ db,
2413
+ `SELECT id, name FROM features
2414
+ WHERE id = ?1 OR name = ?1
2415
+ OR id LIKE ?2 OR name LIKE ?2
2416
+ ORDER BY CASE WHEN id = ?1 OR name = ?1 THEN 0 ELSE 1 END
2417
+ LIMIT 1`,
2418
+ [nameOrId, `%${nameOrId}%`]
2419
+ );
2420
+ if (!feature) {
2421
+ console.log(chalk14.red(`
2422
+ Feature "${nameOrId}" not found.
2423
+ `));
2424
+ closeDb();
2425
+ process.exit(1);
2426
+ }
2427
+ const sets = [];
2428
+ const params = [];
2429
+ if (options.description !== void 0) {
2430
+ sets.push("description = ?");
2431
+ params.push(options.description);
2432
+ }
2433
+ if (options.items !== void 0) {
2434
+ const items = options.items.split(",").map((s) => s.trim()).filter(Boolean).join(",");
2435
+ sets.push("items = ?");
2436
+ params.push(items);
2437
+ }
2438
+ if (options.progress !== void 0) {
2439
+ const p = parseProgress2(options.progress);
2440
+ if (!p) {
2441
+ console.log(chalk14.yellow(` \u26A0 --progress "${options.progress}" ignored (expected N/N format)
2442
+ `));
2443
+ } else {
2444
+ sets.push("progress_done = ?", "progress_total = ?");
2445
+ params.push(p.done, p.total);
2446
+ }
2447
+ }
2448
+ if (options.priority !== void 0) {
2449
+ sets.push("priority = ?");
2450
+ params.push(options.priority);
2451
+ }
2452
+ if (options.status !== void 0) {
2453
+ sets.push("status = ?");
2454
+ params.push(options.status);
2455
+ }
2456
+ if (sets.length === 0) {
2457
+ console.log(chalk14.yellow("\n Nothing to update \u2014 pass at least one option.\n"));
2458
+ closeDb();
2459
+ return;
2460
+ }
2461
+ sets.push("updated_at = datetime('now')");
2462
+ params.push(feature.id);
2463
+ db.run(
2464
+ `UPDATE features SET ${sets.join(", ")} WHERE id = ?`,
2465
+ params
2466
+ );
2467
+ saveDb();
2468
+ closeDb();
2469
+ console.log(chalk14.green(` \u2713 Updated: ${feature.name}`));
2470
+ }
2471
+
2095
2472
  // src/index.ts
2096
2473
  var require2 = createRequire(import.meta.url);
2097
2474
  var pkg = require2("../package.json");
@@ -2104,7 +2481,7 @@ program.command("complete <feature>").description("Mark a feature as done and re
2104
2481
  program.command("sync").description("Regenerate PROJECT_STATE.md and AGENTS.md from SQLite").action(syncCommand);
2105
2482
  program.command("next").description("Show next available (unclaimed) feature").action(nextCommand);
2106
2483
  program.command("log").description("Show session timeline").option("-s, --session <id>", "Show details for a specific session").action(logCommand);
2107
- program.command("add <type>").description("Add a feature or session (type: feature, session)").option("-n, --name <name>", "Name").option("-p, --priority <priority>", "Priority (critical, high, medium, low)").option("-d, --description <desc>", "Description").option("--agent <agent>", "Agent type for sessions").action(addCommand);
2484
+ program.command("add <type>").description("Add a feature or session (type: feature, session)").option("-n, --name <name>", "Name").option("-p, --priority <priority>", "Priority (critical, high, medium, low)").option("-d, --description <desc>", "Description").option("--agent <agent>", "Agent type for sessions").option("--items <items>", "Comma-separated list of sub-items (features only)").option("--progress <N/N>", "Progress fraction e.g. 11/11 (features only)").action(addCommand);
2108
2485
  program.command("ingest").description("Parse a transcript and write session data to SQLite").option("--source <source>", "Source agent (claude-code, codex)", "claude-code").option("--session-id <id>", "Session ID").option("--transcript <path>", "Path to transcript JSONL file (auto-detected if omitted)").option("--project-path <path>", "Project path (defaults to cwd)").option("--no-sync", "Skip regenerating markdown after ingest").action(
2109
2486
  (opts) => ingestCommand({
2110
2487
  source: opts.source,
@@ -2123,4 +2500,13 @@ program.command("watch").description("Watch for session end and auto-ingest tran
2123
2500
  projectPath: opts.projectPath
2124
2501
  })
2125
2502
  );
2503
+ program.command("update <type> <name>").description("Update a feature's description, items, progress, or priority").option("-d, --description <desc>", "New description").option("--items <items>", "Comma-separated sub-items").option("--progress <N/N>", "Progress fraction e.g. 3/5").option("-p, --priority <priority>", "New priority").option("--status <status>", "New status (pending|in_progress|done|blocked)").action(
2504
+ (type, name, opts) => updateCommand(type, name, {
2505
+ description: opts.description,
2506
+ items: opts.items,
2507
+ progress: opts.progress,
2508
+ priority: opts.priority,
2509
+ status: opts.status
2510
+ })
2511
+ );
2126
2512
  program.parse();