forge-delegate 1.0.4 → 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 CHANGED
@@ -116,6 +116,17 @@ inline; long ones are auto-backgrounded by Claude Code (past ~2 min) or run deta
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") registerClaude(launch, scope);
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/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,18p' "$0"
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 { execFile, spawn } from "node:child_process";
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.4",
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 gives you a crew of external AI models to delegate coding work to. Hand off the high-volume, low-risk work (boilerplate, tests, lint/type fixes, mechanical edits) to a cheap or free model and keep the expensive agent focused on architecture and judgment calls.
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
- COST POLICY — MANDATORY, NON-NEGOTIABLE:
31
- The user pays per delegated call on a budget plan. You MUST pick only cheap or free models for EVERY delegation (ask_model, delegate). NEVER pick top-tier, premium, flagship, or frontier models — not even "just this once" or for hard tasks. If a task seems too hard for a cheap model, split it into smaller delegated steps instead of upgrading. Run list_models and prefer free/cheap models (deepseek flash tiers, free opencode models, local ollama).
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
- - Quick one-shot questions → ask_model (direct API, no file access).
35
- - Multi-file edits/refactors/tests → delegate (inline by default; Claude Code auto-backgrounds calls past 2 min).
36
- - Parallel or fire-and-forget → delegate with background:true (returns a job id instantly); poll check_delegation for the result.
37
- - Follow-ups on a delegated area: pass the returned session id as continue_session.
38
- - Omit model/directory to use the configured defaults (get_delegate_config); change them anytime with set_delegate_config — never reinstall.
39
- - Never paste large files into your own context for delegation — delegate and let the agent read files itself.
40
-
41
- The per-tool descriptions are the authoritative reference.
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.
50
+
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: "1.0.0" },
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
- writeFileSync(join(RUNS_DIR, `${id}.json`), JSON.stringify({ id, pid: child.pid, model, directory: cwd, started: Date.now(), logPath }));
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
- "Send a one-shot prompt to an external AI model and get its raw response. Fast direct API call; the model has NO file access and CANNOT take actions. For work requiring tools/files use delegate instead. model defaults to the configured defaultModel.",
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(),
@@ -137,7 +162,7 @@ server.tool(
137
162
 
138
163
  server.tool(
139
164
  "delegate",
140
- "Delegate a coding 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 — invisible to the caller's context. Runs INLINE by default (the result is returned in your context when done; Claude Code auto-backgrounds calls running past 2 minutes). Pass background:true to run detached and return a job id instantly, then poll check_delegation for the result — 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. Optionally attach specific files in inline mode. model/directory/agent/autoApprove default from config.",
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(),
@@ -159,6 +184,7 @@ server.tool(
159
184
  directory: r.directory,
160
185
  agent: r.agent,
161
186
  autoApprove: r.autoApprove,
187
+ timeoutMs: r.timeoutMs,
162
188
  });
163
189
  return `Started background job ${id}. Poll with check_delegation(id: "${id}").`;
164
190
  });
@@ -202,7 +228,9 @@ server.tool(
202
228
  const parsed = parseEvents(log);
203
229
  const tail = parsed.text || "(agent still thinking, no text output yet)";
204
230
  const footer = parsed.sessionId ? ` · session: ${parsed.sessionId}` : "";
205
- return `[${running ? "RUNNING" : "DONE"}] ${id} (${meta.model}, ${Math.round((Date.now() - meta.started) / 1000)}s)${footer}\n\n${tail.slice(-8000)}`;
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)}`;
206
234
  })
207
235
  );
208
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.