Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
116 changes: 116 additions & 0 deletions .claude/skills/delegate-issue/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
---
name: delegate-issue
description: Delegate well-scoped coding issues to a SWE-AF node as a cheap sub-harness via implement_issue, then collect and merge the returned branches. Use when the user asks to offload, delegate, or fan out scoped tasks to SWE-AF / swe-planner / swe-fast, or when several independent, fully-specified changes could run in parallel without burning main-harness tokens.
---

# Delegate scoped issues to SWE-AF

You are the main harness. SWE-AF is the sub-harness: it implements one
fully-scoped issue per call on an isolated branch, using cheap models, and
hands the branch back. You keep planning, merging, review, and CI.

## When to delegate (and when not to)

Delegate an issue only when ALL of these hold:

- The change is **fully specified**: you can name the files, the behavior, and
the acceptance criteria without the sub-harness needing to plan anything.
- It is **independent** of your other in-flight edits (no shared files with
another delegation or with your own uncommitted work).
- The repo is reachable by the SWE-AF node (same machine or shared volume) and
has at least one commit.

Do NOT delegate: vague feature requests (use `swe-planner.build` instead),
changes to files you are editing yourself right now, or anything whose spec
you'd have to guess. Garbage spec in → garbage branch out.

## Preflight (once per session)

```bash
# Control plane up and the node registered?
curl -s ${AGENTFIELD_SERVER:-http://localhost:8080}/api/v1/nodes | grep -o '"swe-[a-z-]*"' | sort -u
```

Pick the node (`swe-planner`, `swe-fast`, or the `-go` twins — the
`implement_issue` surface is identical). If an `X-API-Key` is configured for
the control plane, add `-H "X-API-Key: $AGENTFIELD_API_KEY"` to every curl.

## Compose the issue

Write the spec from YOUR context — the whole point is that the sub-harness
skips planning. Required: `title`, `description`. Strongly recommended:
`acceptance_criteria` (verifier checks these), `files_to_modify` /
`files_to_create`, `testing_strategy`. Set `needs_deeper_qa: true` only for
risky/interface-heavy changes (doubles the review cost per iteration).

Commit (or at least be aware of) your local state first: the issue branch is
created from the **committed** base, so your uncommitted edits are invisible
to it.

## Fire the delegation (async, never sync)

```bash
curl -s -X POST ${AGENTFIELD_SERVER:-http://localhost:8080}/api/v1/execute/async/swe-planner.implement_issue \
-H "Content-Type: application/json" \
-d @issue.json
```

with `issue.json` shaped like:

```json
{
"input": {
"issue": {
"title": "...",
"description": "...",
"acceptance_criteria": ["..."],
"files_to_modify": ["..."],
"testing_strategy": "..."
},
"repo_path": "/abs/path/to/checkout",
"base_branch": "main",
"config": { "models": { "default": "haiku" } }
}
}
```

Capture `execution_id` from the 202 response. Track every in-flight
delegation in your todo list (one item per execution_id).

**Cap fan-out at 3 concurrent delegations per repo** unless the user
explicitly asks for more — each one is a paid multi-agent run, and an
unbounded loop of delegations is a surprise bill. Never re-fire a delegation
just because it is slow; poll it.

## Track progress

```bash
# Terminal status + result
curl -s $SERVER/api/v1/executions/<execution_id>
# Live agent notes (what the coder/reviewer are doing)
curl -s $SERVER/api/v1/executions/<execution_id>/notes
```

Poll on a backoff (30–60s is plenty; a typical issue lands in 10–30 min).
Batch-check several: `POST /api/v1/executions/batch-status` with
`{"execution_ids": [...]}`.

## Collect the result

The execution's `result` is an IssueBuildResult:

- `success: true` → `branch` holds the implementation
(`issue/<build_id>-<slug>`). Review the diff yourself
(`git diff main...<branch>`), run YOUR test gate, then merge:
`git merge --no-ff <branch>` (or cherry-pick), and delete the branch.
- `success: false` with a `branch` → partial work was salvaged. Triage:
read `verification` / `debt_items` / `error_message`, then either fix
forward on the branch yourself, re-delegate with a sharpened spec, or drop
the branch (`git branch -D`).
- `success: false` with `branch: ""` → nothing usable was produced; the
sub-harness already cleaned up. Sharpen the spec before retrying — do not
retry verbatim.

After merging all accepted branches, run the project's full test suite once
yourself before reporting done. The sub-harness verifier is a per-issue
check, not your integration gate.
5 changes: 3 additions & 2 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -39,8 +39,9 @@ _worktrees/
# Root runtime artifacts for this node (example artifacts are versioned separately)
/.artifacts/

# Claude Code
.claude/
# Claude Code — local state stays ignored; versioned skills ship with the repo
.claude/*
!.claude/skills/

# Python packaging / dist
dist/
Expand Down
115 changes: 115 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -623,6 +623,118 @@ Configuration on `BuildConfig`:
| `ci_wait_seconds` | `1500` | Wall-clock cap per `gh pr checks` watch (25 min). |
| `ci_poll_seconds` | `30` | Poll interval for `gh pr checks`. |

## Use SWE-AF as a Sub-Harness (Issue-Level Builds)

The full `build` pipeline is **feature-level**: it plans, decomposes, and
verifies a whole feature, which takes hours. When the caller is itself a
coding harness — Claude Code, Codex, OpenCode — it has already done the
planning. For that case SWE-AF exposes an **issue-level** entry point,
`implement_issue`, that skips every planning agent and runs just the coding
loop on an isolated branch. Delegating well-scoped issues to SWE-AF on cheap
or open-weight models keeps the main harness's token budget for the work that
needs it.

Rule of thumb for the two prompt shapes:

| Prompt shape | Entry point |
| --- | --- |
| "Implement X feature" (needs decomposition) | `swe-planner.build` / `swe-fast.build` |
| "Change this code in this file, like this" (fully scoped, context supplied) | `swe-planner.implement_issue` / `swe-fast.implement_issue` |

Each call creates its own git worktree and an `issue/<build_id>-<slug>` branch
off `base_branch` (default: the current branch), implements the issue with the
coder → reviewer loop (a QA + synthesizer path when `needs_deeper_qa` is set),
optionally runs one verifier pass against the acceptance criteria, removes the
worktree, and returns the branch. The caller's checkout, current branch, and
`git status` are untouched — so a main harness can fan out several issues
against the same `repo_path` concurrently and merge the returned branches
itself. Nothing is pushed and no PR is opened unless `enable_github_pr` is set:
the caller owns merge and CI. Typical cost is 4–8 LLM calls (vs hundreds for a
feature-level build).

```bash
# Delegate one scoped issue (async; returns an execution_id immediately)
curl -X POST http://localhost:8080/api/v1/execute/async/swe-planner.implement_issue \
-H "Content-Type: application/json" \
-d @- <<'JSON'
{
"input": {
"issue": {
"title": "Add retry with exponential backoff to fetch_user",
"description": "In src/api/client.py, wrap fetch_user's HTTP call in a retry helper: 3 attempts, 0.5s base delay, doubling. Reuse the existing logger for retry warnings.",
"acceptance_criteria": [
"fetch_user retries up to 3 times on ConnectionError",
"tests cover the retry-then-succeed path"
],
"files_to_modify": ["src/api/client.py"],
"testing_strategy": "pytest tests/api/test_client.py"
},
"repo_path": "/workspaces/my-project",
"base_branch": "main",
"config": { "models": { "default": "haiku" } }
},
"webhook": { "url": "https://my-harness.example/hooks/swe-af" }
}
JSON

# Poll instead of (or in addition to) the webhook
curl http://localhost:8080/api/v1/executions/<execution_id>
# Progress notes while it runs
curl http://localhost:8080/api/v1/executions/<execution_id>/notes
```

The result's `branch` field is the deliverable:

```json
{
"success": true,
"outcome": "completed",
"branch": "issue/a1b2c3d4-add-retry-with-exponential-backoff",
"base_branch": "main",
"commits": ["<sha>"],
"files_changed": ["src/api/client.py", "tests/api/test_client.py"],
"iterations": 1,
"verification": { "passed": true, "criteria_results": ["..."] },
"debt_items": [],
"pr_url": ""
}
```

`issue` fields: `title` + `description` (required), `acceptance_criteria`,
`files_to_create` / `files_to_modify`, `testing_strategy`, `needs_deeper_qa`
(routes through QA + reviewer + synthesizer), `estimated_complexity`, `name`.
`additional_context` (top-level) is appended to the description.

`config` keys (full schema: [`swe_af/issue/schemas.py`](swe_af/issue/schemas.py)):

| Key | Default | Description |
| --- | --- | --- |
| `runtime` / `models` | as in `build` | Same runtime + flat role map; valid role keys: `default`, `coder`, `code_reviewer`, `qa`, `qa_synthesizer`, `verifier`, `git` |
| `max_coding_iterations` | `3` | Inner-loop budget (the feature-level default is 5) |
| `verify` | `true` | One verifier pass against the acceptance criteria |
| `enable_github_pr` | `false` | Push the branch and open a PR (needs an `origin` remote) |
| `agent_timeout_seconds` | `1800` | Per-agent timeout |
| `agent_max_turns` | `50` | Tool-use turn budget per agent |
| `keep_worktree` | `false` | Leave the worktree in place for debugging |

Notes for main-harness authors:

- `repo_path` must be a checkout the SWE-AF node can reach (same machine, or
the shared `workspaces` volume in the Docker setup) with at least one commit.
- Uncommitted changes in the caller's tree are **not** visible to the issue
branch — it is created from the committed base state.
- A failed build with commits still returns the branch (`success: false`) so
the caller can triage; a build that produced no commits deletes its branch
and returns `branch: ""`.
- Cap your fan-out: each delegation is a paid multi-agent run. A handful of
concurrent issues per repo is the sweet spot — the node also bounds its own
concurrency.
- Available identically on `swe-fast.implement_issue` and, in the Go port, on
`swe-planner-go` / `swe-fast-go`.

A ready-made Claude Code skill for this flow ships in
[`.claude/skills/delegate-issue/`](.claude/skills/delegate-issue/SKILL.md).

## API Reference

<details>
Expand All @@ -634,6 +746,9 @@ Core async endpoints (returns an `execution_id` immediately):
# Full build: plan -> execute -> verify
POST /api/v1/execute/async/swe-planner.build

# Issue-level build (sub-harness entry): coding loop only, no planning
POST /api/v1/execute/async/swe-planner.implement_issue

# Plan only
POST /api/v1/execute/async/swe-planner.plan

Expand Down
114 changes: 114 additions & 0 deletions go/internal/config/issueconfig.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
package config

// This file ports swe_af/issue/schemas.py::IssueBuildConfig — configuration for
// a single issue-level build run (the implement_issue reasoner). Like
// FastBuildConfig it has no legacy-key scan in Python (only
// ConfigDict(extra="forbid")), plus a model-key validation restricted to the
// roles the issue-level path can invoke.

import (
"fmt"
"sort"
"strings"
)

// issueValidModelKeys is {"default"} | set(ISSUE_MODEL_ROLE_KEYS): the roles the
// issue-level path can invoke ("git" covers the optional PR step).
var issueValidModelKeys = map[string]struct{}{
"default": {},
"coder": {},
"code_reviewer": {},
"qa": {},
"qa_synthesizer": {},
"verifier": {},
"git": {},
}

// IssueBuildConfig ports issue/schemas.py::IssueBuildConfig.
type IssueBuildConfig struct {
Runtime string `json:"runtime"`
Models map[string]string `json:"models"`

MaxCodingIterations int `json:"max_coding_iterations"`
AgentMaxTurns int `json:"agent_max_turns"`
AgentTimeoutSeconds int `json:"agent_timeout_seconds"`
PermissionMode string `json:"permission_mode"`
Verify bool `json:"verify"`
EnableGithubPR bool `json:"enable_github_pr"`
GithubPRBase string `json:"github_pr_base"`
BranchPrefix string `json:"branch_prefix"`
KeepWorktree bool `json:"keep_worktree"`
}

// defaultIssueBuildConfig seeds every non-zero Pydantic default.
func defaultIssueBuildConfig() IssueBuildConfig {
return IssueBuildConfig{
Runtime: DefaultRuntime(),
Models: nil,
MaxCodingIterations: 3,
AgentMaxTurns: 50,
AgentTimeoutSeconds: 1800,
PermissionMode: "",
Verify: true,
EnableGithubPR: false,
GithubPRBase: "",
BranchPrefix: "issue/",
KeepWorktree: false,
}
}

// LoadIssueBuildConfig constructs an IssueBuildConfig from a raw input map:
// strict decode (extra="forbid") then issue-role model-key validation.
func LoadIssueBuildConfig(raw map[string]any) (*IssueBuildConfig, error) {
if raw == nil {
raw = map[string]any{}
}
cfg := defaultIssueBuildConfig()
if err := strictDecode(raw, &cfg); err != nil {
return nil, err
}

var unknown []string
for k := range cfg.Models {
if _, ok := issueValidModelKeys[k]; !ok {
unknown = append(unknown, fmt.Sprintf("%q", k))
}
}
if len(unknown) > 0 {
sort.Strings(unknown)
valid := make([]string, 0, len(issueValidModelKeys))
for k := range issueValidModelKeys {
valid = append(valid, k)
}
sort.Strings(valid)
return nil, fmt.Errorf(
"Unknown model keys for implement_issue: %s. Valid keys: %s",
strings.Join(unknown, ", "), strings.Join(valid, ", "),
)
}

return &cfg, nil
}

// ToExecutionRaw builds the raw map handed to LoadExecutionConfig — the Go
// equivalent of constructing ExecutionConfig(...) inside
// _implement_issue_impl: the coding loop runs with advisor/replanning/
// integration-testing/CI/learning all disabled.
func (c *IssueBuildConfig) ToExecutionRaw() map[string]any {
raw := map[string]any{
"runtime": c.Runtime,
"max_coding_iterations": c.MaxCodingIterations,
"agent_max_turns": c.AgentMaxTurns,
"agent_timeout_seconds": c.AgentTimeoutSeconds,
"permission_mode": c.PermissionMode,
"enable_learning": false,
"enable_replanning": false,
"enable_issue_advisor": false,
"enable_integration_testing": false,
"check_ci": false,
}
if c.Models != nil {
raw["models"] = c.Models
}
return raw
}
Loading
Loading