From ee4b1dbf329791ba6f512932cdc8335fd36db514 Mon Sep 17 00:00:00 2001 From: Kirill Kit Date: Sun, 30 Aug 2026 00:59:56 +0300 Subject: [PATCH] refactor: turn Delegate Kit into a universal orchestration policy --- .github/workflows/ci.yml | 61 +- README.md | 328 +++---- skills/delegate-kit/SKILL.md | 228 ++++- skills/delegate-kit/agents/dk-implementer.md | 22 - skills/delegate-kit/agents/dk-planner.md | 22 - skills/delegate-kit/agents/dk-researcher.md | 20 - skills/delegate-kit/agents/dk-review-lead.md | 23 - skills/delegate-kit/agents/dk-reviewer.md | 24 - skills/delegate-kit/agents/dk-verifier.md | 15 - skills/delegate-kit/hooks/gate.sh | 60 -- skills/delegate-kit/hooks/install.sh | 99 -- skills/delegate-kit/hooks/uninstall.sh | 32 - .../delegate-kit/references/brief-template.md | 48 - .../delegate-kit/references/codex-agents.toml | 80 -- skills/delegate-kit/references/dispatch.md | 72 -- .../references/result-schema.json | 251 ----- skills/delegate-kit/references/review.md | 83 -- skills/delegate-kit/references/roles.md | 95 -- skills/delegate-kit/scripts/agent-run | 909 ------------------ skills/delegate-kit/scripts/agent-wt | 166 ---- skills/delegate-kit/tests/delivery.sh | 129 --- 21 files changed, 376 insertions(+), 2391 deletions(-) delete mode 100644 skills/delegate-kit/agents/dk-implementer.md delete mode 100644 skills/delegate-kit/agents/dk-planner.md delete mode 100644 skills/delegate-kit/agents/dk-researcher.md delete mode 100644 skills/delegate-kit/agents/dk-review-lead.md delete mode 100644 skills/delegate-kit/agents/dk-reviewer.md delete mode 100644 skills/delegate-kit/agents/dk-verifier.md delete mode 100755 skills/delegate-kit/hooks/gate.sh delete mode 100755 skills/delegate-kit/hooks/install.sh delete mode 100755 skills/delegate-kit/hooks/uninstall.sh delete mode 100644 skills/delegate-kit/references/brief-template.md delete mode 100644 skills/delegate-kit/references/codex-agents.toml delete mode 100644 skills/delegate-kit/references/dispatch.md delete mode 100644 skills/delegate-kit/references/result-schema.json delete mode 100644 skills/delegate-kit/references/review.md delete mode 100644 skills/delegate-kit/references/roles.md delete mode 100755 skills/delegate-kit/scripts/agent-run delete mode 100755 skills/delegate-kit/scripts/agent-wt delete mode 100755 skills/delegate-kit/tests/delivery.sh diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 87810f2..15a13eb 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -6,20 +6,55 @@ on: pull_request: jobs: - check: + validate-skill: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - - uses: actions/setup-node@v4 - with: - node-version: 22 - - name: Bash syntax + shellcheck + - name: Validate compact skill package run: | - sudo apt-get install -y -qq shellcheck >/dev/null - files="skills/delegate-kit/hooks/*.sh skills/delegate-kit/scripts/agent-wt" - for f in $files; do bash -n "$f"; done - shellcheck -S warning $files - - name: Node syntax - run: node --check skills/delegate-kit/scripts/agent-run - - name: Validate result schema JSON - run: python3 -m json.tool skills/delegate-kit/references/result-schema.json >/dev/null + python3 - <<'PY' + from pathlib import Path + + skill = Path("skills/delegate-kit/SKILL.md") + assert skill.is_file(), "SKILL.md is missing" + + text = skill.read_text(encoding="utf-8") + assert text.startswith("---\n"), "SKILL.md must start with frontmatter" + _, frontmatter, body = text.split("---", 2) + + required = { + "name: delegate-kit", + "description:", + "license: MIT", + "compatibility:", + } + missing = sorted(item for item in required if item not in frontmatter) + assert not missing, f"missing frontmatter fields: {missing}" + assert body.strip(), "SKILL.md body is empty" + + size = len(text.encode("utf-8")) + assert size <= 12_000, f"SKILL.md is too large for a routinely loaded policy: {size} bytes" + + required_terms = [ + "DIRECT", + "SCOUT", + "SINGLE", + "PARALLEL", + "SEQUENTIAL", + "Independent review", + ] + missing_terms = [term for term in required_terms if term not in text] + assert not missing_terms, f"missing decision concepts: {missing_terms}" + + forbidden_runtime_terms = [ + "agent-run", + "main-claude", + "main-codex", + "claude -p", + "codex exec", + ] + present = [term for term in forbidden_runtime_terms if term in text] + assert not present, f"provider-specific runtime leaked into core skill: {present}" + + print(f"Validated {skill} ({size} bytes)") + PY diff --git a/README.md b/README.md index 8f39e4d..878bbe9 100644 --- a/README.md +++ b/README.md @@ -1,246 +1,220 @@
-# delegate-kit +# Delegate Kit -**Your coding agent hands work to fresh workers — and the review always comes from the other model family.** +**A small, universal policy that teaches capable coding agents when to work directly, when to delegate, and how to supervise the result.** -One skill for Claude Code, Codex CLI and T3 Code. Your logins, your subscriptions, no new harness. +Policy, not another agent runtime. [![License: MIT](https://img.shields.io/badge/License-MIT-green.svg)](LICENSE) -[![Claude Code](https://img.shields.io/badge/Claude%20Code-parent%20or%20worker-blueviolet)](https://claude.com/claude-code) -[![Codex CLI](https://img.shields.io/badge/Codex%20CLI-parent%20or%20worker-black)](https://github.com/openai/codex) -[![T3 Code](https://img.shields.io/badge/T3%20Code-parent-orange)](https://t3.chat) -[Install](#install) · [How it works](#how-it-works) · [Usage](#usage) · [The rules](#the-rules-that-do-the-work) · [Docs](skills/delegate-kit/) +[Install](#install) · [Decision model](#decision-model) · [Examples](#examples) · [Design](#design)
--- -## What you get +## What it is -You talk to one agent. When a task is big enough, it plans with the strongest model, implements in an isolated git worktree, and gets the change **reviewed by a model from the other vendor** — then reports back with what was checked and what was not. Small tasks it just does. You decide which subscription pays with one word. +Delegate Kit is a single host-neutral skill for Claude Code, Codex, T3 Code, Orca, Pi, OpenCode, and other capable coding agents. -| Without delegate-kit | With delegate-kit | -|---|---| -| The agent delegates on a whim: a one-line fix spawns a 50k-token worker; a ten-module change starts with no plan. | **Triage first.** Up to ~3 files it does itself. Prose requirements or >1 module → a planner before any code. Signals, not moods. | -| Codex reviews Codex. Claude reviews Claude. Same blind spots twice. | **Independent review, always.** The reviewer is from the other model family than the author. No preset, no quota pressure moves that. | -| Workers write into your working tree. Two of them collide. | **Writers live in worktrees**, one per worktree, locked. Your tree stays yours; you merge what you accept. | -| Every worker is a full headless CLI session, even one the agent could have spawned itself. | **Native inside the family, external across it.** A Claude parent spawns Claude workers natively; only the Codex reviewer is a separate session — the cheapest shape there is. | -| One subscription runs out and the work stops. | **Presets.** `main-claude` or `main-codex` moves the heavy roles to the family with quota left; the reviewer stays independent; on a rate limit the other vendor picks up once, and says so. | -| "Review this" means one opinion, however big the diff. | **Review depth from the diff.** Small → one reviewer. Large or risky → a panel of lenses (spec / correctness / standards), proposed with numbers, run on your yes. | -| The worker says "done". Was anything checked? | **One JSON contract.** `status`, `changes`, `checks_run`, `not_verified`, `findings`, `questions`. A check that did not run is reported as not run. | - -
-Table of contents - -- [What you get](#what-you-get) -- [How it works](#how-it-works) -- [The rules that do the work](#the-rules-that-do-the-work) - - [Independence is the first slot](#1-independence-is-the-first-slot) - - [Native inside the family, external across it](#2-native-inside-the-family-external-across-it) - - [Presets: which subscription pays](#3-presets-which-subscription-pays) - - [Review depth: one reviewer or a panel of lenses](#4-review-depth-one-reviewer-or-a-panel-of-lenses) -- [The roles](#the-roles) -- [Install](#install) -- [Usage](#usage) -- [The tools](#the-tools) -- [What it will not do](#what-it-will-not-do) -- [Layout](#layout) -- [License and acknowledgments](#license-and-acknowledgments) - -
- -## How it works +It does not launch a proprietary fleet, replace native subagents, or hard-code a preferred vendor. The model in the current session remains the coordinator and uses the best native tools available in its host. +The skill makes one decision consistent: + +```text +Should I do this myself, ask one scout, delegate one coherent task, +parallelize a few independent tasks, or request an independent review? ``` -you ──► parent (Claude Code / T3 / Codex) "change the tariff logic in the billing module" - │ - │ triage: small? → done here. big / ambiguous / risky? → delegate - │ spec → .scratch/tariffs/spec.md - ▼ - planner ────────────────► strongest model, read-only ──────────► plan + blocking questions - │ - agent-wt create tariffs (git worktree + write-lock) - │ - implementer ────────────► workhorse model, writes, sandboxed ──► commits on dk/tariffs - │ - agent-wt diff tariffs (the frozen diff) - agent-run route --role reviewer --diff → single | panel | led, with the numbers - │ - reviewer(s) ────────────► THE OTHER FAMILY than the author ───► findings - │ - │ mechanical finding → parent fixes. real one → same implementer, resumed. - │ dispute → a command first, then a verifier (third party). - ▼ - merge / PR · agent-wt remove tariffs · report: done, checked, not checked, open -``` -Every worker starts empty. It never sees your conversation; it gets a brief and reads the repository itself, then returns one JSON object. That is the whole interface — and it is why briefs matter more than models. +That prevents common orchestration failures: + +- spawning workers for tiny tasks; +- choosing a different worker count on identical work without a reason; +- parallelizing tasks that share files or unstable interfaces; +- filling the coordinator context with repository exploration; +- letting the author approve its own non-trivial change; +- creating a recursive tree of workers with unclear ownership. -## The rules that do the work +## What changed -### 1. Independence is the first slot +Delegate Kit was originally a larger Claude/Codex-specific package with routing presets, external CLI sessions, model-family rules, hooks, a run ledger, and a custom worktree runtime. -A review by the same family that wrote the code shares its blind spots. So the reviewer is **always** the other family than the author — a Codex-written change is reviewed by Claude, a Claude-written one by Codex, and a change the parent wrote itself goes to the other vendor too. A verifier, when one is needed, is a third party again. Nothing in the skill can flip this; it is the one property delegation is for. +The current design intentionally removes that machinery from the active package. Modern harnesses already provide their own agent lifecycle, visibility, steering, worktrees, permissions, and model selection. Reimplementing those mechanisms made the skill less universal and harder for the coordinator to follow. -### 2. Native inside the family, external across it +The previous implementation remains available in Git history. The current version focuses on the missing layer: **a compact decision policy for the frontier model that is already in control.** -A parent can only spawn its own family natively: +## Decision model -| Worker family | parent = Claude Code / T3 | parent = Codex | -|---|---|---| -| Claude (`fable`, `opus`, `sonnet`, `haiku`) | **native** — `Agent` tool, `dk-` | external — `agent-run run --backend claude` | -| GPT (`gpt-5.6-sol`, `-terra`, `-luna`) | external — `agent-run run --backend codex` | **native** — `spawn_agent`, `dk-` | +Delegate Kit uses the smallest execution shape that creates a real advantage. -Native is cheaper and steerable, so it is the default inside the family. External is what `agent-run` adds: an enforced read-only sandbox, the strict JSON schema, a ledger, a resumable run id, a timeout, detached parallel writers and the cross-vendor quota fallback — worth going external for even inside the family when one of those matters. `agent-run route --role ` answers all of it in one call. Reasoning: [`references/dispatch.md`](skills/delegate-kit/references/dispatch.md). +| Shape | Use it when | +|---|---| +| **DIRECT** | The task is localized, coherent, and the current session already has enough context. This is the default. | +| **SCOUT** | The main problem is finding or verifying information across a repository, documentation, or several plausible causes. Start with one read-only scout. | +| **SINGLE** | One well-specified deliverable is large enough to benefit from a fresh context or supervised ownership. | +| **PARALLEL** | There are 2-3 genuinely independent outcomes with non-overlapping mutable state and independent verification. | +| **SEQUENTIAL** | Tasks depend on earlier results, shared contracts, or shared files and therefore must not run in parallel. | +| **REVIEW** | A fresh, read-only context checks a non-trivial or risky change. One reviewer is the default. | -### 3. Presets: which subscription pays +The central rule is: -| Preset | planner | implementer | researcher | reviewer / verifier | -|---|---|---|---|---| -| `auto` (default) | Claude | Codex | Claude | the other family than the author | -| `main-claude` | Claude | Claude | Claude | the other family than the author | -| `main-codex` | Codex | Codex | Codex | the other family than the author | +> **Worker count equals the number of independent outcomes, not the apparent size of the task.** -A preset moves the token-heavy roles. It never moves the reviewer — so `main-claude` implies a Codex reviewer and `main-codex` a Claude one. That is cheap: a review is one read-only pass over a frozen diff, a fraction of what the implementer spends. +Normal limits are three active workers, two concurrent writers, one scout, and one reviewer. Larger fleets require an unusually clear partition or an explicit user request. -Say it in words ("let Codex implement this", "main model Claude"), type it (`/delegate-kit main-claude`, `$delegate-kit main-codex`), or persist it (`agent-run preset main-claude`). Your own per-role defaults go in `~/.delegate-kit/config.json` — `"roles": { "planner": { "claude": ["fable", "xhigh"] } }` — so "always plan on xhigh" is one edit, and the repository's table stays the default for everyone else. +## Why this scales -### 4. Review depth: one reviewer, or a panel of lenses +Repository size changes the cost of finding context, not automatically the number of writers. -A second reviewer with the same brief finds the same things twice. What a second slot should buy is a second **lens**: +A small change in a monorepo may need one scout and no delegated implementation. A large feature with shared contracts may still need one owner. Two modest changes in independent packages may be ideal for two parallel workers. -| Depth | Reviewers | When | -|---|---|---| -| `single` | one, the other family | the default — small diff, one module, no risk zone | -| `panel` | two lenses, parallel and blind to each other | ~400+ changed lines, 10+ files, 2+ modules, or a risk zone | -| `led` | a review lead plans → three lenses → the lead merges | ~1200+ lines, 25+ files, 3+ modules | +The coordinator evaluates: -Lenses: `spec` (does it do what was asked), `correctness` (is it right), `standards` (the repo's conventions plus a fixed smell baseline). Slot A is the other family; slot B may be the author's family, because independence is already paid for. `agent-run route --role reviewer --diff review.diff` measures the frozen diff and prints the depth, the composition and the cost. A panel is **proposed with those numbers and run on your yes** — it is the one place the skill spends more than one session on one step. Rules, lenses and the baseline: [`references/review.md`](skills/delegate-kit/references/review.md). +- context already known versus context that must be discovered; +- independent deliverables versus coupled phases; +- overlapping files, interfaces, and external state; +- cost of a wrong result; +- checks that prove completion. -## The roles +It then uses native agents or workers only where they provide isolated context, parallel progress, specialization, or independent review. -| Role | Does | Default | Access | -|---|---|---|---| -| **planner** | Ordered steps, files per step, risks, blocking vs non-blocking questions, the checks that prove completion | Claude **Fable** high · Codex **Sol** xhigh | read-only | -| **implementer** | One vertical slice in its own worktree; runs the acceptance checks; commits on its branch | Codex **Sol** high · Claude **Opus** high for UI-heavy work | write, sandboxed, one per worktree | -| **reviewer** | Reads the frozen diff against the spec; findings with severity, kind, lens, file:line, evidence, fix | **the other family than the author**, high effort | read-only | -| **review-lead** | On a large diff: plans the reviewers before, merges their findings after. Two short calls | the planner's family at its strongest | read-only | -| **verifier** | Settles one disputed or high-risk finding: confirmed / refuted / needs-human, with evidence | a third party to the reviewer, strongest tier | read-only | -| **researcher** | Quotes current documentation with URL and date; marks what it could not verify | Claude **Sonnet** medium · Codex **Terra** medium | read-only, web | +## What the coordinator owns -And the non-role that matters most: **the parent itself** — up to ~3 files, a clear spec, low risk, micro-fixes after review, explanations, diagnoses, and everything destructive or production-adjacent, in the foreground, with you watching. +The current session remains responsible for: -Why these defaults: planning is one read-only call whose mistakes propagate everywhere, so it gets the strongest model. Within a family, raising *effort* moves review and planning quality more than raising the tier — which is also why the strongest Codex role is Sol at `xhigh` (the CLI offers Sol, Terra and Luna; there is no `pro` worker). A small model on a micro-task never pays back the ~50k-token start-up cost of a worker. Reasoning per role: [`references/roles.md`](skills/delegate-kit/references/roles.md). +- the user's intent; +- planning and decomposition; +- deciding task boundaries; +- writing worker briefs; +- supervising and steering; +- resolving questions and conflicts; +- reviewing and integrating results; +- final verification and the answer to the user. -## Install +Planning is not outsourced by default. A separate planning agent is a second opinion, not a replacement for the coordinator that holds the conversation. -**Prerequisites:** bash, git, jq, node ≥ 20; `claude` and/or `codex` installed and logged in with your own account. +## Worker briefs -```bash -# 1. the skill -npx skills add tomastaker/delegate-kit -# or by hand: -git clone https://github.com/tomastaker/delegate-kit ~/dev/delegate-kit -ln -s ~/dev/delegate-kit/skills/delegate-kit ~/.agents/skills/delegate-kit -ln -s ../../.agents/skills/delegate-kit ~/.claude/skills/delegate-kit -ln -s ../../.agents/skills/delegate-kit ~/.codex/skills/delegate-kit - -# 2. the safety hook + the native role definitions (backs up your settings, shows the diff first) -~/.agents/skills/delegate-kit/hooks/install.sh --dry-run -~/.agents/skills/delegate-kit/hooks/install.sh # or --claude / --codex / --hooks-only / --agents-only - -# 3. optional: scripts on PATH -echo 'export PATH="$HOME/.agents/skills/delegate-kit/scripts:$PATH"' >> ~/.zshrc +Every worker receives a self-contained brief: + +```text +Goal: +Scope and owned paths: +Relevant context and evidence: +Constraints and forbidden changes: +Acceptance criteria: +Checks to run: +Expected return: ``` -Restart running `claude` / `codex` sessions afterwards. +Workers return status, result, evidence or files, checks run, unverified points, risks, and the recommended next step. -The symlinks make the policy live: `SKILL.md`, the scripts and the references are read straight out of your clone, so a `git pull` is the whole update. The native role definitions are the exception — a harness discovers subagents only in `~/.claude/agents/` and `~/.codex/config.toml` — which is why `install.sh` exists: it symlinks the Claude ones and splices the Codex block between markers, replacing it on every re-run. After pulling a change to the roles, run it again. +Concurrent writers should use isolated worktrees or branches when the host supports them. Workers do not spawn more workers unless the coordinator explicitly permits it. -**Uninstall:** `hooks/uninstall.sh` (removes the hook, the `dk-*` symlinks and the `[agents.dk-*]` block), remove the three skill symlinks, and `rm -rf ~/.delegate-kit` if you do not want to keep the ledger, the preset and the run logs. +## Independent review -## Usage +The author of a non-trivial change does not certify its own work. -Work as usual. The parent loads the skill when a task spans modules, needs a review, or you say "delegate", "plan this", "review this"; invoke it explicitly with `/delegate-kit` in Claude Code or `$delegate-kit` in Codex. +Independence primarily means a fresh read-only context with the exact requirements and diff. A different model or provider is useful as an explicit second opinion, but it is not mandatory and is not hard-coded into the skill. -**What you say and what it does:** +Use one reviewer by default. Add a second only for high-risk work, distinct review lenses, competing evidence, or a direct user request. -| You say | Effect | -|---|---| -| "delegate", "plan this", "review this", "subagent" | the skill triggers | -| "let Codex implement", "main model Claude", `main-claude` / `main-codex` | preset for this task — the heavy roles move, the reviewer stays independent | -| "plan with Fable", "review with Sol" | one model for one role; the preset is untouched | -| "panel", "two reviewers", "led" | your yes to a deeper review, given in advance | -| "this is mechanical" / "a refactor" / "UI work" | `--kind` — always `single` / lenses `correctness`+`standards` / implementer on Claude under `auto` | +## Optional model selection -**What it decides alone:** whether to delegate at all; the family, model, effort and native-vs-external per role; the reviewer's family (always the other one); review depth from the diff; a command before a verifier; one cross-vendor retry on a rate limit. +When the host exposes model choice: -**Where it stops and asks:** before a panel (with the numbers); when a worker returns `blocked` with questions; when your working tree is dirty and the task touches that work; before any command `gate.sh` classes as dangerous. +- use a fast capable model for bounded lookup or extraction; +- use a balanced model for clear implementation; +- use a strong model for multi-module integration or ambiguous debugging; +- use the strongest suitable model for architecture, security, concurrency, migrations, or high-risk review. -If you interview yourself first (a grill skill, a spec session), save the outcome as `.scratch//spec.md` and the workers are pointed at it instead of a retold version. +The cheapest model is not always the cheapest run: repeated turns and corrections can cost more than one reliable pass. Explicit user routing always wins. -
-Driving the scripts by hand +## Examples -```bash -agent-run route --role implementer --preset main-claude # who runs this, where, how -agent-run route --role reviewer --diff review.diff # how deep the review should go -agent-run preset main-claude # persist the preset; alone: show the effective table - -agent-run run --role planner --brief .scratch/tariffs/brief.md -agent-wt create tariffs -agent-run run --role implementer --backend codex --cwd ../repo.worktrees/tariffs --brief .scratch/tariffs/impl.md -agent-wt diff tariffs > .scratch/tariffs/review.diff -agent-run run --role reviewer --backend claude --lens correctness --panel t1 --cwd ../repo.worktrees/tariffs --brief .scratch/tariffs/review.md -agent-run resume --prompt "Fix findings 1 and 3 from the review: ..." -agent-run list | status | wait | kill | log | notify [] +### Small fix + +```text +“Correct this null check.” +→ DIRECT +``` + +### Unknown code location in a large monorepo + +```text +“Find why the billing status is stale and fix it.” +→ one read-only SCOUT maps the data flow +→ coordinator chooses DIRECT or SINGLE after evidence returns ``` -Two implementers in parallel: `--detach` on each, then either block on `agent-run wait ` or have each one report for itself with `--on-finish CMD` — the only channel an external worker has, since no harness announces it. A native writer: `agent-wt lock tariffs` before dispatch, `agent-wt release tariffs` after. `--help` on either script is the reference for flags. +### Coupled full-stack change -
+```text +“Change the auth contract, API, and UI.” +→ coordinator plans dependencies +→ one owner or sequential phases while the contract is unstable +→ not three parallel writers by default +``` -## The tools +### Independent packages -**`agent-run`** — routes, starts, resumes, waits for, lists and kills workers, and delivers each completion to an `--on-finish` hook exactly once, retried and surviving a dead supervisor. Applies role defaults; read-only roles run `codex -s read-only` / `claude --permission-mode plan`, writers `workspace-write` / `acceptEdits`, never the dangerous modes. Writers must be in a git worktree, one per worktree, at most 2 writers and 4 workers at once. Delegation depth is 1: a worker cannot start workers. Every run returns the JSON contract and is archived under `~/.delegate-kit/runs//`; the ledger `~/.delegate-kit/ledger.jsonl` records model, effort, preset, lens, tokens, duration and outcome per run. +```text +“Add the same adapter to package A and package B; their interfaces are stable.” +→ two PARALLEL workers with explicit ownership +→ coordinator integrates and runs broader checks +``` -**`agent-wt`** — git worktrees next to the repo (`.worktrees/`, branch `dk/`): create, status, frozen diff against the recorded base, lock (for a native writer), release, remove, cleanup of merged worktrees. +### Review -**`agents/dk-*.md` · `references/codex-agents.toml`** — the six roles as native subagent definitions, one set per harness, carrying model, effort and tool list. +```text +“Implement this migration and verify it carefully.” +→ implementation +→ one fresh read-only reviewer +→ rerun affected checks after any fix +``` -**`hooks/gate.sh`** — a PreToolUse hook in both CLIs. `sudo`, `rm -rf`, service and firewall changes, certificates, destructive SQL, force-push, history-destroying git, destructive commands over SSH, disk operations — require your explicit confirmation. On Claude Code that is the normal approval prompt; Codex hooks cannot prompt, so the command is denied with an instruction to confirm with you and re-run prefixed `DELEGATE_KIT_CONFIRMED=1`. +## Install -## What it will not do +```bash +npx skills add tomastaker/delegate-kit +``` -- **Steer an external worker mid-run.** Headless sessions run to completion; you read the result and resume with a follow-up. Native subagents can be steered — one more reason the skill prefers them inside the family. -- **Sandbox.** The gate is a list of known dangerous command shapes; the real isolation is the CLIs' own sandboxes plus the worktree. A native read-only role is read-only by instruction and tool list, not by sandbox — dispatch externally when that boundary matters. -- **Make delegation cheap.** A worker is a full session. The skill exists so you pay that price only when independence, parallelism or a clean context is worth it. -- **Record native dispatches in the ledger.** Only external runs are there; a model comparison has to be run externally on both sides. -- **Mix the families.** Claude is `fable | opus | sonnet | haiku`, Codex is `gpt-5.6-sol | gpt-5.6-terra | gpt-5.6-luna`; `agent-run` rejects a name from the family that does not match `--backend`. When vendors rename models, update the table at the top of `scripts/agent-run` and the two role-definition sets. +Or copy/symlink `skills/delegate-kit` into the skill directory used by your coding agent. -Workers run through the official CLIs with the logins you already have — ordinary use of Claude Code and Codex. Do not wrap this into a product that routes other people's subscriptions; that is what the vendors prohibit. +No custom daemon, hook, shell runtime, Node.js dependency, or second provider account is required. -## Layout +To make the policy effectively always-on, add a short line to the global instructions of the host: +```text +Before non-trivial repository work, apply Delegate Kit. +If it selects DIRECT, continue without announcing the classification. ``` -skills/delegate-kit/ - SKILL.md the policy the parent reads - agents/dk-*.md the six roles as native Claude Code subagents - references/codex-agents.toml the six roles as native Codex subagents - references/dispatch.md native vs external, presets — the reasoning behind agent-run route - references/roles.md families, tiers, reasoning per role, prompt hints, tuning - references/review.md review depth, lenses, panel composition, merge rules, smell baseline - references/brief-template.md how to write a brief - references/result-schema.json the JSON contract - scripts/agent-run route / preset / run / resume / list / status / wait / kill / log / notify - scripts/agent-wt create / list / status / diff / lock / release / remove / cleanup - hooks/gate.sh PreToolUse safety gate (Claude Code + Codex) - hooks/install.sh, uninstall.sh - tests/delivery.sh completion-delivery bench; --race N for the concurrency hammer + +The full skill remains small enough to load routinely, and tiny tasks exit through DIRECT without orchestration ceremony. + +## Design + +Delegate Kit deliberately separates **policy** from **runtime**: + +```text +Delegate Kit + decides the work shape and ownership + ↓ +the current host + launches, observes, steers, isolates, and stops workers ``` -## License and acknowledgments +This keeps the skill portable as native agent systems evolve. + +The design draws on useful practices from: + +- [Superpowers](https://github.com/obra/superpowers): one agent per independent problem domain, fresh context, explicit briefs, and independent review; +- [Orca](https://github.com/stablyai/orca): clear coordinator/worker ownership, supervised lifecycle, task dependencies, and worktree-aware execution; +- [Hyperskills](https://github.com/hyperb1iss/hyperskills): matching orchestration strategy to work shape and separating parallel from sequential pipelines. + +Delegate Kit intentionally does **not** adopt large default swarms. For normal software development, a small number of well-bounded workers is more predictable, easier to supervise, and less likely to duplicate work. + +## License -MIT. The review lenses `spec` and `standards` and the smell baseline are adapted from [mattpocock/skills — code-review](https://github.com/mattpocock/skills/blob/main/skills/engineering/code-review/SKILL.md) (MIT), which keeps its two axes separate for the same reason this skill keeps reviewers blind to each other: one angle must not mask another. README structure after [Best-README-Template](https://github.com/othneildrew/Best-README-Template). +MIT. diff --git a/skills/delegate-kit/SKILL.md b/skills/delegate-kit/SKILL.md index faecdb2..bd196a6 100644 --- a/skills/delegate-kit/SKILL.md +++ b/skills/delegate-kit/SKILL.md @@ -1,77 +1,203 @@ --- name: delegate-kit -description: Delegate work to fresh workers from Claude Code or Codex, with the review always on the other model family. Use for multi-module changes, anything that needs an independent review, parallel independent work, a preset ("main-claude", "main-codex") to choose which subscription pays, or when the user says "delegate", "subagent", "worker", "plan this", "review this". +description: Use before repository work to choose a consistent execution shape: work directly, ask one read-only scout, delegate one coherent task, parallelize 2-3 independent tasks, or request independent review. Works with any capable coding agent or harness; use the host's native agent and worker tools when available. license: MIT -compatibility: Requires bash, git, node >= 20, jq; claude (Claude Code CLI) and/or codex (Codex CLI) logged in with your own subscription. +compatibility: No external runtime is required. Subagents are optional. --- -# delegate-kit +# Delegate Kit -One delegation policy from any parent — Claude Code, T3 Code (it runs Claude Code), or Codex CLI. The parent stays the orchestrator. A worker from the parent's own family is spawned natively; a worker from the other family is a headless `claude -p` / `codex exec` session started by `scripts/agent-run`. Either way a worker starts empty: it gets a brief and reads the repository itself. Everything below follows from that. +A compact, host-neutral orchestration policy for a capable coordinator model. It does not replace the current runtime; it teaches the model how to use the agents, workers, tasks, worktrees, and review tools already available. -## 1. Default: do it yourself +## Core rules -A worker is a fresh session — system prompt, project instructions, re-reading files — before it does anything useful. Do the task in the current session when any of these hold: +1. **The current session is the coordinator.** It owns the user's intent, planning, task boundaries, decisions, integration, verification, and final answer. +2. **Use the smallest effective execution shape.** Delegate only for a real gain: isolated context, parallel progress, specialized execution, or independent review. +3. **One worker owns one outcome.** Do not duplicate broad assignments unless the user explicitly wants competing solutions or a second opinion. +4. **Parallel work requires independence.** Shared mutable files, unstable interfaces, or sequential dependencies mean one owner or sequential execution. +5. **Use native capabilities first.** Do not invent cross-provider routing or launch external CLIs unless the user explicitly requests it. +6. **User instructions win.** Explicit model, provider, worker-count, review, or isolation choices override defaults unless unsafe. -- up to ~3 files, clear requirements, low risk; -- an explanation, a question, a diagnosis without code changes; -- a micro-fix after review (typo, rename, missing null check); -- anything destructive or production-adjacent: `sudo`, deletes, services, firewall, certificates, production DB, secrets, SSH. These stay with the parent, in the foreground, with the user watching. +Apply this policy without narrating the classification unless that helps the user. -Delegate for independence, parallelism, or a clean context — those are the only three reasons. Tightly coupled edits stay in one pair of hands: split across workers they come back as merge conflicts and contradictory designs. +## Preflight -## 2. When to delegate — signals +Before acting, identify: -| Signal | Delegate to | -|---|---| -| Requirements in prose with business rules, ambiguity that reading code cannot resolve, > 1 module, or > ~10 files | **planner** (read-only) before any implementation | -| A well-specified vertical task of moderate size; or 2 independent parts that can run in parallel | **implementer**, one per task, each in its own worktree | -| Any delegated implementation; any change in a risk zone (auth, payments, migrations, prod config); a diff > ~50 lines the parent wrote itself; or the user asks | **reviewer** — the other family than the author, always | -| A finding the implementer disputes, or a high-severity finding in a risk zone — **and one that a command cannot settle** | **verifier** (strongest model, read-only, rare) | -| "Fetch the current docs and quote them" — extraction, no recommendation at the end | **researcher** (cheap model, read-only, web) | -| Reading that ends in a **recommendation or a choice** ("which do we adopt", "is this upgrade safe") | **planner**, not researcher — the routing trap | +- the concrete deliverable; +- context already known versus context that must be discovered; +- dependencies and shared mutable state; +- risk and cost of a wrong result; +- checks that prove completion. -**Routing trap: research that ends in a decision is planning.** "Research" covers both "read the docs and quote them" and "read the docs and tell me what to do"; only the first is the researcher. Ask what the worker returns: a quote is research, a verdict is planning, and a wrong verdict propagates into everything downstream. +Choose the smallest matching shape. -**Verify mechanically before spending a verifier.** If one command settles a finding — `npm ls`, a test, a typecheck, a grep — the parent runs it and closes the finding. Reserve the verifier for disputes about intent, severity, or design. +### DIRECT -If the user ran a grill/interview first, its output is the spec: save it as `.scratch//spec.md` (or the repo's own spec location) and point workers at it. +Do the work in the current session when it is localized, coherent, and understandable with current context. -## 3. Process for one task +Typical cases: a small fix, explanation, tightly coupled change, or work involving secrets, production state, destructive commands, or irreversible actions. -1. **Triage** with §1–2. State in one line what you do yourself, what you delegate, and under which preset. **The user's words set the preset**: any phrasing that names who should write the code or carry the bulk of the work — "main model Claude", "let Codex implement", "Claude as the implementer", in any language — is `main-claude` / `main-codex`; naming one model for one role ("plan with Fable", "review with Sol") is a per-call override and leaves the preset alone. The roles by their everyday names: implementer = the one who writes the code (executor, coder, worker); planner = the one who decomposes; reviewer = the one who reads the diff. -2. **Spec**: grill output or the user's text → `.scratch//spec.md` when it is more than a paragraph. -3. **Route** each delegated role once — `agent-run route --role [--preset P]` — and keep its answer (family, model, effort, native or external, exact invocation) for the rest of the task. The reasoning behind the answer: `references/dispatch.md`, `references/roles.md`. -4. **Plan**, when the signals say so: `dk-planner` natively or `agent-run run --role planner --brief brief.md`. Summarise the plan to the user and ask only the questions it marked blocking. -5. **Brief** each worker with `references/brief-template.md`: goal, acceptance criteria, where to look, constraints, what to return. Done when a stranger with the repository and nothing else could start. -6. **Worktree** for every writer: `agent-wt create `. External writer → `--cwd ` on `agent-run`, which locks it. Native writer → `agent-wt lock ` and the path in the brief. One writer per worktree; at most 2 writers and 4 workers at once. - The worktree branches from the **current HEAD commit**, so uncommitted work is invisible to the worker. If `git status` is dirty and the task touches that work, tell the user and commit or stash first. Read-only roles see the working tree as it is. -7. **Implement**: `dk-implementer` in the locked worktree, or `agent-run run --role implementer --cwd --brief brief.md`. Two in parallel: `--detach` externally, background dispatch natively. +A frontier coordinator should not outsource ordinary judgment or short implementation merely because subagents exist. - **Learning a worker finished: block on it, or have it push.** One worker and nothing else to do — `agent-run wait ` blocks. Several staggered workers, or a parent that must stay responsive — pass `--on-finish CMD` on `run`. An external worker reports through no harness, so a Codex worker under a Claude parent (or the reverse) stays invisible until you happen to poll. The hook fires once on any terminal state, with the result at `$DK_PAYLOAD_PATH` and `$DK_STATUS` (`--help` has the rest), and has to land where the parent will actually look: a log it tails, a desktop notifier. A non-zero exit is a failed delivery and is retried; delivery survives a dead supervisor and never repeats, so it needs no polling fallback. +### SCOUT - **Run states.** `list` for a snapshot of several. `wait`, `list` and `status` all reconcile a supervisor that died without recording its result, so none can report a corpse as still running: it comes back `orphaned`, or `timeout` when the clock ran out. Both are terminal — but neither means the work is lost: a killed worker has usually committed before dying, so read the worktree before you rerun anything. Alongside `status` each run carries a `lifecycle`: `parked` while its session can still be revived with `resume`, `done` once it cannot. +Use one read-only scout when the main difficulty is finding or verifying information. - **Timeouts are a fuse, not a schedule.** Defaults are per role — 90 min for writers and planners, 45 for reviewers and researchers — and `--timeout MIN` overrides one run. Raise it for a brief you expect to be long rather than discovering the ceiling by losing a run to it. -8. **Freeze and size the review**: `agent-wt diff > review.diff`, then `agent-run route --role reviewer --diff review.diff` (add `--author-backend self` when the parent wrote the change itself — the review still goes to the other family). It returns the depth (`single` | `panel` | `led`), the reviewers with lens and family, and the cost. `single` runs straight away; a panel is **proposed with those numbers and run on the user's yes**. Rules and lenses: `references/review.md`. -9. **Review**: each reviewer gets the diff, the spec and — on a panel — its lens, in parallel and blind to the others. Merge by the rules in `references/review.md`; at `led` depth the `dk-review-lead` plans before and merges after. -10. **Findings**: mechanical ones the parent fixes; substantive ones go back to the same implementer (`agent-run resume `, or continue the native subagent); disputes → a command first, then the verifier. -11. **Integrate**: merge or open a PR with `gh` per repo conventions; `agent-wt release `, `agent-wt remove `. -12. **Report**: what was done, what was checked, what was not, open questions, which preset ran, which family reviewed at which depth. A check that did not run is reported as not run. +Typical cases: -## 4. Worker result contract +- relevant code may be spread across a large repository or monorepo; +- several plausible locations or causes must be mapped; +- current documentation or external facts must be collected; +- preserving coordinator context is valuable. -Every worker returns one JSON object (`references/result-schema.json`): `status` (`done` | `blocked` | `failed`), `summary`, `changes`, `checks_run`, `not_verified`, `findings` (reviewer, verifier, lead), `plan` (planner, lead), `questions`, `sources`, `next_steps`. `agent-run` prints it and stores it under `~/.delegate-kit/runs//result.json`. +The scout returns evidence, relevant files or symbols, invariants, uncertainties, and useful next reads. It does not make the final product or architecture decision. -A worker cannot talk to the user. If it needs an answer it returns `status: blocked` with `questions`; the parent asks the user and continues the same worker with `agent-run resume `. +Start with one scout. Add a second only for genuinely independent questions, such as separate subsystems or competing root-cause hypotheses. After results return, run the preflight again. -## 5. Limits and safety +### SINGLE -- **Delegation depth is 1.** Workers get no subagents of their own — `agent-run` disables them on both CLIs, and the shipped `dk-*` definitions carry no `Agent` tool. -- **Writers** run in a worktree under the backend's own sandbox (`workspace-write` / `acceptEdits`). The dangerous modes (`danger-full-access`, `bypassPermissions`) are outside this skill. -- **Read-only roles** are enforced externally (`read-only` / `plan`) and by instruction plus tool list natively. When the boundary matters — an untrusted diff, a risk zone — dispatch that role externally. -- `hooks/gate.sh` makes dangerous shell commands require the user's confirmation in the parent (Claude: the approval prompt; Codex: denied with instructions to confirm and re-run prefixed `DELEGATE_KIT_CONFIRMED=1`). -- **Quota fallback.** On a usage or rate limit `agent-run` retries the brief once on the other vendor and marks the result `fallback_from`. For a reviewer that can land the review on the author's family — the result says so; report it, or re-run later. `--fallback none` disables it. Resumes never fall back. -- The ledger `~/.delegate-kit/ledger.jsonl` records model, effort, preset, lens, tokens, duration and outcome per external run. Read it before changing a default. +Use one worker when one well-specified deliverable is substantial enough to benefit from a clean context or supervised ownership. -Scripts: `scripts/agent-run` and `scripts/agent-wt`, by absolute path or on `PATH`; `--help` on each is the reference for flags. Native role definitions: `agents/dk-*.md` (Claude Code) and `references/codex-agents.toml` (Codex), installed by `hooks/install.sh`. +The coordinator still owns the plan, brief, acceptance criteria, review, and integration. If explaining the task costs about as much as doing it, use DIRECT. + +### PARALLEL + +Use 2-3 workers only when all are true: + +- there are distinct deliverables; +- each can be understood and verified independently; +- workers do not need the same mutable state; +- write scopes do not overlap, or stable interfaces separate them; +- parallelism is likely to save meaningful time. + +Use one worker per independent outcome. + +Normal limits: + +- **3 active workers total**; +- **2 concurrent writers**; +- **1 scout by default**; +- **1 independent reviewer by default**. + +Exceed them only when the partition is unusually clear or the user explicitly asks. Task size alone never determines worker count. + +### SEQUENTIAL + +When one result changes the assumptions, interfaces, or files needed by the next task, execute in order. + +Examples: schema before API before UI; diagnosis before fix; implementation before review before repair. Sequential workers may preserve context, but dependent work must not be presented as parallel. + +## Scale correctly + +Repository size changes the cost of finding context, not automatically the number of writers. + +- A small change in a monorepo may need one scout and no delegated implementation. +- A large feature with shared contracts may still need one owner. +- Several modest changes in independent packages may justify parallel workers. + +**Worker count equals the number of independent outcomes, not the apparent size of the task.** + +## Plan before dispatch + +Planning belongs to the coordinator because it holds the user conversation. + +Before writers start, define: + +- outcome and non-goals; +- task boundaries and dependencies; +- ownership of shared files and interfaces; +- acceptance criteria and verification commands; +- risk areas and approval points. + +Use a separate planning agent only for an explicit second opinion, an unusually broad design, or plan review. Do not blindly execute another agent's plan. + +## Write a self-contained brief + +Every worker gets only the context it needs: + +```text +Goal: +Scope and owned paths: +Relevant context and evidence: +Constraints and forbidden changes: +Acceptance criteria: +Checks to run: +Expected return: +``` + +A fresh agent should be able to start without the parent conversation. The brief must be narrow enough to prevent accidental redesign and complete enough to prevent needless rediscovery. + +Concurrent writers should use isolated worktrees or branches when supported. Without safe isolation, allow one writer unless scopes are provably disjoint. + +Workers do not spawn their own workers unless the coordinator explicitly permits it. + +## Supervise without micromanaging + +Use native status, messaging, waiting, stop, and resume controls when available. + +The coordinator: + +- corrects false assumptions or scope drift; +- answers worker questions or asks the user when genuinely blocking; +- waits for every required result before integration; +- stops or replaces only demonstrably stuck, failed, or unsafe workers; +- avoids repeated polling when completion events exist. + +Do not finish while required workers are still running. Do not promise a result later. + +## Independent review + +The author of a non-trivial change does not certify its own work. + +A review is independent when it uses a fresh, read-only context and receives the exact requirements and diff. + +- Worker-authored code: the coordinator always inspects and verifies it; use a fresh reviewer when the change is non-trivial, risky, or explicitly requested. +- Coordinator-authored code: use a fresh reviewer for non-trivial or risky changes. +- A different model or provider increases independence but is optional unless requested. +- Use one reviewer by default. Add another only for high-risk work, competing evidence, distinct review lenses, or an explicit request. + +A reviewer reports concrete findings with severity, location, evidence, impact, and proposed correction. Run mechanical checks before escalating a disagreement to another model. + +A behavior-changing fix invalidates prior approval for the affected area. Re-run relevant checks and review. + +## Optional model selection + +When the host allows model choice, use the least expensive model likely to finish reliably in one pass: + +- bounded lookup or extraction: fast capable model; +- clear limited implementation: balanced model; +- multi-module integration or ambiguous debugging: strong model; +- architecture, security, concurrency, migrations, or high-risk review: strongest suitable model. + +Cheap models can cost more through extra turns and corrections. Do not hard-code vendors. Explicit user routing takes precedence. + +## Worker return + +Require a concise structured result: + +```text +Status: done | blocked | failed +Result: +Evidence / files: +Checks run: +Not verified: +Risks or questions: +Recommended next step: +``` + +Treat self-reported success as evidence to inspect, not proof. + +## Completion + +Before reporting success: + +1. integrate only accepted changes; +2. check for conflicts and unintended edits; +3. run narrow checks, then appropriate broader checks; +4. resolve or record open findings; +5. state what was completed, verified, and not verified. + +Never delegate a task whose brief costs as much as the work, ask several workers to “investigate everything,” parallelize shared state, allow uncontrolled nested delegation, or accept “done” without evidence and checks. + +The goal is not to maximize agent usage. The goal is predictable, economical, and useful delegation. diff --git a/skills/delegate-kit/agents/dk-implementer.md b/skills/delegate-kit/agents/dk-implementer.md deleted file mode 100644 index def1b84..0000000 --- a/skills/delegate-kit/agents/dk-implementer.md +++ /dev/null @@ -1,22 +0,0 @@ ---- -name: dk-implementer -description: delegate-kit IMPLEMENTER — implements exactly one vertical slice inside the git worktree named in its dispatch, runs the acceptance checks, commits on that branch. Dispatch natively from a Claude Code parent when the implementation should run on the Claude family; for a GPT implementer use `agent-run run --role implementer --backend codex`. -model: opus -effort: high -tools: Read, Glob, Grep, Bash, Edit, Write, NotebookEdit ---- - -You are the delegate-kit IMPLEMENTER for exactly one task. - -**Work only inside the worktree path your dispatch gives you.** Address every file by that absolute path and run git as `git -C ...`. Never edit the main checkout, never touch another worktree, never `git push`. The parent holds a write-lock on that worktree for you and integrates your branch afterwards. - -Rules: - -- Implement the brief and nothing else. Out-of-scope improvements go into `next_steps`, not into the diff. -- Follow the repository's own `AGENTS.md` / `CLAUDE.md` and existing patterns. -- Run the acceptance checks the brief names. Report what you ran and what you could not run. -- If something is ambiguous, stop and return `status: blocked` with precise questions instead of guessing. -- Commit your work on the worktree's current branch with a clear message. -- Anything destructive or production-adjacent (`sudo`, deletes outside the worktree, services, firewall, certificates, production databases, SSH to servers) is not yours: return `blocked` and let the parent do it in the foreground. - -RETURN FORMAT: your final message must be a single JSON object matching the delegate-kit result schema (`references/result-schema.json` in this skill): `status`, `summary`, `changes`, `checks_run`, `not_verified`, `plan`, `findings`, `questions`, `sources`, `next_steps`. Emit every top-level key; use `[]` for arrays you have nothing for. No prose outside the JSON. diff --git a/skills/delegate-kit/agents/dk-planner.md b/skills/delegate-kit/agents/dk-planner.md deleted file mode 100644 index 8bd37cf..0000000 --- a/skills/delegate-kit/agents/dk-planner.md +++ /dev/null @@ -1,22 +0,0 @@ ---- -name: dk-planner -description: delegate-kit PLANNER — read-only decomposition of a task into an ordered plan with files, risks, blocking questions and acceptance checks. Dispatch natively from a Claude Code parent when the plan should run on the Claude family; for a GPT planner use `agent-run run --role planner --backend codex`. -model: fable -effort: high -tools: Read, Glob, Grep, Bash, WebSearch, WebFetch ---- - -You are the delegate-kit PLANNER. You are read-only: do not create, modify or delete any file, and do not write through the shell either (no `>`, `tee`, `sed -i`, `git commit`, package installs). Shell access is for reading only — `git log`, `git diff`, `rg`, `ls`, test listings. - -Your dispatch names the task and, usually, a spec file. Read the repository yourself; you did not inherit the parent's conversation. - -Produce: - -- an ordered plan, each step with the files it touches and its risk; -- assumptions you had to make, stated explicitly; -- questions split into `blocking` (work cannot start) and `nice-to-know`; -- the acceptance checks that prove the work is done — commands where possible. - -Do not write code. Do not restate the repository back to the parent. - -RETURN FORMAT: your final message must be a single JSON object matching the delegate-kit result schema (`references/result-schema.json` in this skill): `status`, `summary`, `changes`, `checks_run`, `not_verified`, `plan`, `findings`, `questions`, `sources`, `next_steps`. Emit every top-level key; use `[]` for arrays you have nothing for. No prose outside the JSON. diff --git a/skills/delegate-kit/agents/dk-researcher.md b/skills/delegate-kit/agents/dk-researcher.md deleted file mode 100644 index 8f5042e..0000000 --- a/skills/delegate-kit/agents/dk-researcher.md +++ /dev/null @@ -1,20 +0,0 @@ ---- -name: dk-researcher -description: delegate-kit RESEARCHER — read-only extraction from primary sources: fetch current documentation, quote it with URL and date, mark what could not be verified. Not for recommendations — a deliverable that is a verdict or a choice belongs to dk-planner. -model: sonnet -effort: medium -tools: Read, Glob, Grep, Bash, WebSearch, WebFetch ---- - -You are the delegate-kit RESEARCHER. You are read-only: do not create, modify or delete any file, and do not write through the shell. - -You extract, you do not decide. If the dispatch asks you for a recommendation, a risk verdict, or a pick between options, return `status: blocked` and say that the task belongs to the planner role — a wrong call there propagates into everything downstream. - -Rules: - -- Primary sources first: official docs, the repository, the changelog, the RFC. A search-result snippet is not evidence. -- Every claim carries a URL and the date you read it. -- Anything you could not open, or could only infer, is marked `UNVERIFIED` explicitly. -- Quote, do not paraphrase, where the exact wording matters (flags, limits, version boundaries). - -RETURN FORMAT: your final message must be a single JSON object matching the delegate-kit result schema (`references/result-schema.json` in this skill): `status`, `summary`, `changes`, `checks_run`, `not_verified`, `plan`, `findings`, `questions`, `sources`, `next_steps`. Emit every top-level key; use `[]` for arrays you have nothing for. No prose outside the JSON. diff --git a/skills/delegate-kit/agents/dk-review-lead.md b/skills/delegate-kit/agents/dk-review-lead.md deleted file mode 100644 index 131fe59..0000000 --- a/skills/delegate-kit/agents/dk-review-lead.md +++ /dev/null @@ -1,23 +0,0 @@ ---- -name: dk-review-lead -description: delegate-kit REVIEW LEAD — plans a multi-reviewer review of a large or risky diff (lenses, files, briefs) before it runs, and merges the reviewers' findings into one deduplicated, ranked list afterwards. Read-only, two short calls per review. Dispatch natively from a Claude Code parent; for a GPT lead use `agent-run run --role review-lead --backend codex`. -model: fable -effort: high -tools: Read, Glob, Grep, Bash ---- - -You are the delegate-kit REVIEW LEAD. You are read-only: do not create, modify or delete any file, and do not write through the shell. - -You are called twice per review, and both calls are meant to be short. You are the judgement around the review, not the review itself: the reviewers read the diff body, you read around it. - -**Call 1 — before the review.** Your dispatch gives you the spec, the diff stat (`git diff --stat` or `agent-run route --role reviewer --diff` output) and the intended depth. Read the spec and the stat; open a file only when its name does not tell you what it is. Return `plan`: one step per reviewer with its lens (`spec` | `correctness` | `standards`), the files it should concentrate on, what to exclude (generated, lockfiles, vendored, pure renames), and the brief text for that reviewer following `references/brief-template.md`. Say which lens pairs cover the risk you see in the stat. - -**Call 2 — after the review.** Your dispatch gives you the reviewers' result JSONs. Return one `findings` list: - -- dedupe by meaning — the same defect described twice in different words is one finding; set `raised_by` to every slot that raised it; -- a finding two reviewers agree on gets higher confidence and needs no verifier; -- a finding one reviewer raised and the other did not mention is coverage, not a dispute — keep it; -- a finding one reviewer rates high and another explicitly calls fine is a dispute — mark it with `verdict: needs-human` and put the conflicting claims in `evidence`, so the parent can settle it by command first and by a verifier only if a command cannot; -- rank by severity, then by how many slots raised it; drop nothing silently — say in `summary` what you merged and what you dropped and why. - -RETURN FORMAT: your final message must be a single JSON object matching the delegate-kit result schema (`references/result-schema.json` in this skill): `status`, `summary`, `changes`, `checks_run`, `not_verified`, `plan`, `findings`, `questions`, `sources`, `next_steps`. Emit every top-level key; use `[]` for arrays you have nothing for. No prose outside the JSON. diff --git a/skills/delegate-kit/agents/dk-reviewer.md b/skills/delegate-kit/agents/dk-reviewer.md deleted file mode 100644 index 6e2f1dc..0000000 --- a/skills/delegate-kit/agents/dk-reviewer.md +++ /dev/null @@ -1,24 +0,0 @@ ---- -name: dk-reviewer -description: delegate-kit REVIEWER — read-only review of a frozen diff against its spec, returning findings with severity, kind, file, line, evidence and a suggested fix. Dispatch natively from a Claude Code parent only when the change was written by the GPT family; a Claude-written change must be reviewed by `agent-run run --role reviewer --backend codex`. -model: opus -effort: high -tools: Read, Glob, Grep, Bash ---- - -You are the delegate-kit REVIEWER. You are read-only: do not create, modify or delete any file, and do not write through the shell (no `>`, `tee`, `sed -i`, `git commit`, package installs). Shell access is for reading and for running the checks the brief names. - -**Independence is the point of your existence.** You are dispatched because a different model family wrote the change. Review it as an outsider: assume nothing about intent that the spec does not state. - -Your dispatch gives you a frozen diff and a spec, and — when you are one reviewer of a panel — a **lens**: `spec`, `correctness` or `standards`. The lens is your priority, not your boundary: report a high-severity problem outside it too, because a gap between lenses is worse than a duplicate. Set `lens` on every finding. Lens definitions and the standards smell baseline are in `references/review.md`. - -Report findings with: - -`severity` (high | medium | low), `kind` (spec | correctness | standards | nit), `file`, `line`, `claim`, `evidence`, `suggested_fix`. - -- Separate "does not match the spec" from "violates the repo's standards" from "nit". -- Do not restate the diff. Do not propose refactors outside its scope. -- Prefer evidence you can point at: a file:line, a command output, a contradiction with the spec. -- Say plainly when you could not verify something rather than guessing. - -RETURN FORMAT: your final message must be a single JSON object matching the delegate-kit result schema (`references/result-schema.json` in this skill): `status`, `summary`, `changes`, `checks_run`, `not_verified`, `plan`, `findings`, `questions`, `sources`, `next_steps`. Emit every top-level key; use `[]` for arrays you have nothing for. No prose outside the JSON. diff --git a/skills/delegate-kit/agents/dk-verifier.md b/skills/delegate-kit/agents/dk-verifier.md deleted file mode 100644 index a5d2c3d..0000000 --- a/skills/delegate-kit/agents/dk-verifier.md +++ /dev/null @@ -1,15 +0,0 @@ ---- -name: dk-verifier -description: delegate-kit VERIFIER — settles one disputed or high-risk review finding as confirmed, refuted or needs-human, with evidence. Rare and read-only. Dispatch natively from a Claude Code parent when the reviewer was on the GPT family; otherwise use `agent-run run --role verifier --backend codex`. -model: fable -effort: high -tools: Read, Glob, Grep, Bash ---- - -You are the delegate-kit VERIFIER. You are read-only: do not create, modify or delete any file, and do not write through the shell. - -You are expensive and rare. You exist for claims that turn on judgement — is this severity right, is this the intended behaviour, is this design defensible. Anything a single command can settle should never have reached you; if you find that a command settles it, run the command, say so, and return the verdict from the evidence rather than from an opinion. - -Your dispatch gives you: the finding, the counter-argument, and the relevant diff hunk. Return `findings[0].verdict` as `confirmed`, `refuted` or `needs-human`, with concrete evidence from the code. `needs-human` is a legitimate answer when the question is about product intent rather than about the code. - -RETURN FORMAT: your final message must be a single JSON object matching the delegate-kit result schema (`references/result-schema.json` in this skill): `status`, `summary`, `changes`, `checks_run`, `not_verified`, `plan`, `findings`, `questions`, `sources`, `next_steps`. Emit every top-level key; use `[]` for arrays you have nothing for. No prose outside the JSON. diff --git a/skills/delegate-kit/hooks/gate.sh b/skills/delegate-kit/hooks/gate.sh deleted file mode 100755 index 6cce2c5..0000000 --- a/skills/delegate-kit/hooks/gate.sh +++ /dev/null @@ -1,60 +0,0 @@ -#!/usr/bin/env bash -# delegate-kit gate — PreToolUse hook for shell commands in Claude Code and Codex CLI. -# Dangerous commands must be confirmed by the user: -# Claude Code : returns permissionDecision "ask" (native approval prompt). -# Codex CLI : "ask" is not supported by Codex hooks, so the command is denied with -# instructions to get the user's explicit confirmation and re-run with the -# DELEGATE_KIT_CONFIRMED=1 prefix, which this gate lets through. -# Usage (registered by hooks/install.sh): gate.sh --harness claude|codex -set -euo pipefail - -HARNESS="claude" -[ "${1:-}" = "--harness" ] && HARNESS="${2:-claude}" - -INPUT=$(cat) -CMD=$(printf '%s' "$INPUT" | jq -r '.tool_input.command // empty' 2>/dev/null || true) -[ -z "$CMD" ] && exit 0 - -# explicit confirmation prefix (visible in the approval UI / transcript) -if printf '%s' "$CMD" | grep -Eq '^[[:space:]]*DELEGATE_KIT_CONFIRMED=1[[:space:]]'; then exit 0; fi -[ "${DELEGATE_KIT_CONFIRMED:-}" = "1" ] && exit 0 - -# Patterns: one reason per line. Keep them specific enough not to fire on read-only diagnostics. -# command-position anchor: start, after ; & | ( ` or after xargs/exec/env/nohup/time -A='(^|[;&|(`][[:space:]]*|(xargs|exec|env|nohup|time|nice|caffeinate)[[:space:]]+)' -reason="" -check() { if printf '%s' "$CMD" | grep -Eiq "$1"; then reason="$2"; fi; } -check "${A}(sudo|doas)[[:space:]]" "privilege escalation (sudo)" -check "${A}rm[[:space:]]+(-[a-z]*r[a-z]*f|-[a-z]*f[a-z]*r|-r[[:space:]]+-f|-f[[:space:]]+-r)[a-z]*([[:space:]]|\$)" "recursive forced delete (rm -rf)" -check "${A}rm[[:space:]]+-[a-z]*r[a-z]*[[:space:]]+(/|~|\\\$HOME|\\.\\.?)([[:space:]]|\$)" "recursive delete of a root-like path" -check 'launchctl[[:space:]]+(load|unload|bootstrap|bootout|kickstart|remove|disable|enable)' "launchd service change" -check 'systemctl[[:space:]]+(stop|restart|disable|mask|enable|daemon-reload)|service[[:space:]]+[a-z0-9._-]+[[:space:]]+(stop|restart)' "system service change" -check "ufw[[:space:]]+(enable|disable|allow|deny|reject|limit|delete|insert|reset|route|default)" "firewall change" -check "(iptables|ip6tables)[[:space:]]+.*(-A|-I|-D|-F|-X|-P|--append|--insert|--delete|--flush|--policy)([[:space:]]|\$)" "firewall change" -check "(nft[[:space:]]+(add|delete|flush|insert|replace)|pfctl[[:space:]]+.*-(e|d|F|f)\\b|firewall-cmd[[:space:]]+.*--(add|remove|permanent|reload))" "firewall change" -check '(certbot|acme\.sh|openssl[[:space:]]+(req|x509|genrsa|ecparam))' "certificate / key operation" -check '(psql|mysql|mariadb|mongosh|mongo|redis-cli|sqlite3)\b.*\b(DROP|TRUNCATE|DELETE[[:space:]]+FROM|FLUSHALL|FLUSHDB|ALTER[[:space:]]+TABLE)' "destructive database statement" -check '\b(DROP[[:space:]]+(DATABASE|TABLE|SCHEMA)|TRUNCATE[[:space:]]+TABLE)\b' "destructive database statement" -check 'git[[:space:]]+push[[:space:]].*(--force([[:space:]]|$)|-f([[:space:]]|$)|--force-with-lease|\+[a-zA-Z])' "force push" -check 'git[[:space:]]+(reset[[:space:]]+--hard|clean[[:space:]]+-[a-z]*f|branch[[:space:]]+-D|checkout[[:space:]]+--[[:space:]]+\.)' "history/worktree-destroying git command" -check "${A}(reboot|shutdown|halt|poweroff)([[:space:]]|\$)" "host reboot/shutdown" -check '(diskutil[[:space:]]+(erase|partition|reformat)|mkfs\.|(^|[[:space:]])dd[[:space:]]+if=|fdisk|parted)' "disk/partition operation" -check 'chmod[[:space:]]+(-R[[:space:]]+)?(777|a\+rwx)|chown[[:space:]]+-R[[:space:]]+[^[:space:]]+[[:space:]]+/([[:space:]]|$)' "broad permission change" -check 'docker[[:space:]]+(system[[:space:]]+prune|volume[[:space:]]+(rm|prune)|rm[[:space:]]+-f|compose[[:space:]]+down[[:space:]].*(-v|--volumes))' "destructive docker operation" -check 'kill[[:space:]]+-9[[:space:]]+-1|killall[[:space:]]+-9|pkill[[:space:]]+-9[[:space:]]+-f[[:space:]]+\.' "broad process kill" -check 'crontab[[:space:]]+-r' "crontab wipe" -check '>[[:space:]]*/dev/(sd|disk|nvme)|>[[:space:]]*/etc/' "write to device or /etc" -check 'ssh[[:space:]].*[[:space:]](sudo|rm[[:space:]]+-rf|systemctl[[:space:]]+(stop|restart|disable)|reboot|shutdown|docker[[:space:]]+(rm|system[[:space:]]+prune)|dd[[:space:]]+if=|mkfs)' "destructive command over SSH" -check '(^|[[:space:]])(security[[:space:]]+delete|defaults[[:space:]]+delete|tmutil[[:space:]]+delete|rm[[:space:]].*\.(pem|key|p12)\b)' "secrets/keychain/backup deletion" - -[ -z "$reason" ] && exit 0 - -short=$(printf '%s' "$CMD" | head -c 200 | tr '\n' ' ') -if [ "$HARNESS" = "codex" ]; then - msg="delegate-kit gate: '$short' looks dangerous ($reason). Do NOT retry blindly. Show the exact command to the user, explain the blast radius, and only after the user explicitly confirms re-run it prefixed with: DELEGATE_KIT_CONFIRMED=1 " - jq -cn --arg m "$msg" '{hookSpecificOutput:{hookEventName:"PreToolUse",permissionDecision:"deny",permissionDecisionReason:$m}}' -else - msg="delegate-kit gate: $reason — confirm this command explicitly: $short" - jq -cn --arg m "$msg" '{hookSpecificOutput:{hookEventName:"PreToolUse",permissionDecision:"ask",permissionDecisionReason:$m}}' -fi -exit 0 diff --git a/skills/delegate-kit/hooks/install.sh b/skills/delegate-kit/hooks/install.sh deleted file mode 100755 index 63c477c..0000000 --- a/skills/delegate-kit/hooks/install.sh +++ /dev/null @@ -1,99 +0,0 @@ -#!/usr/bin/env bash -# Install delegate-kit into Claude Code and Codex CLI: -# 1. the gate hook -> ~/.claude/settings.json (PreToolUse) and ~/.codex/hooks.json -# 2. the native subagent roles -> ~/.claude/agents/dk-*.md and [agents.dk-*] in ~/.codex/config.toml -# Idempotent. Backs up every file it edits and prints a diff first. -# --claude / --codex only that harness --hooks-only / --agents-only only that half -# --dry-run show diffs, change nothing -set -euo pipefail -HERE=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd) -SKILL=$(cd "$HERE/.." && pwd) -GATE="$HERE/gate.sh" -AGENTS_DIR="$SKILL/agents" -CODEX_AGENTS="$SKILL/references/codex-agents.toml" -CLAUDE_HOME="${CLAUDE_CONFIG_DIR:-$HOME/.claude}" -CODEX_DIR="${CODEX_HOME:-$HOME/.codex}" -DO_CLAUDE=1; DO_CODEX=1; DO_HOOKS=1; DO_AGENTS=1; DRY=0 -for a in "$@"; do case "$a" in - --claude) DO_CODEX=0;; --codex) DO_CLAUDE=0;; - --hooks-only) DO_AGENTS=0;; --agents-only) DO_HOOKS=0;; - --dry-run) DRY=1;; - *) echo "unknown arg $a" >&2; exit 1;; esac; done -command -v node >/dev/null || { echo "node is required" >&2; exit 1; } -[ -x "$GATE" ] || chmod +x "$GATE" -TS=$(date +%Y%m%d-%H%M%S) - -apply() { # $1 target file, $2 tmp file with the new content, $3 label - local file=$1 tmp=$2 - if diff -u "$file" "$tmp" >/dev/null 2>&1; then echo "$file: already up to date"; rm -f "$tmp"; return; fi - echo "--- changes for $file:"; diff -u "$file" "$tmp" || true - if [ $DRY -eq 1 ]; then rm -f "$tmp"; return; fi - [ -f "$file" ] && cp "$file" "$file.bak-delegate-kit-$TS" - mv "$tmp" "$file"; echo "updated $file${file:+ (backup: $file.bak-delegate-kit-$TS)}" -} - -merge_hook() { # $1 file, $2 harness - local file=$1 harness=$2 - mkdir -p "$(dirname "$file")" - [ -f "$file" ] || echo '{}' > "$file" - local tmp; tmp=$(mktemp) - node - "$file" "$GATE" "$harness" > "$tmp" <<'EOF' -const fs = require("fs"); -const [file, gate, harness] = process.argv.slice(2); -const cfg = JSON.parse(fs.readFileSync(file, "utf8")); -cfg.hooks ??= {}; -cfg.hooks.PreToolUse ??= []; -const entry = { matcher: "Bash", hooks: [{ type: "command", command: `${JSON.stringify(gate).slice(1, -1)} --harness ${harness}`, timeout: 10, statusMessage: "delegate-kit gate" }] }; -// replace an existing delegate-kit entry, otherwise append -cfg.hooks.PreToolUse = cfg.hooks.PreToolUse.filter((g) => !(g.hooks || []).some((h) => /hooks\/gate\.sh --harness (claude|codex)/.test(h.command || ""))); -cfg.hooks.PreToolUse.push(entry); -process.stdout.write(JSON.stringify(cfg, null, 2) + "\n"); -EOF - apply "$file" "$tmp" -} - -link_claude_agents() { # symlink the role definitions so edits in the repo take effect immediately - local dir="$CLAUDE_HOME/agents" - [ $DRY -eq 1 ] || mkdir -p "$dir" - for src in "$AGENTS_DIR"/dk-*.md; do - local name; name=$(basename "$src"); local dst="$dir/$name" - if [ -L "$dst" ] && [ "$(readlink "$dst")" = "$src" ]; then echo "$dst: already linked"; continue; fi - if [ -e "$dst" ] && [ ! -L "$dst" ]; then - echo "$dst exists and is not our symlink" - [ $DRY -eq 1 ] || { cp "$dst" "$dst.bak-delegate-kit-$TS"; echo " backed up to $dst.bak-delegate-kit-$TS"; } - fi - echo "link $dst -> $src" - [ $DRY -eq 1 ] || ln -sfn "$src" "$dst" - done -} - -merge_codex_agents() { # splice the [agents.dk-*] block between markers in config.toml - local file="$CODEX_DIR/config.toml" - mkdir -p "$CODEX_DIR"; [ -f "$file" ] || : > "$file" - local tmp; tmp=$(mktemp) - node - "$file" "$CODEX_AGENTS" > "$tmp" <<'EOF' -const fs = require("fs"); -const [file, block] = process.argv.slice(2); -const START = "# >>> delegate-kit agents >>>", END = "# <<< delegate-kit agents <<<"; -const body = [START, fs.readFileSync(block, "utf8").trimEnd(), END].join("\n"); -let cfg = fs.readFileSync(file, "utf8"); -// markers match whole lines only, so the same text inside a comment never counts -const lineIdx = (s, marker) => { const m = new RegExp(`^${marker.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\s*$`, "m").exec(s); return m ? m.index : -1; }; -const i = lineIdx(cfg, START); -const j = i === -1 ? -1 : (() => { const k = lineIdx(cfg.slice(i + START.length), END); return k === -1 ? -1 : i + START.length + k; })(); -if (i !== -1 && j !== -1) cfg = cfg.slice(0, i) + body + cfg.slice(j + END.length); -else cfg = cfg.replace(/\s*$/, "") + (cfg.trim() ? "\n\n" : "") + body + "\n"; -process.stdout.write(cfg); -EOF - apply "$file" "$tmp" -} - -if [ $DO_HOOKS -eq 1 ]; then - [ $DO_CLAUDE -eq 1 ] && merge_hook "$CLAUDE_HOME/settings.json" claude - [ $DO_CODEX -eq 1 ] && merge_hook "$CODEX_DIR/hooks.json" codex -fi -if [ $DO_AGENTS -eq 1 ]; then - [ $DO_CLAUDE -eq 1 ] && link_claude_agents - [ $DO_CODEX -eq 1 ] && merge_codex_agents -fi -echo "done. Restart running claude/codex sessions for hooks and roles to take effect." diff --git a/skills/delegate-kit/hooks/uninstall.sh b/skills/delegate-kit/hooks/uninstall.sh deleted file mode 100755 index 5d4027d..0000000 --- a/skills/delegate-kit/hooks/uninstall.sh +++ /dev/null @@ -1,32 +0,0 @@ -#!/usr/bin/env bash -# Remove delegate-kit's gate hook from Claude Code and Codex CLI configs (backs up first). -set -euo pipefail -strip() { local file=$1; [ -f "$file" ] || { echo "$file: absent"; return; } - local ts tmp; ts=$(date +%Y%m%d-%H%M%S); tmp=$(mktemp) - node - "$file" > "$tmp" <<'JS' -const fs = require("fs"); const file = process.argv[2]; -const cfg = JSON.parse(fs.readFileSync(file, "utf8")); -if (cfg.hooks?.PreToolUse) { - cfg.hooks.PreToolUse = cfg.hooks.PreToolUse.filter((g) => !(g.hooks || []).some((h) => /hooks\/gate\.sh --harness (claude|codex)/.test(h.command || ""))); - if (cfg.hooks.PreToolUse.length === 0) delete cfg.hooks.PreToolUse; - if (Object.keys(cfg.hooks).length === 0) delete cfg.hooks; -} -process.stdout.write(JSON.stringify(cfg, null, 2) + "\n"); -JS - if diff -u "$file" "$tmp" >/dev/null; then echo "$file: nothing to remove"; rm -f "$tmp"; return; fi - cp "$file" "$file.bak-delegate-kit-$ts"; mv "$tmp" "$file"; echo "updated $file (backup: $file.bak-delegate-kit-$ts)"; } -strip "${CLAUDE_CONFIG_DIR:-$HOME/.claude}/settings.json" -strip "${CODEX_HOME:-$HOME/.codex}/hooks.json" - -# native subagent roles -CLAUDE_HOME="${CLAUDE_CONFIG_DIR:-$HOME/.claude}"; CODEX_DIR="${CODEX_HOME:-$HOME/.codex}" -for f in "$CLAUDE_HOME"/agents/dk-*.md; do - [ -L "$f" ] || continue - case "$(readlink "$f")" in *delegate-kit/agents/dk-*) rm -f "$f"; echo "removed $f";; esac -done -cfg="$CODEX_DIR/config.toml" -if [ -f "$cfg" ] && grep -q '^# >>> delegate-kit agents >>>' "$cfg"; then - ts=$(date +%Y%m%d-%H%M%S); cp "$cfg" "$cfg.bak-delegate-kit-$ts" - awk '/^# >>> delegate-kit agents >>>/{skip=1} !skip{print} /^# <<< delegate-kit agents << "$cfg" - echo "removed [agents.dk-*] from $cfg (backup: $cfg.bak-delegate-kit-$ts)" -fi diff --git a/skills/delegate-kit/references/brief-template.md b/skills/delegate-kit/references/brief-template.md deleted file mode 100644 index 78c452b..0000000 --- a/skills/delegate-kit/references/brief-template.md +++ /dev/null @@ -1,48 +0,0 @@ -# Brief template - -Keep it under ~40 lines. The worker reads the code itself; your job is to remove ambiguity, not to narrate the repository. - -The same brief serves both dispatch paths: `--brief` for an external `agent-run` worker, or the prompt body for a native subagent. Do not restate the role in it — the role preamble comes from `agent-run` or from the installed `dk-*` definition. A native writer needs one extra line the external one gets from `--cwd`: **the absolute worktree path it may touch.** - -```markdown -# Task: - -## Goal - - -## Spec -/spec.md, or 3-8 bullet requirements> - -## Acceptance criteria -- -- - -## Where to look -- -- - -## Constraints -- Follow the repo's AGENTS.md/CLAUDE.md. Do not touch: . -- No new dependencies without stating why. -- - -## Worktree (native writers only) -Work only inside ``. Run git as `git -C ...`. - -## Return -The delegate-kit result JSON. If anything is ambiguous, return `status: blocked` with precise `questions` instead of guessing. -``` - -## Role-specific additions - -**planner**: "Do not change files. Return `plan` as ordered steps with the files each step touches, `questions` split into blocking and non-blocking, and the checks that prove completion." - -**reviewer**: "Read-only. The diff is at ``; the spec at ``. Return `findings` with severity (`high` | `medium` | `low`), `file`, `line`, `claim`, `evidence`, `suggested_fix`, and `kind` (`spec` | `correctness` | `standards` | `nit`). Findings only — the diff is already known, and scope is the diff." - -**reviewer on a panel** (add to the above): "Your lens is `` — its definition is in `references/review.md`; for `standards`, the smell baseline there applies under the repo's own rules. The lens is your priority, not your boundary: report a high-severity problem outside it too. Concentrate on ``; skip ``. Set `lens` on every finding. You are one of `` reviewers; you do not see the others' findings." Externally the lens also goes on the command: `--lens --panel `. - -**review-lead**, call 1: "Spec at ``. Diff stat: ``. Depth: `led`. Return `plan`: one step per reviewer with lens, files to concentrate on, exclusions, and the brief text." Call 2: "Reviewer results: ``. Return one merged `findings` list with `raised_by`; disputes as `verdict: needs-human`." - -**verifier**: "Finding: . Counter-argument: . Return `findings[0].verdict` as `confirmed`, `refuted` or `needs-human` with evidence." - -**researcher**: "Primary sources only. Every claim with URL and date. Mark anything you could not open as UNVERIFIED. Return `summary` and `sources`." diff --git a/skills/delegate-kit/references/codex-agents.toml b/skills/delegate-kit/references/codex-agents.toml deleted file mode 100644 index eb2d3fe..0000000 --- a/skills/delegate-kit/references/codex-agents.toml +++ /dev/null @@ -1,80 +0,0 @@ -# delegate-kit — native Codex subagent roles. -# -# `hooks/install.sh` copies this block into ~/.codex/config.toml between two -# marker comment lines, replacing whatever is already there. You can also paste -# it by hand. -# -# These roles are for the case where the *parent* is Codex and the worker should -# stay in the GPT family: the parent spawns them with its own `spawn_agent` tool -# instead of paying for a fresh `codex exec` session. A worker from the Claude -# family is always started externally with `agent-run run --backend claude`. -# -# Models: the Codex CLI exposes gpt-5.6-sol, gpt-5.6-terra and gpt-5.6-luna -# (check `~/.codex/models_cache.json`). There is no `gpt-5.6-pro` worker, so the -# strongest Codex role is sol at xhigh — within a family, effort moves quality -# more than tier anyway. - -[agents.dk-planner] -description = "Read-only planner for decomposing ambiguous or cross-module work before implementation." -model = "gpt-5.6-sol" -reasoning_effort = "xhigh" -developer_instructions = """ -You are the delegate-kit PLANNER. Read-only: do not create, modify or delete any file, and do not write through the shell. Read the repository yourself; you did not inherit the parent's conversation. -Produce an ordered plan (each step with the files it touches and its risk), the assumptions you had to make, questions split into blocking and nice-to-know, and the acceptance checks that prove the work is done — commands where possible. Do not write code. -Your final message must be a single JSON object matching the delegate-kit result schema: status, summary, changes, checks_run, not_verified, plan, findings, questions, sources, next_steps. Emit every top-level key; use [] for arrays you have nothing for. No prose outside the JSON. -""" - -[agents.dk-implementer] -description = "Implementation worker for one scoped vertical task in an isolated git worktree." -model = "gpt-5.6-sol" -reasoning_effort = "high" -developer_instructions = """ -You are the delegate-kit IMPLEMENTER for exactly one task. Work only inside the git worktree path your dispatch gives you; address files by that absolute path and run git as `git -C ...`. Never edit the main checkout, never touch another worktree, never `git push`. -Implement the brief and nothing else — out-of-scope improvements go into next_steps, not into the diff. Follow the repository's AGENTS.md/CLAUDE.md and existing patterns. Run the acceptance checks the brief names and report what you ran and what you could not run. Commit on the worktree's current branch with a clear message. -If anything is ambiguous, return status=blocked with precise questions instead of guessing. Anything destructive or production-adjacent (sudo, deletes outside the worktree, services, firewall, certificates, production databases, SSH to servers) is not yours: return blocked and let the parent do it in the foreground. -Your final message must be a single JSON object matching the delegate-kit result schema: status, summary, changes, checks_run, not_verified, plan, findings, questions, sources, next_steps. Emit every top-level key; use [] for arrays you have nothing for. No prose outside the JSON. -""" - -[agents.dk-reviewer] -description = "Read-only reviewer of a frozen diff against its spec, focused on concrete findings." -model = "gpt-5.6-sol" -reasoning_effort = "high" -developer_instructions = """ -You are the delegate-kit REVIEWER. Read-only: do not create, modify or delete any file, and do not write through the shell; shell access is for reading and for running the checks the brief names. -Independence is the point of your existence — you are dispatched because a different model family wrote the change. Review the frozen diff against the spec as an outsider; assume nothing about intent the spec does not state. -When the dispatch names a lens (spec | correctness | standards) it is your priority, not your boundary: report high-severity problems outside it too, and set lens on every finding. -Report findings with severity (high|medium|low), kind (spec|correctness|standards|nit), lens, file, line, claim, evidence, suggested_fix. Separate spec mismatches from standards violations from nits. Do not restate the diff; do not propose refactors outside its scope. Say plainly what you could not verify. -Your final message must be a single JSON object matching the delegate-kit result schema: status, summary, changes, checks_run, not_verified, plan, findings, questions, sources, next_steps. Emit every top-level key; use [] for arrays you have nothing for. No prose outside the JSON. -""" - -[agents.dk-review-lead] -description = "Read-only coordinator for planning and consolidating multi-reviewer analysis of large or risky diffs." -model = "gpt-5.6-sol" -reasoning_effort = "xhigh" -developer_instructions = """ -You are the delegate-kit REVIEW LEAD. Read-only: do not create, modify or delete any file, and do not write through the shell. You are called twice per review and both calls are short: you are the judgement around the review, not the review itself. -Call 1, before the review: given the spec, the diff stat and the intended depth, return `plan` with one step per reviewer — its lens (spec | correctness | standards), the files to concentrate on, what to exclude (generated, lockfiles, vendored, renames) and the brief text for that reviewer. Read the stat, not the diff body. -Call 2, after the review: given the reviewers' result JSONs, return one `findings` list. Dedupe by meaning and set raised_by to every slot that raised it; agreement between reviewers raises confidence and needs no verifier; a finding only one reviewer raised is coverage, not a dispute; one reviewer high versus another explicitly fine is a dispute — mark verdict=needs-human with both claims in evidence. Rank by severity then by how many slots raised it; say in summary what you merged and dropped. -Your final message must be a single JSON object matching the delegate-kit result schema: status, summary, changes, checks_run, not_verified, plan, findings, questions, sources, next_steps. Emit every top-level key; use [] for arrays you have nothing for. No prose outside the JSON. -""" - -[agents.dk-verifier] -description = "Read-only adjudicator for one disputed or high-risk review finding." -model = "gpt-5.6-sol" -reasoning_effort = "xhigh" -developer_instructions = """ -You are the delegate-kit VERIFIER. Read-only: do not create, modify or delete any file, and do not write through the shell. -You are expensive and rare; you exist for claims that turn on judgement. If a single command settles the finding, run it, say so, and return the verdict from that evidence. Given the finding, the counter-argument and the relevant diff hunk, return findings[0].verdict as confirmed, refuted or needs-human with concrete evidence from the code. needs-human is a legitimate answer when the question is about product intent rather than about the code. -Your final message must be a single JSON object matching the delegate-kit result schema: status, summary, changes, checks_run, not_verified, plan, findings, questions, sources, next_steps. Emit every top-level key; use [] for arrays you have nothing for. No prose outside the JSON. -""" - -[agents.dk-researcher] -description = "Read-only primary-source researcher for extracting current facts without making recommendations." -model = "gpt-5.6-terra" -reasoning_effort = "medium" -developer_instructions = """ -You are the delegate-kit RESEARCHER. Read-only: do not create, modify or delete any file, and do not write through the shell. -You extract, you do not decide. If the dispatch asks for a recommendation, a risk verdict or a pick between options, return status=blocked and say the task belongs to the planner role. -Primary sources first; a search-result snippet is not evidence. Every claim carries a URL and the date you read it. Anything you could not open is marked UNVERIFIED explicitly. Quote rather than paraphrase where exact wording matters (flags, limits, version boundaries). -Your final message must be a single JSON object matching the delegate-kit result schema: status, summary, changes, checks_run, not_verified, plan, findings, questions, sources, next_steps. Emit every top-level key; use [] for arrays you have nothing for. No prose outside the JSON. -""" diff --git a/skills/delegate-kit/references/dispatch.md b/skills/delegate-kit/references/dispatch.md deleted file mode 100644 index 83911b8..0000000 --- a/skills/delegate-kit/references/dispatch.md +++ /dev/null @@ -1,72 +0,0 @@ -# Dispatch: native or external, and which subscription pays - -Every worker has two independent properties: which **family** the model comes from (Claude or GPT) and how it is **dispatched** — natively by the parent harness, or externally as a headless `claude -p` / `codex exec` session through `agent-run`. `agent-run route --role ` resolves both; this file is the reasoning behind its answer. - -## Native inside the family, external across it - -A parent can only spawn its own family natively: - -| Worker family | parent = Claude Code / T3 | parent = Codex | -|---|---|---| -| Claude (`fable`, `opus`, `sonnet`, `haiku`) | **native** — `Agent` tool, `subagent_type: dk-` | external — `agent-run run --backend claude` | -| GPT (`gpt-5.6-sol`, `-terra`, `-luna`) | external — `agent-run run --backend codex` | **native** — `spawn_agent`, agent `dk-` | - -Native is the default inside the family: no CLI cold start, no log to read back, the result lands in the parent's own turn, and the subagent can be continued in place. The role definitions carry the model, the effort and the tool list — `agents/dk-*.md` for Claude Code, `[agents.dk-*]` in `~/.codex/config.toml` for Codex, both installed by `hooks/install.sh` — so a native dispatch is the role name plus the brief. The brief is the same document either way; `references/brief-template.md`. - -### What native gives up - -Exactly the things a bad delegation loses first, so choose external — even inside the family — when one of them matters: - -- **An enforced sandbox.** External read-only roles run under `claude --permission-mode plan` / `codex -s read-only`. A native role is read-only by instruction and by a tool list with no writer in it — fine for a reviewer you dispatched yourself, not fine as the isolation boundary around an untrusted change. -- **The strict result schema.** External workers are held to `references/result-schema.json` by the vendor's structured-output flag; native ones follow it because the definition asks. Expect to re-read a malformed result occasionally. -- **Ledger, run id, timeout, quota fallback.** All live in `agent-run`. To compare models later, resume a worker tomorrow, or have the other vendor pick up when this one hits its limit, go external. -- **The write-lock for free.** `agent-run` takes the worktree lock itself; a native writer needs the parent to take and release it: - -``` -agent-wt create && agent-wt lock --label dk-implementer -# dispatch the native subagent; the brief names the worktree path -agent-wt diff > review.diff ; agent-wt release -``` - -`agent-run` refuses to start an external writer in a worktree locked this way, and the reverse — one writer per worktree, however it was dispatched. - -## Presets: which subscription pays - -Quota is not symmetric over time. A preset moves the token-heavy roles — **planner, implementer, researcher** — onto one family: - -| Preset | planner | implementer | researcher | reviewer / verifier / lead | -|---|---|---|---|---| -| `auto` (default) | claude | codex | claude | derived from the author | -| `main-claude` | claude | claude | claude | derived from the author | -| `main-codex` | codex | codex | codex | derived from the author | - -The reviewer and verifier are **never moved by a preset**. They are derived from whoever wrote the code, because independence is the reason they exist; a knob that could flip them would buy quota with the one property delegation is for. The consequence is explicit: `main-claude` implies a Codex reviewer, `main-codex` a Claude one. That is cheap — a review is one read-only pass over a frozen diff, a fraction of what the implementer spends — so the preset still moves the bulk of the cost. If the family you are sparing is also the one that must review, run the review later rather than on the author's own family. - -Under `auto`, a UI/design-heavy implementation goes to Claude (`--kind ui`); the reviewer follows. - -Set it, in order of precedence: - -1. in the invocation — `/delegate-kit main-claude`, `$delegate-kit main-codex`, or the words in the request. Aliases: `main-gpt`, `main-openai` → `main-codex`; `main-anthropic` → `main-claude`. Pass it as `--preset` to every `agent-run` call in that task; -2. per call — `agent-run run --preset main-claude …`; -3. for the shell — `DELEGATE_KIT_PRESET=main-claude`; -4. persistently — `agent-run preset main-claude` (`~/.delegate-kit/config.json`; `agent-run preset` alone prints the current one). - -Explicit `--backend` / `--model` / `--effort` always win over the preset, and the user can simply name a model ("plan with Fable", "review with Sol"). - -## Changing the defaults for yourself - -The role table shipped in `scripts/agent-run` is the default for every user of the skill: planner, verifier and review lead on the strongest model, implementer and reviewer on the workhorse tier. Change it for yourself — not for everyone — in `~/.delegate-kit/config.json`: - -```json -{ - "preset": "main-claude", - "roles": { - "planner": { "claude": ["fable", "xhigh"] }, - "reviewer": { "codex": ["gpt-5.6-sol", "xhigh"] } - } -} -``` - -Each entry is `["model", "effort"]` per family; anything you leave out keeps the shipped default, and `agent-run preset` prints the effective table. A malformed entry is reported and ignored, never applied half-way. This is the place for "I always want the planner on xhigh" — one edit instead of saying it every time, and the repository's defaults stay universal. - -A Claude parent under `main-claude` runs almost everything natively and pays for exactly one external session — the Codex reviewer. That is the cheapest shape this skill has. diff --git a/skills/delegate-kit/references/result-schema.json b/skills/delegate-kit/references/result-schema.json deleted file mode 100644 index e11da12..0000000 --- a/skills/delegate-kit/references/result-schema.json +++ /dev/null @@ -1,251 +0,0 @@ -{ - "title": "delegate-kit worker result", - "type": "object", - "additionalProperties": false, - "required": [ - "status", - "summary", - "changes", - "checks_run", - "not_verified", - "plan", - "findings", - "questions", - "sources", - "next_steps" - ], - "properties": { - "status": { - "type": "string", - "enum": [ - "done", - "blocked", - "failed" - ] - }, - "summary": { - "type": "string", - "description": "2-6 sentences: what was done or found, and the single most important caveat." - }, - "changes": { - "type": "array", - "description": "Files this worker modified. Empty array for read-only roles.", - "items": { - "type": "object", - "additionalProperties": false, - "required": [ - "file", - "note" - ], - "properties": { - "file": { - "type": "string" - }, - "note": { - "type": [ - "string", - "null" - ] - } - } - } - }, - "checks_run": { - "type": "array", - "items": { - "type": "string" - }, - "description": "Commands actually executed and their outcome, e.g. 'pnpm test -- tariffs: 42 passed'. Empty array if none." - }, - "not_verified": { - "type": "array", - "items": { - "type": "string" - }, - "description": "Claims or steps this worker could not confirm. Empty array if none." - }, - "plan": { - "type": "array", - "description": "Ordered steps. Planner role only; empty array otherwise.", - "items": { - "type": "object", - "additionalProperties": false, - "required": [ - "step", - "files", - "risk" - ], - "properties": { - "step": { - "type": "string" - }, - "files": { - "type": "array", - "items": { - "type": "string" - } - }, - "risk": { - "type": [ - "string", - "null" - ] - } - } - } - }, - "findings": { - "type": "array", - "description": "Reviewer/verifier/researcher findings. Empty array if none.", - "items": { - "type": "object", - "additionalProperties": false, - "required": [ - "severity", - "kind", - "lens", - "file", - "line", - "claim", - "evidence", - "suggested_fix", - "verdict", - "raised_by" - ], - "properties": { - "severity": { - "type": "string", - "enum": [ - "high", - "medium", - "low" - ] - }, - "kind": { - "type": [ - "string", - "null" - ], - "enum": [ - "spec", - "correctness", - "standards", - "nit", - null - ] - }, - "lens": { - "type": [ - "string", - "null" - ], - "enum": [ - "spec", - "correctness", - "standards", - null - ], - "description": "Which review lens raised it; null for a single reviewer without a lens." - }, - "file": { - "type": [ - "string", - "null" - ] - }, - "line": { - "type": [ - "integer", - "null" - ] - }, - "claim": { - "type": "string" - }, - "evidence": { - "type": [ - "string", - "null" - ] - }, - "suggested_fix": { - "type": [ - "string", - "null" - ] - }, - "verdict": { - "type": [ - "string", - "null" - ], - "enum": [ - "confirmed", - "refuted", - "needs-human", - null - ] - }, - "raised_by": { - "type": [ - "string", - "null" - ], - "description": "Panel merges only: slot or run id(s) that raised this finding, e.g. \"A\" or \"A,B\". Null otherwise." - } - } - } - }, - "questions": { - "type": "array", - "items": { - "type": "string" - }, - "description": "Questions for the user; non-empty when status is blocked, empty array otherwise." - }, - "sources": { - "type": "array", - "description": "Primary sources actually opened. Researcher role especially; empty array if none.", - "items": { - "type": "object", - "additionalProperties": false, - "required": [ - "url", - "title", - "date", - "verified" - ], - "properties": { - "url": { - "type": "string" - }, - "title": { - "type": [ - "string", - "null" - ] - }, - "date": { - "type": [ - "string", - "null" - ] - }, - "verified": { - "type": [ - "boolean", - "null" - ] - } - } - } - }, - "next_steps": { - "type": "array", - "items": { - "type": "string" - }, - "description": "Concrete follow-up actions. Empty array if none." - } - } -} diff --git a/skills/delegate-kit/references/review.md b/skills/delegate-kit/references/review.md deleted file mode 100644 index 9a5dc76..0000000 --- a/skills/delegate-kit/references/review.md +++ /dev/null @@ -1,83 +0,0 @@ -# Review: depth, lenses, composition - -How many reviewers a diff deserves, which angle each one takes, and how their findings become one list. `agent-run route --role reviewer --diff ` applies all of it and prints the result; this file is the reasoning behind that output. - -## Independence is the first slot, not the whole review - -One reviewer from the other family than the author buys **independence**: two families share fewer blind spots than one. That is the invariant of this skill and nothing moves it — not a preset, not a panel, not quota. - -A second reviewer with the same brief buys almost nothing: the obvious findings come back twice and the subtle ones stay missed, because both reviewers looked from the same angle. What a second slot should buy is a second **lens**. So a panel is composed as lenses first, families second: - -| Slot | Lens | Family | Buys | -|---|---|---|---| -| A | `correctness` (or the lead's pick) | the other family than the author | independence | -| B | `spec` | the author's family is allowed | coverage | -| C | `standards` | alternates back | coverage | - -Slot B may sit on the author's family because independence is already paid for by A; that is also what keeps a panel affordable when one subscription is the scarce one. - -## Depth - -| Depth | Reviewers | When | -|---|---|---| -| `single` | A | the default: under ~400 changed lines, ≤ 10 files, one module, no risk zone | -| `panel` | A + B, parallel and blind to each other | above any of those, or any risk zone touched | -| `led` | lead → A + B + C → lead | ~1200+ lines, 25+ files, 3+ modules, or a risk zone with a large diff | - -A **mechanical** diff (formatting, lockfile bump, generated client) is always `single`; size means nothing there. Pass `--kind mechanical`. - -The thresholds are starting points. The ledger records `lens` and `panel` per run: after a few panels, look at how many findings slot B raised that A did not and how many of those survived verification. If B keeps returning one low-severity nit per panel, raise the thresholds. - -**A panel is always proposed, never assumed.** More than one session on one review is a cost the user decides on. `route` prints the numbers (`lines`, `files`, `modules`, `risk_zones`, `cost_note`); put them in the proposal and wait for the yes. `--depth` set explicitly is that yes. - -## Lenses - -Three, deliberately few, and aligned with the two axes of [mattpocock's code-review skill](https://github.com/mattpocock/skills/blob/main/skills/engineering/code-review/SKILL.md) (MIT) plus the one that finds bugs: - -- **`spec`** — does the diff do what was asked: requirements missing or partial, behaviour nobody asked for, requirements that look implemented but wrong. Every finding quotes the spec line. -- **`correctness`** — is the code right: edge cases, error paths, concurrency, data loss, leaks, callers not updated, contract and schema drift. -- **`standards`** — does the code follow the repository's documented conventions, and the smell baseline below. - -A lens is a **priority, not a boundary**. Each reviewer reports a high-severity problem outside its lens too; a gap between lenses costs more than a duplicate, and the merge step removes duplicates anyway. - -### Standards: the smell baseline - -Adapted from the same skill, which took it from Fowler, *Refactoring*, ch. 3. It applies even when the repository documents nothing, under two rules: **a documented repo standard overrides the baseline**, and **a baseline smell is always a judgement call** — label it ("possible Feature Envy"), never report it as a violation. Skip anything a linter or formatter already enforces. - -Each reads *what it is* → *how to fix*: - -- **Mysterious Name** — a name that does not reveal what it does or holds → rename; if no honest name comes, the design is murky. -- **Duplicated Code** — the same logic shape in more than one hunk or file → extract the shape, call it from both. -- **Feature Envy** — a method reaching into another object's data more than its own → move it onto the data it envies. -- **Data Clumps** — the same few fields or params always travelling together → bundle them into one type. -- **Primitive Obsession** — a primitive standing in for a domain concept → give the concept its own small type. -- **Repeated Switches** — the same `switch`/`if`-cascade on the same type recurring → polymorphism, or one shared map. -- **Shotgun Surgery** — one logical change forcing scattered edits across many files → gather what changes together. -- **Divergent Change** — one module edited for several unrelated reasons → split so each changes for one reason. -- **Speculative Generality** — abstraction or hooks for needs the spec does not have → delete; inline until a real need shows. -- **Message Chains** — long `a.b().c().d()` navigation the caller should not depend on → hide the walk behind one method. -- **Middle Man** — a class or function that mostly delegates onward → cut it, call the real target. -- **Refused Bequest** — a subclass ignoring most of what it inherits → drop the inheritance, compose. - -## The lead - -At `led` depth the **review lead** (`dk-review-lead`; strongest model of the preset's planner family) is called twice, and both calls are short because it reads *around* the diff, not through it: - -1. **Before** — spec plus diff stat in, `plan` out: one reviewer per step with lens, files to concentrate on, exclusions, and the brief text. The brief is the most consequential artifact of the whole review, which is why the strongest model writes it. -2. **After** — the reviewers' result JSONs in, one merged `findings` list out, by the rules below. - -At `panel` depth the parent does both jobs itself with the same rules; the lead exists for the size at which the parent would otherwise be reading three reports into its own context. - -## Merge rules - -Reviewers run **in parallel and blind to each other**. A reviewer that reads another's findings anchors on them and the second opinion collapses into agreement; the merge is a separate step. - -- Same defect from two reviewers, even in different words → one finding, `raised_by: "A,B"`, higher confidence, no verifier. -- Raised by one, not mentioned by the other → coverage, not a dispute. Keep it. -- One says high, the other **explicitly** says the same place is fine → a dispute. Settle it by a command first (a test, a typecheck, `npm ls`); spend a verifier only when a command cannot. -- Dedupe by meaning; `file:line` catches only the trivial duplicates. -- Rank by severity, then by how many slots raised it. Drop nothing silently. - -## What it costs - -`panel` is two review sessions instead of one. `led` is three plus two short lead calls, plus a verifier per real dispute — five to seven read-only sessions on one review, which is often more than the implementation cost. That is why the default is `single`, the thresholds are conservative, and the proposal always carries the numbers. diff --git a/skills/delegate-kit/references/roles.md b/skills/delegate-kit/references/roles.md deleted file mode 100644 index 8352aa8..0000000 --- a/skills/delegate-kit/references/roles.md +++ /dev/null @@ -1,95 +0,0 @@ -# Roles: who, why, and how to prompt them - -`agent-run route --role ` prints the default for any role; this file is the reasoning, so you can deviate deliberately. Where a worker runs and which subscription pays are in `dispatch.md`; review depth and lenses in `review.md`. - -## The two families - -`--backend` picks the family; `--model` must name a model from it, and `agent-run` rejects a mismatch instead of falling back. - -| Tier | `claude` | `codex` | -|---|---|---| -| strongest — planning, the review lead, hard verification | `fable` | `gpt-5.6-sol` at `xhigh` | -| high — the workhorse for implement and review | `opus` | `gpt-5.6-sol` | -| mid — extraction and mechanical work | `sonnet` | `gpt-5.6-terra` | -| small — rarely worth a worker | `haiku` | `gpt-5.6-luna` | - -The Codex CLI is offered `sol`, `terra` and `luna` only (`~/.codex/models_cache.json` lists them and the efforts each accepts); there is no `pro` worker, so the strongest Codex role is sol at `xhigh` — consistent with the rule below that effort moves quality more than tier. Effort: `low | medium | high | xhigh | max`, plus `ultra` on Codex. - -### Defaults per role - -| Role | Model / effort | Family | Access | -|---|---|---|---| -| planner | `fable` high / `gpt-5.6-sol` xhigh | preset | read-only | -| implementer | `opus` high / `gpt-5.6-sol` high | preset; `--kind ui` → Claude under `auto` | write, in a worktree | -| reviewer | `opus` high / `gpt-5.6-sol` high | **the other family than the author** | read-only | -| review-lead | `fable` high / `gpt-5.6-sol` xhigh | the planner's family | read-only | -| verifier | `fable` high / `gpt-5.6-sol` xhigh | third party to the reviewer | read-only | -| researcher | `sonnet` medium / `gpt-5.6-terra` medium | preset | read-only, web | - -## The cost model that drives every choice - -- A worker is a fresh session. Fixed cost per worker: its system prompt, project instructions, skill listing, and re-reading the files it needs. Tens of thousands of tokens before it does anything useful. -- Therefore small tasks never pay off as workers, whatever the model. The parent does them in-context. -- Within a model family, **reasoning effort** moves quality more than model size for review and planning. Raise effort before you raise the model tier. -- Prompt caching works per vendor on identical prefixes: five Codex reviewers share one cached prefix. Cross-vendor calls share nothing — that is fine, it is the price of independence. -- The parent's own turns are cheap if the parent is a mid-tier model (Opus 5, Sol, Sonnet 5, Terra). Running the parent on the strongest model for routine orchestration is the most common waste. - -## planner - -- **Default** `claude` fable high. Fallback opus high. On the Codex side gpt-5.6-sol xhigh (there is no `pro` worker). -- **Why strongest**: one read-only call; errors here propagate into implementation and review. Decomposition quality is the whole point. -- **Prompt hints**: give the spec, ask for a plan with ordered steps, file-level touch list, risks, open questions marked `blocking` vs `nice-to-know`, and acceptance checks. Ask it to state assumptions explicitly. Forbid code changes. -- **When to skip**: the parent is already a strong model and the task is clear, or the task is small. - -## implementer - -- **Default** `codex` gpt-5.6-sol high for backend, refactors, types, scripts; `claude` opus high for UI/design-heavy work or when the repo's conventions are Claude-shaped (CLAUDE.md heavy). -- **One worker = one vertical slice.** Do not split a feature horizontally across workers. -- **Always in a worktree** created by `agent-wt`. The implementer commits on its branch; the parent integrates. -- **Prompt hints**: spec path, acceptance criteria, the commands that prove success (tests, typecheck, build), conventions to follow, files not to touch, and "return `blocked` with questions instead of guessing" for anything ambiguous. - -## reviewer - -- **Default** the other family than the author, high effort. After a Codex implementer: `claude` opus high. After a Claude implementer: `codex` gpt-5.6-sol high. UX/product review: `claude` opus. -- **Why the other family**: the same family reviewing itself shares blind spots. Independence is the value — and it is the first slot of a panel, never something a preset moves. -- **Depth and lenses**: one reviewer by default; a panel of two or three lenses for large or risky diffs, proposed with numbers and run only on the user's yes. `review.md`. -- **Read-only.** Give it the frozen diff (`agent-wt diff`) and the spec. Ask for findings with severity, file:line, the claim, the evidence, and a suggested fix. Ask it to separate "spec mismatch" from "standards" from "nit". Ask it not to restate the diff. -- **Effort**: high by default; xhigh only in risk zones (auth, payments, migrations). - -## review-lead - -- **Default** the planner's family at its strongest: `claude` fable high or `codex` gpt-5.6-sol xhigh. -- **When**: `led` depth only — a diff large enough that the parent would otherwise read three reviewers' reports into its own context. At `panel` depth the parent merges by the same rules itself. -- **Two short calls.** Before the review it reads the spec and the diff *stat*, not the body, and returns the reviewer plan with one brief per lens. After the review it reads the result JSONs and returns one merged list. If it is reading the whole diff it has become a fourth reviewer, which is the expensive mistake to watch for. -- **Prompt**: call 1 — spec path, `agent-run route --role reviewer --diff` output, intended depth; ask for `plan`. Call 2 — the result JSON paths; ask for `findings` with `raised_by`, disputes as `needs-human`. - -## verifier - -- **Default** third party: `codex` gpt-5.6-sol xhigh after an Opus review; `claude` fable high after a Sol review. Dispatch it externally even when the family matches the parent — a verdict you will act on deserves the enforced sandbox and the recorded run. -- **When**: the implementer disputes a finding, or a high-severity finding lands in a risk zone. Not for every review. -- **First ask whether a command settles it.** Many findings are mechanically checkable: `npm ls`, a test, a typecheck, a grep, a diff against the previous state. Those the parent verifies directly — one command beats another opinion, and it produces evidence instead of a second guess. Spend a verifier only on claims that turn on judgement: is this severity right, is this the intended behaviour, is this design defensible. -- **Prompt**: the finding, the counter-argument, the relevant diff hunk. Ask for a verdict (`confirmed` / `refuted` / `needs-human`) with evidence. - -## researcher - -- **Default** `claude` sonnet medium or `codex` gpt-5.6-terra medium. -- **Why cheap**: "fetch official docs, quote with URL and date, say what is verified and what is not" is extraction, not judgement. -- **The routing trap.** If judgement is needed — compare architectures, pick a library, decide whether an upgrade is safe — that is a **planner** call, not a researcher one. The word "research" hides two different jobs. Decide by what the worker returns: a quote is research, a verdict is planning. Getting this wrong is the most common misuse of this skill, because "go read the docs" sounds like extraction right up until the answer has to be a recommendation. -- **Escalate on risk.** Even for genuine extraction, raise to `--model opus --effort high` (or `gpt-5.6-sol high`) when the subject is security, auth, payments, data loss, or cross-version compatibility. Observed failure mode: a cheap researcher quotes changelogs correctly and then predicts tooling behaviour wrongly — it said a peer-dependency mismatch would be a warning; the package manager hard-failed on it. -- **Rules**: primary sources first; a search snippet is not evidence; return URLs and dates; mark `UNVERIFIED` explicitly. - -## Small models — when - -The small tier is `gpt-5.6-luna` on the Codex side and `haiku` on the Claude side. **Within it, prefer Luna**: it reasons better and follows a brief more literally, and literal brief-following is exactly what fails first on a cheap worker. - -Almost never as workers, though. The start-up cost dominates — a worker that reads one line out of one file still costs ~50k input tokens. Candidates: bulk mechanical transforms across many files with an unambiguous rule, where per-unit cost actually matters. Even then Terra or Sonnet at medium effort is usually the better trade. As a *parent* for trivial chat-level work a small model is fine. - -## Parent model choice - -- Routine orchestration: Opus 5 / Sol / Sonnet 5 / Terra. The parent writes briefs and reads reports; that does not need the top model. -- Switch the parent up (Fable / Sol xhigh) for a grill session, an architecture decision, or a hard bug the parent must reason about itself. Switch back afterwards. -- The parent's family decides which roles can be native at all (`dispatch.md`). - -## Tuning - -`~/.delegate-kit/ledger.jsonl` has one line per external run: role, backend, model, effort, preset, lens, panel, tokens, duration, status. Native dispatches are not in it — that is a real gap when you are comparing families, and a reason to run a comparison externally on both sides rather than trusting an impression. After a couple of weeks, look for roles where the expensive model never changes the outcome (downgrade) and roles with repeated `failed`/`blocked` (upgrade or fix the brief template). diff --git a/skills/delegate-kit/scripts/agent-run b/skills/delegate-kit/scripts/agent-run deleted file mode 100755 index 4bc5235..0000000 --- a/skills/delegate-kit/scripts/agent-run +++ /dev/null @@ -1,909 +0,0 @@ -#!/usr/bin/env node -// agent-run — start, resume, watch and stop headless workers (Claude Code / Codex CLI) -// with role-based defaults, a JSON result contract, worktree write-locks and a ledger. -// No dependencies. Node >= 20. - -import { spawn, spawnSync } from "node:child_process"; -import fs from "node:fs"; -import os from "node:os"; -import path from "node:path"; -import { fileURLToPath } from "node:url"; - -const HERE = path.dirname(fileURLToPath(import.meta.url)); -const SKILL_DIR = path.resolve(HERE, ".."); -const SCHEMA_PATH = path.join(SKILL_DIR, "references", "result-schema.json"); -const STATE_DIR = process.env.DELEGATE_KIT_HOME || path.join(os.homedir(), ".delegate-kit"); -const RUNS_DIR = path.join(STATE_DIR, "runs"); -const LEDGER = path.join(STATE_DIR, "ledger.jsonl"); -const MAX_WORKERS = Number(process.env.DELEGATE_KIT_MAX_WORKERS || 4); -const MAX_WRITERS = Number(process.env.DELEGATE_KIT_MAX_WRITERS || 2); - -// ---------- role matrix (defaults; override with --preset/--backend/--model/--effort) ---------- -// Model and effort per role, per model family. `--backend` picks the family; which family -// a role lands in is decided by the preset below, not baked into the role. -// The Codex CLI exposes gpt-5.6-sol | gpt-5.6-terra | gpt-5.6-luna (see ~/.codex/models_cache.json); -// there is no `pro` worker, so the strongest Codex role is sol at xhigh. -const ROLES = { - planner: { write: false, claude: ["fable", "high"], codex: ["gpt-5.6-sol", "xhigh"] }, - implementer: { write: true, claude: ["opus", "high"], codex: ["gpt-5.6-sol", "high"] }, - reviewer: { write: false, claude: ["opus", "high"], codex: ["gpt-5.6-sol", "high"] }, - verifier: { write: false, claude: ["fable", "high"], codex: ["gpt-5.6-sol", "xhigh"] }, - researcher: { write: false, claude: ["sonnet", "medium"], codex: ["gpt-5.6-terra", "medium"] }, - "review-lead": { write: false, claude: ["fable", "high"], codex: ["gpt-5.6-sol", "xhigh"] }, -}; -/* - * Потолок времени на прогон, по ролям. - * - * Общие 30 минут на всех оказались впритык именно для пишущих ролей. Замер по - * ledger на 2026-08-24: 17 прогонов implementer на codex дали медиану 17,1 мин - * и максимум 29,3 мин — один финишировал за 42 секунды до убийства, а - * следующий (20260824172835-tv4q) в потолок упёрся и был снят на тридцатой - * минуте уже после коммита, на финальных проверках. Ревьюеры в тех же данных - * укладываются в 4-13 минут и запаса не требуют. - * - * Потолок здесь — предохранитель от зависшего процесса, а не инструмент - * планирования: платить за него приходится только в аварии, поэтому запас - * выбран кратный, а не впритык к наблюдаемому максимуму. Конкретный прогон - * по-прежнему может задать своё число флагом `--timeout`. - */ -const ROLE_TIMEOUT_MIN = { - planner: 90, - implementer: 90, - reviewer: 45, - verifier: 45, - researcher: 45, - "review-lead": 45, -}; - -// Per-user overrides live in ~/.delegate-kit/config.json under "roles": -// { "roles": { "planner": { "claude": ["opus", "high"] }, "reviewer": { "codex": ["gpt-5.6-sol", "xhigh"] } } } -// The shipped table above is the default for everyone; the config is how one user changes it -// without editing this file. Applied before any routing. -const CONFIG_FILE = path.join(STATE_DIR, "config.json"); -(function applyRoleOverrides() { - let cfg; try { cfg = JSON.parse(fs.readFileSync(CONFIG_FILE, "utf8")); } catch { return; } - const roles = cfg && cfg.roles; if (!roles || typeof roles !== "object") return; - for (const [role, fam] of Object.entries(roles)) { - if (!ROLES[role]) { process.stderr.write(`agent-run: config.json roles.${role} ignored — unknown role\n`); continue; } - for (const backend of ["claude", "codex"]) { - const v = fam && fam[backend]; if (v === undefined) continue; - if (!Array.isArray(v) || v.length !== 2 || typeof v[0] !== "string" || typeof v[1] !== "string") { process.stderr.write(`agent-run: config.json roles.${role}.${backend} ignored — expected ["model", "effort"]\n`); continue; } - ROLES[role][backend] = [v[0], v[1]]; - } - } -})(); -const ROLE_PREAMBLE = { - planner: "You are a read-only PLANNER. Do not modify files. Produce an ordered plan with files per step, risks, blocking vs non-blocking questions, and the checks that prove completion.", - implementer: "You are the IMPLEMENTER for exactly this task. Work only inside the current directory (a dedicated git worktree). Run the acceptance checks you can. Commit your work on the current branch with a clear message. If something is ambiguous, stop and return status=blocked with precise questions instead of guessing.", - reviewer: "You are a read-only REVIEWER. Do not modify files. Review the frozen diff against the spec. Report findings with severity, kind (spec|correctness|standards|nit), file, line, claim, evidence, suggested_fix. Do not restate the diff; do not propose out-of-scope refactors.", - verifier: "You are a read-only VERIFIER. Decide whether the given finding is confirmed, refuted, or needs-human, with concrete evidence from the code.", - researcher: "You are a read-only RESEARCHER. Use primary sources; return URLs and dates; a search snippet is not evidence; mark anything you could not open as UNVERIFIED.", - "review-lead": "You are the read-only REVIEW LEAD. Do not modify files. Before the review: read the spec and the diff stat (not the diff body) and return a plan — one reviewer per step with its lens, files, exclusions, and the brief. After the review: read the reviewers' findings and return one merged, deduplicated, ranked list; mark disputes (one reviewer high, another explicitly fine) for the verifier.", -}; -const LENSES = { - spec: "LENS: spec. Your priority is whether the diff does what the spec asks: missing or partial requirements, behaviour nobody asked for, requirements that look implemented but wrong. Quote the spec line for each finding.", - correctness: "LENS: correctness. Your priority is whether the code is right: edge cases, error paths, concurrency, data loss, leaks, unupdated callers, contract and schema drift.", - standards: "LENS: standards. Your priority is whether the code follows this repository's documented conventions and the smell baseline in the brief. A documented repo rule overrides the baseline; skip what tooling already enforces; baseline smells are judgement calls, never hard violations.", -}; -const LENS_TAIL = "Report high-severity problems outside your lens too; another reviewer covers the other lenses, but a gap between lenses is worse than a duplicate. Set `lens` on every finding."; -const RESULT_CONTRACT = `\n\nRETURN FORMAT: your final answer must be a single JSON object matching the delegate-kit result schema: {"status":"done|blocked|failed","summary":string,"changes":[{"file","note"}],"checks_run":[string],"not_verified":[string],"plan":[{"step","files","risk"}],"findings":[{"severity","kind","lens","file","line","claim","evidence","suggested_fix","verdict","raised_by"}],"questions":[string],"sources":[{"url","title","date","verified"}],"next_steps":[string]}. Emit every top-level key: use [] for arrays you have nothing for, and null for optional scalars inside items. Do not omit keys — strict output schemas reject partial objects. No prose outside the JSON.`; - -// ---------- helpers ---------- -const die = (msg, code = 1) => { process.stderr.write(`agent-run: ${msg}\n`); process.exit(code); }; -const nowIso = () => new Date().toISOString(); -const newId = () => `${new Date().toISOString().replace(/[-:T]/g, "").slice(0, 14)}-${Math.random().toString(36).slice(2, 6)}`; -const readJson = (p, fallback = null) => { try { return JSON.parse(fs.readFileSync(p, "utf8")); } catch { return fallback; } }; -const writeJson = (p, v) => fs.writeFileSync(p, JSON.stringify(v, null, 2) + "\n"); -const runDir = (id) => path.join(RUNS_DIR, id); -const metaOf = (id) => readJson(path.join(runDir(id), "meta.json")); -const saveMeta = (m) => writeJson(path.join(runDir(m.id), "meta.json"), m); -const alive = (pid) => { try { process.kill(pid, 0); return true; } catch { return false; } }; - -/* - * Сверка записанного статуса с фактической жизнью процесса. - * - * Запись «running» делает супервизор при старте, а снимает — его обработчик - * `exit`. Если сам супервизор не пережил прогон (убит вместе с группой по - * таймауту, снят извне, умер вместе с родительской оболочкой), обработчик не - * отрабатывает, и прогон остаётся «running» навсегда: он держит write-lock на - * worktree и занимает слот в лимите воркеров, а оркестратор ждёт его вечно. - * - * Раньше эта сверка была скопирована внутрь `list` и `wait`, а `status` — - * единственная команда, которой пользуются для опроса, — печатал meta как есть - * и врал. Замер 2026-08-24: `status` отдавал «running» для прогона, чей pid был - * мёртв 7 минут, а `list` в ту же секунду — «orphaned». Теперь сверка одна на - * всех, и обойти её нельзя. - * - * `killReason` важнее факта смерти: если труп оставлен таймером или `kill`, - * причина уже известна и заменять её на «orphaned» значит терять её. - */ -function reconcile(m) { - if (!m || m.status !== "running" || alive(m.pid)) return m; - m.status = m.killReason || "orphaned"; - m.finished = m.finished || nowIso(); - if (!m.result) { - m.result = { - status: "failed", - summary: m.killReason === "timeout" - ? `Прогон убит по таймауту ${m.timeoutMin} мин. Работа могла быть частично выполнена и закоммичена — проверь worktree ${m.cwd} и stdout.log.` - : `Супервизор прогона умер, не записав результат. Проверь worktree ${m.cwd} и stdout.log.`, - changes: [], checks_run: [], not_verified: [], findings: [], plan: [], questions: [], sources: [], next_steps: [], - }; - writeJson(path.join(runDir(m.id), "result.json"), m.result); - } - saveMeta(m); - if (m.write) releaseWriteLock(m.cwd, m.id); - return m; -} -const sh = (cmd, args, opts = {}) => spawnSync(cmd, args, { encoding: "utf8", ...opts }); - -function parseArgs(argv) { - const out = { _: [] }; - for (let i = 0; i < argv.length; i++) { - const a = argv[i]; - if (a.startsWith("--")) { - const k = a.slice(2); - const next = argv[i + 1]; - if (next === undefined || next.startsWith("--")) out[k] = true; else { out[k] = next; i++; } - } else out._.push(a); - } - return out; -} - -function gitDirOf(cwd) { - const r = sh("git", ["rev-parse", "--git-dir"], { cwd }); - if (r.status !== 0) return null; - return path.resolve(cwd, r.stdout.trim()); -} -function lockPath(cwd) { const g = gitDirOf(cwd); return g ? path.join(g, "delegate-kit.lock") : null; } - -function activeRuns() { - if (!fs.existsSync(RUNS_DIR)) return []; - return fs.readdirSync(RUNS_DIR).map(metaOf).filter(Boolean).filter((m) => m.status === "running" && alive(m.pid)); -} - -function acquireWriteLock(cwd, id, role) { - const lp = lockPath(cwd); - if (!lp) die(`--cwd ${cwd} is not inside a git repository; writers must run in a git worktree`); - const existing = readJson(lp); - if (existing && existing.kind === "native" && existing.id !== id) { - die(`worktree is locked for a native subagent (${existing.label || existing.id}, since ${existing.since}). Release it with \`agent-wt release \` when that subagent is done.`); - } - if (existing && existing.pid && alive(existing.pid) && existing.id !== id) { - die(`worktree is locked by another writer (run ${existing.id}, pid ${existing.pid}, role ${existing.role}). One writer per worktree.`); - } - writeJson(lp, { id, role, kind: "process", pid: process.pid, owner: os.userInfo().username, cwd, since: nowIso() }); - return lp; -} -function releaseWriteLock(cwd, id) { - const lp = lockPath(cwd); - if (!lp) return; - const existing = readJson(lp); - if (existing && existing.id === id) fs.rmSync(lp, { force: true }); -} - -/* ---------- доставка завершений (порт из omp AsyncJobManager) ---------- - * - * Проблема, которую это закрывает: родитель-оркестратор узнаёт о конце - * внешнего воркера только когда сам спросит. Для воркера своей семьи это - * решает harness, для чужой (Codex из Claude и наоборот) — никто. Замер - * 2026-08-26: три воркера отработали, и о каждом стало известно только - * потому, что пользователь спросил вслух. - * - * Устройство взято у omp (`packages/coding-agent/src/async/job-manager.ts`): - * сообщает не родитель себе опросом, а сам прогон — родителю, через сток - * доставки. Оттуда же две детали, без которых сток бесполезен: - * - * 1. Ретраи. Хук может не сработать по причине, не связанной с прогоном - * (демон уведомлений не поднят, сеть, занятый ресурс). Один выстрел в - * пустоту теряет завершение навсегда. - * 2. Ровно однажды. Доставку пробуют несколько процессов: супервизор в - * обработчике `exit`, и любой последующий `list`/`status`/`wait`/`notify` - * — в том числе тогда, когда супервизор умер, не доставив. Без атомарной - * заявки родитель получит одно и то же завершение дважды. - * - * Заявка — файл `delivery.lock`, создаваемый с флагом `wx`: атомарно на любой - * POSIX-ФС. Труп заявки опознаётся по мёртвому pid, иначе брошенный хук - * заблокировал бы прогон навсегда. - * - * Чего здесь намеренно нет: собственного демона. Очередь дренируется тем, кто - * и так обращается к прогону, плюс явной командой `notify`. Пока супервизор - * жив, он ретраит сам — на поллинг падает только остаток. - */ - -const HOOK_TIMEOUT_MS = Number(process.env.DELEGATE_KIT_HOOK_TIMEOUT_MS || 60_000); -const DELIVERY_MAX_ATTEMPTS = 5; -// Пауза перед попыткой N+1, мс. Длина массива задаёт число ретраев. -const DELIVERY_BACKOFF_MS = [5_000, 15_000, 45_000, 120_000]; -// Сколько супервизор готов ждать, дренируя очередь, прежде чем выйти и -// оставить остаток поллингу. Покрывает первые три паузы из расписания. -const SUPERVISOR_DRAIN_BUDGET_MS = 90_000; - -const claimPath = (id) => path.join(runDir(id), "delivery.lock"); - -/* - * Жизненный цикл прогона, отдельно от `status`. - * - * `status` отвечает на вопрос «чем кончилось», и его значения менять нельзя — - * на них завязаны и скилл, и внешние вызовы. Здесь второй, ортогональный - * вопрос: «можно ли к этому прогону вернуться». - * - * У omp состояния живого агента — running | idle | parked | aborted, потому - * что субагент там живёт внутри процесса и между ходами простаивает. У нас - * процесс воркера после ответа мёртв всегда, поэтому `idle` был бы враньём: - * простаивать нечему. Переживает прогон только транскрипт сессии на диске, и - * `agent-run resume` поднимает его — это ровно `parked`. Раньше узнать это - * можно было, лишь попробовав `resume` и получив отказ. - */ -function lifecycleOf(m) { - if (!m) return null; - if (m.status === "running") return "running"; - return m.sessionId ? "parked" : "done"; -} - -/* - * Заявка создаётся через `link`, а не через `open(…, "wx")`. - * - * `open` с "wx" атомарен только в части создания: pid дописывается следующей - * операцией, и между ними файл существует, но пуст. Конкурент в этом окне - * читал пустоту, получал `Number("") === 0`, считал заявку трупом, сносил её и - * заходил внутрь. Замер 2026-08-26, 25 раундов по 8 процессов: 2 раунда с - * двойным вызовом хука и 9 раундов, где проигравший затёр `delivered` своим - * `pending`. То есть ровно те две гарантии, ради которых заявка и заведена. - * - * `link` переносит уже готовый файл: он либо появляется целиком, либо падает с - * EEXIST. Пустой заявки не существует в принципе, поэтому нечитаемая заявка - * теперь означает «занято», а не «труп». Чтобы это не превратилось в вечную - * блокировку, нечитаемая заявка всё же признаётся трупом, но по возрасту — с - * запасом от потолка одного хука. - */ -function claimDelivery(id) { - const p = claimPath(id); - const tmp = `${p}.${process.pid}`; - for (let i = 0; i < 2; i++) { - try { - fs.writeFileSync(tmp, `${process.pid}\n`); - fs.linkSync(tmp, p); - fs.rmSync(tmp, { force: true }); - return true; - } catch (e) { - fs.rmSync(tmp, { force: true }); - if (e.code !== "EEXIST") return false; - let holder = 0; - try { holder = Number(fs.readFileSync(p, "utf8").trim()) || 0; } catch { return false; } - if (holder && holder !== process.pid && alive(holder)) return false; - if (!holder) { - // Нечитаемая заявка — «занято», а не труп: пустой её оставить может только - // чужая старая версия скрипта. Чтобы это не стало вечной блокировкой, - // такая заявка всё же снимается, но по возрасту. - let ageMs = 0; - try { ageMs = Date.now() - fs.statSync(p).mtimeMs; } catch { return false; } - if (ageMs < HOOK_TIMEOUT_MS * 2) return false; - } - fs.rmSync(p, { force: true }); // труп заявки: держатель мёртв или заявка протухла - } - } - return false; -} -const releaseDelivery = (id) => fs.rmSync(claimPath(id), { force: true }); - -function deliveryPayload(m, attempt) { - return { - id: m.id, role: m.role, backend: m.backend, model: m.model, effort: m.effort, - status: m.status, worker_status: m.workerStatus ?? null, lifecycle: lifecycleOf(m), - cwd: m.cwd, started: m.started, finished: m.finished ?? null, - summary: m.result?.summary ?? null, result: m.result ?? null, - logs: runDir(m.id), attempt, - }; -} - -/* - * Одна попытка доставки. Возвращает "delivered" | "failed" | null (нечего - * делать сейчас: хука нет, прогон жив, уже доставлено, не пришёл срок, заявку - * держит другой процесс). - */ -function deliverPending(m, opts = {}) { - if (!m || !m.onFinish) return null; - if (m.status === "running") return null; - // Провал по квоте — не конец работы: fallback уже перезапустил бриф на - // другом вендоре, и завершение придёт от нового прогона. Доставлять здесь - // значит сообщить родителю о неудаче, которой не было. - if (m.status === "failed-quota") return null; - const d = m.delivery || { state: "pending", attempts: 0, nextAttemptAt: null, lastError: null, deliveredAt: null }; - if (d.state === "delivered" && !opts.force) return null; - if (d.state === "failed" && !opts.force) return null; - if (d.nextAttemptAt && Date.parse(d.nextAttemptAt) > Date.now() && !opts.force) return null; - if (!claimDelivery(m.id)) return null; - try { - // Перечитать под заявкой: между проверкой и захватом другой процесс мог - // доставить. Это и есть точка, где обеспечивается «ровно однажды». - const fresh = metaOf(m.id); - if (!fresh || !fresh.onFinish) return null; - const fd = fresh.delivery || d; - if (fd.state === "delivered" && !opts.force) return null; - const attempt = (fd.attempts || 0) + 1; - const payload = deliveryPayload(fresh, attempt); - const summary = (payload.summary || "").slice(0, 500); - // Payload кладётся и файлом: однострочному хуку читать stdin неудобно, а - // не прочитать его нельзя — см. трактовку EPIPE ниже. - const payloadPath = path.join(runDir(fresh.id), "delivery-payload.json"); - writeJson(payloadPath, payload); - const r = spawnSync("/bin/sh", ["-c", fresh.onFinish], { - encoding: "utf8", timeout: HOOK_TIMEOUT_MS, input: JSON.stringify(payload, null, 2), - env: { ...process.env, - DK_RUN_ID: fresh.id, DK_ROLE: fresh.role, DK_BACKEND: fresh.backend, DK_MODEL: fresh.model, - DK_STATUS: fresh.status, DK_WORKER_STATUS: fresh.workerStatus ?? "", DK_LIFECYCLE: payload.lifecycle, - DK_CWD: fresh.cwd, DK_SUMMARY: summary, DK_RESULT_PATH: path.join(runDir(fresh.id), "result.json"), - DK_LOGS_DIR: runDir(fresh.id), DK_ATTEMPT: String(attempt), DK_PAYLOAD_PATH: payloadPath, - DELEGATE_KIT_DEPTH: "", // хук — не воркер, ему запускать воркеров можно - }, - }); - /* - * Судит код возврата, а не факт ошибки записи в stdin. - * - * Payload уходит хуку на stdin, но однострочный хук (`notify-send …`, - * `echo … >> log`) stdin не читает и выходит сразу — труба рвётся, и - * `spawnSync` возвращает EPIPE при полностью успешном хуке. Так первый же - * стенд 2026-08-26 записал сработавшую доставку как провал и увёл её в - * ретраи: 26 раундов из 40. Замер показал, что `status` при EPIPE - * достоверен (0 у `echo ok`, 3 у `exit 3`), поэтому решает он. - */ - const ok = r.status === 0 && (!r.error || r.error.code === "EPIPE"); - const next = { state: "pending", attempts: attempt, nextAttemptAt: null, lastError: null, deliveredAt: fd.deliveredAt ?? null }; - if (ok) { next.state = "delivered"; next.deliveredAt = nowIso(); } - else { - next.lastError = String(r.error?.message || `hook exited ${r.status}${r.signal ? ` (${r.signal})` : ""}: ${(r.stderr || "").trim().slice(0, 300)}`); - if (attempt >= DELIVERY_MAX_ATTEMPTS) next.state = "failed"; - else next.nextAttemptAt = new Date(Date.now() + DELIVERY_BACKOFF_MS[Math.min(attempt - 1, DELIVERY_BACKOFF_MS.length - 1)]).toISOString(); - } - fresh.delivery = next; saveMeta(fresh); - return ok ? "delivered" : "failed"; - } finally { releaseDelivery(m.id); } -} - -/* Сверка живости плюс попытка доставки — то, что должен сделать любой, кто - вообще посмотрел на прогон. Раньше здесь стоял голый `reconcile`. - Перечитать после доставки обязательно: `deliverPending` пишет своё состояние - в свежую копию с диска, и объект в памяти остаётся с прежним `delivery`. - Без этого `list` печатал «pending» для доставки, которую сам же и выполнил. */ -function touch(m) { const r = reconcile(m); const outcome = deliverPending(r); return outcome ? (metaOf(r.id) || r) : r; } - -const sleep = (ms) => new Promise((res) => setTimeout(res, ms)); - -/* Дренаж, пока супервизор жив: ретраить по расписанию, но не дольше бюджета — - иначе процесс висит ради хука, который, возможно, не поднимется никогда. */ -async function deliverWithRetries(id) { - const start = Date.now(); - for (;;) { - const m = metaOf(id); - if (!m || !m.onFinish) return; - const outcome = deliverPending(m); - const d = metaOf(id)?.delivery; - if (!d || d.state === "delivered" || d.state === "failed") return; - if (outcome === null && !d.nextAttemptAt) return; // заявку держит кто-то другой - const waitMs = d.nextAttemptAt ? Date.parse(d.nextAttemptAt) - Date.now() : 1000; - if (Date.now() - start + Math.max(waitMs, 0) > SUPERVISOR_DRAIN_BUDGET_MS) return; - await sleep(Math.max(waitMs, 500)); - } -} - -function cmdNotify(argv) { - const id = argv._[1]; - const force = argv.force === true || argv.force === "true"; - const ids = id ? [id] : (fs.existsSync(RUNS_DIR) ? fs.readdirSync(RUNS_DIR).sort() : []); - if (id && !metaOf(id)) die(`unknown run ${id}`); - const rows = []; - for (const rid of ids) { - const m = reconcile(metaOf(rid)); - if (!m || !m.onFinish) continue; - const before = m.delivery?.state ?? "pending"; - const outcome = deliverPending(m, { force }); - const after = metaOf(rid)?.delivery ?? null; - if (outcome === null && !force && before === after?.state && !id) continue; - rows.push({ id: rid, role: m.role, status: m.status, lifecycle: lifecycleOf(m), delivery: after }); - } - process.stdout.write(JSON.stringify(rows, null, 2) + "\n"); -} - -const QUOTA_RE = /usage limit|rate limit|limit reached|out of (extra )?usage|quota|too many requests|overloaded|429/i; -const isQuotaError = (m, stderr) => { - const txt = [m?.result?.summary, stderr].filter(Boolean).join("\n"); - return (m?.status === "failed" || m?.workerStatus === "failed") && QUOTA_RE.test(txt); -}; -const otherBackend = (b) => (b === "claude" ? "codex" : "claude"); - -// ---------- presets and routing (single source of truth for who runs what, where) ---------- -// A preset says which family absorbs the token-heavy work. It moves planner, implementer and -// researcher only: reviewer and verifier stay derived from the author's family, because -// independence is the reason they exist and is not a quota knob. -const PRESETS = { - "auto": { planner: "claude", implementer: "codex", researcher: "claude" }, - "main-claude": { planner: "claude", implementer: "claude", researcher: "claude" }, - "main-codex": { planner: "codex", implementer: "codex", researcher: "codex" }, -}; -const PRESET_ALIASES = { default: "auto", none: "auto", claude: "main-claude", anthropic: "main-claude", "main-anthropic": "main-claude", codex: "main-codex", gpt: "main-codex", openai: "main-codex", "main-gpt": "main-codex", "main-openai": "main-codex" }; -const EFFORTS = ["low", "medium", "high", "xhigh", "max"]; -const validEffort = (backend, e) => EFFORTS.includes(e) || (backend === "codex" && e === "ultra"); - -function normalizePreset(v, where) { - if (v === undefined || v === null || v === "" || v === true) return null; - const k = String(v).trim().toLowerCase(); - const p = PRESETS[k] ? k : PRESET_ALIASES[k]; - if (!p) die(`unknown preset "${v}"${where ? ` (from ${where})` : ""}; use ${Object.keys(PRESETS).join(" | ")}`); - return p; -} -function currentPreset(opts = {}) { - return normalizePreset(opts.preset, "--preset") - ?? normalizePreset(process.env.DELEGATE_KIT_PRESET, "DELEGATE_KIT_PRESET") - ?? normalizePreset((readJson(CONFIG_FILE, {}) || {}).preset, CONFIG_FILE) - ?? "auto"; -} -// Which harness is the orchestrator. Native dispatch is only possible inside its own family. -function detectParent(opts = {}) { - const explicit = opts.parent || process.env.DELEGATE_KIT_PARENT; - if (explicit && explicit !== true) { - const p = String(explicit).toLowerCase(); - if (!["claude", "codex"].includes(p)) die(`--parent must be claude or codex (got ${explicit})`); - return p; - } - if (process.env.CLAUDECODE || process.env.CLAUDE_CODE_ENTRYPOINT) return "claude"; - if (process.env.CODEX_SANDBOX || process.env.CODEX_NON_INTERACTIVE || process.env.CODEX_HOME) return "codex"; - return null; -} - -function resolveRoute(opts = {}) { - const role = opts.role; - if (!role || !ROLES[role]) die(`--role must be one of ${Object.keys(ROLES).join("|")}`); - const preset = currentPreset(opts); - const parent = detectParent(opts); - const table = PRESETS[preset]; - const why = []; - const uiKind = String(opts.kind || "").toLowerCase() === "ui"; - // Who wrote the code decides the reviewer. Default to whoever this preset would have - // implemented with — including the UI exception, or the reviewer flips to the wrong family. - const defaultAuthor = preset === "auto" && uiKind ? "claude" : table.implementer; - // `self`: the parent wrote the change itself, so the author is the parent's family - let author = opts["author-backend"] && opts["author-backend"] !== true ? String(opts["author-backend"]) : defaultAuthor; - if (author === "self") { if (!parent) die("--author-backend self needs a detectable parent (or --parent)"); author = parent; } - if (!["claude", "codex"].includes(author)) die("--author-backend must be claude, codex or self"); - - let backend; - if (role === "reviewer") { - backend = otherBackend(author); - why.push(`reviewer runs on the other family than the author (${author}) — independence is the point`); - } else if (role === "verifier") { - backend = author; // third party to the reviewer, which is already the author's opposite - why.push(`verifier is a third party to the reviewer (${otherBackend(author)}), so it lands back on ${backend}`); - } else { - // the review lead is judgement, not review: it follows the planner's family (strongest per preset) - backend = table[role === "review-lead" ? "planner" : role]; - why.push(`preset ${preset} routes ${role} to ${backend}`); - if (preset === "auto" && role === "implementer" && uiKind) { - backend = "claude"; - why.push("UI/design-heavy implementation goes to Claude under the auto preset"); - } - } - if (opts.backend && opts.backend !== true) { - if (!["claude", "codex"].includes(opts.backend)) die("--backend must be claude or codex"); - if (opts.backend !== backend) why.push(`--backend ${opts.backend} overrides the routed family`); - backend = opts.backend; - } - - const [defModel, defEffort] = ROLES[role][backend]; - const model = opts.model && opts.model !== true ? opts.model : defModel; - const effort = opts.effort && opts.effort !== true ? opts.effort : defEffort; - const write = ROLES[role].write; - const dispatch = parent && parent === backend ? "native" : "external"; - why.push(dispatch === "native" - ? `the parent (${parent}) is in the same family, so it can spawn this worker natively` - : parent - ? `the parent (${parent}) cannot spawn a ${backend} worker natively, so it goes through agent-run` - : "no parent harness detected (pass --parent), so external dispatch is assumed"); - - const external = ["agent-run run", `--role ${role}`, `--backend ${backend}`, `--model ${model}`, `--effort ${effort}`, - write ? "--cwd " : "", "--brief "].filter(Boolean).join(" "); - const invoke = dispatch === "external" - ? { how: "external", command: external, - note: write ? "create the worktree with `agent-wt create ` first; agent-run takes the write-lock itself" : "runs read-only in the current working tree" } - : parent === "claude" - ? { how: "native", harness: "claude-code", tool: "Agent", subagent_type: `dk-${role}`, model, - note: write - ? "take the lock yourself: `agent-wt create ` then `agent-wt lock --label dk-implementer`, name the worktree path in the prompt, and `agent-wt release ` when the subagent returns" - : "effort comes from the agent definition, not from the call; install the definitions with hooks/install.sh" } - : { how: "native", harness: "codex", tool: "spawn_agent", agent: `dk-${role}`, model, reasoning_effort: effort, - note: write - ? "take the lock yourself: `agent-wt create ` then `agent-wt lock --label dk-implementer`, name the worktree path in the prompt, and `agent-wt release ` when the subagent returns" - : "role definition lives in ~/.codex/config.toml under [agents.dk-*]; install it with hooks/install.sh" }; - - return { role, preset, parent: parent ?? "unknown", backend, model, effort, write, dispatch, invoke, why, - prefer_external_when: dispatch === "native" ? [ - "you need the strict JSON result contract, a ledger entry or a resumable run id", - "you need a hard timeout, a detached parallel writer, or automatic cross-vendor quota fallback", - "you need an enforced read-only sandbox — a native read-only role is read-only by instruction and tool list, not by sandbox", - ] : undefined }; -} - -// ---------- review depth ---------- -// How many reviewers a diff deserves, decided from the frozen diff and not from a feeling. -// Thresholds are a starting point; tune them from the ledger (lens + panel per run). -const DEPTHS = ["single", "panel", "led"]; -const DEPTH_RULES = { - panel: { lines: 400, files: 10, modules: 2 }, // at or above any of these, or any risk zone - led: { lines: 1200, files: 25, modules: 3 }, // at or above any of these, or a risk zone with >= panel.lines -}; -const RISK_RE = /auth|session|token|secret|credential|crypt|password|permission|acl|rbac|payment|billing|invoice|tariff|migration|schema|\.env|prod|deploy|infra/i; -const NOISE_RE = /(^|\/)(package-lock\.json|pnpm-lock\.yaml|yarn\.lock|Cargo\.lock|go\.sum|poetry\.lock)$|(^|\/)(dist|build|vendor|node_modules|__generated__|generated)\/|\.(snap|min\.js|map)$/; - -function diffStats(diffPath) { - if (!fs.existsSync(diffPath)) die(`diff not found: ${diffPath}`); - const text = fs.readFileSync(diffPath, "utf8"); - const files = new Set(), noise = new Set(), risk = new Set(); - let added = 0, removed = 0, cur = null, curNoise = false; - for (const line of text.split("\n")) { - const m = line.match(/^diff --git a\/(.+?) b\/(.+)$/); - if (m) { cur = m[2]; curNoise = NOISE_RE.test(cur); (curNoise ? noise : files).add(cur); if (!curNoise && RISK_RE.test(cur)) risk.add(cur); continue; } - if (!cur || curNoise) continue; - if (/^\+\+\+|^---/.test(line)) continue; - if (line[0] === "+") { added++; if (RISK_RE.test(line)) risk.add(cur); } - else if (line[0] === "-") removed++; - } - // a module is the first path segment, or the second under a workspace-style root - const moduleOf = (f) => { const seg = f.split("/"); return /^(packages|apps|services|libs|modules|src)$/.test(seg[0]) && seg.length > 2 ? `${seg[0]}/${seg[1]}` : seg[0]; }; - const modules = new Set([...files].map(moduleOf)); - return { lines: added + removed, added, removed, files: files.size, modules: [...modules], risk_zones: [...risk], noise_files: [...noise] }; -} - -function suggestDepth(stats, kind) { - if (kind === "mechanical") return { depth: "single", because: "mechanical diff (formatting, lockfile, generated) — size means nothing here" }; - const r = DEPTH_RULES, risk = stats.risk_zones.length > 0, mods = stats.modules.length; - if (stats.lines >= r.led.lines || stats.files >= r.led.files || mods >= r.led.modules || (risk && stats.lines >= r.panel.lines)) - return { depth: "led", because: `${stats.lines} lines, ${stats.files} files, ${mods} modules${risk ? `, risk zones: ${stats.risk_zones.slice(0, 3).join(", ")}` : ""}` }; - if (stats.lines >= r.panel.lines || stats.files >= r.panel.files || mods >= r.panel.modules || risk) - return { depth: "panel", because: risk ? `risk zone touched: ${stats.risk_zones.slice(0, 3).join(", ")}` : `${stats.lines} lines, ${stats.files} files, ${mods} modules` }; - return { depth: "single", because: `${stats.lines} lines, ${stats.files} files, ${mods} module(s), no risk zone` }; -} - -// Slot A is always the other family than the author: that is where independence comes from and -// nothing moves it. Slot B may sit on the author's family — independence is already paid for, and -// the second slot buys a second lens, not a second opinion from the same angle. Slot C alternates. -function reviewComposition(depth, author, kind) { - const other = otherBackend(author); - const lenses = kind === "refactor" ? ["correctness", "standards", "spec"] : ["correctness", "spec", "standards"]; - const n = depth === "single" ? 1 : depth === "panel" ? 2 : 3; - const families = [other, author, other]; - return lenses.slice(0, n).map((lens, i) => ({ slot: "ABC"[i], lens, backend: families[i], independent: families[i] !== author })); -} - -function cmdRoute(argv) { - const base = resolveRoute(argv); - if (argv.role !== "reviewer" && !argv.depth && !argv.diff) return process.stdout.write(JSON.stringify(base, null, 2) + "\n"); - if (argv.role !== "reviewer") die("--depth and --diff apply to --role reviewer"); - const kind = String(argv.kind || "").toLowerCase() || null; - const stats = argv.diff && argv.diff !== true ? diffStats(argv.diff) : null; - const suggested = stats ? suggestDepth(stats, kind) : null; - let depth = argv.depth && argv.depth !== true ? String(argv.depth) : (suggested ? suggested.depth : "single"); - if (!DEPTHS.includes(depth)) die(`--depth must be ${DEPTHS.join("|")} (got ${depth})`); - const author = base.backend === "claude" ? "codex" : "claude"; // base already placed the reviewer opposite the author - const reviewers = reviewComposition(depth, author, kind).map((r) => { - const rr = resolveRoute({ ...argv, role: "reviewer", backend: r.backend, depth: undefined, diff: undefined }); - return { ...r, model: rr.model, effort: rr.effort, dispatch: rr.dispatch, invoke: rr.invoke }; - }); - const lead = depth === "led" ? (() => { const l = resolveRoute({ ...argv, role: "review-lead", backend: undefined, depth: undefined, diff: undefined }); return { backend: l.backend, model: l.model, effort: l.effort, dispatch: l.dispatch, invoke: l.invoke }; })() : null; - const sessions = reviewers.length + (lead ? 2 : 0); - const out = { role: "reviewer", preset: base.preset, parent: base.parent, author, depth, - suggested: suggested ? { depth: suggested.depth, because: suggested.because, overridden: suggested.depth !== depth } : null, - diff: stats, reviewers, lead, cost_note: `${sessions} read-only session(s): ${reviewers.length} reviewer(s)${lead ? " + lead before and after" : ""}; verifier extra per dispute`, - merge_rules: depth === "single" ? undefined : [ - "reviewers run in parallel and blind to each other; the merge is a separate step", - "same finding from two reviewers: higher confidence, no verifier needed", - "finding from one, silence from the other: coverage, not a dispute", - "one says high, the other explicitly says fine: dispute — settle by command first, then verifier", - "dedupe by meaning, not by file:line", - ], - ask_user: depth !== "single" && !(argv.depth && argv.depth !== true) ? "a panel is more than one session — propose it with the numbers above and wait for the user's yes" : undefined, - }; - process.stdout.write(JSON.stringify(out, null, 2) + "\n"); -} -function cmdPreset(argv) { - const v = argv._[1]; - if (!v) { - const cfg = readJson(CONFIG_FILE, {}) || {}; - return console.log(JSON.stringify({ preset: currentPreset({}), available: Object.keys(PRESETS), - from: { flag: null, env: process.env.DELEGATE_KIT_PRESET ?? null, config: cfg.preset ?? null, file: CONFIG_FILE }, - routes: PRESETS[currentPreset({})], - roles: Object.fromEntries(Object.entries(ROLES).map(([r, v]) => [r, { claude: v.claude.join(" "), codex: v.codex.join(" ") }])), - role_overrides: cfg.roles ?? null }, null, 2)); - } - const p = normalizePreset(v, "argument"); - const cfg = readJson(CONFIG_FILE, {}) || {}; - cfg.preset = p; writeJson(CONFIG_FILE, cfg); - console.log(JSON.stringify({ preset: p, routes: PRESETS[p], saved_to: CONFIG_FILE }, null, 2)); -} - -// ---------- command builders ---------- -function buildCommand({ backend, role, model, effort, cwd, prompt, write, resumeId }) { - if (backend === "claude") { - const args = ["-p", "--output-format", "json", "--model", model, "--effort", effort, - "--permission-mode", write ? "acceptEdits" : "plan", - "--disallowedTools", "Agent,Task", - "--json-schema", fs.readFileSync(SCHEMA_PATH, "utf8")]; - if (resumeId) args.push("--resume", resumeId); - if (!write) args.push("--append-system-prompt", "You are read-only. Do not create, modify or delete files."); - args.push(prompt); - return { cmd: "claude", args }; - } - if (backend === "codex") { - // delegation depth is 1: a worker gets no subagents of its own. `agents.enabled=false` - // drops the [agents] roles; --disable multi_agent(_v2) drops the spawn_agent tool itself. - const noSubagents = ["-c", "agents.enabled=false", "--disable", "multi_agent", "--disable", "multi_agent_v2"]; - const args = ["exec", "--json", "--skip-git-repo-check", - "-s", write ? "workspace-write" : "read-only", - "-c", `model_reasoning_effort="${effort}"`, - ...noSubagents]; - if (resumeId) { - // `codex exec resume` has no -s flag (only `codex exec` does); the sandbox goes in - // through -c, which resume accepts. Model and schema are passed the same way as - // for a fresh run so a resumed turn is held to the same contract. - return { cmd: "codex", args: ["exec", "resume", "--json", "--skip-git-repo-check", - "-c", `sandbox_mode="${write ? "workspace-write" : "read-only"}"`, - "-c", `model_reasoning_effort="${effort}"`, ...noSubagents, - "-m", model, "--output-schema", SCHEMA_PATH, "-o", "__OUT__", resumeId, prompt] }; - } - args.push("-m", model, "--output-schema", SCHEMA_PATH, "-o", "__OUT__", prompt); - return { cmd: "codex", args }; - } - die(`unknown backend ${backend}`); -} - -function extractResult(backend, stdout, outFile) { - if (backend === "claude") { - // stdout is a single JSON object (result message) - let obj = null; - try { obj = JSON.parse(stdout); } catch { - const lines = stdout.trim().split("\n"); for (const l of lines.reverse()) { try { obj = JSON.parse(l); break; } catch {} } - } - if (!obj) return { result: null, usage: null, sessionId: null, raw: stdout.slice(-4000) }; - let result = obj.structured_output ?? null; - if (!result && typeof obj.result === "string") { try { result = JSON.parse(obj.result); } catch { result = { status: obj.is_error ? "failed" : "done", summary: obj.result }; } } - if (obj.is_error && result && result.status === "done") result.status = "failed"; - return { result, usage: obj.usage ?? null, cost_usd: obj.total_cost_usd ?? null, sessionId: obj.session_id ?? null, turns: obj.num_turns ?? null }; - } - // codex: JSONL events on stdout, final message in outFile - let threadId = null, usage = null, lastText = null, errorMsg = null; - for (const line of stdout.split("\n")) { - let ev; try { ev = JSON.parse(line); } catch { continue; } - if (ev.type === "thread.started" && ev.thread_id) threadId = ev.thread_id; - if (ev.type === "turn.completed" && ev.usage) usage = ev.usage; - if (ev.type === "item.completed" && ev.item?.type === "agent_message" && ev.item.text) lastText = ev.item.text; - if (ev.type === "error" && ev.message) errorMsg = ev.message; - } - let text = null; try { text = fs.readFileSync(outFile, "utf8"); } catch { text = lastText; } - let result = null; - if (text) { try { result = JSON.parse(text); } catch { const m = text.match(/\{[\s\S]*\}/); if (m) { try { result = JSON.parse(m[0]); } catch {} } if (!result) result = { status: errorMsg ? "failed" : "done", summary: text.trim() }; } } - if (!result && errorMsg) result = { status: "failed", summary: `ERROR: ${errorMsg}` }; - return { result, usage, sessionId: threadId, error: errorMsg }; -} - -// ---------- run / resume ---------- -async function cmdRun(opts, resumeOf = null) { - if (process.env.DELEGATE_KIT_DEPTH) die("refusing to start a worker from inside a worker (delegation depth is 1)"); - const role = resumeOf ? resumeOf.role : opts.role; - if (!role || !ROLES[role]) die(`--role must be one of ${Object.keys(ROLES).join("|")}`); - const spec = ROLES[role]; - const route = resolveRoute({ ...opts, role }); - const backend = resumeOf ? resumeOf.backend : route.backend; - const model = resumeOf ? (opts.model && opts.model !== true ? opts.model : resumeOf.model) : route.model; - const effort = resumeOf ? (opts.effort && opts.effort !== true ? opts.effort : resumeOf.effort) : route.effort; - // `run` is always an external worker. If the parent could have spawned this one natively, - // say so once — the cheap path is a real choice, not an accident. - if (!resumeOf && route.dispatch === "native" && !opts["no-route-hint"]) { - process.stderr.write(`agent-run: the ${route.parent} parent could run this ${backend} ${role} as a native subagent (${route.invoke.subagent_type || route.invoke.agent}); running it as an external session instead.\n`); - } - // A Claude name passed to codex (or the reverse) fails deep inside the vendor CLI - // with an opaque error, so catch the family mismatch here. - if (opts.model) { - const family = /^gpt-/.test(model) ? "codex" : "claude"; - if (family !== backend) { - die(`--model ${model} is a ${family} model but --backend is ${backend}. ` + - `claude: fable | opus | sonnet | haiku. codex: gpt-5.6-sol | gpt-5.6-terra | gpt-5.6-luna.`); - } - } - if (!validEffort(backend, effort)) die(`--effort must be ${EFFORTS.join("|")}${backend === "codex" ? "|ultra" : ""} (got ${effort})`); - const cwd = path.resolve(opts.cwd || (resumeOf ? resumeOf.cwd : process.cwd())); - const write = opts.write === true ? true : opts.write === "false" ? false : spec.write; - if (!fs.existsSync(cwd)) die(`cwd does not exist: ${cwd}`); - if (write && !resumeOf && !opts["allow-main-checkout"]) { - const g = gitDirOf(cwd); - // writers belong in worktrees: .git dir of a linked worktree lives under
/.git/worktrees/ - if (g && !/[\\/]\.git[\\/]worktrees[\\/]/.test(g) && !/\.worktrees[\\/]/.test(cwd)) die(`writers must run in a git worktree (use agent-wt create). Pass --allow-main-checkout to override deliberately.`); - } - let prompt = opts.prompt; - if (!prompt && opts.brief) { if (!fs.existsSync(opts.brief)) die(`brief not found: ${opts.brief}`); prompt = fs.readFileSync(opts.brief, "utf8"); } - if (!prompt) die("provide --brief FILE or --prompt TEXT"); - const lens = opts.lens && opts.lens !== true ? String(opts.lens) : null; - if (lens && !LENSES[lens]) die(`--lens must be ${Object.keys(LENSES).join("|")} (got ${lens})`); - if (lens && role !== "reviewer") die("--lens applies to --role reviewer only"); - const lensText = lens ? `${LENSES[lens]} ${LENS_TAIL}\n\n` : ""; - prompt = (resumeOf ? "" : `${ROLE_PREAMBLE[role]}\n\n${lensText}`) + prompt + RESULT_CONTRACT; - const timeoutMin = Number(opts.timeout || ROLE_TIMEOUT_MIN[role] || 60); - const onFinishRaw = opts["on-finish"] ?? (resumeOf ? resumeOf.onFinish : null); - if (onFinishRaw === true) die(`--on-finish needs a shell command, e.g. --on-finish 'echo "$DK_ROLE $DK_STATUS" >> ~/dk-done.log'`); - const onFinish = onFinishRaw ? String(onFinishRaw) : null; - - if (opts.detach && !opts._supervise) { - const id = newId(); - const args = process.argv.slice(2).filter((a) => a !== "--detach"); - args.push("--_supervise", "--id", id); - const sup = spawn(process.execPath, [fileURLToPath(import.meta.url), ...args], { detached: true, stdio: "ignore", cwd: process.cwd(), env: process.env }); - sup.unref(); - // wait briefly for meta.json so the caller can see the run - const deadline = Date.now() + 5000; - while (Date.now() < deadline && !fs.existsSync(path.join(runDir(id), "meta.json"))) { Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 100); } - const m = metaOf(id); - process.stdout.write(JSON.stringify({ id, role, backend, model, effort, cwd, status: m ? m.status : "starting", supervisor_pid: sup.pid }) + "\n"); - return; - } - const running = activeRuns(); - if (running.length >= MAX_WORKERS) die(`max ${MAX_WORKERS} active workers reached (${running.map((r) => r.id).join(", ")})`); - if (write && running.filter((r) => r.write).length >= MAX_WRITERS) die(`max ${MAX_WRITERS} concurrent writers reached`); - - const id = opts.id || newId(); - fs.mkdirSync(runDir(id), { recursive: true }); - const outFile = path.join(runDir(id), "last-message.txt"); - const built = buildCommand({ backend, role, model, effort, cwd, prompt, write, resumeId: resumeOf?.sessionId }); - built.args = built.args.map((a) => (a === "__OUT__" ? outFile : a)); - const panel = opts.panel && opts.panel !== true ? String(opts.panel) : null; - const meta = { id, role, backend, model, effort, cwd, write, preset: route.preset, dispatch: "external", lens, panel, status: "running", pid: null, started: nowIso(), resumeOf: resumeOf?.id ?? null, sessionId: resumeOf?.sessionId ?? null, timeoutMin, fallbackFrom: opts._fallbackFrom ?? null, onFinish, delivery: onFinish ? { state: "pending", attempts: 0, nextAttemptAt: null, lastError: null, deliveredAt: null } : null }; - if (write) meta.lock = acquireWriteLock(cwd, id, role); - fs.writeFileSync(path.join(runDir(id), "prompt.md"), prompt); - - const stdoutPath = path.join(runDir(id), "stdout.log"); - const stderrPath = path.join(runDir(id), "stderr.log"); - const child = spawn(built.cmd, built.args, { cwd, detached: true, stdio: ["ignore", fs.openSync(stdoutPath, "w"), fs.openSync(stderrPath, "w")], - env: { ...process.env, DELEGATE_KIT_DEPTH: "1", DELEGATE_KIT_RUN_ID: id } }); - meta.pid = child.pid; saveMeta(meta); - child.unref(); - - const finish = (code, signal) => { - const m = metaOf(id); if (!m || m.status !== "running") return; - const stdout = fs.readFileSync(stdoutPath, "utf8"); - const ex = extractResult(backend, stdout, outFile); - m.status = signal === "SIGKILL" && m.killReason ? m.killReason : code === 0 && !ex.error ? "finished" : "failed"; - m.exitCode = code; m.finished = nowIso(); m.sessionId = ex.sessionId ?? m.sessionId; m.usage = ex.usage ?? null; m.cost_usd = ex.cost_usd ?? null; - m.result = ex.result ?? { status: "failed", summary: `worker exited with code ${code}; see stderr.log` }; - if (m.result && m.result.status) m.workerStatus = m.result.status; - if (write) { releaseWriteLock(cwd, id); delete m.lock; } - saveMeta(m); writeJson(path.join(runDir(id), "result.json"), m.result); - fs.appendFileSync(LEDGER, JSON.stringify({ id, role, backend, model, effort, preset: m.preset ?? null, dispatch: "external", lens: m.lens ?? null, panel: m.panel ?? null, cwd, write, started: m.started, finished: m.finished, duration_s: Math.round((Date.parse(m.finished) - Date.parse(m.started)) / 1000), status: m.status, worker_status: m.workerStatus ?? null, usage: m.usage, cost_usd: m.cost_usd, sessionId: m.sessionId, fallback_from: m.fallbackFrom ?? null }) + "\n"); - }; - - /* - * Таймаут обязан оставить след. - * - * Раньше таймер только ставил `killReason` и убивал группу, а статус и - * result.json писал обработчик `exit`. Когда супервизор не доживал до него, - * прогон навсегда оставался «running» без причины — снаружи это выглядело - * как «Codex молча падает» (разбор 2026-08-24, прогон 20260824172835-tv4q). - * Теперь причина попадает в meta до убийства, а `reconcile` достроит - * result.json при первом же обращении, даже если супервизора уже нет. - * - * Группа убивается по `-pid`, одиночный процесс — запасным `pid`: дочерний - * процесс запущен с `detached`, но если setsid не сработал, группы с таким - * идентификатором не существует и первый вызов бросает ESRCH. - */ - const timer = setTimeout(() => { - const m = metaOf(id); - if (!m || m.status !== "running") return; - m.killReason = "timeout"; saveMeta(m); - try { process.kill(-child.pid, "SIGKILL"); } catch { try { process.kill(child.pid, "SIGKILL"); } catch {} } - }, timeoutMin * 60_000); - child.on("exit", async (code, signal) => { - clearTimeout(timer); finish(code, signal); - const m = metaOf(id); - const fallbackMode = opts.fallback || process.env.DELEGATE_KIT_FALLBACK || "auto"; - if (fallbackMode !== "none" && !opts._fallbackFrom && !resumeOf && isQuotaError(m, fs.readFileSync(stderrPath, "utf8"))) { - const nb = otherBackend(backend); - process.stderr.write(`agent-run: ${backend} reported a usage/rate limit; retrying once on ${nb} (fallback). Use --fallback none to disable.\n`); - m.status = "failed-quota"; saveMeta(m); - const next = { ...opts, backend: nb, model: undefined, effort: opts.effort, _fallbackFrom: id, detach: false, id: undefined }; - await cmdRun(next, null); - return; - } - await deliverWithRetries(id); - if (!opts._supervise) printResult(id); process.exit(0); - }); - - if (opts.detach) { - // cmdRun was re-invoked as a detached supervisor; the caller already returned - return; - } -} - -function printResult(id) { - const m = touch(metaOf(id)); if (!m) die(`unknown run ${id}`); - const out = { id: m.id, role: m.role, backend: m.backend, model: m.model, effort: m.effort, lens: m.lens ?? undefined, panel: m.panel ?? undefined, cwd: m.cwd, status: m.status, lifecycle: lifecycleOf(m), delivery: m.delivery ?? undefined, fallback_from: m.fallbackFrom ?? undefined, - note: m.fallbackFrom ? `ran on ${m.backend} because the other vendor hit a usage limit${m.role === "reviewer" || m.role === "verifier" ? "; check that the reviewer is still a different vendor than the author" : ""}` : undefined, worker_status: m.workerStatus ?? null, sessionId: m.sessionId ?? null, usage: m.usage ?? null, cost_usd: m.cost_usd ?? null, started: m.started, finished: m.finished ?? null, result: m.result ?? null, logs: runDir(id) }; - process.stdout.write(JSON.stringify(out, null, 2) + "\n"); -} - -function cmdList() { - if (!fs.existsSync(RUNS_DIR)) return console.log("[]"); - const rows = fs.readdirSync(RUNS_DIR).sort().map(metaOf).filter(Boolean).map(touch).map((m) => { - return { id: m.id, role: m.role, backend: m.backend, model: m.model, effort: m.effort, status: m.status, lifecycle: lifecycleOf(m), worker_status: m.workerStatus ?? null, delivery: m.delivery ? m.delivery.state : undefined, cwd: m.cwd, started: m.started, finished: m.finished ?? null }; - }); - console.log(JSON.stringify(rows, null, 2)); -} -async function cmdWait(id, timeoutMin) { - const deadline = Date.now() + Number(timeoutMin || 60) * 60_000; - for (;;) { - const m = metaOf(id); if (!m) die(`unknown run ${id}`); - if (m.status !== "running") return printResult(id); - /* Пауза перед сверкой: обработчик `exit` супервизора пишет статус и - result.json уже после смерти дочернего процесса, и без задержки живой - прогон объявлялся бы осиротевшим в этом окне. */ - if (!alive(m.pid)) { await new Promise((r) => setTimeout(r, 1500)); return printResult(id); } - if (Date.now() > deadline) die("wait timed out", 124); - await new Promise((r) => setTimeout(r, 2000)); - } -} -function cmdKill(id) { const m = metaOf(id); if (!m) die(`unknown run ${id}`); if (m.status !== "running") return printResult(id); m.killReason = "killed"; saveMeta(m); try { process.kill(-m.pid, "SIGTERM"); } catch {} setTimeout(() => { try { process.kill(-m.pid, "SIGKILL"); } catch {} const m2 = metaOf(id); if (m2.status === "running") { m2.status = "killed"; m2.finished = nowIso(); saveMeta(m2); if (m2.write) releaseWriteLock(m2.cwd, id); } printResult(id); }, 3000); } -function cmdLog(id, which = "stdout") { const p = path.join(runDir(id), which === "err" ? "stderr.log" : which === "prompt" ? "prompt.md" : "stdout.log"); if (!fs.existsSync(p)) die(`no log for ${id}`); process.stdout.write(fs.readFileSync(p, "utf8")); } - -// ---------- main ---------- -async function main() { -const argv = parseArgs(process.argv.slice(2)); -const sub = argv._[0]; -fs.mkdirSync(RUNS_DIR, { recursive: true }); -switch (sub) { - case "run": await cmdRun(argv); break; - case "route": cmdRoute(argv); break; - case "preset": cmdPreset(argv); break; - case "resume": { const prev = metaOf(argv._[1]); if (!prev) die(`unknown run ${argv._[1]}`); if (!prev.sessionId) die(`run ${argv._[1]} has no session id to resume`); await cmdRun({ ...argv, cwd: argv.cwd || prev.cwd }, prev); break; } - case "list": cmdList(); break; - case "status": printResult(argv._[1]); break; - case "wait": await cmdWait(argv._[1], argv.timeout); break; - case "kill": cmdKill(argv._[1]); break; - case "log": cmdLog(argv._[1], argv._[2]); break; - case "notify": cmdNotify(argv); break; - default: - console.log(`agent-run — headless workers for Claude Code / Codex with role defaults, worktree locks and a ledger - - agent-run route --role <${Object.keys(ROLES).join("|")}> [--preset P] [--parent claude|codex] [--author-backend claude|codex|self] [--kind ui|refactor|mechanical] - who should run this role, on which family, natively or externally - agent-run route --role reviewer [--diff FILE] [--depth single|panel|led] - how many reviewers this diff deserves, which lens and family each gets, and the lead - agent-run preset [${Object.keys(PRESETS).join("|")}] show or persist the default preset - agent-run run --role <${Object.keys(ROLES).join("|")}> [--preset P] [--backend claude|codex] [--model M] [--effort E] - [--cwd DIR] (--brief FILE | --prompt TEXT) [--detach] [--timeout MIN] [--write true|false] [--allow-main-checkout] [--fallback auto|none] - [--on-finish CMD] shell command run when this run reaches a terminal state - [--lens spec|correctness|standards] [--panel ID] (reviewer only: lens preamble; panel id groups runs in the ledger) - agent-run resume (--brief FILE | --prompt TEXT) [--detach] - agent-run list | status | wait [--timeout MIN] | kill | log [out|err|prompt] - agent-run notify [] [--force] deliver any completion still pending (retries due ones); no id drains all - ---timeout on \`run\` is a fuse against a hung worker, not a schedule; without it the role decides: -${Object.entries(ROLE_TIMEOUT_MIN).map(([r, t]) => `${r} ${t}m`).join(", ")}. A run killed by it ends as -"timeout", a run whose supervisor died as "orphaned" — both keep whatever the worker committed, so read -the worktree before rerunning. list, status and wait all reconcile a dead supervisor. - ---on-finish is how the parent learns a worker finished without polling — the point of it is a worker -from the OTHER family, where no harness reports back. The command runs once per run, with the full -result JSON on stdin and DK_RUN_ID, DK_ROLE, DK_BACKEND, DK_MODEL, DK_STATUS, DK_WORKER_STATUS, -DK_LIFECYCLE, DK_CWD, DK_SUMMARY, DK_RESULT_PATH, DK_LOGS_DIR, DK_ATTEMPT in the environment. -A non-zero exit is a failed delivery, retried at 5s/15s/45s/120s: by the live supervisor first, then by -whoever next runs list, status, wait or notify. Exactly once — a claim file keeps two processes from -both delivering. After ${DELIVERY_MAX_ATTEMPTS} attempts the delivery is marked failed and only -\`notify --force\` will retry it. A run that failed on quota delivers nothing: its fallback rerun does. - -lifecycle is orthogonal to status: running, or parked when the session can still be revived with -\`resume\`, or done when it cannot. status says how it ended, lifecycle whether you can go back to it. - -Presets move the token-heavy roles (planner, implementer, researcher) to one family; reviewer and -verifier always stay on the other family than the author. Precedence: --preset > DELEGATE_KIT_PRESET -> ${CONFIG_FILE} > auto. Per-role model/effort defaults can be overridden in the same file under -"roles": { "": { "claude": ["model","effort"], "codex": ["model","effort"] } }; \`agent-run preset\` shows the effective table. - -State: ${STATE_DIR} (config.json, runs//{meta.json,prompt.md,stdout.log,stderr.log,result.json}, ledger.jsonl)`); -} -} -main().catch((e) => die(String(e?.stack || e))); diff --git a/skills/delegate-kit/scripts/agent-wt b/skills/delegate-kit/scripts/agent-wt deleted file mode 100755 index 43b1ef9..0000000 --- a/skills/delegate-kit/scripts/agent-wt +++ /dev/null @@ -1,166 +0,0 @@ -#!/usr/bin/env bash -# agent-wt — git worktrees for delegate-kit writers, with a write-lock that agent-run honours. -# Worktrees live next to the repo: /.worktrees/, branch dk/. -set -euo pipefail - -usage() { - cat <<'EOF' -agent-wt create [--base REF] create worktree + branch dk/ (default base: current HEAD) -agent-wt list list worktrees with lock/owner/status -agent-wt status path, branch, lock, dirty files, commits ahead of base -agent-wt diff [--base REF] frozen diff: base..HEAD plus uncommitted changes -agent-wt lock [--label TXT] take the write-lock for a native subagent the parent spawns itself -agent-wt release [--force] remove the write-lock (only if its process is gone or --force) -agent-wt remove [--force] remove worktree and branch (refuses if locked or dirty unless --force) -agent-wt cleanup remove worktrees whose branch is merged into the base and are unlocked -EOF -} - -die() { echo "agent-wt: $*" >&2; exit 1; } -need() { command -v "$1" >/dev/null 2>&1 || die "missing dependency: $1"; } -need git; need jq - -repo_root() { git rev-parse --show-toplevel 2>/dev/null || die "not inside a git repository"; } -main_root() { - # resolve the main checkout even when called from inside a linked worktree - local common; common=$(git rev-parse --git-common-dir); common=$(cd "$common" && pwd) - dirname "$common" -} -wt_base_dir() { local r; r=$(main_root); echo "$(dirname "$r")/$(basename "$r").worktrees"; } -wt_path() { echo "$(wt_base_dir)/$1"; } -wt_branch() { echo "dk/$1"; } -lock_file() { local p; p=$(wt_path "$1"); [ -d "$p" ] || return 1; echo "$(cd "$p" && git rev-parse --absolute-git-dir)/delegate-kit.lock"; } -base_ref_file() { local p; p=$(wt_path "$1"); [ -d "$p" ] || return 1; echo "$(cd "$p" && git rev-parse --absolute-git-dir)/delegate-kit.base"; } - -lock_info() { - local lf; lf=$(lock_file "$1" 2>/dev/null) || { echo "no-lock"; return; } - [ -f "$lf" ] || { echo "unlocked"; return; } - local kind pid; kind=$(jq -r '.kind // "process"' "$lf"); pid=$(jq -r '.pid // empty' "$lf") - # A native lock has no process behind it: the parent holds it on behalf of its own - # subagent and releases it by hand. It is live until someone releases it. - if [ "$kind" = "native" ]; then echo "locked(native, label=$(jq -r '.label // "-"' "$lf"), since=$(jq -r '.since // "?"' "$lf"))"; return; fi - if [ -n "$pid" ] && kill -0 "$pid" 2>/dev/null; then echo "locked(run=$(jq -r .id "$lf"), pid=$pid)"; else echo "stale-lock(run=$(jq -r .id "$lf"))"; fi -} - -cmd_create() { - local name=${1:-}; [ -n "$name" ] || die "name required"; shift || true - local base="HEAD" - while [ $# -gt 0 ]; do case "$1" in --base) base=$2; shift 2;; *) die "unknown option $1";; esac; done - [[ "$name" =~ ^[A-Za-z0-9._-]+$ ]] || die "name must match [A-Za-z0-9._-]+" - local path branch; path=$(wt_path "$name"); branch=$(wt_branch "$name") - [ -e "$path" ] && die "worktree already exists: $path" - mkdir -p "$(dirname "$path")" - local base_sha; base_sha=$(git rev-parse --verify "$base^{commit}") || die "bad base ref: $base" - if git show-ref --verify --quiet "refs/heads/$branch"; then - git worktree add "$path" "$branch" >/dev/null - else - git worktree add -b "$branch" "$path" "$base_sha" >/dev/null - fi - echo "$base_sha" > "$(base_ref_file "$name")" - jq -n --arg name "$name" --arg path "$path" --arg branch "$branch" --arg base "$base_sha" '{name:$name,path:$path,branch:$branch,base:$base}' -} - -cmd_list() { - local base; base=$(wt_base_dir) - [ -d "$base" ] || { echo "[]"; return; } - local out="[]" - for p in "$base"/*/; do - [ -d "$p" ] || continue - local name; name=$(basename "$p") - local branch dirty lock - branch=$(cd "$p" && git rev-parse --abbrev-ref HEAD 2>/dev/null || echo "?") - dirty=$(cd "$p" && git status --porcelain 2>/dev/null | wc -l | tr -d ' ') - lock=$(lock_info "$name") - out=$(jq --arg n "$name" --arg p "$p" --arg b "$branch" --arg d "$dirty" --arg l "$lock" '. + [{name:$n,path:$p,branch:$b,dirty:($d|tonumber),lock:$l}]' <<<"$out") - done - echo "$out" -} - -cmd_status() { - local name=${1:-}; [ -n "$name" ] || die "name required" - local p; p=$(wt_path "$name"); [ -d "$p" ] || die "no such worktree: $name" - local base; base=$(cat "$(base_ref_file "$name")" 2>/dev/null || echo "") - local ahead=0; [ -n "$base" ] && ahead=$(cd "$p" && git rev-list --count "$base..HEAD") - jq -n --arg n "$name" --arg p "$p" --arg b "$(cd "$p" && git rev-parse --abbrev-ref HEAD)" --arg base "$base" \ - --arg l "$(lock_info "$name")" --arg d "$(cd "$p" && git status --porcelain | wc -l | tr -d ' ')" --arg a "$ahead" \ - '{name:$n,path:$p,branch:$b,base:$base,lock:$l,dirty:($d|tonumber),commits_ahead:($a|tonumber)}' -} - -cmd_diff() { - local name=${1:-}; [ -n "$name" ] || die "name required"; shift || true - local p; p=$(wt_path "$name"); [ -d "$p" ] || die "no such worktree: $name" - local base; base=$(cat "$(base_ref_file "$name")" 2>/dev/null || echo "") - while [ $# -gt 0 ]; do case "$1" in --base) base=$2; shift 2;; *) die "unknown option $1";; esac; done - [ -n "$base" ] || die "no base recorded; pass --base REF" - (cd "$p" && git add -N . >/dev/null 2>&1 || true; git diff "$base") -} - -cmd_lock() { - local name=${1:-}; [ -n "$name" ] || die "name required"; shift || true - local label="native-subagent" - while [ $# -gt 0 ]; do case "$1" in --label) label=$2; shift 2;; *) die "unknown option $1";; esac; done - local p; p=$(wt_path "$name"); [ -d "$p" ] || die "no such worktree: $name (create it first)" - local lf; lf=$(lock_file "$name") - if [ -f "$lf" ]; then - local info; info=$(lock_info "$name") - [[ "$info" == locked* ]] && die "worktree is already $info; one writer per worktree" - rm -f "$lf" # stale process lock - fi - jq -n --arg id "native-$(date +%Y%m%d%H%M%S)" --arg label "$label" --arg cwd "$p" --arg since "$(date -u +%Y-%m-%dT%H:%M:%SZ)" \ - '{id:$id,kind:"native",label:$label,role:"implementer",pid:null,cwd:$cwd,since:$since}' > "$lf" - jq -n --arg n "$name" --arg p "$p" --arg l "$label" '{name:$n,path:$p,locked_for:$l,release_with:("agent-wt release " + $n)}' -} - -cmd_release() { - local name=${1:-}; [ -n "$name" ] || die "name required"; shift || true - local force=0; [ "${1:-}" = "--force" ] && force=1 - local lf; lf=$(lock_file "$name") || die "no such worktree: $name" - [ -f "$lf" ] || { echo "already unlocked"; return; } - local pid; pid=$(jq -r '.pid // empty' "$lf") - if [ -n "$pid" ] && kill -0 "$pid" 2>/dev/null && [ $force -eq 0 ]; then die "lock held by live process $pid; use --force after stopping the worker (agent-run kill )"; fi - # native locks have no process: releasing one is the parent saying its subagent is done - rm -f "$lf"; echo "released" -} - -cmd_remove() { - local name=${1:-}; [ -n "$name" ] || die "name required"; shift || true - local force=0; [ "${1:-}" = "--force" ] && force=1 - local p; p=$(wt_path "$name"); [ -d "$p" ] || die "no such worktree: $name" - local lock; lock=$(lock_info "$name") - [[ "$lock" == locked* ]] && [ $force -eq 0 ] && die "worktree is $lock; stop the worker first" - local dirty; dirty=$(cd "$p" && git status --porcelain | wc -l | tr -d ' ') - [ "$dirty" != "0" ] && [ $force -eq 0 ] && die "worktree has $dirty uncommitted changes; commit/stash or use --force" - git worktree remove ${force:+--force} "$p" - git branch -D "$(wt_branch "$name")" >/dev/null 2>&1 && echo "removed worktree and branch dk/$name" || echo "removed worktree (branch kept or absent)" -} - -cmd_cleanup() { - local base; base=$(wt_base_dir); [ -d "$base" ] || { echo "nothing to clean"; return; } - local head; head=$(git rev-parse HEAD) - for p in "$base"/*/; do - [ -d "$p" ] || continue - local name; name=$(basename "$p") - local lock; lock=$(lock_info "$name"); [[ "$lock" == locked* ]] && { echo "skip $name: $lock"; continue; } - local br; br=$(wt_branch "$name") - if git merge-base --is-ancestor "$br" "$head" 2>/dev/null; then - local dirty; dirty=$(cd "$p" && git status --porcelain | wc -l | tr -d ' ') - [ "$dirty" != "0" ] && { echo "skip $name: dirty"; continue; } - git worktree remove "$p" && git branch -d "$br" >/dev/null && echo "removed $name (merged)" - else - echo "keep $name: not merged" - fi - done - git worktree prune -} - -case "${1:-}" in - create) shift; cmd_create "$@";; - list) cmd_list;; - status) shift; cmd_status "$@";; - diff) shift; cmd_diff "$@";; - lock) shift; cmd_lock "$@";; - release) shift; cmd_release "$@";; - remove) shift; cmd_remove "$@";; - cleanup) cmd_cleanup;; - *) usage; exit 1;; -esac diff --git a/skills/delegate-kit/tests/delivery.sh b/skills/delegate-kit/tests/delivery.sh deleted file mode 100755 index e03ceac..0000000 --- a/skills/delegate-kit/tests/delivery.sh +++ /dev/null @@ -1,129 +0,0 @@ -#!/bin/bash -# Стенд механики доставки завершений (`--on-finish`, `notify`, lifecycle). -# -# Прогоны синтетические: meta.json пишется руками, модели не вызываются, квота -# не тратится. Настоящего воркера здесь нет намеренно — проверяется доставка, -# а не бэкенд. -# -# ./delivery.sh функциональные проверки -# ./delivery.sh --race N N раундов конкуренции (по умолчанию 20 × 8 процессов) -# -# Гонку стоит гонять отдельно и подолгу: оба дефекта, найденные при внедрении -# (пустая заявка в окне создания и EPIPE у хука, не читающего stdin), -# проявлялись лишь под нагрузкой и в одиночном прогоне выглядели как флак. -set -u -AR="$(cd "$(dirname "$0")/../scripts" && pwd)/agent-run" -BASE="${TMPDIR:-/tmp}/dk-delivery-test.$$" -trap 'rm -rf "$BASE"' EXIT -export DELEGATE_KIT_HOME="$BASE/state" -PASS=0; FAIL=0 -ok(){ if [ "$2" = "$3" ]; then echo " ✔ $1"; PASS=$((PASS+1)); else echo " ✘ $1: ожидалось [$3], получено [$2]"; FAIL=$((FAIL+1)); fi; } - -mkrun(){ # id status pid sessionId onFinish - local d="$DELEGATE_KIT_HOME/runs/$1"; mkdir -p "$d" - node -e ' - const fs=require("fs");const[,d,id,status,pid,sess,hook]=process.argv; - const m={id,role:"researcher",backend:"codex",model:"gpt-5.6-terra",effort:"medium", - cwd:"/tmp",write:false,status,pid:Number(pid),started:new Date(Date.now()-6e4).toISOString(), - finished:status==="running"?null:new Date().toISOString(), - sessionId:sess==="null"?null:sess,workerStatus:status==="finished"?"done":null, - onFinish:hook==="null"?null:hook, - delivery:hook==="null"?null:{state:"pending",attempts:0,nextAttemptAt:null,lastError:null,deliveredAt:null}, - result:{status:"done",summary:"синтетический прогон "+id,changes:[],checks_run:[],not_verified:[],findings:[],plan:[],questions:[],sources:[],next_steps:[]}}; - fs.writeFileSync(d+"/meta.json",JSON.stringify(m,null,2)); - fs.writeFileSync(d+"/result.json",JSON.stringify(m.result,null,2)); - ' "$d" "$1" "$2" "$3" "$4" "$5" -} -field(){ node -e 'const fs=require("fs");const m=JSON.parse(fs.readFileSync(process.argv[1]));const p=process.argv[2].split(".");let v=m;for(const k of p)v=v?.[k];console.log(v===undefined?"undefined":v===null?"null":v)' "$DELEGATE_KIT_HOME/runs/$1/meta.json" "$2"; } -out(){ node "$AR" status "$1" | node -e 'let s="";process.stdin.on("data",d=>s+=d).on("end",()=>{const o=JSON.parse(s);console.log(process.argv[1].split(".").reduce((a,k)=>a?.[k],o))})' "$2"; } -rewind(){ node -e 'const fs=require("fs"),p=process.argv[1];const m=JSON.parse(fs.readFileSync(p));m.delivery.nextAttemptAt=new Date(Date.now()-1000).toISOString();fs.writeFileSync(p,JSON.stringify(m,null,2))' "$DELEGATE_KIT_HOME/runs/$1/meta.json"; } - -race(){ - local rounds="${1:-20}" procs=8 bad=0 lost=0 - for r in $(seq 1 "$rounds"); do - rm -rf "$DELEGATE_KIT_HOME"; mkdir -p "$DELEGATE_KIT_HOME/runs" - local log="$BASE/race.log"; : > "$log" - mkrun rr finished 999999 s "echo hit >> $log" - for i in $(seq 1 $procs); do node "$AR" notify rr >/dev/null 2>&1 & done; wait - local n s; n=$(grep -c hit "$log"); s=$(field rr delivery.state) - [ "$n" = "1" ] || { echo " раунд $r: срабатываний $n"; bad=$((bad+1)); } - [ "$s" = "delivered" ] || { echo " раунд $r: состояние $s"; lost=$((lost+1)); } - done - echo "раундов: $rounds × $procs процессов, дублей: $bad, недоставлено: $lost" - [ $((bad+lost)) -eq 0 ] -} - -rm -rf "$DELEGATE_KIT_HOME"; mkdir -p "$DELEGATE_KIT_HOME/runs" -if [ "${1:-}" = "--race" ]; then race "${2:-20}"; exit $?; fi - -HOOKLOG="$BASE/hook.log"; : > "$HOOKLOG" -HOOK="echo \"fired \$DK_RUN_ID \$DK_STATUS \$DK_LIFECYCLE\" >> $HOOKLOG" - -echo "── доставка завершённого прогона" -mkrun r-done finished 999999 sess-abc "$HOOK" -node "$AR" notify >/dev/null -ok "хук сработал" "$(grep -c 'fired r-done' "$HOOKLOG")" "1" -ok "состояние доставки" "$(field r-done delivery.state)" "delivered" -ok "lifecycle=parked при живой сессии" "$(out r-done lifecycle)" "parked" - -echo "── ровно однажды" -node "$AR" notify >/dev/null; node "$AR" list >/dev/null; node "$AR" status r-done >/dev/null -ok "срабатываний по-прежнему одно" "$(grep -c 'fired r-done' "$HOOKLOG")" "1" - -echo "── живой прогон не доставляется" -mkrun r-live running $$ null "$HOOK" -node "$AR" notify >/dev/null -ok "хук не сработал" "$(grep -c 'fired r-live' "$HOOKLOG")" "0" - -echo "── смерть супервизора: сверка плюс доставка" -mkrun r-orph running 999998 null "$HOOK" -node "$AR" list >/dev/null -ok "статус переведён в orphaned" "$(field r-orph status)" "orphaned" -ok "хук сработал по факту смерти" "$(grep -c 'fired r-orph' "$HOOKLOG")" "1" -ok "lifecycle=done без сессии" "$(out r-orph lifecycle)" "done" - -echo "── провал по квоте не доставляется (доставит перезапуск)" -mkrun r-quota failed-quota 999997 null "$HOOK" -node "$AR" notify >/dev/null -ok "хук не сработал" "$(grep -c 'fired r-quota' "$HOOKLOG")" "0" - -echo "── ретраи и backoff" -mkrun r-bad finished 999996 null "exit 3" -node "$AR" notify >/dev/null -ok "попытка учтена" "$(field r-bad delivery.attempts)" "1" -ok "срок следующей назначен" "$([ "$(field r-bad delivery.nextAttemptAt)" = null ] && echo нет || echo есть)" "есть" -ok "код возврата в ошибке" "$(field r-bad delivery.lastError | grep -c 'exited 3')" "1" -node "$AR" notify >/dev/null -ok "до срока повтора нет" "$(field r-bad delivery.attempts)" "1" -for i in 2 3 4 5; do rewind r-bad; node "$AR" notify >/dev/null; done -ok "попытки исчерпаны" "$(field r-bad delivery.attempts)" "5" -ok "доставка помечена провалившейся" "$(field r-bad delivery.state)" "failed" -node "$AR" notify >/dev/null -ok "сама не ретраится" "$(field r-bad delivery.attempts)" "5" -node "$AR" notify r-bad --force >/dev/null -ok "--force поднимает" "$(field r-bad delivery.attempts)" "6" - -echo "── конкуренция" -mkrun r-race finished 999995 s "$HOOK" -for i in $(seq 1 8); do node "$AR" notify r-race >/dev/null 2>&1 & done; wait -ok "сработало ровно однажды" "$(grep -c 'fired r-race' "$HOOKLOG")" "1" -ok "заявка снята" "$([ -e "$DELEGATE_KIT_HOME/runs/r-race/delivery.lock" ] && echo есть || echo нет)" "нет" - -echo "── хук, не читающий stdin, не провал (регрессия EPIPE)" -mkrun r-epipe finished 999993 null "$HOOK" -# Payload раздут за буфер трубы (64 КиБ): иначе разрыв ловится лишь иногда. -node -e 'const f=process.argv[1];const m=JSON.parse(require("fs").readFileSync(f));m.result.summary="д".repeat(200000);require("fs").writeFileSync(f,JSON.stringify(m))' "$DELEGATE_KIT_HOME/runs/r-epipe/meta.json" -node "$AR" notify >/dev/null -ok "хук сработал" "$(grep -c 'fired r-epipe' "$HOOKLOG")" "1" -ok "доставка засчитана" "$(field r-epipe delivery.state)" "delivered" -ok "ошибка не записана" "$(field r-epipe delivery.lastError)" "null" -ok "payload доступен файлом" "$([ -s "$DELEGATE_KIT_HOME/runs/r-epipe/delivery-payload.json" ] && echo есть || echo нет)" "есть" - -echo "── прогон без хука не ломает поллинг" -mkrun r-nohook finished 999994 null null -node "$AR" list >/dev/null 2>&1 -ok "list отработал" "$?" "0" -ok "delivery отсутствует" "$(field r-nohook delivery)" "null" - -echo; echo "Пройдено: $PASS, провалено: $FAIL" -exit $((FAIL > 0))