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
@@ -1,99 +0,0 @@
1
- ---
2
- description: Parse plan dependencies and spawn parallel workers for independent streams
3
- argument-hint: [optional-prompt]
4
- ---
5
-
6
- <objective>
7
- Analyze plan for parallelization opportunities. Identifies independent work streams and spawns parallel workers for concurrent implementation while main agent handles primary stream.
8
- </objective>
9
-
10
- <quick_start>
11
- 1. Check plan status via `envoy plans frontmatter`
12
- 2. Parse unchecked items for dependency analysis
13
- 3. Identify independent streams (different files/subsystems)
14
- 4. Main agent takes Stream 1, spawn workers for others
15
- </quick_start>
16
-
17
- <success_criteria>
18
- - Plan analyzed for parallelization opportunities
19
- - Independent streams identified (or determined sequential)
20
- - Workers spawned for parallel streams (if any)
21
- - Main agent implementing Stream 1 or fallback prompt
22
- </success_criteria>
23
-
24
- <process>
25
-
26
- ## Step 1: Check Plan Status
27
-
28
- Run: `.claude/envoy/envoy plans frontmatter`
29
-
30
- - **{exists: false}** or **status != active** → Go to Step 4 (Fallback)
31
- - **active** → Continue to Step 2
32
-
33
- ## Step 2: Parse Plan Dependencies
34
-
35
- Read `.claude/plans/<branch>/plan.md`
36
-
37
- Extract remaining steps (unchecked `- [ ]` items, excluding `/plan-checkpoint`).
38
-
39
- **Dependency analysis:**
40
- - Steps are sequential by default (each depends on previous)
41
- - Independent steps = no code/file overlap with earlier unchecked steps
42
- - Group: independent step + all subsequent steps that depend on it = **work stream**
43
-
44
- **Heuristics for independence:**
45
- - Different files/directories mentioned
46
- - Different subsystems (tests vs implementation vs docs)
47
- - Explicit "no dependencies" or "parallel-safe" markers
48
- - TEST steps often independent from each other
49
-
50
- ## Step 3: Spawn Workers or Continue
51
-
52
- **If multiple independent streams found:**
53
-
54
- 1. Main agent takes Stream 1 (first/largest stream)
55
- 2. For each additional stream, spawn parallel-worker:
56
- ```
57
- Task(subagent_type="parallel-worker", run_in_background=true)
58
- Prompt:
59
- Tasks: [list of tasks in this stream]
60
- Feature: <feature-name from branch>
61
- Plan branch: <current branch>
62
- ```
63
- 3. Continue implementing Stream 1
64
- 4. When workers complete (AgentOutputTool), run `/plan-checkpoint`
65
-
66
- **If single stream only:**
67
- - Report: "Plan has single sequential stream. No parallelization possible."
68
- - Go to Step 4 (Fallback)
69
-
70
- ## Step 4: Fallback to Prompt
71
-
72
- If $ARGUMENTS provided:
73
- - Execute the prompt as a parallel task alongside main work
74
- - Spawn single parallel-worker with prompt as task
75
-
76
- If no $ARGUMENTS:
77
- - Report: "No parallelization opportunities found and no fallback prompt provided."
78
- - Workflow cannot run - continue normal sequential implementation
79
-
80
- ## Output
81
-
82
- After analysis, report:
83
- ```
84
- Streams identified: N
85
- - Stream 1 (main): [task summary]
86
- - Stream 2 (worker): [task summary]
87
- ...
88
- Workers spawned: N-1
89
- Action: [implementing stream 1 | fallback to prompt | no action]
90
- ```
91
-
92
- </process>
93
-
94
- <constraints>
95
- - NEVER spawn workers for dependent tasks (will cause conflicts)
96
- - MAX 3 parallel workers (diminishing returns)
97
- - NEVER parallelize single-file changes (merge conflicts)
98
- - ALWAYS run `/plan-checkpoint` when workers complete
99
- </constraints>
@@ -1,37 +0,0 @@
1
- ---
2
- description: Trigger this when the plan file requests its use WITHOUT USER INPUT
3
- argument-hint: [--last-commit]
4
- ---
5
-
6
- <objective>
7
- Trigger automated checkpoint review during plan execution. Called by plan file markers, not user input. Validates progress and determines next steps.
8
- </objective>
9
-
10
- <quick_start>
11
- Delegate immediately to planner agent with:
12
- - Current branch name
13
- - `--last-commit` flag if reviewing incremental changes
14
- </quick_start>
15
-
16
- <success_criteria>
17
- - Planner has reviewed current state
18
- - Plan status updated appropriately
19
- - Next action determined (continue, pause, complete)
20
- </success_criteria>
21
-
22
- <process>
23
-
24
- Delegate checkpoint review to planner agent.
25
-
26
- Provide planner:
27
- - Current branch name
28
- - `--last-commit` if reviewing incremental changes
29
-
30
- Planner handles full checkpoint workflow and returns status.
31
-
32
- </process>
33
-
34
- <constraints>
35
- - DO NOT trigger manually - plan file requests this command
36
- - DO NOT modify plan directly - planner agent handles all updates
37
- </constraints>
@@ -1 +0,0 @@
1
- # Command modules are auto-discovered by envoy.py
@@ -1,95 +0,0 @@
1
- """Base command class for claude-envoy commands.
2
-
3
- To add a new command:
4
- 1. Create a new file in commands/ (e.g., myapi.py)
5
- 2. Subclass BaseCommand for each subcommand
6
- 3. Export a COMMANDS dict mapping subcommand names to classes
7
-
8
- Example:
9
- class MySearchCommand(BaseCommand):
10
- name = "search"
11
- description = "Search something"
12
-
13
- def add_arguments(self, parser):
14
- parser.add_argument("query", help="Search query")
15
-
16
- def execute(self, query: str, **kwargs) -> dict:
17
- # Implementation
18
- return self.success({"results": [...]})
19
-
20
- COMMANDS = {"search": MySearchCommand}
21
- """
22
-
23
- from __future__ import annotations
24
-
25
- import os
26
- import time
27
- from abc import ABC, abstractmethod
28
- from typing import Any, Optional
29
-
30
-
31
- class BaseCommand(ABC):
32
- """Base class for all envoy commands."""
33
-
34
- name: str = ""
35
- description: str = ""
36
-
37
- @property
38
- def timeout_ms(self) -> int:
39
- return int(os.environ.get("ENVOY_TIMEOUT_MS", "120000"))
40
-
41
- @abstractmethod
42
- def add_arguments(self, parser) -> None:
43
- """Add command-specific arguments to the parser."""
44
- pass
45
-
46
- @abstractmethod
47
- def execute(self, *args, **kwargs) -> dict:
48
- """Execute the command and return result dict.
49
-
50
- Subclasses should define explicit parameters for clarity,
51
- e.g.: def execute(self, query: str, max_results: int = None, **kwargs)
52
- """
53
- pass
54
-
55
- def success(self, data: dict, metadata: Optional[dict] = None) -> dict:
56
- """Return a success response."""
57
- result = {"status": "success", "data": data}
58
- if metadata:
59
- result["metadata"] = metadata
60
- return result
61
-
62
- def error(self, error_type: str, message: str, suggestion: Optional[str] = None) -> dict:
63
- """Return an error response."""
64
- error_data = {
65
- "type": error_type,
66
- "message": message,
67
- "command": f"{self.__class__.__module__}.{self.name}",
68
- }
69
- if suggestion:
70
- error_data["suggestion"] = suggestion
71
- return {"status": "error", "error": error_data}
72
-
73
- def timed_execute(self, func, *args, **kwargs) -> tuple[Any, int]:
74
- """Execute a function and return (result, duration_ms)."""
75
- start = time.time()
76
- result = func(*args, **kwargs)
77
- duration_ms = int((time.time() - start) * 1000)
78
- return result, duration_ms
79
-
80
- def read_file(self, path: str) -> Optional[str]:
81
- """Read a file, return None if not found."""
82
- try:
83
- with open(path, "r") as f:
84
- return f.read()
85
- except FileNotFoundError:
86
- return None
87
-
88
- def read_files(self, paths: list[str]) -> dict[str, str]:
89
- """Read multiple files, return {path: content} for existing files."""
90
- result = {}
91
- for path in paths:
92
- content = self.read_file(path)
93
- if content is not None:
94
- result[path] = content
95
- return result
@@ -1,439 +0,0 @@
1
- """Parallel worker management via git worktrees and synchronous Claude sessions."""
2
-
3
- from __future__ import annotations
4
-
5
- import json
6
- import os
7
- import shutil
8
- import subprocess
9
- import sys
10
- import time
11
- from pathlib import Path
12
- from typing import Optional
13
-
14
- from .base import BaseCommand
15
-
16
- # Configurable via settings.json env block (defaults below)
17
- PARALLEL_MAX_WORKERS = int(os.environ.get("PARALLEL_MAX_WORKERS", "3"))
18
- HEARTBEAT_INTERVAL = 30 # seconds between heartbeat emissions
19
-
20
-
21
- def get_project_root() -> Path:
22
- """Get project root from git."""
23
- result = subprocess.run(
24
- ["git", "rev-parse", "--show-toplevel"],
25
- capture_output=True,
26
- text=True,
27
- )
28
- if result.returncode != 0:
29
- raise RuntimeError(f"Not a git repository or git not found: {result.stderr.strip()}")
30
- return Path(result.stdout.strip())
31
-
32
-
33
- def get_workers_dir() -> Path:
34
- """Get .trees/ directory for worktrees (inside project, gitignored)."""
35
- trees_dir = get_project_root() / ".trees"
36
- trees_dir.mkdir(exist_ok=True)
37
- return trees_dir
38
-
39
-
40
- def get_worker_path(worker_name: str) -> Path:
41
- """Get path for a specific worker worktree."""
42
- return get_workers_dir() / worker_name
43
-
44
-
45
- def sanitize_branch_name(name: str) -> str:
46
- """Sanitize string for use in branch/worker names.
47
-
48
- Git branch names cannot contain: ~ ^ : ? * [ \\ @{ consecutive dots (..)
49
- Also removes control characters and leading/trailing dots/slashes.
50
- """
51
- import re
52
- # Replace forbidden characters with dash
53
- result = re.sub(r'[~^:?*\[\]\\@{}\s/]', '-', name)
54
- # Remove consecutive dots
55
- result = re.sub(r'\.{2,}', '.', result)
56
- # Remove consecutive dashes
57
- result = re.sub(r'-{2,}', '-', result)
58
- # Remove leading/trailing dots, dashes, slashes
59
- result = result.strip('.-/')
60
- # Lowercase
61
- return result.lower()
62
-
63
-
64
- class SpawnCommand(BaseCommand):
65
- name = "spawn"
66
- description = "Create worktree + inject plan + run synchronous Claude session"
67
-
68
- def add_arguments(self, parser) -> None:
69
- parser.add_argument("--branch", required=True, help="Branch name for worker")
70
- parser.add_argument("--task", required=True, help="Task description for the worker")
71
- parser.add_argument("--from", dest="from_branch", default="HEAD", help="Base branch (default: HEAD)")
72
- parser.add_argument("--plan", help="Mini-plan markdown to inject into worktree")
73
-
74
- def execute(
75
- self,
76
- branch: str,
77
- task: str,
78
- from_branch: str = "HEAD",
79
- plan: Optional[str] = None,
80
- **kwargs,
81
- ) -> dict:
82
- # Check for nested worker prevention
83
- if os.environ.get("PARALLEL_WORKER_DEPTH"):
84
- return self.error(
85
- "nesting_blocked",
86
- "Cannot spawn workers from within a worker",
87
- suggestion="Use Task tool for subagents if needed"
88
- )
89
-
90
- # Check worker limit
91
- existing = self._list_workers()
92
- if len(existing) >= PARALLEL_MAX_WORKERS:
93
- return self.error(
94
- "limit_exceeded",
95
- f"Max {PARALLEL_MAX_WORKERS} concurrent workers. Run 'envoy parallel cleanup' first.",
96
- suggestion=f"Active workers: {', '.join(existing)}"
97
- )
98
-
99
- # Sanitize and create worker name
100
- worker_name = sanitize_branch_name(branch)
101
- worker_path = get_worker_path(worker_name)
102
-
103
- if worker_path.exists():
104
- return self.error(
105
- "already_exists",
106
- f"Worker '{worker_name}' already exists at {worker_path}",
107
- suggestion="Use 'envoy parallel status' to check or 'envoy parallel cleanup' to remove"
108
- )
109
-
110
- try:
111
- # Create worktree with new branch
112
- result = subprocess.run(
113
- ["git", "worktree", "add", "-b", branch, str(worker_path), from_branch],
114
- capture_output=True,
115
- text=True,
116
- )
117
- if result.returncode != 0:
118
- return self.error("git_error", f"Failed to create worktree: {result.stderr}")
119
-
120
- # Copy .env if exists
121
- project_root = get_project_root()
122
- env_file = project_root / ".env"
123
- if env_file.exists():
124
- shutil.copy(env_file, worker_path / ".env")
125
-
126
- # Create plan file if --plan provided
127
- if plan:
128
- plan_dir = worker_path / ".claude" / "plans" / worker_name
129
- plan_dir.mkdir(parents=True, exist_ok=True)
130
- plan_file = plan_dir / "plan.md"
131
- plan_content = f"---\nstatus: active\nbranch: {branch}\n---\n\n{plan}"
132
- plan_file.write_text(plan_content)
133
-
134
- # Build Claude command
135
- cmd = ["claude", "--print"]
136
-
137
- # Prepend anti-nesting directive to task
138
- worker_prompt = (
139
- "IMPORTANT: Do not use `envoy parallel spawn` - nested workers are not allowed. "
140
- "You may use Task tool for subagents if needed.\n\n"
141
- f"{task}"
142
- )
143
- cmd.extend(["-p", worker_prompt])
144
-
145
- # Set worker depth env var to prevent nesting and block planning
146
- worker_env = os.environ.copy()
147
- worker_env["PARALLEL_WORKER_DEPTH"] = "1"
148
-
149
- # Log file for debugging (full output)
150
- log_file = worker_path / ".claude-worker.log"
151
-
152
- # Save worker metadata before running
153
- metadata = {
154
- "branch": branch,
155
- "task": task,
156
- "from_branch": from_branch,
157
- "worker_path": str(worker_path),
158
- "started_at": time.time(),
159
- }
160
- with open(worker_path / ".claude-worker-meta.json", "w") as f:
161
- json.dump(metadata, f, indent=2)
162
-
163
- # Run synchronously - block until completion
164
- # Emit heartbeats to caller while waiting, write full log to file
165
- with open(log_file, "w") as log:
166
- process = subprocess.Popen(
167
- cmd,
168
- cwd=str(worker_path),
169
- stdout=subprocess.PIPE,
170
- stderr=subprocess.STDOUT,
171
- text=True,
172
- env=worker_env,
173
- )
174
-
175
- last_heartbeat = time.time()
176
- output_lines = []
177
-
178
- # Stream output: write to log, emit heartbeats to caller
179
- while True:
180
- line = process.stdout.readline()
181
- if not line and process.poll() is not None:
182
- break
183
-
184
- if line:
185
- # Write full output to log file
186
- log.write(line)
187
- log.flush()
188
- output_lines.append(line)
189
-
190
- # Emit heartbeat every HEARTBEAT_INTERVAL seconds
191
- now = time.time()
192
- if now - last_heartbeat >= HEARTBEAT_INTERVAL:
193
- # Heartbeat to stderr so it doesn't pollute JSON output
194
- print(f"[heartbeat] {worker_name} - {len(output_lines)} lines", file=sys.stderr)
195
- last_heartbeat = now
196
-
197
- exit_code = process.returncode
198
-
199
- # Update metadata with completion info
200
- metadata["completed_at"] = time.time()
201
- metadata["exit_code"] = exit_code
202
- metadata["output_lines"] = len(output_lines)
203
- with open(worker_path / ".claude-worker-meta.json", "w") as f:
204
- json.dump(metadata, f, indent=2)
205
-
206
- # Extract final summary (last non-empty lines)
207
- summary_lines = [l.strip() for l in output_lines[-10:] if l.strip()]
208
- summary = "\n".join(summary_lines[-3:]) if summary_lines else "(no output)"
209
-
210
- return self.success({
211
- "worker": worker_name,
212
- "branch": branch,
213
- "path": str(worker_path),
214
- "exit_code": exit_code,
215
- "status": "success" if exit_code == 0 else "failed",
216
- "summary": summary,
217
- "log_path": str(log_file),
218
- "output_lines": len(output_lines),
219
- })
220
-
221
- except Exception as e:
222
- # Cleanup on failure
223
- if worker_path.exists():
224
- subprocess.run(["git", "worktree", "remove", "--force", str(worker_path)], capture_output=True)
225
- return self.error("spawn_error", str(e))
226
-
227
- def _list_workers(self) -> list[str]:
228
- """List existing worker names."""
229
- workers_dir = get_workers_dir()
230
- if not workers_dir.exists():
231
- return []
232
- return [
233
- d.name
234
- for d in workers_dir.iterdir()
235
- if d.is_dir() and (d / ".claude-worker-meta.json").exists()
236
- ]
237
-
238
-
239
- class StatusCommand(BaseCommand):
240
- name = "status"
241
- description = "List all parallel workers and their status"
242
-
243
- def add_arguments(self, parser) -> None:
244
- pass
245
-
246
- def execute(self, **kwargs) -> dict:
247
- workers_dir = get_workers_dir()
248
- workers = []
249
-
250
- if not workers_dir.exists():
251
- return self.success({"workers": [], "count": 0, "max_workers": PARALLEL_MAX_WORKERS})
252
-
253
- for d in workers_dir.iterdir():
254
- meta_file = d / ".claude-worker-meta.json"
255
- if not d.is_dir() or not meta_file.exists():
256
- continue
257
-
258
- worker_name = d.name
259
- log_file = d / ".claude-worker.log"
260
-
261
- worker_info = {
262
- "name": worker_name,
263
- "path": str(d),
264
- "status": "unknown",
265
- }
266
-
267
- # Load metadata
268
- with open(meta_file) as f:
269
- meta = json.load(f)
270
- worker_info.update({
271
- "branch": meta.get("branch"),
272
- "task": meta.get("task"),
273
- "pid": meta.get("pid"),
274
- })
275
-
276
- # Check if process still running
277
- pid = meta.get("pid")
278
- if pid:
279
- try:
280
- os.kill(pid, 0) # Check if process exists
281
- worker_info["status"] = "running"
282
- except OSError:
283
- worker_info["status"] = "completed"
284
-
285
- # Get log tail
286
- if log_file.exists():
287
- with open(log_file) as f:
288
- lines = f.readlines()
289
- worker_info["log_lines"] = len(lines)
290
- worker_info["log_tail"] = "".join(lines[-5:]) if lines else ""
291
-
292
- workers.append(worker_info)
293
-
294
- return self.success({
295
- "workers": workers,
296
- "count": len(workers),
297
- "max_workers": PARALLEL_MAX_WORKERS,
298
- })
299
-
300
-
301
- class ResultsCommand(BaseCommand):
302
- name = "results"
303
- description = "Get output from worker(s)"
304
-
305
- def add_arguments(self, parser) -> None:
306
- parser.add_argument("--worker", help="Specific worker name (default: all)")
307
- parser.add_argument("--tail", type=int, default=50, help="Number of log lines (default: 50)")
308
-
309
- def execute(self, worker: Optional[str] = None, tail: int = 50, **kwargs) -> dict:
310
- workers_dir = get_workers_dir()
311
- results = []
312
-
313
- if not workers_dir.exists():
314
- if worker:
315
- return self.error("not_found", f"Worker '{worker}' not found")
316
- return self.success({"results": []})
317
-
318
- for d in workers_dir.iterdir():
319
- meta_file = d / ".claude-worker-meta.json"
320
- if not d.is_dir() or not meta_file.exists():
321
- continue
322
-
323
- worker_name = d.name
324
- if worker and worker_name != worker:
325
- continue
326
-
327
- log_file = d / ".claude-worker.log"
328
-
329
- result = {"name": worker_name, "path": str(d)}
330
-
331
- with open(meta_file) as f:
332
- meta = json.load(f)
333
- result["task"] = meta.get("task")
334
- result["branch"] = meta.get("branch")
335
-
336
- if log_file.exists():
337
- with open(log_file) as f:
338
- lines = f.readlines()
339
- result["output"] = "".join(lines[-tail:])
340
- result["total_lines"] = len(lines)
341
- else:
342
- result["output"] = "(no output yet)"
343
-
344
- results.append(result)
345
-
346
- if worker and not results:
347
- return self.error("not_found", f"Worker '{worker}' not found")
348
-
349
- return self.success({"results": results})
350
-
351
-
352
- class CleanupCommand(BaseCommand):
353
- name = "cleanup"
354
- description = "Remove worker worktrees"
355
-
356
- def add_arguments(self, parser) -> None:
357
- parser.add_argument("--worker", help="Specific worker to remove (default: all completed)")
358
- parser.add_argument("--all", action="store_true", dest="remove_all", help="Remove all workers including running")
359
- parser.add_argument("--force", action="store_true", help="Force removal even with uncommitted changes")
360
-
361
- def execute(self, worker: Optional[str] = None, remove_all: bool = False, force: bool = False, **kwargs) -> dict:
362
- workers_dir = get_workers_dir()
363
- removed = []
364
- skipped = []
365
- errors = []
366
-
367
- if not workers_dir.exists():
368
- return self.success({"removed": [], "skipped": [], "errors": []})
369
-
370
- for d in workers_dir.iterdir():
371
- meta_file = d / ".claude-worker-meta.json"
372
- if not d.is_dir() or not meta_file.exists():
373
- continue
374
-
375
- worker_name = d.name
376
- if worker and worker_name != worker:
377
- continue
378
-
379
- # Check if running
380
- is_running = False
381
- pid = None
382
- with open(meta_file) as f:
383
- meta = json.load(f)
384
- pid = meta.get("pid")
385
- if pid:
386
- try:
387
- os.kill(pid, 0)
388
- is_running = True
389
- except OSError:
390
- pass
391
-
392
- # Skip running workers unless --all
393
- if is_running and not remove_all:
394
- skipped.append({"name": worker_name, "reason": "still running"})
395
- continue
396
-
397
- # Kill process if running
398
- if is_running and pid:
399
- try:
400
- os.kill(pid, 9)
401
- except OSError:
402
- pass
403
-
404
- # Get branch name for cleanup
405
- branch_name = meta.get("branch")
406
-
407
- # Remove worktree
408
- cmd = ["git", "worktree", "remove", str(d)]
409
- if force:
410
- cmd.append("--force")
411
-
412
- result = subprocess.run(cmd, capture_output=True, text=True)
413
- if result.returncode != 0:
414
- errors.append({"name": worker_name, "error": result.stderr})
415
- continue
416
-
417
- # Delete the branch
418
- if branch_name:
419
- subprocess.run(
420
- ["git", "branch", "-D", branch_name],
421
- capture_output=True,
422
- text=True,
423
- )
424
-
425
- removed.append(worker_name)
426
-
427
- return self.success({
428
- "removed": removed,
429
- "skipped": skipped,
430
- "errors": errors,
431
- })
432
-
433
-
434
- COMMANDS = {
435
- "spawn": SpawnCommand,
436
- "status": StatusCommand,
437
- "results": ResultsCommand,
438
- "cleanup": CleanupCommand,
439
- }