claude-all-hands 1.0.1 → 1.0.3

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 (170) hide show
  1. package/.claude/agents/code-simplifier.md +52 -0
  2. package/.claude/agents/curator.md +186 -246
  3. package/.claude/agents/documentation-taxonomist.md +255 -0
  4. package/.claude/agents/documentation-writer.md +366 -0
  5. package/.claude/agents/planner.md +123 -166
  6. package/.claude/agents/researcher.md +58 -41
  7. package/.claude/agents/surveyor.md +81 -0
  8. package/.claude/agents/worker.md +74 -0
  9. package/.claude/commands/continue.md +122 -0
  10. package/.claude/commands/create-skill.md +107 -0
  11. package/.claude/commands/create-specialist.md +111 -0
  12. package/.claude/commands/curator-audit.md +4 -0
  13. package/.claude/commands/debug.md +183 -0
  14. package/.claude/commands/docs-adjust.md +214 -0
  15. package/.claude/commands/docs-audit.md +172 -0
  16. package/.claude/commands/docs-init.md +210 -0
  17. package/.claude/commands/plan.md +199 -102
  18. package/.claude/commands/validate.md +11 -0
  19. package/.claude/commands/whats-next.md +106 -134
  20. package/.claude/envoy/README.md +5 -5
  21. package/.claude/envoy/envoy +11 -14
  22. package/.claude/envoy/package-lock.json +1594 -0
  23. package/.claude/envoy/package.json +38 -0
  24. package/.claude/envoy/src/cli.ts +126 -0
  25. package/.claude/envoy/src/commands/base.ts +216 -0
  26. package/.claude/envoy/src/commands/docs.ts +881 -0
  27. package/.claude/envoy/src/commands/gemini.ts +999 -0
  28. package/.claude/envoy/src/commands/git.ts +639 -0
  29. package/.claude/envoy/src/commands/index.ts +73 -0
  30. package/.claude/envoy/src/commands/knowledge.ts +178 -0
  31. package/.claude/envoy/src/commands/perplexity.ts +129 -0
  32. package/.claude/envoy/src/commands/plan/core.ts +134 -0
  33. package/.claude/envoy/src/commands/plan/findings.ts +446 -0
  34. package/.claude/envoy/src/commands/plan/gates.ts +672 -0
  35. package/.claude/envoy/src/commands/plan/index.ts +135 -0
  36. package/.claude/envoy/src/commands/plan/lifecycle.ts +648 -0
  37. package/.claude/envoy/src/commands/plan/plan-file.ts +138 -0
  38. package/.claude/envoy/src/commands/plan/prompts.ts +285 -0
  39. package/.claude/envoy/src/commands/plan/protocols.ts +166 -0
  40. package/.claude/envoy/src/commands/repomix.ts +99 -0
  41. package/.claude/envoy/src/commands/tavily.ts +220 -0
  42. package/.claude/envoy/src/commands/xai.ts +168 -0
  43. package/.claude/envoy/src/lib/ast-queries.ts +261 -0
  44. package/.claude/envoy/src/lib/design.ts +41 -0
  45. package/.claude/envoy/src/lib/feedback-schemas.ts +154 -0
  46. package/.claude/envoy/src/lib/findings.ts +215 -0
  47. package/.claude/envoy/src/lib/gates.ts +572 -0
  48. package/.claude/envoy/src/lib/git.ts +132 -0
  49. package/.claude/envoy/src/lib/index.ts +188 -0
  50. package/.claude/envoy/src/lib/knowledge.ts +646 -0
  51. package/.claude/envoy/src/lib/markdown.ts +75 -0
  52. package/.claude/envoy/src/lib/observability.ts +262 -0
  53. package/.claude/envoy/src/lib/paths.ts +130 -0
  54. package/.claude/envoy/src/lib/plan-io.ts +117 -0
  55. package/.claude/envoy/src/lib/prompts.ts +231 -0
  56. package/.claude/envoy/src/lib/protocols.ts +314 -0
  57. package/.claude/envoy/src/lib/repomix.ts +133 -0
  58. package/.claude/envoy/src/lib/retry.ts +138 -0
  59. package/.claude/envoy/src/lib/tree-sitter-utils.ts +301 -0
  60. package/.claude/envoy/src/lib/watcher.ts +167 -0
  61. package/.claude/envoy/src/types/tree-sitter.d.ts +76 -0
  62. package/.claude/envoy/tsconfig.json +21 -0
  63. package/.claude/hooks/scripts/enforce_research_fetch.py +1 -1
  64. package/.claude/hooks/scripts/scan_agents.py +62 -0
  65. package/.claude/hooks/scripts/scan_commands.py +50 -0
  66. package/.claude/hooks/scripts/scan_skills.py +46 -70
  67. package/.claude/hooks/scripts/validate_artifacts.py +128 -0
  68. package/.claude/hooks/startup.sh +26 -24
  69. package/.claude/protocols/bug-discovery.yaml +55 -0
  70. package/.claude/protocols/debugging.yaml +51 -0
  71. package/.claude/protocols/discovery.yaml +53 -0
  72. package/.claude/protocols/implementation.yaml +84 -0
  73. package/.claude/settings.json +38 -97
  74. package/.claude/skills/brainstorming/SKILL.md +54 -0
  75. package/.claude/skills/commands-development/SKILL.md +630 -0
  76. package/.claude/skills/commands-development/references/arguments.md +252 -0
  77. package/.claude/skills/commands-development/references/patterns.md +796 -0
  78. package/.claude/skills/commands-development/references/tool-restrictions.md +376 -0
  79. package/.claude/skills/discovery-mode/SKILL.md +108 -0
  80. package/.claude/skills/documentation-taxonomy/SKILL.md +287 -0
  81. package/.claude/skills/hooks-development/SKILL.md +332 -0
  82. package/.claude/skills/hooks-development/references/command-vs-prompt.md +269 -0
  83. package/.claude/skills/hooks-development/references/examples.md +658 -0
  84. package/.claude/skills/hooks-development/references/hook-types.md +463 -0
  85. package/.claude/skills/hooks-development/references/input-output-schemas.md +469 -0
  86. package/.claude/skills/hooks-development/references/matchers.md +470 -0
  87. package/.claude/skills/hooks-development/references/troubleshooting.md +587 -0
  88. package/.claude/skills/implementation-mode/SKILL.md +171 -0
  89. package/.claude/skills/knowledge-discovery/SKILL.md +178 -0
  90. package/.claude/skills/research-tools/SKILL.md +35 -33
  91. package/.claude/skills/skills-development/SKILL.md +192 -0
  92. package/.claude/skills/skills-development/references/api-security.md +226 -0
  93. package/.claude/skills/skills-development/references/be-clear-and-direct.md +531 -0
  94. package/.claude/skills/skills-development/references/common-patterns.md +595 -0
  95. package/.claude/skills/skills-development/references/core-principles.md +437 -0
  96. package/.claude/skills/skills-development/references/executable-code.md +175 -0
  97. package/.claude/skills/skills-development/references/iteration-and-testing.md +474 -0
  98. package/.claude/skills/skills-development/references/recommended-structure.md +168 -0
  99. package/.claude/skills/skills-development/references/skill-structure.md +372 -0
  100. package/.claude/skills/skills-development/references/use-xml-tags.md +466 -0
  101. package/.claude/skills/skills-development/references/using-scripts.md +113 -0
  102. package/.claude/skills/skills-development/references/using-templates.md +112 -0
  103. package/.claude/skills/skills-development/references/workflows-and-validation.md +510 -0
  104. package/.claude/skills/skills-development/templates/router-skill.md +73 -0
  105. package/.claude/skills/skills-development/templates/simple-skill.md +33 -0
  106. package/.claude/skills/skills-development/workflows/add-reference.md +96 -0
  107. package/.claude/skills/skills-development/workflows/add-script.md +93 -0
  108. package/.claude/skills/skills-development/workflows/add-template.md +74 -0
  109. package/.claude/skills/skills-development/workflows/add-workflow.md +120 -0
  110. package/.claude/skills/skills-development/workflows/audit-skill.md +138 -0
  111. package/.claude/skills/skills-development/workflows/create-domain-expertise-skill.md +605 -0
  112. package/.claude/skills/skills-development/workflows/create-new-skill.md +191 -0
  113. package/.claude/skills/skills-development/workflows/get-guidance.md +121 -0
  114. package/.claude/skills/skills-development/workflows/upgrade-to-router.md +161 -0
  115. package/.claude/skills/skills-development/workflows/verify-skill.md +204 -0
  116. package/.claude/skills/subagents-development/SKILL.md +325 -0
  117. package/.claude/skills/subagents-development/references/context-management.md +567 -0
  118. package/.claude/skills/subagents-development/references/debugging-agents.md +714 -0
  119. package/.claude/skills/subagents-development/references/error-handling-and-recovery.md +502 -0
  120. package/.claude/skills/subagents-development/references/evaluation-and-testing.md +374 -0
  121. package/.claude/skills/subagents-development/references/orchestration-patterns.md +591 -0
  122. package/.claude/skills/subagents-development/references/subagents.md +508 -0
  123. package/.claude/skills/subagents-development/references/writing-subagent-prompts.md +517 -0
  124. package/.claude/statusline.sh +24 -0
  125. package/bin/cli.js +150 -72
  126. package/package.json +1 -1
  127. package/.claude/agents/explorer.md +0 -62
  128. package/.claude/agents/parallel-worker.md +0 -121
  129. package/.claude/commands/curation-fix.md +0 -92
  130. package/.claude/commands/new-branch.md +0 -36
  131. package/.claude/commands/parallel-discovery.md +0 -69
  132. package/.claude/commands/parallel-orchestration.md +0 -99
  133. package/.claude/commands/plan-checkpoint.md +0 -37
  134. package/.claude/envoy/commands/__init__.py +0 -1
  135. package/.claude/envoy/commands/base.py +0 -95
  136. package/.claude/envoy/commands/parallel.py +0 -439
  137. package/.claude/envoy/commands/perplexity.py +0 -86
  138. package/.claude/envoy/commands/plans.py +0 -451
  139. package/.claude/envoy/commands/tavily.py +0 -156
  140. package/.claude/envoy/commands/vertex.py +0 -358
  141. package/.claude/envoy/commands/xai.py +0 -124
  142. package/.claude/envoy/envoy.py +0 -122
  143. package/.claude/envoy/pyrightconfig.json +0 -4
  144. package/.claude/envoy/requirements.txt +0 -2
  145. package/.claude/hooks/capture-queries.sh +0 -3
  146. package/.claude/hooks/scripts/enforce_planning.py +0 -118
  147. package/.claude/hooks/scripts/enforce_rg.py +0 -34
  148. package/.claude/hooks/scripts/validate_skill.py +0 -81
  149. package/.claude/skills/claude-envoy-curation/SKILL.md +0 -162
  150. package/.claude/skills/claude-envoy-usage/SKILL.md +0 -46
  151. package/.claude/skills/command-development/SKILL.md +0 -206
  152. package/.claude/skills/command-development/examples/simple-commands.md +0 -212
  153. package/.claude/skills/command-development/references/frontmatter-reference.md +0 -221
  154. package/.claude/skills/hook-development/SKILL.md +0 -127
  155. package/.claude/skills/hook-development/examples/command-hooks.md +0 -301
  156. package/.claude/skills/hook-development/examples/prompt-hooks.md +0 -114
  157. package/.claude/skills/hook-development/references/event-reference.md +0 -226
  158. package/.claude/skills/repomix-extraction/SKILL.md +0 -91
  159. package/.claude/skills/skill-development/SKILL.md +0 -168
  160. package/.claude/skills/skill-development/examples/complete-skill-examples.md +0 -281
  161. package/.claude/skills/skill-development/references/progressive-disclosure.md +0 -141
  162. package/.claude/skills/skill-development/references/writing-style.md +0 -180
  163. package/.claude/skills/skill-development/scripts/validate-skill.sh +0 -144
  164. package/.claude/skills/specialist-builder/SKILL.md +0 -327
  165. package/.claude/skills/specialist-builder/docs/agent-catalog.md +0 -28
  166. package/.claude/skills/specialist-builder/examples/complete-agent-examples.md +0 -206
  167. package/.claude/skills/specialist-builder/references/system-prompt-patterns.md +0 -281
  168. package/.claude/skills/specialist-builder/references/triggering-examples.md +0 -162
  169. package/.claude/skills/specialist-builder/scripts/validate-agent.sh +0 -137
  170. /package/.claude/{envoy/claude-envoy.py → skills/claude-envoy-patterns/SKILL.md} +0 -0
@@ -0,0 +1,639 @@
1
+ /**
2
+ * Git helper commands for claude-envoy.
3
+ *
4
+ * Wraps git/gh CLI operations for orchestration system:
5
+ * - get-base-branch: Returns base branch name (main/master/develop)
6
+ * - is-base-branch: Returns if currently on base branch
7
+ * - checkout-base: Checks out the base branch
8
+ * - diff-base: Git diff vs base branch
9
+ * - create-pr: Creates PR via gh cli
10
+ * - cleanup-worktrees: Cleans merged/orphaned worktrees
11
+ * - merge-worktree: Merges worktree branch into feature branch, records commit hash
12
+ */
13
+
14
+ import { spawnSync } from "child_process";
15
+ import { Command } from "commander";
16
+ import { BaseCommand, type CommandResult } from "./base.js";
17
+ import { getBaseBranch, getBranch } from "../lib/git.js";
18
+ import { readPrompt, writePrompt, getPromptId } from "../lib/index.js";
19
+
20
+ interface ChangedFile {
21
+ path: string;
22
+ added: number;
23
+ modified: number;
24
+ deleted: number;
25
+ }
26
+
27
+ interface WorktreeInfo {
28
+ path: string;
29
+ branch: string;
30
+ commit: string;
31
+ lastCommitDate?: string;
32
+ }
33
+
34
+ /**
35
+ * Run a git command and return stdout.
36
+ */
37
+ function runGit(args: string[]): { success: boolean; stdout: string; stderr: string } {
38
+ const result = spawnSync("git", args, {
39
+ encoding: "utf-8",
40
+ maxBuffer: 10 * 1024 * 1024, // 10MB
41
+ });
42
+ return {
43
+ success: result.status === 0,
44
+ stdout: result.stdout?.trim() || "",
45
+ stderr: result.stderr?.trim() || "",
46
+ };
47
+ }
48
+
49
+ /**
50
+ * Run gh CLI command and return stdout.
51
+ */
52
+ function runGh(args: string[]): { success: boolean; stdout: string; stderr: string } {
53
+ const result = spawnSync("gh", args, {
54
+ encoding: "utf-8",
55
+ maxBuffer: 10 * 1024 * 1024,
56
+ });
57
+ return {
58
+ success: result.status === 0,
59
+ stdout: result.stdout?.trim() || "",
60
+ stderr: result.stderr?.trim() || "",
61
+ };
62
+ }
63
+
64
+ /**
65
+ * Parse git diff --stat output to get changed files with line counts.
66
+ */
67
+ function parseChangedFiles(diffStat: string): ChangedFile[] {
68
+ const files: ChangedFile[] = [];
69
+ const lines = diffStat.split("\n");
70
+
71
+ for (const line of lines) {
72
+ // Match: path | N + M - pattern or simpler variations
73
+ // Examples:
74
+ // src/file.ts | 10 +++++-----
75
+ // src/new.ts | 5 +++++
76
+ // src/del.ts | 3 ---
77
+ const match = line.match(/^\s*(.+?)\s+\|\s+(\d+)\s*([\+\-]*)?/);
78
+ if (match) {
79
+ const path = match[1].trim();
80
+ const plusCount = (match[3] || "").match(/\+/g)?.length || 0;
81
+ const minusCount = (match[3] || "").match(/-/g)?.length || 0;
82
+
83
+ // Skip summary line (e.g., "3 files changed, 10 insertions...")
84
+ if (path.includes("changed") || path.includes("insertion") || path.includes("deletion")) {
85
+ continue;
86
+ }
87
+
88
+ files.push({
89
+ path,
90
+ added: plusCount,
91
+ modified: Math.min(plusCount, minusCount), // Overlapping changes
92
+ deleted: minusCount,
93
+ });
94
+ }
95
+ }
96
+
97
+ return files;
98
+ }
99
+
100
+ /**
101
+ * Parse git worktree list output.
102
+ */
103
+ function parseWorktrees(): WorktreeInfo[] {
104
+ const { success, stdout } = runGit(["worktree", "list", "--porcelain"]);
105
+ if (!success) return [];
106
+
107
+ const worktrees: WorktreeInfo[] = [];
108
+ let current: Partial<WorktreeInfo> = {};
109
+
110
+ for (const line of stdout.split("\n")) {
111
+ if (line.startsWith("worktree ")) {
112
+ if (current.path) {
113
+ worktrees.push(current as WorktreeInfo);
114
+ }
115
+ current = { path: line.substring(9) };
116
+ } else if (line.startsWith("HEAD ")) {
117
+ current.commit = line.substring(5);
118
+ } else if (line.startsWith("branch ")) {
119
+ // refs/heads/branch-name -> branch-name
120
+ current.branch = line.substring(7).replace("refs/heads/", "");
121
+ }
122
+ }
123
+
124
+ if (current.path) {
125
+ worktrees.push(current as WorktreeInfo);
126
+ }
127
+
128
+ return worktrees;
129
+ }
130
+
131
+ /**
132
+ * Get last commit date for a branch.
133
+ */
134
+ function getLastCommitDate(branch: string): string | undefined {
135
+ const { success, stdout } = runGit([
136
+ "log",
137
+ "-1",
138
+ "--format=%ci",
139
+ branch,
140
+ ]);
141
+ return success ? stdout : undefined;
142
+ }
143
+
144
+ // ============================================================================
145
+ // Git Commands
146
+ // ============================================================================
147
+
148
+ /**
149
+ * Get base branch name for this repository.
150
+ */
151
+ class GetBaseBranchCommand extends BaseCommand {
152
+ readonly name = "get-base-branch";
153
+ readonly description = "Returns base branch name (main/master/develop)";
154
+
155
+ defineArguments(_cmd: Command): void {
156
+ // No arguments
157
+ }
158
+
159
+ async execute(_args: Record<string, unknown>): Promise<CommandResult> {
160
+ const branch = getBranch();
161
+ if (!branch) {
162
+ return this.error("no_branch", "Not in a git repository or no branch checked out");
163
+ }
164
+
165
+ const baseBranch = getBaseBranch();
166
+
167
+ return this.success({
168
+ branch: baseBranch,
169
+ });
170
+ }
171
+ }
172
+
173
+ /**
174
+ * Check if currently on base branch.
175
+ */
176
+ class IsBaseBranchCommand extends BaseCommand {
177
+ readonly name = "is-base-branch";
178
+ readonly description = "Returns if currently on base branch";
179
+
180
+ defineArguments(_cmd: Command): void {
181
+ // No arguments
182
+ }
183
+
184
+ async execute(_args: Record<string, unknown>): Promise<CommandResult> {
185
+ const currentBranch = getBranch();
186
+ if (!currentBranch) {
187
+ return this.error("no_branch", "Not in a git repository or no branch checked out");
188
+ }
189
+
190
+ const baseBranch = getBaseBranch();
191
+ const isBase = currentBranch === baseBranch;
192
+
193
+ return this.success({
194
+ is_base: isBase,
195
+ current_branch: currentBranch,
196
+ base_branch: baseBranch,
197
+ });
198
+ }
199
+ }
200
+
201
+ /**
202
+ * Checkout the base branch.
203
+ */
204
+ class CheckoutBaseCommand extends BaseCommand {
205
+ readonly name = "checkout-base";
206
+ readonly description = "Checks out the base branch";
207
+
208
+ defineArguments(_cmd: Command): void {
209
+ // No arguments
210
+ }
211
+
212
+ async execute(_args: Record<string, unknown>): Promise<CommandResult> {
213
+ const currentBranch = getBranch();
214
+ if (!currentBranch) {
215
+ return this.error("no_branch", "Not in a git repository or no branch checked out");
216
+ }
217
+
218
+ const baseBranch = getBaseBranch();
219
+
220
+ const { success, stderr } = runGit(["checkout", baseBranch]);
221
+
222
+ if (!success) {
223
+ return this.error("checkout_failed", `Failed to checkout ${baseBranch}: ${stderr}`);
224
+ }
225
+
226
+ return this.success({
227
+ success: true,
228
+ branch: baseBranch,
229
+ previous_branch: currentBranch,
230
+ });
231
+ }
232
+ }
233
+
234
+ /**
235
+ * Git diff vs base branch.
236
+ */
237
+ class DiffBaseCommand extends BaseCommand {
238
+ readonly name = "diff-base";
239
+ readonly description = "Git diff vs base branch";
240
+
241
+ defineArguments(cmd: Command): void {
242
+ cmd.option("--path <path>", "Optional path to scope the diff");
243
+ cmd.option("--summary", "Return summary instead of full diff");
244
+ }
245
+
246
+ async execute(args: Record<string, unknown>): Promise<CommandResult> {
247
+ const currentBranch = getBranch();
248
+ if (!currentBranch) {
249
+ return this.error("no_branch", "Not in a git repository or no branch checked out");
250
+ }
251
+
252
+ const baseBranch = getBaseBranch();
253
+ const path = args.path as string | undefined;
254
+ const summaryOnly = !!args.summary;
255
+
256
+ // Build diff command
257
+ const diffArgs = ["diff", `${baseBranch}...HEAD`];
258
+ if (path) {
259
+ diffArgs.push("--", path);
260
+ }
261
+
262
+ // Get full diff or summary
263
+ let diff: string;
264
+ if (summaryOnly) {
265
+ diffArgs.push("--stat");
266
+ const result = runGit(diffArgs);
267
+ diff = result.stdout;
268
+ } else {
269
+ const result = runGit(diffArgs);
270
+ diff = result.stdout || "(No changes)";
271
+ }
272
+
273
+ // Always get stat for changed_files
274
+ const statArgs = ["diff", `${baseBranch}...HEAD`, "--stat"];
275
+ if (path) {
276
+ statArgs.push("--", path);
277
+ }
278
+ const statResult = runGit(statArgs);
279
+ const changedFiles = parseChangedFiles(statResult.stdout);
280
+
281
+ return this.success({
282
+ diff,
283
+ changed_files: changedFiles,
284
+ base_branch: baseBranch,
285
+ current_branch: currentBranch,
286
+ path: path || null,
287
+ });
288
+ }
289
+ }
290
+
291
+ /**
292
+ * Create PR via gh cli.
293
+ */
294
+ class CreatePrCommand extends BaseCommand {
295
+ readonly name = "create-pr";
296
+ readonly description = "Creates PR via gh cli";
297
+
298
+ defineArguments(cmd: Command): void {
299
+ cmd.requiredOption("--title <title>", "PR title");
300
+ cmd.requiredOption("--body <body>", "PR body/description");
301
+ cmd.option("--draft", "Create as draft PR");
302
+ }
303
+
304
+ async execute(args: Record<string, unknown>): Promise<CommandResult> {
305
+ const currentBranch = getBranch();
306
+ if (!currentBranch) {
307
+ return this.error("no_branch", "Not in a git repository or no branch checked out");
308
+ }
309
+
310
+ const baseBranch = getBaseBranch();
311
+ const title = args.title as string;
312
+ const body = args.body as string;
313
+ const draft = !!args.draft;
314
+
315
+ // Check if gh CLI is available
316
+ const ghCheck = runGh(["--version"]);
317
+ if (!ghCheck.success) {
318
+ return this.error(
319
+ "gh_not_found",
320
+ "GitHub CLI (gh) not found",
321
+ "Install with: brew install gh"
322
+ );
323
+ }
324
+
325
+ // Check if authenticated
326
+ const authCheck = runGh(["auth", "status"]);
327
+ if (!authCheck.success) {
328
+ return this.error(
329
+ "gh_not_authenticated",
330
+ "GitHub CLI not authenticated",
331
+ "Run: gh auth login"
332
+ );
333
+ }
334
+
335
+ // Ensure current branch is pushed
336
+ const pushResult = runGit(["push", "-u", "origin", currentBranch]);
337
+ if (!pushResult.success) {
338
+ return this.error(
339
+ "push_failed",
340
+ `Failed to push branch: ${pushResult.stderr}`
341
+ );
342
+ }
343
+
344
+ // Create PR
345
+ const prArgs = ["pr", "create", "--base", baseBranch, "--title", title, "--body", body];
346
+ if (draft) {
347
+ prArgs.push("--draft");
348
+ }
349
+
350
+ const prResult = runGh(prArgs);
351
+ if (!prResult.success) {
352
+ return this.error(
353
+ "pr_create_failed",
354
+ `Failed to create PR: ${prResult.stderr}`
355
+ );
356
+ }
357
+
358
+ // Extract PR URL from output
359
+ const prUrl = prResult.stdout.trim();
360
+
361
+ return this.success({
362
+ success: true,
363
+ pr_url: prUrl,
364
+ base_branch: baseBranch,
365
+ head_branch: currentBranch,
366
+ });
367
+ }
368
+ }
369
+
370
+ /**
371
+ * Clean merged/orphaned worktrees.
372
+ */
373
+ class CleanupWorktreesCommand extends BaseCommand {
374
+ readonly name = "cleanup-worktrees";
375
+ readonly description = "Cleans merged/orphaned worktrees";
376
+
377
+ defineArguments(cmd: Command): void {
378
+ cmd.option("--dry-run", "Show what would be cleaned without actually cleaning");
379
+ cmd.option("--force-orphans", "Force delete orphaned worktrees without prompting");
380
+ }
381
+
382
+ async execute(args: Record<string, unknown>): Promise<CommandResult> {
383
+ const currentBranch = getBranch();
384
+ if (!currentBranch) {
385
+ return this.error("no_branch", "Not in a git repository or no branch checked out");
386
+ }
387
+
388
+ const baseBranch = getBaseBranch();
389
+ const dryRun = !!args["dry-run"];
390
+ const forceOrphans = !!args["force-orphans"];
391
+
392
+ // Get all worktrees
393
+ const worktrees = parseWorktrees();
394
+
395
+ // Filter to implementation worktrees (pattern: *--implementation-*)
396
+ // Uses double-dash separator because git doesn't allow branch/subbranch if branch exists
397
+ const implWorktrees = worktrees.filter((wt) =>
398
+ wt.branch && /--implementation-/.test(wt.branch)
399
+ );
400
+
401
+ const cleaned: string[] = [];
402
+ const orphaned: WorktreeInfo[] = [];
403
+ const kept: string[] = [];
404
+ const errors: string[] = [];
405
+
406
+ // Check merged status for each worktree
407
+ for (const wt of implWorktrees) {
408
+ // Check if branch is merged into base
409
+ const mergeCheck = runGit(["branch", "--merged", baseBranch]);
410
+ const isMerged = mergeCheck.success &&
411
+ mergeCheck.stdout.split("\n").some((b) => b.trim() === wt.branch);
412
+
413
+ if (isMerged) {
414
+ // Branch is merged - clean it up
415
+ if (!dryRun) {
416
+ // Remove worktree
417
+ const removeWt = runGit(["worktree", "remove", wt.path, "--force"]);
418
+ if (!removeWt.success) {
419
+ errors.push(`Failed to remove worktree ${wt.path}: ${removeWt.stderr}`);
420
+ continue;
421
+ }
422
+
423
+ // Delete branch
424
+ const removeBranch = runGit(["branch", "-d", wt.branch]);
425
+ if (!removeBranch.success) {
426
+ errors.push(`Failed to delete branch ${wt.branch}: ${removeBranch.stderr}`);
427
+ }
428
+ }
429
+ cleaned.push(wt.branch);
430
+ } else {
431
+ // Check if orphaned (no matching prompt in plan directory)
432
+ // For now, mark as orphaned if not merged
433
+ const lastCommit = getLastCommitDate(wt.branch);
434
+ orphaned.push({
435
+ ...wt,
436
+ lastCommitDate: lastCommit,
437
+ });
438
+ }
439
+ }
440
+
441
+ // Handle orphaned worktrees
442
+ for (const wt of orphaned) {
443
+ if (forceOrphans && !dryRun) {
444
+ // Force delete
445
+ const removeWt = runGit(["worktree", "remove", wt.path, "--force"]);
446
+ if (removeWt.success) {
447
+ const removeBranch = runGit(["branch", "-D", wt.branch]);
448
+ cleaned.push(wt.branch);
449
+ } else {
450
+ errors.push(`Failed to remove orphaned worktree ${wt.path}: ${removeWt.stderr}`);
451
+ kept.push(wt.branch);
452
+ }
453
+ } else {
454
+ kept.push(wt.branch);
455
+ }
456
+ }
457
+
458
+ return this.success({
459
+ cleaned,
460
+ orphaned: orphaned.map((wt) => ({
461
+ branch: wt.branch,
462
+ path: wt.path,
463
+ last_commit: wt.lastCommitDate,
464
+ })),
465
+ kept,
466
+ errors: errors.length > 0 ? errors : undefined,
467
+ dry_run: dryRun,
468
+ });
469
+ }
470
+ }
471
+
472
+ /**
473
+ * Merge a worktree branch back into feature branch and record the merge commit hash.
474
+ * Handles three scenarios:
475
+ * 1. Already merged → find merge commit and record hash
476
+ * 2. Merge in progress (conflicts) → error with resolution instructions
477
+ * 3. Not merged → attempt merge, record hash on success
478
+ */
479
+ class MergeWorktreeCommand extends BaseCommand {
480
+ readonly name = "merge-worktree";
481
+ readonly description = "Merge worktree branch into feature branch, record commit hash";
482
+
483
+ defineArguments(cmd: Command): void {
484
+ cmd.argument("<prompt_num>", "Prompt number (integer)");
485
+ cmd.argument("[variant]", "Optional variant letter (A, B, etc.)");
486
+ }
487
+
488
+ async execute(args: Record<string, unknown>): Promise<CommandResult> {
489
+ const featureBranch = getBranch();
490
+ if (!featureBranch) {
491
+ return this.error("no_branch", "Not in a git repository or no branch checked out");
492
+ }
493
+
494
+ const promptNum = parseInt(args.prompt_num as string, 10);
495
+ if (isNaN(promptNum) || promptNum < 1) {
496
+ return this.error("invalid_number", "Prompt number must be a positive integer");
497
+ }
498
+
499
+ const variantArg = args.variant as string | undefined;
500
+ const variant = variantArg && /^[A-Z]$/.test(variantArg) ? variantArg : null;
501
+ if (variantArg && !variant) {
502
+ return this.error("invalid_variant", "Variant must be a single uppercase letter (A-Z)");
503
+ }
504
+
505
+ // Read prompt to get worktree branch name
506
+ const prompt = readPrompt(promptNum, variant);
507
+ if (!prompt) {
508
+ const id = getPromptId(promptNum, variant);
509
+ return this.error("not_found", `Prompt ${id} not found`);
510
+ }
511
+
512
+ const worktreeBranch = prompt.frontMatter.worktree_branch_name;
513
+ if (!worktreeBranch) {
514
+ return this.error("no_worktree", "Prompt has no worktree branch assigned");
515
+ }
516
+
517
+ const promptId = getPromptId(promptNum, variant);
518
+
519
+ // Check if already has merge_commit_hash recorded
520
+ if (prompt.frontMatter.merge_commit_hash) {
521
+ return this.success({
522
+ skipped: true,
523
+ reason: "Already has merge commit hash recorded",
524
+ merge_commit_hash: prompt.frontMatter.merge_commit_hash,
525
+ });
526
+ }
527
+
528
+ // Check for merge in progress (unresolved conflicts)
529
+ const mergeHead = runGit(["rev-parse", "--verify", "MERGE_HEAD"]);
530
+ if (mergeHead.success) {
531
+ return this.error(
532
+ "merge_in_progress",
533
+ "Merge in progress with unresolved conflicts. Resolve conflicts and commit, then re-run this command.",
534
+ "After resolving: git add . && git commit, then re-run merge-worktree"
535
+ );
536
+ }
537
+
538
+ // Check if worktree branch exists
539
+ const branchCheck = runGit(["rev-parse", "--verify", worktreeBranch]);
540
+ if (!branchCheck.success) {
541
+ return this.error("branch_not_found", `Worktree branch ${worktreeBranch} not found`);
542
+ }
543
+
544
+ // Check if already merged (worktree branch is ancestor of current HEAD)
545
+ const mergeBase = runGit(["merge-base", "--is-ancestor", worktreeBranch, "HEAD"]);
546
+ if (mergeBase.success) {
547
+ // Already merged - find the merge commit
548
+ // Look for merge commits that mention this prompt
549
+ const mergeCommit = runGit([
550
+ "log", "--oneline", "--merges", "--grep", `Merge prompt ${promptId}`,
551
+ "-n", "1", "--format=%H"
552
+ ]);
553
+
554
+ if (mergeCommit.success && mergeCommit.stdout) {
555
+ // Found the merge commit
556
+ const updatedFrontMatter = {
557
+ ...prompt.frontMatter,
558
+ merge_commit_hash: mergeCommit.stdout,
559
+ status: "merged" as const,
560
+ };
561
+ writePrompt(promptNum, variant, updatedFrontMatter, prompt.content);
562
+
563
+ return this.success({
564
+ prompt_id: promptId,
565
+ worktree_branch: worktreeBranch,
566
+ merge_commit_hash: mergeCommit.stdout,
567
+ status: "merged",
568
+ found_existing: true,
569
+ });
570
+ }
571
+
572
+ // Branch is merged but can't find specific commit - use most recent merge or HEAD
573
+ const headHash = runGit(["rev-parse", "HEAD"]);
574
+ if (headHash.success) {
575
+ const updatedFrontMatter = {
576
+ ...prompt.frontMatter,
577
+ merge_commit_hash: headHash.stdout,
578
+ status: "merged" as const,
579
+ };
580
+ writePrompt(promptNum, variant, updatedFrontMatter, prompt.content);
581
+
582
+ return this.success({
583
+ prompt_id: promptId,
584
+ worktree_branch: worktreeBranch,
585
+ merge_commit_hash: headHash.stdout,
586
+ status: "merged",
587
+ found_existing: true,
588
+ note: "Branch was already merged, using HEAD as merge commit",
589
+ });
590
+ }
591
+ }
592
+
593
+ // Not merged yet - perform the merge
594
+ const merge = runGit(["merge", "--no-ff", worktreeBranch, "-m", `Merge prompt ${promptId} implementation`]);
595
+ if (!merge.success) {
596
+ // Check if it's a conflict
597
+ if (merge.stderr.includes("CONFLICT") || merge.stderr.includes("Automatic merge failed")) {
598
+ return this.error(
599
+ "merge_conflict",
600
+ "Merge conflict detected. Resolve conflicts manually, commit, then re-run this command to record hash.",
601
+ "After resolving: git add . && git commit -m 'Resolve merge conflicts', then re-run merge-worktree"
602
+ );
603
+ }
604
+ return this.error("merge_failed", `Merge failed: ${merge.stderr}`);
605
+ }
606
+
607
+ // Get the merge commit hash
608
+ const commitHash = runGit(["rev-parse", "HEAD"]);
609
+ if (!commitHash.success) {
610
+ return this.error("git_error", `Failed to get merge commit hash: ${commitHash.stderr}`);
611
+ }
612
+
613
+ // Update prompt with merge commit hash and status
614
+ const updatedFrontMatter = {
615
+ ...prompt.frontMatter,
616
+ merge_commit_hash: commitHash.stdout,
617
+ status: "merged" as const,
618
+ };
619
+ writePrompt(promptNum, variant, updatedFrontMatter, prompt.content);
620
+
621
+ return this.success({
622
+ prompt_id: promptId,
623
+ worktree_branch: worktreeBranch,
624
+ merge_commit_hash: commitHash.stdout,
625
+ status: "merged",
626
+ });
627
+ }
628
+ }
629
+
630
+ // Auto-discovered by cli.ts
631
+ export const COMMANDS = {
632
+ "get-base-branch": GetBaseBranchCommand,
633
+ "is-base-branch": IsBaseBranchCommand,
634
+ "checkout-base": CheckoutBaseCommand,
635
+ "diff-base": DiffBaseCommand,
636
+ "create-pr": CreatePrCommand,
637
+ "cleanup-worktrees": CleanupWorktreesCommand,
638
+ "merge-worktree": MergeWorktreeCommand,
639
+ };
@@ -0,0 +1,73 @@
1
+ /**
2
+ * Command registry - auto-discovers command modules.
3
+ */
4
+
5
+ import { readdirSync, statSync } from "fs";
6
+ import { dirname, join } from "path";
7
+ import { fileURLToPath } from "url";
8
+ import type { CommandClass } from "./base.js";
9
+
10
+ const __dirname = dirname(fileURLToPath(import.meta.url));
11
+
12
+ export interface CommandModule {
13
+ COMMANDS: Record<string, CommandClass>;
14
+ }
15
+
16
+ /**
17
+ * Discover all command modules in the commands directory.
18
+ * Returns a map of module name -> COMMANDS object.
19
+ *
20
+ * Supports:
21
+ * - Single-file modules: foo.ts -> import ./foo.js
22
+ * - Directory modules: foo/index.ts -> import ./foo/index.js
23
+ */
24
+ export async function discoverCommands(): Promise<
25
+ Map<string, Record<string, CommandClass>>
26
+ > {
27
+ const commands = new Map<string, Record<string, CommandClass>>();
28
+
29
+ const entries = readdirSync(__dirname);
30
+
31
+ for (const entry of entries) {
32
+ const entryPath = join(__dirname, entry);
33
+ const stat = statSync(entryPath);
34
+
35
+ let moduleName: string;
36
+ let importPath: string;
37
+
38
+ if (stat.isDirectory()) {
39
+ // Directory module: check for index.ts
40
+ const indexPath = join(entryPath, "index.ts");
41
+ try {
42
+ statSync(indexPath);
43
+ moduleName = entry;
44
+ importPath = `./${entry}/index.js`;
45
+ } catch {
46
+ // No index.ts, skip this directory
47
+ continue;
48
+ }
49
+ } else if (
50
+ entry.endsWith(".ts") &&
51
+ !entry.startsWith("base") &&
52
+ !entry.startsWith("index")
53
+ ) {
54
+ // Single-file module
55
+ moduleName = entry.replace(".ts", "");
56
+ importPath = `./${moduleName}.js`;
57
+ } else {
58
+ continue;
59
+ }
60
+
61
+ try {
62
+ const module = (await import(importPath)) as CommandModule;
63
+ if (module.COMMANDS && Object.keys(module.COMMANDS).length > 0) {
64
+ commands.set(moduleName, module.COMMANDS);
65
+ }
66
+ } catch (e) {
67
+ // Skip modules with missing dependencies or errors
68
+ console.error(`Warning: Could not load ${moduleName}: ${e}`);
69
+ }
70
+ }
71
+
72
+ return commands;
73
+ }