mirror of
https://github.com/ksyasuda/dotfiles.git
synced 2026-08-25 06:15:24 -07:00
update
This commit is contained in:
@@ -5,9 +5,13 @@ description: Delegate self-contained tasks to Claude or Codex subagents run as f
|
||||
|
||||
# Delegate
|
||||
|
||||
Hand a self-contained task to a fresh Claude or Codex subagent and get back a
|
||||
structured report. Each invocation is one subagent; run several in parallel for
|
||||
independent tasks.
|
||||
Hand a self-contained task to a fresh `claude` or `codex` CLI process and get
|
||||
back a structured report. Each invocation is one subagent; run several in
|
||||
parallel for independent tasks.
|
||||
|
||||
You call the CLIs directly. The command shapes below are the default recipe,
|
||||
not a fixed harness — adjust flags when the task needs it (extra directories,
|
||||
different tools, a longer timeout), and say what you changed in your report.
|
||||
|
||||
## When to delegate
|
||||
|
||||
@@ -61,45 +65,123 @@ The subagent starts with zero context. Write a self-contained task containing:
|
||||
|
||||
Pass raw evidence; do not tell the subagent the answer you expect.
|
||||
|
||||
Wrap the brief in this scaffold. Keep the constraint block matching the mode
|
||||
and keep the report sections verbatim — the result-handling steps below assume
|
||||
them.
|
||||
|
||||
```
|
||||
You are a subagent completing a delegated task inside the current repository.
|
||||
Work independently; the caller cannot answer questions mid-task. If the task is
|
||||
ambiguous, choose the safest reasonable interpretation and note the choice in
|
||||
your report.
|
||||
|
||||
<constraints for the chosen mode — see below>
|
||||
- Do not delegate further or invoke Claude or Codex recursively.
|
||||
|
||||
End your reply with a report containing these sections:
|
||||
- Summary: what you did or found, in a few sentences.
|
||||
- Details: key evidence, decisions, or findings, with file:line references.
|
||||
- Files changed: list each changed file, or "none".
|
||||
- Verification: what you ran and the outcome, or "not verified" and why.
|
||||
- Open questions: anything unresolved the caller must decide, or "none".
|
||||
|
||||
Task:
|
||||
<self-contained task brief>
|
||||
```
|
||||
|
||||
`read` constraints:
|
||||
|
||||
```
|
||||
Hard constraints:
|
||||
- You are in read-only mode. Do not modify, create, rename, or delete files.
|
||||
- Do not commit, branch, push, or contact external systems that change state.
|
||||
- Base claims on repository evidence. Cite file paths and line numbers.
|
||||
```
|
||||
|
||||
`write` constraints:
|
||||
|
||||
```
|
||||
Hard constraints:
|
||||
- Work only inside the given repository directory.
|
||||
- Do not commit, branch, push, or open pull requests unless the task says to.
|
||||
- Do not delete or rename files the task does not cover; if something looks
|
||||
wrong or unexpected, stop and report instead of guessing.
|
||||
- Verify your work (build, tests, or a targeted check) when feasible.
|
||||
```
|
||||
|
||||
## Run it
|
||||
|
||||
Resolve `<skill-dir>` to the directory containing this `SKILL.md`. Send the
|
||||
task on stdin; do not interpolate it into the shell command.
|
||||
Always send the prompt on stdin via a quoted heredoc (`<<'TASK'`) so nothing in
|
||||
the brief is expanded or re-parsed by the shell. Run the CLI from the target
|
||||
repo with `cd <repo> && ...`.
|
||||
|
||||
Codex, read-only:
|
||||
|
||||
```bash
|
||||
python3 <skill-dir>/scripts/delegate.py \
|
||||
--provider codex \
|
||||
--tier standard \
|
||||
--mode read \
|
||||
--repo "$PWD" <<'TASK'
|
||||
<self-contained task brief>
|
||||
cd /path/to/repo && codex exec \
|
||||
--ignore-user-config \
|
||||
--model gpt-5.6-sol \
|
||||
--config model_reasoning_effort="medium" \
|
||||
--config approval_policy="never" \
|
||||
--sandbox read-only \
|
||||
--ephemeral \
|
||||
--skip-git-repo-check \
|
||||
--output-last-message /tmp/delegate-codex-1.md \
|
||||
--cd /path/to/repo - <<'TASK'
|
||||
<scaffold + brief>
|
||||
TASK
|
||||
cat /tmp/delegate-codex-1.md
|
||||
```
|
||||
|
||||
Codex, write mode: swap `--sandbox read-only` for `--sandbox workspace-write`.
|
||||
|
||||
`--output-last-message` writes just the final report; Codex's stdout also
|
||||
carries its reasoning stream, which is useful when a run fails or you want to
|
||||
see what it actually did. Add `--add-dir <dir>` for extra writable roots.
|
||||
|
||||
Claude, read-only:
|
||||
|
||||
```bash
|
||||
cd /path/to/repo && claude --print \
|
||||
--model claude-opus-4-8 \
|
||||
--effort medium \
|
||||
--disable-slash-commands \
|
||||
--no-session-persistence \
|
||||
--permission-mode plan \
|
||||
--tools Read,Glob,Grep <<'TASK'
|
||||
<scaffold + brief>
|
||||
TASK
|
||||
```
|
||||
|
||||
Flags:
|
||||
Claude, write mode: replace the last two lines with
|
||||
|
||||
- `--provider claude|codex` (required)
|
||||
- `--tier quick|quick-context|standard|deep` (default `standard`)
|
||||
- `--mode read|write` (default `read`)
|
||||
- `--model <approved-model>` for an explicit allowed override
|
||||
- `--repo <dir>` (default cwd)
|
||||
- `--timeout <seconds>` (default 1800)
|
||||
- `--dry-run` to inspect routing and command construction without running
|
||||
```bash
|
||||
--permission-mode acceptEdits \
|
||||
--tools Read,Glob,Grep,Edit,Write,Bash \
|
||||
--allowedTools Edit,Write,Bash
|
||||
```
|
||||
|
||||
Claude prints the final message on stdout. Widen `--tools` when a task
|
||||
genuinely needs more (for example `WebSearch` for external research, or
|
||||
`Bash` in read mode for a `git diff` the subagent must see) — that is a
|
||||
deliberate choice, so note it when you report back.
|
||||
|
||||
Give long runs a timeout that fits the task (the Bash tool's `timeout` is in
|
||||
milliseconds; 1800000 is a reasonable ceiling for a `deep` tier run).
|
||||
|
||||
Parallel delegation: launch each invocation as a separate background shell
|
||||
command, then collect the outputs. Never point two `write`-mode subagents at
|
||||
overlapping files; split by file/directory or run them sequentially.
|
||||
command, write each Codex report to its own `--output-last-message` path, then
|
||||
collect the outputs. Never point two `write`-mode subagents at overlapping
|
||||
files; split by file/directory or run them sequentially.
|
||||
|
||||
## Handle the result
|
||||
|
||||
The report ends with Summary / Details / Files changed / Verification / Open
|
||||
questions sections. Then:
|
||||
|
||||
- Treat it as a subagent's claim, not ground truth: spot-check load-bearing
|
||||
findings, and for `write` mode review the diff (`git diff`) and re-run
|
||||
verification before building on it.
|
||||
- Treat the report as a subagent's claim, not ground truth: spot-check
|
||||
load-bearing findings, and for `write` mode review the diff (`git diff`) and
|
||||
re-run verification before building on it.
|
||||
- Relay the outcome to the user in your own words; credit which provider/model
|
||||
produced it when it matters.
|
||||
- On failure, report the provider, model, and exact error. Retry once with the
|
||||
same route if transient; escalate tier or switch provider only deliberately,
|
||||
and say you did.
|
||||
- On failure, report the provider, model, and exact error. A nonzero exit or
|
||||
empty output is a failure even if the CLI printed something — check both.
|
||||
Retry once with the same route if transient; escalate tier or switch provider
|
||||
only deliberately, and say you did.
|
||||
|
||||
Binary file not shown.
@@ -1,270 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Delegate a self-contained task to a Claude or Codex subagent."""
|
||||
|
||||
import argparse
|
||||
import os
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
ALLOWED_MODELS = {
|
||||
"codex": frozenset({"gpt-5.6-terra", "gpt-5.6-sol"}),
|
||||
"claude": frozenset(
|
||||
{"claude-sonnet-4-6", "claude-sonnet-5", "claude-opus-4-8"}
|
||||
),
|
||||
}
|
||||
|
||||
# tier -> provider -> (model, effort)
|
||||
ROUTES = {
|
||||
"quick": {
|
||||
"codex": ("gpt-5.6-terra", "medium"),
|
||||
"claude": ("claude-sonnet-4-6", "medium"),
|
||||
},
|
||||
"quick-context": {
|
||||
"codex": ("gpt-5.6-terra", "high"),
|
||||
"claude": ("claude-sonnet-5", "high"),
|
||||
},
|
||||
"standard": {
|
||||
"codex": ("gpt-5.6-sol", "medium"),
|
||||
"claude": ("claude-opus-4-8", "medium"),
|
||||
},
|
||||
"deep": {
|
||||
"codex": ("gpt-5.6-sol", "high"),
|
||||
"claude": ("claude-opus-4-8", "high"),
|
||||
},
|
||||
}
|
||||
|
||||
READ_ONLY_RULES = """\
|
||||
Hard constraints:
|
||||
- You are in read-only mode. Do not modify, create, rename, or delete files.
|
||||
- Do not commit, branch, push, or contact external systems that change state.
|
||||
- Base claims on repository evidence. Cite file paths and line numbers."""
|
||||
|
||||
WRITE_RULES = """\
|
||||
Hard constraints:
|
||||
- Work only inside the given repository directory.
|
||||
- Do not commit, branch, push, or open pull requests unless the task says to.
|
||||
- Do not delete or rename files the task does not cover; if something looks
|
||||
wrong or unexpected, stop and report instead of guessing.
|
||||
- Verify your work (build, tests, or a targeted check) when feasible."""
|
||||
|
||||
REPORT_FORMAT = """\
|
||||
End your reply with a report containing these sections:
|
||||
- Summary: what you did or found, in a few sentences.
|
||||
- Details: key evidence, decisions, or findings, with file:line references.
|
||||
- Files changed: list each changed file, or "none".
|
||||
- Verification: what you ran and the outcome, or "not verified" and why.
|
||||
- Open questions: anything unresolved the caller must decide, or "none"."""
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class Route:
|
||||
provider: str
|
||||
model: str
|
||||
effort: str
|
||||
|
||||
|
||||
def select_route(provider: str, tier: str) -> Route:
|
||||
if tier not in ROUTES:
|
||||
raise ValueError(f"Unknown tier: {tier}")
|
||||
if provider not in ("claude", "codex"):
|
||||
raise ValueError(f"Unknown provider: {provider}")
|
||||
model, effort = ROUTES[tier][provider]
|
||||
return Route(provider, model, effort)
|
||||
|
||||
|
||||
def apply_model_override(route: Route, model: str) -> Route:
|
||||
if model not in ALLOWED_MODELS[route.provider]:
|
||||
raise ValueError(f"Model {model!r} is not allowed for {route.provider}")
|
||||
return Route(route.provider, model, route.effort)
|
||||
|
||||
|
||||
def build_command(
|
||||
route: Route,
|
||||
repo: Path,
|
||||
mode: str,
|
||||
output_path: Path | None = None,
|
||||
) -> list[str]:
|
||||
if route.provider == "codex":
|
||||
if output_path is None:
|
||||
raise ValueError("Codex requires an output path")
|
||||
sandbox = "read-only" if mode == "read" else "workspace-write"
|
||||
return [
|
||||
"codex",
|
||||
"exec",
|
||||
"--ignore-user-config",
|
||||
"--model",
|
||||
route.model,
|
||||
"--config",
|
||||
f'model_reasoning_effort="{route.effort}"',
|
||||
"--config",
|
||||
'approval_policy="never"',
|
||||
"--sandbox",
|
||||
sandbox,
|
||||
"--ephemeral",
|
||||
"--skip-git-repo-check",
|
||||
"--output-last-message",
|
||||
str(output_path),
|
||||
"--cd",
|
||||
str(repo),
|
||||
"-",
|
||||
]
|
||||
|
||||
command = [
|
||||
"claude",
|
||||
"--print",
|
||||
"--model",
|
||||
route.model,
|
||||
"--effort",
|
||||
route.effort,
|
||||
"--disable-slash-commands",
|
||||
"--no-session-persistence",
|
||||
]
|
||||
if mode == "read":
|
||||
command += [
|
||||
"--permission-mode",
|
||||
"plan",
|
||||
"--tools",
|
||||
"Read,Glob,Grep",
|
||||
]
|
||||
else:
|
||||
command += [
|
||||
"--permission-mode",
|
||||
"acceptEdits",
|
||||
"--tools",
|
||||
"Read,Glob,Grep,Edit,Write,Bash",
|
||||
"--allowedTools",
|
||||
"Edit,Write,Bash",
|
||||
]
|
||||
return command
|
||||
|
||||
|
||||
def build_prompt(task: str, route: Route, mode: str) -> str:
|
||||
rules = READ_ONLY_RULES if mode == "read" else WRITE_RULES
|
||||
return f"""You are a {route.provider} subagent completing a delegated task \
|
||||
inside the current repository. Work independently; the caller cannot answer \
|
||||
questions mid-task. If the task is ambiguous, choose the safest reasonable \
|
||||
interpretation and note the choice in your report.
|
||||
|
||||
{rules}
|
||||
- Do not delegate further or invoke Claude or Codex recursively.
|
||||
|
||||
{REPORT_FORMAT}
|
||||
|
||||
Task:
|
||||
{task.strip()}
|
||||
"""
|
||||
|
||||
|
||||
def run_delegation(
|
||||
route: Route,
|
||||
repo: Path,
|
||||
prompt: str,
|
||||
mode: str,
|
||||
*,
|
||||
timeout: int = 1800,
|
||||
) -> str:
|
||||
if shutil.which(route.provider) is None:
|
||||
raise RuntimeError(f"Required CLI is not installed: {route.provider}")
|
||||
|
||||
with tempfile.TemporaryDirectory(prefix="delegate-") as temp_dir:
|
||||
output_path = (
|
||||
Path(temp_dir) / "report.md" if route.provider == "codex" else None
|
||||
)
|
||||
command = build_command(route, repo, mode, output_path)
|
||||
try:
|
||||
result = subprocess.run(
|
||||
command,
|
||||
cwd=repo,
|
||||
input=prompt,
|
||||
text=True,
|
||||
capture_output=True,
|
||||
timeout=timeout,
|
||||
check=False,
|
||||
)
|
||||
except subprocess.TimeoutExpired as error:
|
||||
raise RuntimeError(
|
||||
f"{route.provider} delegation timed out after {timeout}s"
|
||||
) from error
|
||||
if result.returncode != 0:
|
||||
detail = (result.stderr or result.stdout).strip() or "no error output"
|
||||
raise RuntimeError(
|
||||
f"{route.provider} delegation failed ({result.returncode}): {detail}"
|
||||
)
|
||||
if route.provider == "codex":
|
||||
if output_path is None or not output_path.is_file():
|
||||
raise RuntimeError("Codex did not produce a report")
|
||||
report = output_path.read_text().strip()
|
||||
else:
|
||||
report = result.stdout.strip()
|
||||
if not report:
|
||||
raise RuntimeError(f"{route.provider} returned an empty report")
|
||||
return report
|
||||
|
||||
|
||||
def parse_args(argv: list[str] | None = None) -> argparse.Namespace:
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Delegate a self-contained task to a Claude or Codex subagent."
|
||||
)
|
||||
parser.add_argument("--provider", choices=("claude", "codex"), required=True)
|
||||
parser.add_argument(
|
||||
"--tier",
|
||||
choices=("quick", "quick-context", "standard", "deep"),
|
||||
default="standard",
|
||||
)
|
||||
parser.add_argument("--mode", choices=("read", "write"), default="read")
|
||||
parser.add_argument("--model", help="Approved model override")
|
||||
parser.add_argument("--repo", type=Path, default=Path.cwd())
|
||||
parser.add_argument("--timeout", type=int, default=1800)
|
||||
parser.add_argument("--dry-run", action="store_true")
|
||||
return parser.parse_args(argv)
|
||||
|
||||
|
||||
def main(argv: list[str] | None = None) -> int:
|
||||
args = parse_args(argv)
|
||||
try:
|
||||
task = sys.stdin.read().strip()
|
||||
if not task:
|
||||
raise ValueError("Task must be provided on stdin")
|
||||
repo = args.repo.expanduser().resolve()
|
||||
if not repo.is_dir():
|
||||
raise ValueError(f"Repository directory does not exist: {repo}")
|
||||
route = select_route(args.provider, args.tier)
|
||||
if args.model:
|
||||
route = apply_model_override(route, args.model)
|
||||
prompt = build_prompt(task, route, args.mode)
|
||||
if args.dry_run:
|
||||
output_path = (
|
||||
Path(tempfile.gettempdir()) / "delegate-dry-run-report.md"
|
||||
if route.provider == "codex"
|
||||
else None
|
||||
)
|
||||
command = build_command(route, repo, args.mode, output_path)
|
||||
print(f"provider: {route.provider}")
|
||||
print(f"model: {route.model}")
|
||||
print(f"effort: {route.effort}")
|
||||
print(f"mode: {args.mode}")
|
||||
print(f"command: {' '.join(command)}")
|
||||
print(f"prompt:\n{prompt}")
|
||||
return 0
|
||||
report = run_delegation(
|
||||
route,
|
||||
repo,
|
||||
prompt,
|
||||
args.mode,
|
||||
timeout=args.timeout,
|
||||
)
|
||||
print(f"[{route.provider} · {route.model} · {route.effort} · {args.mode}]")
|
||||
print(report)
|
||||
return 0
|
||||
except (OSError, RuntimeError, ValueError) as error:
|
||||
print(f"delegate: error: {error}", file=sys.stderr)
|
||||
return 2
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
Reference in New Issue
Block a user