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.
- package/.claude/agents/code-simplifier.md +52 -0
- package/.claude/agents/curator.md +186 -246
- package/.claude/agents/documentation-taxonomist.md +255 -0
- package/.claude/agents/documentation-writer.md +366 -0
- package/.claude/agents/planner.md +123 -166
- package/.claude/agents/researcher.md +58 -41
- package/.claude/agents/surveyor.md +81 -0
- package/.claude/agents/worker.md +74 -0
- package/.claude/commands/continue.md +122 -0
- package/.claude/commands/create-skill.md +107 -0
- package/.claude/commands/create-specialist.md +111 -0
- package/.claude/commands/curator-audit.md +4 -0
- package/.claude/commands/debug.md +183 -0
- package/.claude/commands/docs-adjust.md +214 -0
- package/.claude/commands/docs-audit.md +172 -0
- package/.claude/commands/docs-init.md +210 -0
- package/.claude/commands/plan.md +199 -102
- package/.claude/commands/validate.md +11 -0
- package/.claude/commands/whats-next.md +106 -134
- package/.claude/envoy/README.md +5 -5
- package/.claude/envoy/envoy +11 -14
- package/.claude/envoy/package-lock.json +1594 -0
- package/.claude/envoy/package.json +38 -0
- package/.claude/envoy/src/cli.ts +126 -0
- package/.claude/envoy/src/commands/base.ts +216 -0
- package/.claude/envoy/src/commands/docs.ts +881 -0
- package/.claude/envoy/src/commands/gemini.ts +999 -0
- package/.claude/envoy/src/commands/git.ts +639 -0
- package/.claude/envoy/src/commands/index.ts +73 -0
- package/.claude/envoy/src/commands/knowledge.ts +178 -0
- package/.claude/envoy/src/commands/perplexity.ts +129 -0
- package/.claude/envoy/src/commands/plan/core.ts +134 -0
- package/.claude/envoy/src/commands/plan/findings.ts +446 -0
- package/.claude/envoy/src/commands/plan/gates.ts +672 -0
- package/.claude/envoy/src/commands/plan/index.ts +135 -0
- package/.claude/envoy/src/commands/plan/lifecycle.ts +648 -0
- package/.claude/envoy/src/commands/plan/plan-file.ts +138 -0
- package/.claude/envoy/src/commands/plan/prompts.ts +285 -0
- package/.claude/envoy/src/commands/plan/protocols.ts +166 -0
- package/.claude/envoy/src/commands/repomix.ts +99 -0
- package/.claude/envoy/src/commands/tavily.ts +220 -0
- package/.claude/envoy/src/commands/xai.ts +168 -0
- package/.claude/envoy/src/lib/ast-queries.ts +261 -0
- package/.claude/envoy/src/lib/design.ts +41 -0
- package/.claude/envoy/src/lib/feedback-schemas.ts +154 -0
- package/.claude/envoy/src/lib/findings.ts +215 -0
- package/.claude/envoy/src/lib/gates.ts +572 -0
- package/.claude/envoy/src/lib/git.ts +132 -0
- package/.claude/envoy/src/lib/index.ts +188 -0
- package/.claude/envoy/src/lib/knowledge.ts +646 -0
- package/.claude/envoy/src/lib/markdown.ts +75 -0
- package/.claude/envoy/src/lib/observability.ts +262 -0
- package/.claude/envoy/src/lib/paths.ts +130 -0
- package/.claude/envoy/src/lib/plan-io.ts +117 -0
- package/.claude/envoy/src/lib/prompts.ts +231 -0
- package/.claude/envoy/src/lib/protocols.ts +314 -0
- package/.claude/envoy/src/lib/repomix.ts +133 -0
- package/.claude/envoy/src/lib/retry.ts +138 -0
- package/.claude/envoy/src/lib/tree-sitter-utils.ts +301 -0
- package/.claude/envoy/src/lib/watcher.ts +167 -0
- package/.claude/envoy/src/types/tree-sitter.d.ts +76 -0
- package/.claude/envoy/tsconfig.json +21 -0
- package/.claude/hooks/scripts/enforce_research_fetch.py +1 -1
- package/.claude/hooks/scripts/scan_agents.py +62 -0
- package/.claude/hooks/scripts/scan_commands.py +50 -0
- package/.claude/hooks/scripts/scan_skills.py +46 -70
- package/.claude/hooks/scripts/validate_artifacts.py +128 -0
- package/.claude/hooks/startup.sh +26 -24
- package/.claude/protocols/bug-discovery.yaml +55 -0
- package/.claude/protocols/debugging.yaml +51 -0
- package/.claude/protocols/discovery.yaml +53 -0
- package/.claude/protocols/implementation.yaml +84 -0
- package/.claude/settings.json +38 -97
- package/.claude/skills/brainstorming/SKILL.md +54 -0
- package/.claude/skills/commands-development/SKILL.md +630 -0
- package/.claude/skills/commands-development/references/arguments.md +252 -0
- package/.claude/skills/commands-development/references/patterns.md +796 -0
- package/.claude/skills/commands-development/references/tool-restrictions.md +376 -0
- package/.claude/skills/discovery-mode/SKILL.md +108 -0
- package/.claude/skills/documentation-taxonomy/SKILL.md +287 -0
- package/.claude/skills/hooks-development/SKILL.md +332 -0
- package/.claude/skills/hooks-development/references/command-vs-prompt.md +269 -0
- package/.claude/skills/hooks-development/references/examples.md +658 -0
- package/.claude/skills/hooks-development/references/hook-types.md +463 -0
- package/.claude/skills/hooks-development/references/input-output-schemas.md +469 -0
- package/.claude/skills/hooks-development/references/matchers.md +470 -0
- package/.claude/skills/hooks-development/references/troubleshooting.md +587 -0
- package/.claude/skills/implementation-mode/SKILL.md +171 -0
- package/.claude/skills/knowledge-discovery/SKILL.md +178 -0
- package/.claude/skills/research-tools/SKILL.md +35 -33
- package/.claude/skills/skills-development/SKILL.md +192 -0
- package/.claude/skills/skills-development/references/api-security.md +226 -0
- package/.claude/skills/skills-development/references/be-clear-and-direct.md +531 -0
- package/.claude/skills/skills-development/references/common-patterns.md +595 -0
- package/.claude/skills/skills-development/references/core-principles.md +437 -0
- package/.claude/skills/skills-development/references/executable-code.md +175 -0
- package/.claude/skills/skills-development/references/iteration-and-testing.md +474 -0
- package/.claude/skills/skills-development/references/recommended-structure.md +168 -0
- package/.claude/skills/skills-development/references/skill-structure.md +372 -0
- package/.claude/skills/skills-development/references/use-xml-tags.md +466 -0
- package/.claude/skills/skills-development/references/using-scripts.md +113 -0
- package/.claude/skills/skills-development/references/using-templates.md +112 -0
- package/.claude/skills/skills-development/references/workflows-and-validation.md +510 -0
- package/.claude/skills/skills-development/templates/router-skill.md +73 -0
- package/.claude/skills/skills-development/templates/simple-skill.md +33 -0
- package/.claude/skills/skills-development/workflows/add-reference.md +96 -0
- package/.claude/skills/skills-development/workflows/add-script.md +93 -0
- package/.claude/skills/skills-development/workflows/add-template.md +74 -0
- package/.claude/skills/skills-development/workflows/add-workflow.md +120 -0
- package/.claude/skills/skills-development/workflows/audit-skill.md +138 -0
- package/.claude/skills/skills-development/workflows/create-domain-expertise-skill.md +605 -0
- package/.claude/skills/skills-development/workflows/create-new-skill.md +191 -0
- package/.claude/skills/skills-development/workflows/get-guidance.md +121 -0
- package/.claude/skills/skills-development/workflows/upgrade-to-router.md +161 -0
- package/.claude/skills/skills-development/workflows/verify-skill.md +204 -0
- package/.claude/skills/subagents-development/SKILL.md +325 -0
- package/.claude/skills/subagents-development/references/context-management.md +567 -0
- package/.claude/skills/subagents-development/references/debugging-agents.md +714 -0
- package/.claude/skills/subagents-development/references/error-handling-and-recovery.md +502 -0
- package/.claude/skills/subagents-development/references/evaluation-and-testing.md +374 -0
- package/.claude/skills/subagents-development/references/orchestration-patterns.md +591 -0
- package/.claude/skills/subagents-development/references/subagents.md +508 -0
- package/.claude/skills/subagents-development/references/writing-subagent-prompts.md +517 -0
- package/.claude/statusline.sh +24 -0
- package/bin/cli.js +150 -72
- package/package.json +1 -1
- package/.claude/agents/explorer.md +0 -62
- package/.claude/agents/parallel-worker.md +0 -121
- package/.claude/commands/curation-fix.md +0 -92
- package/.claude/commands/new-branch.md +0 -36
- package/.claude/commands/parallel-discovery.md +0 -69
- package/.claude/commands/parallel-orchestration.md +0 -99
- package/.claude/commands/plan-checkpoint.md +0 -37
- package/.claude/envoy/commands/__init__.py +0 -1
- package/.claude/envoy/commands/base.py +0 -95
- package/.claude/envoy/commands/parallel.py +0 -439
- package/.claude/envoy/commands/perplexity.py +0 -86
- package/.claude/envoy/commands/plans.py +0 -451
- package/.claude/envoy/commands/tavily.py +0 -156
- package/.claude/envoy/commands/vertex.py +0 -358
- package/.claude/envoy/commands/xai.py +0 -124
- package/.claude/envoy/envoy.py +0 -122
- package/.claude/envoy/pyrightconfig.json +0 -4
- package/.claude/envoy/requirements.txt +0 -2
- package/.claude/hooks/capture-queries.sh +0 -3
- package/.claude/hooks/scripts/enforce_planning.py +0 -118
- package/.claude/hooks/scripts/enforce_rg.py +0 -34
- package/.claude/hooks/scripts/validate_skill.py +0 -81
- package/.claude/skills/claude-envoy-curation/SKILL.md +0 -162
- package/.claude/skills/claude-envoy-usage/SKILL.md +0 -46
- package/.claude/skills/command-development/SKILL.md +0 -206
- package/.claude/skills/command-development/examples/simple-commands.md +0 -212
- package/.claude/skills/command-development/references/frontmatter-reference.md +0 -221
- package/.claude/skills/hook-development/SKILL.md +0 -127
- package/.claude/skills/hook-development/examples/command-hooks.md +0 -301
- package/.claude/skills/hook-development/examples/prompt-hooks.md +0 -114
- package/.claude/skills/hook-development/references/event-reference.md +0 -226
- package/.claude/skills/repomix-extraction/SKILL.md +0 -91
- package/.claude/skills/skill-development/SKILL.md +0 -168
- package/.claude/skills/skill-development/examples/complete-skill-examples.md +0 -281
- package/.claude/skills/skill-development/references/progressive-disclosure.md +0 -141
- package/.claude/skills/skill-development/references/writing-style.md +0 -180
- package/.claude/skills/skill-development/scripts/validate-skill.sh +0 -144
- package/.claude/skills/specialist-builder/SKILL.md +0 -327
- package/.claude/skills/specialist-builder/docs/agent-catalog.md +0 -28
- package/.claude/skills/specialist-builder/examples/complete-agent-examples.md +0 -206
- package/.claude/skills/specialist-builder/references/system-prompt-patterns.md +0 -281
- package/.claude/skills/specialist-builder/references/triggering-examples.md +0 -162
- package/.claude/skills/specialist-builder/scripts/validate-agent.sh +0 -137
- /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
|
+
}
|