forge-delegate 1.0.3 → 1.0.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +15 -4
- package/cli.mjs +20 -4
- package/docs/DELEGATION.md +2 -2
- package/install.sh +5 -1
- package/lib.mjs +1 -14
- package/package.json +2 -1
- package/server.mjs +62 -48
- package/skills/delegate-work/SKILL.md +61 -0
package/README.md
CHANGED
|
@@ -91,16 +91,16 @@ rate-limited; switch anytime with `forge-delegate config set --model <m>`.
|
|
|
91
91
|
| Tool | What it does | Context cost |
|
|
92
92
|
|---|---|---|
|
|
93
93
|
| `ask_model` | One-shot question to any model (direct API, no file access) | Small |
|
|
94
|
-
| `delegate` | Model works **autonomously**
|
|
95
|
-
| `delegate_sync` | Send a task + optional files to a model agent running via opencode; returns the result inline in your context | Medium |
|
|
94
|
+
| `delegate` | Model works **autonomously** via opencode's own tool loop — inline by default; `background:true` returns a job id to poll via `check_delegation` | Tiny→Medium |
|
|
96
95
|
| `check_delegation` | Poll a background job (or list all with no id) | Tiny |
|
|
97
96
|
| `list_models` | List local/keyless, direct-API, and agentic models | Small |
|
|
98
97
|
| `get_delegate_config` | Read current configuration (default model, directory, autoApprove, profiles) | Tiny |
|
|
99
98
|
| `set_delegate_config` | Change configuration at runtime — no reinstall | Tiny |
|
|
100
99
|
|
|
101
100
|
`delegate` is the token-saver: the external model gets its own tool loop executed by
|
|
102
|
-
opencode locally. File contents never touch your main agent's context.
|
|
103
|
-
|
|
101
|
+
opencode locally. File contents never touch your main agent's context. Fast tasks return
|
|
102
|
+
inline; long ones are auto-backgrounded by Claude Code (past ~2 min) or run detached with
|
|
103
|
+
`background:true` — so the delegation never blocks your session.
|
|
104
104
|
|
|
105
105
|
## When to delegate
|
|
106
106
|
|
|
@@ -116,6 +116,17 @@ detached background process, so the MCP call returns instantly.
|
|
|
116
116
|
|
|
117
117
|
Full guide: [docs/DELEGATION.md](docs/DELEGATION.md)
|
|
118
118
|
|
|
119
|
+
### The `delegate-work` skill
|
|
120
|
+
|
|
121
|
+
Setup also installs a Claude Code skill (`~/.claude/skills/delegate-work/`, or
|
|
122
|
+
`<project>/.claude/skills/` under `--scope project`). Skip it with `--no-skill`.
|
|
123
|
+
|
|
124
|
+
It exists because MCP server `instructions` are injected once at session start and
|
|
125
|
+
get buried as the context fills — by turn 30 they lose to whatever is in front of the
|
|
126
|
+
model. A skill description is re-matched against every user turn, so the delegation
|
|
127
|
+
triggers ("write tests for", "rename X everywhere", "fix the lint errors") stay live
|
|
128
|
+
for the whole session instead of decaying.
|
|
129
|
+
|
|
119
130
|
## Configuration
|
|
120
131
|
|
|
121
132
|
Stored in `~/.forge-delegate/config.json`, read fresh on every call — change it anytime without
|
package/cli.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { createInterface } from "node:readline/promises";
|
|
3
|
-
import { existsSync, readFileSync, writeFileSync, mkdirSync } from "node:fs";
|
|
3
|
+
import { existsSync, readFileSync, writeFileSync, mkdirSync, copyFileSync } from "node:fs";
|
|
4
4
|
import { join, dirname } from "node:path";
|
|
5
5
|
import { homedir } from "node:os";
|
|
6
6
|
import { fileURLToPath } from "node:url";
|
|
@@ -35,6 +35,19 @@ function launchArray() {
|
|
|
35
35
|
return isPublished ? ["npx", "-y", "forge-delegate", "serve"] : [process.execPath, join(here, "cli.mjs"), "serve"];
|
|
36
36
|
}
|
|
37
37
|
|
|
38
|
+
// The MCP server's `instructions` are injected once at session start and decay as the
|
|
39
|
+
// context fills. A skill is re-matched against every user turn, so the delegation
|
|
40
|
+
// triggers stay live for the whole session.
|
|
41
|
+
function installClaudeSkill(scope, projectDir) {
|
|
42
|
+
const src = join(here, "skills", "delegate-work", "SKILL.md");
|
|
43
|
+
if (!existsSync(src)) return;
|
|
44
|
+
const root = scope === "user" ? join(homedir(), ".claude") : join(projectDir, ".claude");
|
|
45
|
+
const dest = join(root, "skills", "delegate-work", "SKILL.md");
|
|
46
|
+
mkdirSync(dirname(dest), { recursive: true });
|
|
47
|
+
copyFileSync(src, dest);
|
|
48
|
+
console.log(` \u2713 delegate-work skill (${dest})`);
|
|
49
|
+
}
|
|
50
|
+
|
|
38
51
|
function registerClaude(launch, scope) {
|
|
39
52
|
try {
|
|
40
53
|
execSync(`claude mcp remove --scope ${scope} forge-delegate`, { stdio: "ignore" });
|
|
@@ -125,7 +138,10 @@ async function setup() {
|
|
|
125
138
|
// 2. Register with chosen hosts (claude / codex / opencode)
|
|
126
139
|
for (const target of targets) {
|
|
127
140
|
try {
|
|
128
|
-
if (target === "claude")
|
|
141
|
+
if (target === "claude") {
|
|
142
|
+
registerClaude(launch, scope);
|
|
143
|
+
if (!flags["no-skill"]) installClaudeSkill(scope, projectDir);
|
|
144
|
+
}
|
|
129
145
|
else if (target === "codex") registerCodex(launchArr, scope, projectDir);
|
|
130
146
|
else if (target === "opencode") registerOpencode(launchArr, scope, projectDir);
|
|
131
147
|
else console.log(` ✗ unknown target '${target}' (use claude,codex,opencode)`);
|
|
@@ -169,7 +185,6 @@ function configCmd(argv) {
|
|
|
169
185
|
if (flags.variant) patch.defaultVariant = flags.variant;
|
|
170
186
|
if (flags.timeout) patch.timeoutMs = Number(flags.timeout) * 1000;
|
|
171
187
|
if (flags.dir) patch.defaultDirectory = flags.dir;
|
|
172
|
-
if (flags["default-dir"]) patch.defaultDirectory = flags["default-dir"];
|
|
173
188
|
if (flags.autoApprove !== undefined) patch.autoApprove = flags.autoApprove === "true" || flags.autoApprove === "1" || flags.autoApprove === true;
|
|
174
189
|
saveConfig(patch);
|
|
175
190
|
console.log(`updated ${configPath()}`);
|
|
@@ -236,8 +251,9 @@ forge-delegate — delegate coding work from Claude Code/Codex/opencode to other
|
|
|
236
251
|
|
|
237
252
|
Usage:
|
|
238
253
|
forge-delegate setup [--model provider/model] [--targets claude,codex,opencode]
|
|
239
|
-
[--scope user|project] [--project-dir <path>]
|
|
254
|
+
[--scope user|project] [--project-dir <path>] [--no-skill]
|
|
240
255
|
Interactive setup; flags optional (run bare to be prompted)
|
|
256
|
+
--no-skill skips installing the delegate-work skill
|
|
241
257
|
forge-delegate serve Start the MCP server (used internally by agents)
|
|
242
258
|
forge-delegate config get Show current configuration
|
|
243
259
|
forge-delegate config set --model <m> [--agent <a>] [--variant <v>]
|
package/docs/DELEGATION.md
CHANGED
|
@@ -30,8 +30,8 @@ Unsure → keep it.
|
|
|
30
30
|
| Need | Tool |
|
|
31
31
|
|---|---|
|
|
32
32
|
| Quick one-shot question, no file access needed | `ask_model` |
|
|
33
|
-
| A self-contained task
|
|
34
|
-
|
|
|
33
|
+
| A self-contained coding task (inline; auto-backgrounds past ~2 min in Claude Code) | `delegate` |
|
|
34
|
+
| Parallel fan-out or fire-and-forget | `delegate` with `background:true` + `check_delegation` |
|
|
35
35
|
| See what's available | `list_models` |
|
|
36
36
|
|
|
37
37
|
## Cost policy (non-negotiable)
|
package/install.sh
CHANGED
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
# --targets a,b,c hosts to register with: claude,codex,opencode (default all)
|
|
13
13
|
# --scope user|project registration scope (default user)
|
|
14
14
|
# --project-dir <path> project for --scope project (default current dir)
|
|
15
|
+
# --no-skill skip installing the delegate-work skill for Claude Code
|
|
15
16
|
# --no-opencode skip auto-installing opencode if missing
|
|
16
17
|
# --no-npx skip the npx route, force clone/checkout
|
|
17
18
|
# --help
|
|
@@ -23,6 +24,7 @@ SCOPE="user"
|
|
|
23
24
|
PROJECT_DIR=""
|
|
24
25
|
NO_OPENCODE=0
|
|
25
26
|
NO_NPX=0
|
|
27
|
+
NO_SKILL=0
|
|
26
28
|
|
|
27
29
|
while [[ $# -gt 0 ]]; do
|
|
28
30
|
case "$1" in
|
|
@@ -34,10 +36,11 @@ while [[ $# -gt 0 ]]; do
|
|
|
34
36
|
--scope=*) SCOPE="${1#*=}"; shift ;;
|
|
35
37
|
--project-dir) PROJECT_DIR="${2:-}"; shift 2 ;;
|
|
36
38
|
--project-dir=*) PROJECT_DIR="${1#*=}"; shift ;;
|
|
39
|
+
--no-skill) NO_SKILL=1; shift ;;
|
|
37
40
|
--no-opencode) NO_OPENCODE=1; shift ;;
|
|
38
41
|
--no-npx) NO_NPX=1; shift ;;
|
|
39
42
|
--help|-h)
|
|
40
|
-
sed -n '3,
|
|
43
|
+
sed -n '3,19p' "$0"
|
|
41
44
|
exit 0 ;;
|
|
42
45
|
*) echo "unknown option: $1"; exit 1 ;;
|
|
43
46
|
esac
|
|
@@ -68,6 +71,7 @@ fi
|
|
|
68
71
|
ARGS=(--targets "$TARGETS" --scope "$SCOPE")
|
|
69
72
|
if [[ -n "$MODEL" ]]; then ARGS+=(--model "$MODEL"); fi
|
|
70
73
|
if [[ "$SCOPE" == "project" ]]; then ARGS+=(--project-dir "$PROJECT_DIR"); fi
|
|
74
|
+
if [[ "$NO_SKILL" -eq 1 ]]; then ARGS+=(--no-skill); fi
|
|
71
75
|
|
|
72
76
|
# 3. Preferred route: published npm package (works on Windows, always latest)
|
|
73
77
|
if [[ "$NO_NPX" -eq 0 ]] && command -v npx >/dev/null 2>&1; then
|
package/lib.mjs
CHANGED
|
@@ -1,10 +1,8 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { promisify } from "node:util";
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
3
2
|
import { appendFileSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
4
3
|
import { join } from "node:path";
|
|
5
4
|
import { homedir } from "node:os";
|
|
6
5
|
|
|
7
|
-
const execFileAsync = promisify(execFile);
|
|
8
6
|
export const OPENCODE_TIMEOUT = Number(process.env.FORGE_OPENCODE_TIMEOUT ?? 900_000);
|
|
9
7
|
const CONFIG_DIR = join(homedir(), ".forge-delegate");
|
|
10
8
|
const AUTH_PATH = join(homedir(), ".local/share/opencode/auth.json");
|
|
@@ -344,14 +342,3 @@ export async function runOne(model, prompt) {
|
|
|
344
342
|
throw new Error(`unknown model '${model}' — see list_models`);
|
|
345
343
|
}
|
|
346
344
|
|
|
347
|
-
export async function gitDiff() { try {
|
|
348
|
-
const { stdout } = await execFileAsync("git", ["diff", "HEAD"], {
|
|
349
|
-
cwd: process.cwd(),
|
|
350
|
-
maxBuffer: 5 * 1024 * 1024,
|
|
351
|
-
});
|
|
352
|
-
return stdout.trim();
|
|
353
|
-
} catch {
|
|
354
|
-
return null;
|
|
355
|
-
}
|
|
356
|
-
}
|
|
357
|
-
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "forge-delegate",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.5",
|
|
4
4
|
"description": "MCP server that relays coding work to other AI models (DeepSeek, Gemini, OpenAI, Ollama) so Claude Code saves tokens",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
"server.mjs",
|
|
17
17
|
"lib.mjs",
|
|
18
18
|
"install.sh",
|
|
19
|
+
"skills/",
|
|
19
20
|
"docs/",
|
|
20
21
|
"README.md"
|
|
21
22
|
],
|
package/server.mjs
CHANGED
|
@@ -4,13 +4,13 @@ import { z } from "zod";
|
|
|
4
4
|
import { execFile, spawn } from "node:child_process";
|
|
5
5
|
import { promisify } from "node:util";
|
|
6
6
|
import { mkdirSync, writeFileSync, readFileSync, existsSync, openSync, readdirSync } from "node:fs";
|
|
7
|
-
import { join } from "node:path";
|
|
7
|
+
import { join, dirname } from "node:path";
|
|
8
|
+
import { fileURLToPath } from "node:url";
|
|
8
9
|
import { homedir } from "node:os";
|
|
9
10
|
import {
|
|
10
11
|
logUsage,
|
|
11
12
|
opencodeRun,
|
|
12
13
|
runOne,
|
|
13
|
-
gitDiff,
|
|
14
14
|
opencodeArgs,
|
|
15
15
|
parseEvents,
|
|
16
16
|
pickVariant,
|
|
@@ -24,25 +24,35 @@ import {
|
|
|
24
24
|
|
|
25
25
|
const execFileAsync = promisify(execFile);
|
|
26
26
|
const RUNS_DIR = join(homedir(), ".forge-delegate", "runs");
|
|
27
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
28
|
+
const VERSION = JSON.parse(readFileSync(join(here, "package.json"), "utf8")).version;
|
|
27
29
|
|
|
28
|
-
const INSTRUCTIONS = `forge-delegate
|
|
30
|
+
const INSTRUCTIONS = `forge-delegate hands coding work to cheap external models running as full agents. They read the project and write the code in their own loop, so the work never passes through your context. Spend your context on judgment; delegate the typing.
|
|
29
31
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
+
DELEGATE BY DEFAULT — do not hand-write these yourself:
|
|
33
|
+
- Tests for code that already exists.
|
|
34
|
+
- Mechanical refactors: renames, signature changes, import rewrites, API migrations.
|
|
35
|
+
- Lint, type, and build errors — paste the error output and delegate the fix.
|
|
36
|
+
- Boilerplate: CRUD endpoints, config and schema files, adapters, fixtures, scaffolding.
|
|
37
|
+
- Ports and translations between languages, frameworks, or file formats.
|
|
38
|
+
- The same edit repeated across 3 or more files.
|
|
39
|
+
- Reading a large file or directory to answer one narrow question — delegate the reading, keep your context clean.
|
|
40
|
+
|
|
41
|
+
KEEP FOR YOURSELF: architecture, debugging an unknown root cause, security-sensitive code, anything where being wrong is expensive, and the review of whatever comes back. Delegation does not transfer responsibility for the result.
|
|
32
42
|
|
|
33
43
|
Routing:
|
|
34
|
-
-
|
|
35
|
-
-
|
|
36
|
-
-
|
|
37
|
-
- Follow-
|
|
38
|
-
- Omit model/directory
|
|
39
|
-
- Never paste
|
|
44
|
+
- One-shot question, no file access needed -> ask_model.
|
|
45
|
+
- Anything touching files -> delegate (inline; Claude Code auto-backgrounds calls past 2 minutes).
|
|
46
|
+
- Parallel fan-out or long jobs -> delegate with background:true, then poll check_delegation.
|
|
47
|
+
- Follow-up in the same area -> pass the previous session id as continue_session.
|
|
48
|
+
- Omit model/directory/agent: the configured defaults are already correct (get_delegate_config to see them, set_delegate_config to change them — never reinstall).
|
|
49
|
+
- Never paste file contents into a task. Name the paths; the agent reads them itself.
|
|
40
50
|
|
|
41
|
-
|
|
51
|
+
Cost: the configured default model is already a cheap or free tier — just use it. Do not override \`model\` upward to a premium or flagship tier; if a task looks too big for the default, split it into smaller delegations instead.
|
|
42
52
|
`;
|
|
43
53
|
|
|
44
54
|
const server = new McpServer(
|
|
45
|
-
{ name: "forge-delegate", version:
|
|
55
|
+
{ name: "forge-delegate", version: VERSION },
|
|
46
56
|
{ instructions: INSTRUCTIONS }
|
|
47
57
|
);
|
|
48
58
|
|
|
@@ -79,7 +89,22 @@ async function startBackgroundJob(model, task, opts = {}) {
|
|
|
79
89
|
stdio: ["ignore", out, out],
|
|
80
90
|
});
|
|
81
91
|
child.unref();
|
|
82
|
-
|
|
92
|
+
const started = Date.now();
|
|
93
|
+
const deadline = opts.timeoutMs ? started + opts.timeoutMs : null;
|
|
94
|
+
if (deadline) {
|
|
95
|
+
setTimeout(() => {
|
|
96
|
+
try {
|
|
97
|
+
process.kill(-child.pid, "SIGKILL");
|
|
98
|
+
} catch {}
|
|
99
|
+
try {
|
|
100
|
+
process.kill(child.pid, "SIGKILL");
|
|
101
|
+
} catch {}
|
|
102
|
+
}, opts.timeoutMs).unref();
|
|
103
|
+
}
|
|
104
|
+
writeFileSync(
|
|
105
|
+
join(RUNS_DIR, `${id}.json`),
|
|
106
|
+
JSON.stringify({ id, pid: child.pid, model, directory: cwd, started, deadline, logPath })
|
|
107
|
+
);
|
|
83
108
|
return id;
|
|
84
109
|
}
|
|
85
110
|
|
|
@@ -120,7 +145,7 @@ server.tool(
|
|
|
120
145
|
|
|
121
146
|
server.tool(
|
|
122
147
|
"ask_model",
|
|
123
|
-
"
|
|
148
|
+
"USE THIS INSTEAD OF ANSWERING FROM YOUR OWN REASONING when the question is self-contained and cheap to verify: a syntax or API detail, a regex, a short snippet, a second opinion on a design, a summary of text you already have. Sends a one-shot prompt to an external model and returns its raw response. Fast direct API call; the model has NO file access and CANNOT take actions — for anything touching files use delegate instead. Omit model to use the configured default (already a cheap tier).",
|
|
124
149
|
{
|
|
125
150
|
model: z.string().optional().describe("provider/model, e.g. deepseek/deepseek-v4-flash, ollama:llama3 (defaults to config defaultModel)"),
|
|
126
151
|
prompt: z.string(),
|
|
@@ -136,21 +161,35 @@ server.tool(
|
|
|
136
161
|
);
|
|
137
162
|
|
|
138
163
|
server.tool(
|
|
139
|
-
"
|
|
140
|
-
"
|
|
164
|
+
"delegate",
|
|
165
|
+
"USE THIS INSTEAD OF WRITING THE CODE YOURSELF when the task is: tests for code that already exists; a mechanical refactor (rename, signature change, import rewrite, API migration); fixing lint/type/build errors; boilerplate or scaffolding (CRUD endpoints, config, schemas, adapters, fixtures); a port between languages or frameworks; the same edit repeated across 3 or more files; or reading a large file/directory to answer one narrow question. Hands the task to another AI model running as a full agent via local opencode: it reads your project, explores, and writes concrete code in its own tool loop — none of which enters your context. Keep architecture, unknown-root-cause debugging, and security-sensitive code for yourself, and always review what comes back. Runs INLINE by default (result returned when done; Claude Code auto-backgrounds calls past 2 minutes). Pass background:true to run detached and get a job id instantly, then poll check_delegation — use that for parallel fan-out or hosts that block on long calls. Pass continue_session (returned by a previous delegation) to keep the same agent session instead of re-exploring. Never paste file contents into task: name the paths and let the agent read them. model/directory/agent/autoApprove default from config — omit them.",
|
|
141
166
|
{
|
|
142
167
|
model: z.string().optional().describe("provider/model format, e.g. opencode/mimo-v2.5-free (defaults to config defaultModel)"),
|
|
143
168
|
task: z.string(),
|
|
144
|
-
files: z.array(z.string()).optional(),
|
|
169
|
+
files: z.array(z.string()).optional().describe("specific files to attach (inline mode)"),
|
|
170
|
+
background: z.boolean().optional().describe("run detached and return a job id (default false)"),
|
|
171
|
+
continue_session: z.string().optional().describe("session id returned by a previous delegation"),
|
|
145
172
|
directory: z.string().optional().describe("absolute path to run in (defaults to config defaultDirectory, else server cwd)"),
|
|
146
173
|
agent: z.string().optional().describe("opencode agent to use (defaults to config defaultAgent)"),
|
|
147
174
|
autoApprove: z.boolean().optional().describe("auto-approve permissions (defaults to config autoApprove, true)"),
|
|
148
175
|
profile: z.string().optional().describe("named config profile to apply"),
|
|
149
176
|
},
|
|
150
|
-
async ({ model, task, files, directory, agent, autoApprove, profile }) => {
|
|
177
|
+
async ({ model, task, files, background, continue_session, directory, agent, autoApprove, profile }) => {
|
|
151
178
|
const r = resolveConfig({ model, directory, agent, autoApprove, profile });
|
|
152
179
|
if (!r.model) return { content: [{ type: "text", text: "ERROR: no model specified and no defaultModel configured. Call set_delegate_config or pass model." }], isError: true };
|
|
153
|
-
|
|
180
|
+
if (background) {
|
|
181
|
+
return withUsage("delegate", r.model, async () => {
|
|
182
|
+
const id = await startBackgroundJob(r.model, task, {
|
|
183
|
+
sessionId: continue_session,
|
|
184
|
+
directory: r.directory,
|
|
185
|
+
agent: r.agent,
|
|
186
|
+
autoApprove: r.autoApprove,
|
|
187
|
+
timeoutMs: r.timeoutMs,
|
|
188
|
+
});
|
|
189
|
+
return `Started background job ${id}. Poll with check_delegation(id: "${id}").`;
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
return withUsage("delegate", r.model, async () => {
|
|
154
193
|
const res = await opencodeRun(r.model, task, {
|
|
155
194
|
files,
|
|
156
195
|
directory: r.directory,
|
|
@@ -163,33 +202,6 @@ server.tool(
|
|
|
163
202
|
}
|
|
164
203
|
);
|
|
165
204
|
|
|
166
|
-
server.tool(
|
|
167
|
-
"delegate",
|
|
168
|
-
"Fully delegate a coding task to another AI model working autonomously on this project (via local opencode): it explores, reads, edits and creates files in its own loop — invisible to the caller's context. Runs in the BACKGROUND (recommended): returns a job id instantly, poll check_delegation for the result. Pass continue_session (returned by a previous delegation) to keep the same agent session instead of re-exploring. Use this over delegate_sync to keep the caller's context small. model/directory/agent/autoApprove default from config.",
|
|
169
|
-
{
|
|
170
|
-
model: z.string().optional().describe("provider/model format, e.g. opencode/mimo-v2.5-free (defaults to config defaultModel)"),
|
|
171
|
-
task: z.string(),
|
|
172
|
-
continue_session: z.string().optional().describe("session id returned by a previous delegation"),
|
|
173
|
-
directory: z.string().optional().describe("absolute path to run in (defaults to config defaultDirectory, else server cwd)"),
|
|
174
|
-
agent: z.string().optional().describe("opencode agent to use (defaults to config defaultAgent)"),
|
|
175
|
-
autoApprove: z.boolean().optional().describe("auto-approve permissions (defaults to config autoApprove, true)"),
|
|
176
|
-
profile: z.string().optional().describe("named config profile to apply"),
|
|
177
|
-
},
|
|
178
|
-
async ({ model, task, continue_session, directory, agent, autoApprove, profile }) => {
|
|
179
|
-
const r = resolveConfig({ model, directory, agent, autoApprove, profile });
|
|
180
|
-
if (!r.model) return { content: [{ type: "text", text: "ERROR: no model specified and no defaultModel configured. Call set_delegate_config or pass model." }], isError: true };
|
|
181
|
-
return withUsage("delegate", r.model, async () => {
|
|
182
|
-
const id = await startBackgroundJob(r.model, task, {
|
|
183
|
-
sessionId: continue_session,
|
|
184
|
-
directory: r.directory,
|
|
185
|
-
agent: r.agent,
|
|
186
|
-
autoApprove: r.autoApprove,
|
|
187
|
-
});
|
|
188
|
-
return `Started background job ${id}. Poll with check_delegation(id: "${id}").`;
|
|
189
|
-
});
|
|
190
|
-
}
|
|
191
|
-
);
|
|
192
|
-
|
|
193
205
|
server.tool(
|
|
194
206
|
"check_delegation",
|
|
195
207
|
"Check status of a background delegation job: running/completed plus output so far. Call with no id to list all jobs.",
|
|
@@ -216,7 +228,9 @@ server.tool(
|
|
|
216
228
|
const parsed = parseEvents(log);
|
|
217
229
|
const tail = parsed.text || "(agent still thinking, no text output yet)";
|
|
218
230
|
const footer = parsed.sessionId ? ` · session: ${parsed.sessionId}` : "";
|
|
219
|
-
|
|
231
|
+
let status = running ? "RUNNING" : "DONE";
|
|
232
|
+
if (meta.deadline && Date.now() > meta.deadline) status = "TIMED OUT";
|
|
233
|
+
return `[${status}] ${id} (${meta.model}, ${Math.round((Date.now() - meta.started) / 1000)}s)${footer}\n\n${tail.slice(-8000)}`;
|
|
220
234
|
})
|
|
221
235
|
);
|
|
222
236
|
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: delegate-work
|
|
3
|
+
description: Hand a coding task to a cheap external model via forge-delegate instead of writing it yourself. Load BEFORE starting work that is high-volume and low-judgment - writing tests for code that already exists, mechanical refactors (renames, signature changes, import rewrites, API migrations), fixing lint/type/build errors, boilerplate and scaffolding (CRUD endpoints, config files, schemas, adapters, fixtures), porting code between languages or frameworks, the same edit repeated across 3 or more files, or reading a large file or directory to answer one narrow question. Triggers on - "write tests for", "add tests", "rename X to Y everywhere", "refactor", "migrate", "port this to", "fix the lint errors", "fix the type errors", "scaffold", "boilerplate", "do the same for the other files".
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Delegating work to a cheap model
|
|
7
|
+
|
|
8
|
+
`forge-delegate` runs another AI model as a full agent over this project. It reads
|
|
9
|
+
files, explores, and writes code in its own tool loop. None of that passes through
|
|
10
|
+
your context — you get back a summary and a diff on disk.
|
|
11
|
+
|
|
12
|
+
The point is not to save the user money on your tokens. It is that your context is
|
|
13
|
+
the scarce resource in a long session, and mechanical work burns it fast.
|
|
14
|
+
|
|
15
|
+
## Decide
|
|
16
|
+
|
|
17
|
+
Delegate when the task is **high-volume and low-judgment**: the shape of the correct
|
|
18
|
+
answer is already known, and the work is typing it out across files.
|
|
19
|
+
|
|
20
|
+
| Delegate | Keep |
|
|
21
|
+
|---|---|
|
|
22
|
+
| Tests for code that already exists | Deciding what behaviour to test |
|
|
23
|
+
| Rename / signature change / import rewrite across files | Designing the new interface |
|
|
24
|
+
| Fixing lint, type, or build errors | Debugging an unknown root cause |
|
|
25
|
+
| CRUD endpoints, config, schemas, fixtures, adapters | Architecture and data modelling |
|
|
26
|
+
| Porting between languages or frameworks | Security-sensitive code |
|
|
27
|
+
| The same edit repeated across 3+ files | The first instance, so the pattern is right |
|
|
28
|
+
| Reading a big file to answer one question | Anything where being wrong is expensive |
|
|
29
|
+
|
|
30
|
+
Delegating does not transfer responsibility. Review what comes back before you
|
|
31
|
+
report it as done.
|
|
32
|
+
|
|
33
|
+
## Do it
|
|
34
|
+
|
|
35
|
+
1. Do the judgment part yourself first — decide the interface, write the first
|
|
36
|
+
instance, name the files.
|
|
37
|
+
2. Call `delegate` with a task that names paths. **Never paste file contents into
|
|
38
|
+
the task**; the agent reads them itself. That is the whole point.
|
|
39
|
+
3. Omit `model`, `directory`, and `agent` — the configured defaults are already a
|
|
40
|
+
cheap or free tier. Do not raise `model` to a premium tier; if the task is too
|
|
41
|
+
big for the default, split it into smaller delegations.
|
|
42
|
+
4. Review the result. Run the tests or the build yourself.
|
|
43
|
+
|
|
44
|
+
```
|
|
45
|
+
delegate(task: "Write unit tests for the exported functions in src/parser.ts.
|
|
46
|
+
Follow the style of tests/lexer.test.ts. Cover the error paths.")
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
Parallel or long-running work: `background: true` returns a job id immediately,
|
|
50
|
+
then poll `check_delegation(id)`. Follow-up in the same area: pass the previous
|
|
51
|
+
`session: <id>` back as `continue_session` so the agent skips re-exploring.
|
|
52
|
+
|
|
53
|
+
For a question that needs no file access at all — a syntax detail, a regex, a
|
|
54
|
+
second opinion — use `ask_model` instead; it is a direct API call with no agent loop.
|
|
55
|
+
|
|
56
|
+
## Don't
|
|
57
|
+
|
|
58
|
+
- Don't delegate a task you cannot describe precisely. Vague task, vague diff.
|
|
59
|
+
- Don't delegate and walk away. Unreviewed delegated code is worse than no code.
|
|
60
|
+
- Don't paste large files into the task text.
|
|
61
|
+
- Don't override `model` upward because a task "seems hard" — split it instead.
|