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
|
@@ -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
|
-
}
|