@towles/tool 0.0.61 → 0.0.63

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 (83) hide show
  1. package/package.json +50 -57
  2. package/src/commands/agentboard.ts +176 -0
  3. package/src/commands/{auto-claude.ts → auto-claude/index.ts} +18 -28
  4. package/src/commands/auto-claude/list.ts +114 -0
  5. package/src/commands/auto-claude/retry.test.ts +138 -0
  6. package/src/commands/auto-claude/retry.ts +139 -0
  7. package/src/commands/auto-claude/status.test.ts +147 -0
  8. package/src/commands/auto-claude/status.ts +123 -0
  9. package/src/commands/base.ts +7 -2
  10. package/src/commands/config.ts +5 -7
  11. package/src/commands/doctor.ts +111 -12
  12. package/src/commands/gh/branch.ts +4 -4
  13. package/src/commands/gh/pr.ts +1 -0
  14. package/src/commands/graph/index.ts +169 -0
  15. package/src/commands/graph.test.ts +1 -1
  16. package/src/commands/install.ts +40 -68
  17. package/src/commands/journal/daily-notes.ts +3 -3
  18. package/src/commands/journal/meeting.ts +3 -3
  19. package/src/commands/journal/note.ts +3 -3
  20. package/src/lib/auto-claude/claude-cli.ts +183 -0
  21. package/src/lib/auto-claude/config.test.ts +6 -8
  22. package/src/lib/auto-claude/config.ts +3 -4
  23. package/src/lib/auto-claude/index.ts +2 -3
  24. package/src/lib/auto-claude/labels.test.ts +85 -0
  25. package/src/lib/auto-claude/labels.ts +42 -0
  26. package/src/lib/auto-claude/pipeline-execution.test.ts +129 -33
  27. package/src/lib/auto-claude/pipeline.test.ts +2 -2
  28. package/src/lib/auto-claude/pipeline.ts +120 -36
  29. package/src/lib/auto-claude/prompt-templates/01_plan.prompt.md +68 -0
  30. package/src/lib/auto-claude/prompt-templates/{05_implement.prompt.md → 02_implement.prompt.md} +3 -2
  31. package/src/lib/auto-claude/prompt-templates/03_simplify.prompt.md +52 -0
  32. package/src/lib/auto-claude/prompt-templates/{06_review.prompt.md → 04_review.prompt.md} +29 -6
  33. package/src/lib/auto-claude/prompt-templates/index.test.ts +9 -42
  34. package/src/lib/auto-claude/prompt-templates/index.ts +13 -28
  35. package/src/lib/auto-claude/run-claude.test.ts +48 -68
  36. package/src/lib/auto-claude/shell.ts +6 -0
  37. package/src/lib/auto-claude/steps/create-pr.ts +89 -25
  38. package/src/lib/auto-claude/steps/fetch-issues.ts +4 -1
  39. package/src/lib/auto-claude/steps/implement.ts +9 -16
  40. package/src/lib/auto-claude/steps/simple-steps.ts +34 -0
  41. package/src/lib/auto-claude/steps/steps.test.ts +68 -63
  42. package/src/lib/auto-claude/templates.test.ts +91 -0
  43. package/src/lib/auto-claude/templates.ts +34 -0
  44. package/src/lib/auto-claude/test-helpers.ts +2 -1
  45. package/src/lib/auto-claude/utils-execution.test.ts +9 -57
  46. package/src/lib/auto-claude/utils.test.ts +5 -9
  47. package/src/lib/auto-claude/utils.ts +27 -253
  48. package/src/lib/graph/analyzer.test.ts +451 -0
  49. package/src/lib/graph/analyzer.ts +165 -0
  50. package/src/lib/graph/index.ts +24 -0
  51. package/src/lib/graph/labels.ts +87 -0
  52. package/src/lib/graph/parser.test.ts +150 -0
  53. package/src/lib/graph/parser.ts +65 -0
  54. package/src/lib/graph/render.ts +25 -0
  55. package/src/lib/graph/server.ts +70 -0
  56. package/src/lib/graph/sessions.ts +104 -0
  57. package/src/lib/graph/tools.ts +90 -0
  58. package/src/lib/graph/treemap.ts +211 -0
  59. package/src/lib/graph/types.ts +80 -0
  60. package/src/lib/install/claude-settings.ts +64 -0
  61. package/src/lib/journal/editor.ts +33 -0
  62. package/src/lib/journal/fs.ts +13 -0
  63. package/src/lib/journal/index.ts +11 -0
  64. package/src/lib/journal/paths.ts +106 -0
  65. package/src/lib/journal/{utils.ts → templates.ts} +3 -151
  66. package/src/utils/fs.ts +19 -0
  67. package/src/utils/git/exec.ts +18 -0
  68. package/src/utils/git/gh-cli-wrapper.test.ts +47 -8
  69. package/src/utils/git/gh-cli-wrapper.ts +31 -19
  70. package/src/utils/render.ts +3 -1
  71. package/src/commands/graph.ts +0 -970
  72. package/src/lib/auto-claude/prompt-templates/01_research.prompt.md +0 -21
  73. package/src/lib/auto-claude/prompt-templates/02_plan.prompt.md +0 -27
  74. package/src/lib/auto-claude/prompt-templates/03_plan-annotations.prompt.md +0 -15
  75. package/src/lib/auto-claude/prompt-templates/04_plan-implementation.prompt.md +0 -35
  76. package/src/lib/auto-claude/prompt-templates/07_refresh.prompt.md +0 -30
  77. package/src/lib/auto-claude/steps/plan-annotations.ts +0 -54
  78. package/src/lib/auto-claude/steps/plan-implementation.ts +0 -14
  79. package/src/lib/auto-claude/steps/plan.ts +0 -14
  80. package/src/lib/auto-claude/steps/refresh.ts +0 -114
  81. package/src/lib/auto-claude/steps/remove-label.ts +0 -22
  82. package/src/lib/auto-claude/steps/research.ts +0 -21
  83. package/src/lib/auto-claude/steps/review.ts +0 -14
@@ -0,0 +1,139 @@
1
+ import { rmSync } from "node:fs";
2
+ import { join } from "node:path";
3
+
4
+ import { Flags } from "@oclif/core";
5
+ import consola from "consola";
6
+ import { colors } from "consola/utils";
7
+ import prompts from "prompts";
8
+
9
+ import { BaseCommand } from "../base.js";
10
+ import { initConfig } from "../../lib/auto-claude/index.js";
11
+ import { LABELS, removeLabel, setLabel } from "../../lib/auto-claude/labels.js";
12
+ import { getIssues, isGithubCliInstalled } from "../../utils/git/gh-cli-wrapper.js";
13
+ import type { Issue } from "../../utils/git/gh-cli-wrapper.js";
14
+
15
+ /**
16
+ * Core retry logic: swap labels on failed issues to re-trigger the pipeline.
17
+ * Extracted for testability.
18
+ */
19
+ export async function retryIssues(
20
+ repo: string,
21
+ triggerLabel: string,
22
+ selected: Issue[],
23
+ clean: boolean,
24
+ ): Promise<number> {
25
+ for (const issue of selected) {
26
+ consola.info(`Retrying issue #${issue.number}: ${issue.title}`);
27
+
28
+ await removeLabel(repo, issue.number, LABELS.failed);
29
+ consola.success(` Removed '${LABELS.failed}' label`);
30
+
31
+ await setLabel(repo, issue.number, triggerLabel);
32
+ consola.success(` Added '${triggerLabel}' label`);
33
+
34
+ if (clean) {
35
+ const artifactDir = join(process.cwd(), `.auto-claude/issue-${issue.number}`);
36
+ try {
37
+ rmSync(artifactDir, { recursive: true, force: true });
38
+ consola.success(` Cleaned artifacts: ${artifactDir}`);
39
+ } catch {
40
+ consola.warn(` Could not clean artifacts: ${artifactDir}`);
41
+ }
42
+ }
43
+ }
44
+
45
+ return selected.length;
46
+ }
47
+
48
+ export default class AutoClaudeRetry extends BaseCommand {
49
+ static override description = "Retry failed auto-claude issues by swapping labels";
50
+
51
+ static override examples = [
52
+ {
53
+ description: "Interactively pick failed issues to retry",
54
+ command: "<%= config.bin %> auto-claude retry",
55
+ },
56
+ {
57
+ description: "Retry a specific issue",
58
+ command: "<%= config.bin %> auto-claude retry --issue 42",
59
+ },
60
+ {
61
+ description: "Retry and clean local artifacts",
62
+ command: "<%= config.bin %> auto-claude retry --issue 42 --clean",
63
+ },
64
+ ];
65
+
66
+ static override flags = {
67
+ ...BaseCommand.baseFlags,
68
+ issue: Flags.integer({
69
+ char: "i",
70
+ description: "Issue number to retry",
71
+ }),
72
+ clean: Flags.boolean({
73
+ description: "Delete local .auto-claude/issue-{N}/ artifacts",
74
+ default: false,
75
+ }),
76
+ };
77
+
78
+ async run(): Promise<void> {
79
+ const { flags } = await this.parse(AutoClaudeRetry);
80
+
81
+ const cfg = await initConfig();
82
+
83
+ const cliInstalled = await isGithubCliInstalled();
84
+ if (!cliInstalled) {
85
+ this.error("GitHub CLI (gh) is not installed");
86
+ }
87
+
88
+ const failedIssues = await getIssues({ cwd: process.cwd(), label: LABELS.failed });
89
+
90
+ let selected: Issue[];
91
+
92
+ if (flags.issue) {
93
+ const match = failedIssues.find((i) => i.number === flags.issue);
94
+ if (!match) {
95
+ this.error(`Issue #${flags.issue} not found with '${LABELS.failed}' label`);
96
+ }
97
+ selected = [match];
98
+ } else {
99
+ if (failedIssues.length === 0) {
100
+ consola.info(`No open issues with '${LABELS.failed}' label`);
101
+ return;
102
+ }
103
+
104
+ consola.info(colors.yellow(`${failedIssues.length} failed issue(s) found`));
105
+
106
+ const choices = failedIssues.map((issue) => ({
107
+ title: `#${issue.number} ${issue.title}`,
108
+ value: issue.number,
109
+ }));
110
+
111
+ const result = await prompts(
112
+ {
113
+ name: "selected",
114
+ message: "Select issues to retry:",
115
+ type: "multiselect",
116
+ choices,
117
+ instructions: false,
118
+ hint: "- Space to select, Enter to confirm",
119
+ },
120
+ {
121
+ onCancel: () => {
122
+ consola.info(colors.dim("Canceled"));
123
+ this.exit(0);
124
+ },
125
+ },
126
+ );
127
+
128
+ if (!result.selected || result.selected.length === 0) {
129
+ consola.info(colors.dim("No issues selected"));
130
+ return;
131
+ }
132
+
133
+ selected = failedIssues.filter((i) => result.selected.includes(i.number));
134
+ }
135
+
136
+ const count = await retryIssues(cfg.repo, cfg.triggerLabel, selected, flags.clean);
137
+ consola.box(`Retried ${count} issue(s)`);
138
+ }
139
+ }
@@ -0,0 +1,147 @@
1
+ import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
2
+ import { tmpdir } from "node:os";
3
+ import { join } from "node:path";
4
+
5
+ import { afterAll, beforeAll, describe, expect, it } from "vitest";
6
+
7
+ import type { Issue } from "../../utils/git/gh-cli-wrapper.js";
8
+ import { LABELS } from "../../lib/auto-claude/labels.js";
9
+ import { checkArtifacts, findAcLabel, formatIssueStatus } from "./status.js";
10
+
11
+ // ── Test fixtures ──
12
+
13
+ function makeIssue(overrides: Partial<Issue> = {}): Issue {
14
+ return {
15
+ number: 1,
16
+ title: "Test issue",
17
+ state: "open",
18
+ labels: [{ name: "auto-claude", color: "000000" }],
19
+ ...overrides,
20
+ };
21
+ }
22
+
23
+ // ── checkArtifacts ──
24
+
25
+ describe("checkArtifacts", () => {
26
+ let tmpDir: string;
27
+
28
+ beforeAll(() => {
29
+ tmpDir = mkdtempSync(join(tmpdir(), "ac-status-test-"));
30
+ });
31
+
32
+ afterAll(() => {
33
+ rmSync(tmpDir, { recursive: true, force: true });
34
+ });
35
+
36
+ it("returns all false when no artifacts exist", () => {
37
+ const result = checkArtifacts(99, tmpDir);
38
+ expect(result).toHaveLength(4);
39
+ for (const a of result) {
40
+ expect(a.exists).toBe(false);
41
+ }
42
+ });
43
+
44
+ it("detects existing artifacts", () => {
45
+ const issueDir = join(tmpDir, ".auto-claude/issue-42");
46
+ mkdirSync(issueDir, { recursive: true });
47
+ writeFileSync(join(issueDir, "plan.md"), "# Plan");
48
+ writeFileSync(join(issueDir, "review.md"), "# Review");
49
+
50
+ const result = checkArtifacts(42, tmpDir);
51
+ const planArtifact = result.find((a) => a.name === "plan.md");
52
+ const reviewArtifact = result.find((a) => a.name === "review.md");
53
+ const summaryArtifact = result.find((a) => a.name === "completed-summary.md");
54
+
55
+ expect(planArtifact?.exists).toBe(true);
56
+ expect(reviewArtifact?.exists).toBe(true);
57
+ expect(summaryArtifact?.exists).toBe(false);
58
+ });
59
+ });
60
+
61
+ // ── findAcLabel ──
62
+
63
+ describe("findAcLabel", () => {
64
+ it("returns specific status label when present", () => {
65
+ const issue = makeIssue({
66
+ labels: [
67
+ { name: "auto-claude", color: "000000" },
68
+ { name: LABELS.inProgress, color: "ffff00" },
69
+ ],
70
+ });
71
+ expect(findAcLabel(issue)).toBe(LABELS.inProgress);
72
+ });
73
+
74
+ it("falls back to auto-claude when no status label", () => {
75
+ const issue = makeIssue({
76
+ labels: [{ name: "auto-claude", color: "000000" }],
77
+ });
78
+ expect(findAcLabel(issue)).toBe("auto-claude");
79
+ });
80
+
81
+ it("prefers in-progress over other labels", () => {
82
+ const issue = makeIssue({
83
+ labels: [
84
+ { name: LABELS.inProgress, color: "ffff00" },
85
+ { name: LABELS.success, color: "00ff00" },
86
+ ],
87
+ });
88
+ expect(findAcLabel(issue)).toBe(LABELS.inProgress);
89
+ });
90
+ });
91
+
92
+ // ── formatIssueStatus ──
93
+
94
+ describe("formatIssueStatus", () => {
95
+ it("includes issue number and title", () => {
96
+ const issue = makeIssue({ number: 7, title: "Fix widget" });
97
+ const artifacts = [
98
+ { name: "plan.md", exists: false },
99
+ { name: "completed-summary.md", exists: false },
100
+ { name: "simplify-summary.md", exists: false },
101
+ { name: "review.md", exists: false },
102
+ ];
103
+ const output = formatIssueStatus(issue, artifacts);
104
+ expect(output).toContain("#7");
105
+ expect(output).toContain("Fix widget");
106
+ });
107
+
108
+ it("includes status tag", () => {
109
+ const issue = makeIssue({
110
+ labels: [{ name: LABELS.failed, color: "ff0000" }],
111
+ });
112
+ const artifacts = [
113
+ { name: "plan.md", exists: false },
114
+ { name: "completed-summary.md", exists: false },
115
+ { name: "simplify-summary.md", exists: false },
116
+ { name: "review.md", exists: false },
117
+ ];
118
+ const output = formatIssueStatus(issue, artifacts);
119
+ expect(output).toContain("failed");
120
+ });
121
+
122
+ it("shows completed artifacts", () => {
123
+ const issue = makeIssue();
124
+ const artifacts = [
125
+ { name: "plan.md", exists: true },
126
+ { name: "completed-summary.md", exists: true },
127
+ { name: "simplify-summary.md", exists: false },
128
+ { name: "review.md", exists: false },
129
+ ];
130
+ const output = formatIssueStatus(issue, artifacts);
131
+ expect(output).toContain("plan.md");
132
+ expect(output).toContain("completed-summary.md");
133
+ expect(output).not.toContain("simplify-summary.md");
134
+ });
135
+
136
+ it("omits artifacts line when none exist", () => {
137
+ const issue = makeIssue();
138
+ const artifacts = [
139
+ { name: "plan.md", exists: false },
140
+ { name: "completed-summary.md", exists: false },
141
+ { name: "simplify-summary.md", exists: false },
142
+ { name: "review.md", exists: false },
143
+ ];
144
+ const output = formatIssueStatus(issue, artifacts);
145
+ expect(output).not.toContain("artifacts:");
146
+ });
147
+ });
@@ -0,0 +1,123 @@
1
+ import { existsSync } from "node:fs";
2
+ import { join } from "node:path";
3
+
4
+ import consola from "consola";
5
+ import { colors } from "consola/utils";
6
+
7
+ import type { Issue } from "../../utils/git/gh-cli-wrapper.js";
8
+ import { getIssues, isGithubCliInstalled } from "../../utils/git/gh-cli-wrapper.js";
9
+ import { ARTIFACTS } from "../../lib/auto-claude/prompt-templates/index.js";
10
+ import { LABELS } from "../../lib/auto-claude/labels.js";
11
+ import { BaseCommand } from "../base.js";
12
+
13
+ /** All labels that indicate an issue is part of the auto-claude pipeline. */
14
+ const ALL_AC_LABELS = ["auto-claude", ...Object.values(LABELS)] as const;
15
+
16
+ /** Display config per label: short name + color function. */
17
+ const LABEL_DISPLAY: Record<string, { status: string; color: (t: string) => string }> = {
18
+ [LABELS.inProgress]: { status: "in-progress", color: colors.yellow },
19
+ [LABELS.success]: { status: "success", color: colors.green },
20
+ [LABELS.failed]: { status: "failed", color: colors.red },
21
+ [LABELS.review]: { status: "review", color: colors.blue },
22
+ };
23
+
24
+ const DEFAULT_DISPLAY = { status: "queued", color: colors.dim };
25
+
26
+ interface ArtifactStatus {
27
+ name: string;
28
+ exists: boolean;
29
+ }
30
+
31
+ /** Pipeline artifacts to check, in pipeline order. */
32
+ const CHECKED_ARTIFACTS = [
33
+ ARTIFACTS.plan,
34
+ ARTIFACTS.completedSummary,
35
+ ARTIFACTS.simplifySummary,
36
+ ARTIFACTS.review,
37
+ ] as const;
38
+
39
+ /** Check which pipeline artifacts exist locally for an issue. */
40
+ export function checkArtifacts(issueNumber: number, cwd: string): ArtifactStatus[] {
41
+ const issueDir = join(cwd, `.auto-claude/issue-${issueNumber}`);
42
+ return CHECKED_ARTIFACTS.map((name) => ({ name, exists: existsSync(join(issueDir, name)) }));
43
+ }
44
+
45
+ /** Find the most specific auto-claude label on an issue. */
46
+ export function findAcLabel(issue: Issue): string {
47
+ const labelNames = issue.labels.map((l) => l.name);
48
+ // Prefer the most specific status label; fall back to generic "auto-claude"
49
+ for (const label of Object.values(LABELS)) {
50
+ if (labelNames.includes(label)) return label;
51
+ }
52
+ return "auto-claude";
53
+ }
54
+
55
+ /** Format a single issue for display. */
56
+ export function formatIssueStatus(issue: Issue, artifacts: ArtifactStatus[]): string {
57
+ const label = findAcLabel(issue);
58
+ const { status, color } = LABEL_DISPLAY[label] ?? DEFAULT_DISPLAY;
59
+ const statusTag = color(`[${status}]`);
60
+
61
+ const parts: string[] = [`#${issue.number} ${issue.title} ${statusTag}`];
62
+
63
+ const completedSteps = artifacts.filter((a) => a.exists).map((a) => a.name);
64
+ if (completedSteps.length > 0) {
65
+ parts.push(colors.dim(` artifacts: ${completedSteps.join(", ")}`));
66
+ }
67
+
68
+ return parts.join("\n");
69
+ }
70
+
71
+ /** Fetch issues across all auto-claude labels, deduplicating by issue number. */
72
+ export async function fetchAllAcIssues(cwd: string): Promise<Issue[]> {
73
+ const issueMap = new Map<number, Issue>();
74
+
75
+ const results = await Promise.all(ALL_AC_LABELS.map((label) => getIssues({ cwd, label })));
76
+
77
+ for (const issues of results) {
78
+ for (const issue of issues) {
79
+ if (!issueMap.has(issue.number)) {
80
+ issueMap.set(issue.number, issue);
81
+ }
82
+ }
83
+ }
84
+
85
+ // Sort by issue number ascending
86
+ return [...issueMap.values()].sort((a, b) => a.number - b.number);
87
+ }
88
+
89
+ export default class AutoClaudeStatus extends BaseCommand {
90
+ static override description = "Show pipeline status for auto-claude issues";
91
+
92
+ static override aliases = ["ac:status"];
93
+
94
+ static override examples = [
95
+ {
96
+ description: "Show status of all auto-claude issues",
97
+ command: "<%= config.bin %> auto-claude status",
98
+ },
99
+ ];
100
+
101
+ async run(): Promise<void> {
102
+ const cliInstalled = await isGithubCliInstalled();
103
+ if (!cliInstalled) {
104
+ this.error("GitHub CLI (gh) is not installed");
105
+ }
106
+
107
+ const cwd = process.cwd();
108
+ const issues = await fetchAllAcIssues(cwd);
109
+
110
+ if (issues.length === 0) {
111
+ consola.info("No auto-claude issues found");
112
+ return;
113
+ }
114
+
115
+ consola.info(colors.bold(`Auto-Claude Pipeline Status (${issues.length} issue(s))`));
116
+ consola.log("");
117
+
118
+ for (const issue of issues) {
119
+ const artifacts = checkArtifacts(issue.number, cwd);
120
+ consola.log(formatIssueStatus(issue, artifacts));
121
+ }
122
+ }
123
+ }
@@ -15,13 +15,18 @@ export abstract class BaseCommand extends Command {
15
15
  }),
16
16
  };
17
17
 
18
- protected settings!: SettingsFile;
18
+ protected settingsFile!: SettingsFile;
19
+
20
+ /** Shortcut to avoid `this.settingsFile.settings.X` stutter */
21
+ protected get userSettings() {
22
+ return this.settingsFile.settings;
23
+ }
19
24
 
20
25
  /**
21
26
  * Called before run(). Loads user settings.
22
27
  */
23
28
  async init(): Promise<void> {
24
29
  await super.init();
25
- this.settings = await loadSettings();
30
+ this.settingsFile = await loadSettings();
26
31
  }
27
32
  }
@@ -18,18 +18,16 @@ export default class Config extends BaseCommand {
18
18
  consola.info("Configuration");
19
19
  consola.log("");
20
20
 
21
- consola.info(`Settings File: ${this.settings.path}`);
21
+ consola.info(`Settings File: ${this.settingsFile.path}`);
22
22
  consola.log("");
23
23
 
24
24
  consola.warn("User Config:");
25
+ consola.log(` Daily Path Template: ${this.userSettings.journalSettings.dailyPathTemplate}`);
25
26
  consola.log(
26
- ` Daily Path Template: ${this.settings.settings.journalSettings.dailyPathTemplate}`,
27
+ ` Meeting Path Template: ${this.userSettings.journalSettings.meetingPathTemplate}`,
27
28
  );
28
- consola.log(
29
- ` Meeting Path Template: ${this.settings.settings.journalSettings.meetingPathTemplate}`,
30
- );
31
- consola.log(` Note Path Template: ${this.settings.settings.journalSettings.notePathTemplate}`);
32
- consola.log(` Editor: ${this.settings.settings.preferredEditor}`);
29
+ consola.log(` Note Path Template: ${this.userSettings.journalSettings.notePathTemplate}`);
30
+ consola.log(` Editor: ${this.userSettings.preferredEditor}`);
33
31
  consola.log("");
34
32
 
35
33
  consola.warn("Working Directory:");
@@ -1,5 +1,8 @@
1
+ import { existsSync, readFileSync } from "node:fs";
2
+ import { resolve, join } from "node:path";
3
+ import consola from "consola";
1
4
  import { x } from "tinyexec";
2
- import pc from "picocolors";
5
+ import { colors } from "consola/utils";
3
6
  import { BaseCommand } from "./base.js";
4
7
 
5
8
  interface CheckResult {
@@ -31,25 +34,32 @@ export default class Doctor extends BaseCommand {
31
34
  this.checkCommand("node", ["--version"], /v?([\d.]+)/),
32
35
  this.checkCommand("bun", ["--version"], /([\d.]+)/),
33
36
  this.checkCommand("pnpm", ["--version"], /([\d.]+)/),
37
+ this.checkCommand("claude", ["--version"], /([\d.]+)/),
38
+ this.checkCommand("tmux", ["-V"], /tmux ([\d.]+)/),
39
+ this.checkCommand("ttyd", ["--version"], /ttyd version ([\d.]+)/, true),
34
40
  ]);
35
41
 
36
42
  // Display results
37
43
  for (const check of checks) {
38
- const icon = check.ok ? pc.green("✓") : pc.red("✗");
44
+ const icon = check.ok
45
+ ? colors.green("✓")
46
+ : check.warning
47
+ ? colors.yellow("⚠")
48
+ : colors.red("✗");
39
49
  const version = check.version ?? "not found";
40
50
  this.log(`${icon} ${check.name}: ${version}`);
41
51
  if (check.warning) {
42
- this.log(` ${pc.yellow("⚠")} ${check.warning}`);
52
+ this.log(` ${colors.yellow("⚠")} ${check.warning}`);
43
53
  }
44
54
  }
45
55
 
46
56
  // Check gh auth
47
57
  this.log("");
48
58
  const ghAuth = await this.checkGhAuth();
49
- const authIcon = ghAuth.ok ? pc.green("✓") : pc.yellow("⚠");
59
+ const authIcon = ghAuth.ok ? colors.green("✓") : colors.yellow("⚠");
50
60
  this.log(`${authIcon} gh auth: ${ghAuth.ok ? "authenticated" : "not authenticated"}`);
51
61
  if (!ghAuth.ok) {
52
- this.log(` ${pc.dim("Run: gh auth login")}`);
62
+ this.log(` ${colors.dim("Run: gh auth login")}`);
53
63
  }
54
64
 
55
65
  // Node version check
@@ -58,7 +68,7 @@ export default class Doctor extends BaseCommand {
58
68
  const major = Number.parseInt(nodeCheck.version.split(".")[0], 10);
59
69
  if (major < 18) {
60
70
  this.log("");
61
- this.log(`${pc.yellow("⚠")} Node.js 18+ recommended (found ${nodeCheck.version})`);
71
+ this.log(`${colors.yellow("⚠")} Node.js 18+ recommended (found ${nodeCheck.version})`);
62
72
  }
63
73
  }
64
74
 
@@ -66,21 +76,41 @@ export default class Doctor extends BaseCommand {
66
76
  this.log("");
67
77
  const pluginChecks = await this.checkClaudePlugins();
68
78
  for (const check of pluginChecks) {
69
- const icon = check.ok ? pc.green("✓") : pc.red("✗");
79
+ const icon = check.ok ? colors.green("✓") : colors.red("✗");
70
80
  const status = check.ok ? "installed" : "not installed";
71
81
  this.log(`${icon} claude plugin ${check.name}: ${status}`);
72
82
  if (!check.ok && check.installHint) {
73
- this.log(` ${pc.dim(check.installHint)}`);
83
+ this.log(` ${colors.dim(check.installHint)}`);
84
+ }
85
+ }
86
+
87
+ // AgentBoard checks
88
+ this.log("");
89
+ this.log(colors.bold("AgentBoard:"));
90
+ const agentboardChecks = this.checkAgentBoard();
91
+ for (const check of agentboardChecks) {
92
+ const icon = check.ok
93
+ ? colors.green("✓")
94
+ : check.warning
95
+ ? colors.yellow("⚠")
96
+ : colors.red("✗");
97
+ this.log(`${icon} ${check.name}: ${check.value}`);
98
+ if (check.hint) {
99
+ this.log(` ${colors.dim(check.hint)}`);
74
100
  }
75
101
  }
76
102
 
77
103
  // Summary
78
- const allOk = checks.every((c) => c.ok) && ghAuth.ok && pluginChecks.every((c) => c.ok);
104
+ const allOk =
105
+ checks.every((c) => c.ok || !!c.warning) &&
106
+ ghAuth.ok &&
107
+ pluginChecks.every((c) => c.ok) &&
108
+ agentboardChecks.every((c) => c.ok || !!c.warning);
79
109
  this.log("");
80
110
  if (allOk) {
81
- this.log(pc.green("All checks passed!"));
111
+ this.log(colors.green("All checks passed!"));
82
112
  } else {
83
- this.log(pc.yellow("Some checks failed. See above for details."));
113
+ this.log(colors.yellow("Some checks failed. See above for details."));
84
114
  }
85
115
  }
86
116
 
@@ -88,6 +118,7 @@ export default class Doctor extends BaseCommand {
88
118
  name: string,
89
119
  args: string[],
90
120
  versionPattern: RegExp,
121
+ optional = false,
91
122
  ): Promise<CheckResult> {
92
123
  try {
93
124
  // tinyexec is safe - uses execFile internally, no shell injection risk
@@ -100,7 +131,13 @@ export default class Doctor extends BaseCommand {
100
131
  ok: true,
101
132
  };
102
133
  } catch {
103
- return { name, version: null, ok: false };
134
+ consola.debug(`Tool check failed for "${name}"`);
135
+ return {
136
+ name,
137
+ version: null,
138
+ ok: optional,
139
+ warning: optional ? "optional, not installed" : undefined,
140
+ };
104
141
  }
105
142
  }
106
143
 
@@ -110,10 +147,71 @@ export default class Doctor extends BaseCommand {
110
147
  const result = await x("gh", ["auth", "status"]);
111
148
  return { ok: result.exitCode === 0 };
112
149
  } catch {
150
+ consola.debug("GitHub CLI auth check failed");
113
151
  return { ok: false };
114
152
  }
115
153
  }
116
154
 
155
+ private checkAgentBoard(): {
156
+ name: string;
157
+ value: string;
158
+ ok: boolean;
159
+ warning?: string;
160
+ hint?: string;
161
+ }[] {
162
+ const results: { name: string; value: string; ok: boolean; warning?: string; hint?: string }[] =
163
+ [];
164
+
165
+ const defaultDataDir = resolve(
166
+ process.env.XDG_CONFIG_HOME ?? resolve(process.env.HOME ?? "~", ".config"),
167
+ "towles-tool",
168
+ "agentboard",
169
+ );
170
+ const dataDir = process.env.AGENTBOARD_DATA_DIR ?? defaultDataDir;
171
+ const dbPath = join(dataDir, "agentboard.db");
172
+ const configPath = join(dataDir, "config.json");
173
+
174
+ // DB exists
175
+ const dbExists = existsSync(dbPath);
176
+ results.push({
177
+ name: "database",
178
+ value: dbExists ? dbPath : "not found",
179
+ ok: dbExists,
180
+ hint: dbExists ? undefined : "Run: tt ag (starts server and creates DB automatically)",
181
+ });
182
+
183
+ // Config exists with repoPaths
184
+ let repoPaths: string[] = [];
185
+ if (existsSync(configPath)) {
186
+ try {
187
+ const config = JSON.parse(readFileSync(configPath, "utf-8"));
188
+ repoPaths = config.repoPaths ?? [];
189
+ } catch {
190
+ // Corrupted config
191
+ }
192
+ }
193
+
194
+ results.push({
195
+ name: "scan paths",
196
+ value: repoPaths.length > 0 ? repoPaths.join(", ") : "none configured",
197
+ ok: repoPaths.length > 0,
198
+ warning: repoPaths.length === 0 ? "no scan paths" : undefined,
199
+ hint:
200
+ repoPaths.length === 0
201
+ ? "Run: tt ag → open Workspaces → run the onboarding wizard"
202
+ : undefined,
203
+ });
204
+
205
+ // Data directory
206
+ results.push({
207
+ name: "data dir",
208
+ value: dataDir,
209
+ ok: true,
210
+ });
211
+
212
+ return results;
213
+ }
214
+
117
215
  private async checkClaudePlugins(): Promise<
118
216
  { name: string; ok: boolean; installHint?: string }[]
119
217
  > {
@@ -136,6 +234,7 @@ export default class Doctor extends BaseCommand {
136
234
  installHint: installedIds.has(p.id) ? undefined : `Run: ${p.installCmd}`,
137
235
  }));
138
236
  } catch {
237
+ consola.debug("Failed to list Claude plugins");
139
238
  return requiredPlugins.map((p) => ({
140
239
  name: p.name,
141
240
  ok: false,
@@ -1,15 +1,14 @@
1
1
  import { Flags } from "@oclif/core";
2
+ import consola from "consola";
2
3
  import prompts from "prompts";
3
4
  import type { Choice } from "prompts";
4
5
  import { colors } from "consola/utils";
5
6
  import { Fzf } from "fzf";
6
- import consola from "consola";
7
-
8
- import { exec } from "tinyexec";
9
7
 
10
8
  import { BaseCommand } from "../base.js";
11
9
  import type { Issue } from "../../utils/git/gh-cli-wrapper.js";
12
10
  import { getIssues, isGithubCliInstalled } from "../../utils/git/gh-cli-wrapper.js";
11
+ import { git } from "../../utils/git/exec.js";
13
12
  import { createBranchNameFromIssue } from "../../utils/git/branch-name.js";
14
13
  import { getTerminalColumns, limitText, printWithHexColor } from "../../utils/render.js";
15
14
 
@@ -131,8 +130,9 @@ export default class GhBranch extends BaseCommand {
131
130
  );
132
131
 
133
132
  const branchName = createBranchNameFromIssue(selectedIssue);
134
- await exec("git", ["checkout", "-b", branchName]);
133
+ await git(["checkout", "-b", branchName]);
135
134
  } catch {
135
+ consola.debug("Branch checkout failed");
136
136
  this.exit(1);
137
137
  }
138
138
  }