Named agents, gated execution, and plan-lifecycle skills

ober

36daf735d2d23bd0f09ea08f4f59b2053719ec6a

diff --git a/README.md b/README.md
index 506d4db..54662f6 100644
--- a/README.md
+++ b/README.md
@@ -33,6 +33,12 @@ tools dependably.
   plus [ATLAS](docs/FORGE.md#the-atlas-reliability-layer)-style verify-and-repair
   wraps every call: prose-to-tool rescue, step enforcement, verify-gate, and
   best-of-k. Always on, every provider.
+- **Delegation built in.** Named sub-agent roles for the `task` tool —
+  read-only `delegate`, docs-scoped `doc-explorer`, gated `implementer`
+  (BLUEPRINT → approve → EXECUTE) — with enforced write scopes, per-agent
+  model routing via `jcode.json`, and plan-lifecycle skills
+  (`create-plan` / `resume-plan` / `generate-handover`). See
+  [docs/agents.md](docs/agents.md).
 - **A real TUI.** termbox-based panels, markdown + syntax highlighting, live
   diffs, themes, a sidebar with token / cost / GPU stats.
 
@@ -63,6 +69,7 @@ make binary         # produce the standalone ./jcode
 | **[Architecture](docs/architecture.md)** | The agent loop, module map, and how a turn flows through the guardrails. |
 | **[Providers & models](docs/providers.md)** | The 13 providers, model registry, per-model sampling, and hardware tiers. |
 | **[Tools](docs/tools.md)** | The agent's toolbox (file, bash, web, git, patch, task, MCP) and the safety model. |
+| **[Named agents & plans](docs/agents.md)** | Sub-agent roles with enforced write scopes, per-agent models, gated execution, and the plan-lifecycle skills. |
 | **[TUI](docs/tui.md)** | Layout, keybindings, themes, and the rendering features. |
 | **[Forge & ATLAS reliability](docs/FORGE.md)** | Guardrails, workflows, verify-gate, best-of-k, the proxy, and the eval harness. |
 | **[Remote & Android](docs/remote.md)** | `serve` / `relay` / `connect` and the thin-client architecture. |
diff --git a/docs/README.md b/docs/README.md
index 6dddbaa..5015c6f 100644
--- a/docs/README.md
+++ b/docs/README.md
@@ -14,6 +14,10 @@ elevator pitch.
   model registry, per-model sampling profiles, and local-model hardware tiers.
 - **[Tools](tools.md)** — the toolbox the agent drives (file, bash, web, git,
   patch, task, MCP) and the permission / sandbox / plan-mode safety model.
+- **[Named agents & plan workflow](agents.md)** — sub-agent roles (`delegate`,
+  `doc-explorer`, `implementer`), enforced write scopes, per-agent model
+  routing, the gated BLUEPRINT → EXECUTE protocol, and the plan-lifecycle
+  skills (`/create-plan` … `/resume-plan`).
 - **[TUI](tui.md)** — the terminal UI: layout, the full keybinding table,
   themes, and the markdown / syntax / diff renderers.
 
diff --git a/docs/agents.md b/docs/agents.md
new file mode 100644
index 0000000..e812f12
--- /dev/null
+++ b/docs/agents.md
@@ -0,0 +1,155 @@
+# Named agents, gated execution & plan workflow
+
+jcode's `task` tool can spawn sub-agents. Named agents give those sub-agents
+**roles**: a specialized prompt, an *enforced* limit on what they may write,
+and optionally their own model. On top of that sit a gated execution protocol
+(plan first, get approval, then edit) and a set of plan-lifecycle skills that
+persist work across sessions as files.
+
+## The 30-second version
+
+Think of jcode as a contractor who can call in three kinds of help:
+
+- **`delegate`** — a scout. Goes and reads code, searches, researches; comes
+  back with a short report. *Physically cannot* write files.
+- **`doc-explorer`** — a librarian. Writes documentation and plans, but only
+  under `docs/` and `plans/`. Can't touch your code.
+- **`implementer`** — a builder. The only one who edits code, and only in two
+  steps: first it returns a *blueprint* (numbered steps + one verify command),
+  then — after approval — a second call executes that blueprint.
+
+Each helper runs in its own context window, so the main conversation stays
+small while the helpers do the noisy reading and editing.
+
+## Using it: you mostly don't have to do anything
+
+The agents are driven *by the model* through the `task` tool — the tool
+description tells it what each role is for. Just ask for work the normal way:
+
+```
+› how does the compaction strategy decide what to drop?     ← model may scout with delegate
+› document the provider layer                               ← model may use doc-explorer
+› add a --json flag to the status command                   ← model may run the implementer gate
+```
+
+You can also steer explicitly — these phrasings work well:
+
+```
+› use a delegate to find every caller of refresh-system-prompt
+› have the implementer blueprint this change first, then show me the plan
+› spawn doc-explorer to write docs/modules/provider.md
+```
+
+`/agents` lists the roles (and any variants you configured) at any time.
+
+## The gated implementer protocol
+
+For non-trivial changes, the safest flow is two `task` calls sharing a
+`task_id` (the sub-agent keeps its memory between them):
+
+```
+1. BLUEPRINT   task(agent: "implementer", task_id: "wp-1",
+                    prompt: "MODE: BLUEPRINT\n<the change + context>")
+               → numbered step list + ONE verify command. No edits happen.
+
+2. (review)    the calling agent — or you — reads the blueprint.
+
+3. EXECUTE     task(task_id: "wp-1",
+                    prompt: "MODE: EXECUTE — approved\n<the blueprint>")
+               → implements, runs the verify command, returns a digest:
+                 outcome / files edited / verify result / follow-ups.
+```
+
+Because the EXECUTE call resumes the same sub-agent, all the reading it did
+while blueprinting is still in its head — nothing is re-explored. If a
+blueprint is wrong, reply with objections in another BLUEPRINT call on the
+same `task_id` instead of approving.
+
+Task sessions live in memory for the jcode process (most recent 32 kept).
+
+## Plan workflow: multi-session work that survives restarts
+
+Five built-in skills (run them like any slash command) maintain a file-based
+plan under `plans/<name>/` — the files *are* the shared memory, so a new
+session picks up exactly where the old one stopped:
+
+| Skill | When | What it does |
+|---|---|---|
+| `/create-plan` | starting a feature | writes `plan.md`, `phases/phase-N.md`, `todo.md` |
+| `/review-plan` | after planning | fresh-eyes review; findings rated Critical/Major/Minor/Note |
+| `/execute-work-package` | implementing a phase | drives the BLUEPRINT → approve → EXECUTE gate |
+| `/generate-handover` | end of session | narrative handover: decisions, state, blockers, next steps |
+| `/resume-plan` | next session | reads plan + todo + latest handover, briefs you, waits for go |
+
+A typical week:
+
+```
+Mon  › /create-plan        (discuss; phases written to plans/auth-rework/)
+     › /review-plan        (catches a gap in phase 2 — fix it now, cheap)
+     › /execute-work-package   (phase 1: blueprint → you approve → execute)
+     › /generate-handover
+Tue  › /resume-plan        ("phase 1 done, phase 2 blocked on X — continue?")
+     › ...
+```
+
+The plan directory layout:
+
+```
+plans/<name>/
+├── plan.md                      goal, scope, phase list
+├── phases/phase-N.md            per-phase scope + acceptance criteria
+├── implementation/phase-N-impl.md   concrete steps grounded in the code
+├── reviews/                     review findings
+├── todo.md                      checkbox progress
+└── handovers/session-<date>.md  session narratives
+```
+
+## Variants: routing roles to different models
+
+Add an `"agents"` block to `jcode.json`. `extends` inherits a built-in's
+prompt and write scope; the rest overrides:
+
+```json
+{
+  "agents": {
+    "delegate-fast":    {"extends": "delegate",
+                         "provider": "ollama", "model": "qwen3:8b"},
+    "delegate-strong":  {"extends": "delegate",
+                         "provider": "anthropic", "model": "claude-opus-4-8"},
+    "implementer-safe": {"extends": "implementer",
+                         "provider": "anthropic", "model": "claude-opus-4-8"},
+    "reviewer":         {"description": "fresh-context reviewer",
+                         "prompt": "You are a critical reviewer. ...",
+                         "write_scope": ["plans/"]}
+  }
+}
+```
+
+Then: cheap local model for scouting, strong cloud model for code changes —
+"use delegate-fast to map the test suite, then implementer-safe for the fix."
+A variant with neither `provider` nor `model` uses the session's current
+model. `write_scope` is `"all"`, `"none"`, or a list of path prefixes.
+
+## How the safety actually works
+
+- Write scopes are enforced in the **tool layer** (`core/agent-defs.ss`,
+  checked by `file.ss` and `apply-patch.ss`), not just by prompt. A scoped
+  sub-agent that tries `write src/foo.ss` gets a refusal string back.
+- Nesting only **narrows**: if a docs-scoped agent spawns another task, the
+  child's scope is intersected with `docs/`+`plans/` — it can never widen.
+- Scopes gate the *file* tools. `bash` remains governed by the
+  [permissions rules](tools.md#2-bash-permissions) and OS sandbox, and the
+  delegate's role prompt forbids mutation — but a hostile model could still
+  try `bash` writes; keep `permissions.deny` tight if that matters to you.
+- All existing layers still apply underneath: PLAN/BUILD mode, generated-
+  artifact protection, checkpoints (`/undo`), sensitive-path denylist.
+
+## Reference
+
+`task` tool parameters: `description`, `prompt` (required); `system` (skill
+name or literal system text); `agent` (role name); `task_id` (resumable
+session id — required for BLUEPRINT → EXECUTE).
+
+Related: [tools.md](tools.md) (toolbox + safety model), [cli.md](cli.md)
+(slash commands), [FORGE.md](FORGE.md) (verify-gate — the *after*-edit gate
+that complements the blueprint's *before*-edit gate).
diff --git a/docs/cli.md b/docs/cli.md
index 4ca9f08..9cf452e 100644
--- a/docs/cli.md
+++ b/docs/cli.md
@@ -119,6 +119,7 @@ Available in the REPL and TUI.
 | Command | Effect |
 |---|---|
 | `/tools` | List available tools. |
+| `/agents` | List named sub-agent roles for the `task` tool (built-ins + `jcode.json` variants). |
 | `/mcp` | Toggle MCP tools on/off. |
 | `/plugins` | List loaded plugins. |
 | `/skills` | List built-in and user skills. |
diff --git a/docs/tools.md b/docs/tools.md
index a15054c..76189d1 100644
--- a/docs/tools.md
+++ b/docs/tools.md
@@ -44,7 +44,7 @@ page is the catalogue plus the safety model that wraps every call.
 | Tool | Purpose | Parameters |
 |---|---|---|
 | `batch` | Run several tool calls in parallel (green threads) | `calls[]` of `{tool,args}` |
-| `task` | Spawn a sub-agent with its own context | `description`, `prompt`, `system?` |
+| `task` | Spawn a sub-agent with its own context | `description`, `prompt`, `system?`, `agent?`, `task_id?` |
 | `repomap` | Condensed project map (top files by PageRank + key symbols) | *(none)* |
 
 `task`'s `system` argument can name a skill, so a sub-agent can be launched with
@@ -52,6 +52,51 @@ a specialized prompt. LSP lookups (`lsp_definition` / `lsp_hover` /
 `lsp_references`) exist internally but are **not** exposed to the model — they
 back editor integrations, not the agent loop.
 
+### Named agents (`task`'s `agent` parameter)
+
+Full guide with examples and the plan workflow: **[agents.md](agents.md)**.
+
+Three built-in roles, each bundling a role prompt, an **enforced file-write
+scope**, and optional per-agent model routing (`/agents` lists them):
+
+| Agent | Role | Write scope |
+|---|---|---|
+| `delegate` | Read-only exploration, research, targeted reading | none (file write tools refused) |
+| `doc-explorer` | Documentation + planning artifacts | `docs/`, `plans/` only |
+| `implementer` | Code changes via the gated BLUEPRINT → EXECUTE protocol | unrestricted |
+
+Write scopes are enforced in the tool layer (`agent-defs.ss` +
+`file.ss`/`apply-patch.ss`), not just by prompt — and nesting can only
+*narrow*: a scoped sub-agent spawning another task can never widen access.
+
+Variants live in `jcode.json` under `"agents"`; `extends` inherits a
+built-in's prompt/scope while pinning a different model:
+
+```json
+"agents": {
+  "delegate-strong":  {"extends": "delegate", "provider": "anthropic",
+                       "model": "claude-opus-4-8"},
+  "implementer-fast": {"extends": "implementer", "model": "qwen2.5-coder:14b"}
+}
+```
+
+### Resumable tasks & the gated implementer protocol
+
+Passing `task_id` makes a sub-agent conversation resumable (in-memory, capped
+at 32): a later `task` call with the same id continues it, the new `prompt`
+arriving as the next user message. This powers the two-step implementer gate:
+
+1. `task(agent: "implementer", task_id: "wp-1", prompt: "MODE: BLUEPRINT\n…")`
+   → returns a numbered step plan + one verify command, **no edits**.
+2. Caller (or user) reviews the blueprint.
+3. `task(task_id: "wp-1", prompt: "MODE: EXECUTE — approved\n…")` → implements
+   with the planning context still in the sub-agent's head, runs the verify
+   command, returns a digest.
+
+The plan-lifecycle skills (`/skills`: `create-plan`, `resume-plan`,
+`execute-work-package`, `review-plan`, `generate-handover`) drive this
+protocol against persistent `plans/<name>/` artifacts that survive sessions.
+
 ## The safety model
 
 Four layers gate what the agent can do. They apply to every call regardless of
diff --git a/src/jcode/core/agent-defs.ss b/src/jcode/core/agent-defs.ss
new file mode 100644
index 0000000..270c824
--- /dev/null
+++ b/src/jcode/core/agent-defs.ss
@@ -0,0 +1,285 @@
+;;; jcode named agent definitions
+;;;
+;;; A small taxonomy of sub-agent roles for the task tool, inspired by
+;;; opencode-processing-skills. Each definition bundles:
+;;;   * a role prompt   — prepended to the task prompt (skill-style)
+;;;   * a write scope   — which paths the sub-agent's file tools may write
+;;;   * model routing   — optional provider/model override for the run
+;;;
+;;; Built-ins:
+;;;   delegate     — read-only exploration/research (write scope: none)
+;;;   doc-explorer — writes ONLY under docs/ and plans/
+;;;   implementer  — full write access, gated BLUEPRINT -> EXECUTE protocol
+;;;
+;;; Users add variants in jcode.json under "agents". A variant `extends` a
+;;; built-in (inheriting prompt/scope) and typically pins a model:
+;;;
+;;;   "agents": {
+;;;     "delegate-strong":   {"extends": "delegate",
+;;;                           "provider": "anthropic", "model": "claude-opus-4-8"},
+;;;     "implementer-fast":  {"extends": "implementer", "model": "qwen2.5-coder:14b"},
+;;;     "reviewer":          {"description": "fresh-context plan reviewer",
+;;;                           "prompt": "You are a critical reviewer...",
+;;;                           "write_scope": ["plans/"]}
+;;;   }
+;;;
+;;; write_scope in config: "all" (unrestricted), "none" (read-only file
+;;; tools), or a list of path prefixes relative to the project root.
+;;;
+;;; Enforcement: the task tool parameterizes current-write-scope from the
+;;; resolved definition; file.ss consults write-scope-allows? on every
+;;; write-path tool. This is a guardrail for file tools — bash is still
+;;; governed by the permissions module, and read tools are never scoped.
+
+(export agent-def-lookup
+        agent-def-names
+        agent-def?
+        make-agent-def
+        agent-def-name
+        agent-def-description
+        agent-def-prompt
+        agent-def-provider
+        agent-def-model
+        agent-def-write-scope
+        parse-agent-def
+        parse-write-scope
+        builtin-agent-defs
+        current-write-scope
+        write-scope-allows?
+        write-scope-intersect
+        write-scope-error
+        write-scope-label)
+
+(import :std/misc/string
+        :std/os/path
+        ./config)
+
+(defstruct agent-def (name description prompt provider model write-scope))
+
+;; Write scope of the *current* tool execution context. #f = unrestricted
+;; (normal operation); 'none = file write tools refused; a list of path
+;; prefixes = writes allowed only under those prefixes. The task tool
+;; parameterizes this per sub-agent run.
+(def current-write-scope (make-parameter #f))
+
+;; ── Built-in role prompts ────────────────────────────────────────────
+
+(def *delegate-prompt*
+  (string-append
+"You are a focused research sub-agent (role: delegate). Your job is\n"
+"exploration, targeted reading, and investigation — NOT modification.\n"
+"\n"
+"Rules:\n"
+"- You are READ-ONLY: file write tools are disabled for you. Do not\n"
+"  attempt write/edit/patch; do not mutate state via bash either.\n"
+"- Find things with grep/glob, then read only what you need.\n"
+"- Use batch to parallelize independent reads.\n"
+"\n"
+"Output contract — your final answer is consumed by another agent:\n"
+"- Lead with the conclusion, then supporting evidence.\n"
+"- Cite concrete locations as path:line for every claim.\n"
+"- No exploration narration, no raw file dumps. Concise findings only.\n"))
+
+(def *doc-explorer-prompt*
+  (string-append
+"You are a documentation and planning sub-agent (role: doc-explorer).\n"
+"You read code anywhere, but you write ONLY under docs/ and plans/ —\n"
+"writes elsewhere are refused by the tool layer. Never modify code.\n"
+"\n"
+"Artifact conventions:\n"
+"- docs/overview.md — project map: purpose, module list (one line each),\n"
+"  key entry points. References detail docs instead of duplicating them.\n"
+"- docs/modules/<name>.md — per-module: responsibility, key files and\n"
+"  symbols (each with a one-line purpose), dependencies, gotchas.\n"
+"- plans/<name>/plan.md — goal, scope, phase list (what/why per phase).\n"
+"- plans/<name>/phases/phase-N.md — one phase: scope, acceptance criteria.\n"
+"- plans/<name>/implementation/phase-N-impl.md — concrete steps grounded\n"
+"  in the current code: files to touch, symbols, one verify command, and\n"
+"  a Reality Check section listing mismatches found while grounding.\n"
+"- plans/<name>/todo.md — checkbox list, kept in sync with progress.\n"
+"- plans/<name>/handovers/session-<date>.md — session narrative:\n"
+"  decisions + rationale, current state, blockers, concrete next steps.\n"
+"\n"
+"Working style:\n"
+"- Write early, flush often: materialize findings into the artifact as\n"
+"  you go rather than holding the whole codebase in context.\n"
+"- Inventories must explain: every listed file/symbol gets a purpose.\n"
+"- Update incrementally; preserve manual enrichments in existing docs.\n"
+"- End by listing the artifact paths you wrote.\n"))
+
+(def *implementer-prompt*
+  (string-append
+"You are an execution sub-agent (role: implementer). You make code\n"
+"changes following a gated two-step protocol. The FIRST LINE of your\n"
+"task prompt is either MODE: BLUEPRINT or MODE: EXECUTE.\n"
+"\n"
+"MODE: BLUEPRINT — plan only, NO edits:\n"
+"- Read the relevant code, then return a numbered step list: file to\n"
+"  touch per step, what changes, and ONE verify command that exercises\n"
+"  the changed behavior (not just a lint/typecheck).\n"
+"- Do not write/edit any file in this mode.\n"
+"\n"
+"MODE: EXECUTE — the prompt contains an approved blueprint:\n"
+"- Implement exactly the blueprint steps. If reality contradicts the\n"
+"  blueprint, stop and report the mismatch instead of improvising.\n"
+"- Run the verify command. If it fails, make minimal targeted fixes\n"
+"  only; if a larger change is needed, stop and report.\n"
+"- Return a compact digest: outcome (passed/failed/blocked), files\n"
+"  edited, verify command + result, 1-3 bullets of follow-ups.\n"
+"\n"
+"Coding standards (both modes):\n"
+"- Minimal changes; preserve existing patterns and naming.\n"
+"- Fix root causes, not symptoms. No silent failure paths.\n"
+"- No hardcoded values where the codebase has a config mechanism.\n"
+"- Never commit — git operations belong to the calling agent.\n"))
+
+(def (builtin-agent-defs)
+  (list
+    (make-agent-def "delegate"
+      "read-only exploration, research, targeted reading"
+      *delegate-prompt* #f #f 'none)
+    (make-agent-def "doc-explorer"
+      "documentation + planning artifacts; writes only docs/ and plans/"
+      *doc-explorer-prompt* #f #f '("docs/" "plans/"))
+    (make-agent-def "implementer"
+      "gated code execution: BLUEPRINT then EXECUTE with approval"
+      *implementer-prompt* #f #f #f)))
+
+;; ── Config merge ─────────────────────────────────────────────────────
+
+(def (parse-write-scope v)
+  "Normalize a config write_scope value: \"all\" -> #f (unrestricted),
+   \"none\" -> 'none, list of strings -> that list. Anything else -> #f."
+  (cond
+    ((equal? v "none") 'none)
+    ((equal? v "all") #f)
+    ((and (list? v) (pair? v) (string? (car v))) v)
+    (else #f)))
+
+(def (parse-agent-def name spec base)
+  "Build an agent-def NAME from config hash SPEC, inheriting unset fields
+   from BASE (an agent-def or #f). Exported for tests."
+  (let ((sget (lambda (k) (and (hash-table? spec)
+                          (let ((v (hash-get spec k)))
+                            (and (string? v) (not (string=? v "")) v))))))
+    (make-agent-def
+      name
+      (or (sget "description")
+          (and base (agent-def-description base))
+          "user-defined agent")
+      (or (sget "prompt")
+          (and base (agent-def-prompt base))
+          #f)
+      (or (sget "provider")
+          (and base (agent-def-provider base)))
+      (or (sget "model")
+          (and base (agent-def-model base)))
+      (if (and (hash-table? spec) (hash-get spec "write_scope"))
+        (parse-write-scope (hash-get spec "write_scope"))
+        (if base (agent-def-write-scope base) #f)))))
+
+(def (config-agent-defs)
+  "Agent definitions from the jcode.json \"agents\" block, resolving
+   `extends` against built-ins (and, second pass, other config entries)."
+  (let ((block (config-ref "agents")))
+    (if (not (hash-table? block))
+      '()
+      (let ((names (hash-keys block)))
+        (map
+          (lambda (name)
+            (let* ((spec (hash-get block name))
+                   (ext  (and (hash-table? spec) (hash-get spec "extends")))
+                   (base (and (string? ext) (builtin-lookup ext))))
+              (parse-agent-def name spec base)))
+          (list-sort string<? names))))))
+
+(def (builtin-lookup name)
+  (let loop ((defs (builtin-agent-defs)))
+    (cond
+      ((null? defs) #f)
+      ((string=? (agent-def-name (car defs)) name) (car defs))
+      (else (loop (cdr defs))))))
+
+(def (agent-def-lookup name)
+  "Resolve NAME to an agent-def: config entries override built-ins."
+  (and (string? name)
+       (or (let loop ((defs (config-agent-defs)))
+             (cond
+               ((null? defs) #f)
+               ((string=? (agent-def-name (car defs)) name) (car defs))
+               (else (loop (cdr defs)))))
+           (builtin-lookup name))))
+
+(def (agent-def-names)
+  "Sorted names of all available agent definitions (built-in + config)."
+  (let ((seen (make-hash-table)))
+    (for-each (lambda (d) (hash-put! seen (agent-def-name d) #t))
+              (builtin-agent-defs))
+    (for-each (lambda (d) (hash-put! seen (agent-def-name d) #t))
+              (config-agent-defs))
+    (list-sort string<? (hash-keys seen))))
+
+;; ── Write-scope enforcement ──────────────────────────────────────────
+
+(def (scope-relative-path p)
+  "Normalize P for prefix matching: strip ./ and, for absolute paths
+   under the current directory, strip the cwd prefix. Absolute paths
+   outside the cwd are returned as-is (and so never match a relative
+   scope prefix — correctly refused)."
+  (let* ((p (if (string-prefix? "./" p)
+              (substring p 2 (string-length p))
+              p))
+         (cwd (let ((d (current-directory)))
+                (if (string-suffix? "/" d) d (string-append d "/")))))
+    (if (string-prefix? cwd p)
+      (substring p (string-length cwd) (string-length p))
+      p)))
+
+(def (write-scope-allows? scope path)
+  "Does SCOPE permit writing PATH? #f scope = everything; 'none = nothing;
+   list = path must fall under one of the prefixes."
+  (cond
+    ((not scope) #t)
+    ((eq? scope 'none) #f)
+    ((not (string? path)) #f)
+    (else
+      (let ((rel (scope-relative-path path)))
+        (let loop ((prefixes scope))
+          (cond
+            ((null? prefixes) #f)
+            ((string-prefix? (car prefixes) rel) #t)
+            (else (loop (cdr prefixes)))))))))
+
+(def (write-scope-intersect parent child)
+  "Combine a PARENT scope with a CHILD scope so nesting can only narrow:
+   a scoped sub-agent spawning another task can never widen file access.
+   #f = unrestricted, 'none = no writes, list = allowed prefixes."
+  (cond
+    ((not parent) child)
+    ((not child) parent)
+    ((or (eq? parent 'none) (eq? child 'none)) 'none)
+    (else
+      ;; Keep child prefixes that fall inside some parent prefix.
+      (let ((kept (filter
+                    (lambda (c)
+                      (let loop ((ps parent))
+                        (cond
+                          ((null? ps) #f)
+                          ((string-prefix? (car ps) c) #t)
+                          (else (loop (cdr ps))))))
+                    child)))
+        (if (null? kept) 'none kept)))))
+
+(def (write-scope-label scope)
+  (cond
+    ((not scope) "all")
+    ((eq? scope 'none) "none (read-only)")
+    (else (string-join scope ", "))))
+
+(def (write-scope-error tool path)
+  "Error string if the current write scope refuses PATH, else #f."
+  (let ((scope (current-write-scope)))
+    (if (write-scope-allows? scope path)
+      #f
+      (format "Error: ~a refused — this sub-agent's write scope is [~a] and ~a is outside it. Report the needed change to the calling agent instead."
+              tool (write-scope-label scope) path))))
diff --git a/src/jcode/core/agent.ss b/src/jcode/core/agent.ss
index c235352..7af22fd 100644
--- a/src/jcode/core/agent.ss
+++ b/src/jcode/core/agent.ss
@@ -2,7 +2,9 @@
 
 (export agent-run
         agent-chat
+        agent-chat-messages
         agent-step
+        system-prompt
         current-stream-cb
         current-tool-cb
         current-usage-cb
@@ -1521,6 +1523,32 @@ Be concise. Prefer edit over write for modifying existing files.
                   (new-msgs  (append msgs (list response) results)))
              (agent-chat-loop-stream provider new-msgs tools (+ round 1)))))))))
 
+(def (agent-chat-messages messages)
+  "Run the non-streaming chat loop over an explicit MESSAGES list (system +
+   history already built by the caller). Returns (values final-content
+   final-messages) so callers — the resumable task tool — can persist the
+   transcript and continue the same sub-agent conversation later."
+  (let ((provider (get-current-provider))
+        (tools (get-tool-schemas)))
+    (agent-chat-loop-track provider messages tools 0)))
+
+(def (agent-chat-loop-track provider messages tools round)
+  (let ((response (chat-with-expert provider messages tools)))
+    (cond
+      ((not (message-tool-calls response))
+       (values (or (message-content response) "")
+               (append messages (list response))))
+      ((>= round *max-tool-rounds*)
+       (let* ((results (execute-tool-calls (message-tool-calls response)))
+              (new-messages (append messages (list response) results))
+              (final (chat-with-expert provider new-messages '())))
+         (values (or (message-content final) "")
+                 (append new-messages (list final)))))
+      (else
+       (let* ((results (execute-tool-calls (message-tool-calls response)))
+              (new-messages (append messages (list response) results)))
+         (agent-chat-loop-track provider new-messages tools (+ round 1)))))))
+
 (def (agent-step messages)
   (let* ((provider (get-current-provider))
          (tools (get-tool-schemas)))
diff --git a/src/jcode/core/builtin-skills.ss b/src/jcode/core/builtin-skills.ss
index 790003e..742007c 100644
--- a/src/jcode/core/builtin-skills.ss
+++ b/src/jcode/core/builtin-skills.ss
@@ -100,9 +100,133 @@
 "\n"
 "Report what was saved, suggested, voted for, and security patterns added when done.\n"))
 
+;; ── Plan-lifecycle skills ────────────────────────────────────────────
+;; Ported from opencode-processing-skills: a file-based plan workflow
+;; (plans/<name>/...) that survives sessions. The plan artifacts are the
+;; interface — every skill reads/writes the same structure.
+
+(def *create-plan-prompt*
+  (string-append
+"Create a persistent plan for the feature/task being discussed. Settle\n"
+"open requirement questions with the user BEFORE writing.\n"
+"\n"
+"Write these artifacts (use a short kebab-case <name> for the plan):\n"
+"\n"
+"1. plans/<name>/plan.md — goal, context, scope (in/out), and a phase\n"
+"   list. Each phase: what it delivers and why it's a separate phase.\n"
+"   Size phases to roughly one working session; order them so earlier\n"
+"   phases are foundations for later ones.\n"
+"2. plans/<name>/phases/phase-N.md — per phase: scope, deliverables,\n"
+"   acceptance criteria. WHAT and WHY only — the HOW lives in the\n"
+"   implementation plan, authored later, so the approach can change\n"
+"   without rewriting scope.\n"
+"3. plans/<name>/todo.md — checkbox list seeded with Phase 1 items.\n"
+"\n"
+"Rules:\n"
+"- Ground the plan in the real codebase: verify module/file claims with\n"
+"  grep/read (or delegate exploration via task agent=\"delegate\") before\n"
+"  writing them down.\n"
+"- Do NOT write implementation steps yet; that is a separate pass that\n"
+"  grounds each phase against the code (skill: implementation planning).\n"
+"- Prefer task agent=\"doc-explorer\" for writing the artifacts when the\n"
+"  plan is large; otherwise write them directly.\n"
+"- Finish by showing the user the plan summary and phase list.\n"))
+
+(def *resume-plan-prompt*
+  (string-append
+"Resume work on an existing plan. Read, in order:\n"
+"\n"
+"1. plans/*/plan.md — if several plans exist, ask which one.\n"
+"2. plans/<name>/todo.md — current progress.\n"
+"3. plans/<name>/handovers/ — the LATEST handover, if any.\n"
+"4. The active phase file and its implementation plan\n"
+"   (plans/<name>/implementation/phase-N-impl.md) if present.\n"
+"\n"
+"Then brief the user: where the plan stands, what was last done, any\n"
+"blockers recorded, and the next 1-3 concrete tasks. Do not start\n"
+"implementing until the user confirms the direction. If artifacts are\n"
+"missing (no impl plan for the active phase, stale todo), say so and\n"
+"offer to create them first.\n"))
+
+(def *generate-handover-prompt*
+  (string-append
+"Write a session handover so the next session (or another agent) can\n"
+"continue seamlessly. Target file:\n"
+"plans/<name>/handovers/session-<YYYY-MM-DD>.md (docs/handovers/ if no\n"
+"plan is active). Use today's date; add -2, -3 if the file exists.\n"
+"\n"
+"Sections:\n"
+"- Session Summary — what this session worked on, 2-4 sentences.\n"
+"- Progress — completed / in-progress / not-started, from todo.md and\n"
+"  the actual session work.\n"
+"- Key Decisions — table: decision | alternatives considered | rationale.\n"
+"- Current State — files modified (git_status/git_diff), tests passing\n"
+"  or failing, anything half-done.\n"
+"- Blockers & Issues — anything unresolved, with what was already tried.\n"
+"- Next Steps — concrete and actionable: name the file, symbol, and\n"
+"  starting point (\"implement X in src/foo.ss starting from stub Y\"),\n"
+"  not vague (\"continue with X\").\n"
+"\n"
+"Capture the session NARRATIVE (decisions, rationale, surprises), not a\n"
+"copy of the todo list. Also update plans/<name>/todo.md checkboxes to\n"
+"match reality before writing the handover.\n"))
+
+(def *review-plan-prompt*
+  (string-append
+"Review a plan or implementation artifact with fresh eyes. You did NOT\n"
+"author it — that distance is the point: catch gaps familiarity hides.\n"
+"\n"
+"Steps:\n"
+"1. Read the target artifact (plan.md / phase files / impl plan — or the\n"
+"   implementation diff via git_diff if reviewing executed work).\n"
+"2. Verify claims against the actual codebase: file paths exist, named\n"
+"   symbols exist, steps are executable as written, acceptance criteria\n"
+"   are testable. Delegate spot-checks via task agent=\"delegate\" when\n"
+"   that keeps your context lean.\n"
+"3. Write findings to plans/<name>/reviews/<artifact>-review.md, each\n"
+"   finding rated: Critical (plan will fail / wrong behavior),\n"
+"   Major (significant gap or risk), Minor (worth fixing), Note.\n"
+"\n"
+"For implementation reviews additionally assess test quality: do the\n"
+"tests exercise changed BEHAVIOR (not just mocks/lint), would they catch\n"
+"a regression?\n"
+"\n"
+"Findings are advisory — end with a one-paragraph verdict and let the\n"
+"caller decide. Do not modify the artifacts you review.\n"))
+
+(def *execute-work-package-prompt*
+  (string-append
+"Execute the active phase of a plan through the gated implementer\n"
+"protocol. Precondition: an implementation plan exists\n"
+"(plans/<name>/implementation/phase-N-impl.md). If missing, stop and\n"
+"offer to author it first.\n"
+"\n"
+"1. Read the impl plan and its Required Context files.\n"
+"2. BLUEPRINT call — task with agent=\"implementer\", a fresh task_id\n"
+"   (e.g. wp-phase-N), and a prompt starting 'MODE: BLUEPRINT' that\n"
+"   includes the impl-plan steps and context. The sub-agent returns a\n"
+"   numbered step list + one verify command. NO edits happen.\n"
+"3. GATE — show the blueprint to the user. Proceed only on approval;\n"
+"   on objections, revise via another BLUEPRINT call (same task_id).\n"
+"4. EXECUTE call — task with the SAME task_id, prompt starting\n"
+"   'MODE: EXECUTE — approved: <token>'. The sub-agent implements,\n"
+"   runs the verify command, and returns a digest (outcome, files\n"
+"   edited, verify result, follow-ups).\n"
+"5. Post-process: verify passed -> update plans/<name>/todo.md and\n"
+"   report; verify failed -> show the digest, decide with the user\n"
+"   whether to fix forward (new EXECUTE call) or roll back (/undo);\n"
+"   blocked -> surface the mismatch, update the impl plan, re-blueprint.\n"
+"\n"
+"Never git-commit from the sub-agent; commits belong to the user.\n"))
+
 (def *builtin-skills*
   (list
-    (cons "save-discoveries" *save-discoveries-prompt*)))
+    (cons "create-plan"          *create-plan-prompt*)
+    (cons "execute-work-package" *execute-work-package-prompt*)
+    (cons "generate-handover"    *generate-handover-prompt*)
+    (cons "resume-plan"          *resume-plan-prompt*)
+    (cons "review-plan"          *review-plan-prompt*)
+    (cons "save-discoveries"     *save-discoveries-prompt*)))
 
 (def (builtin-skill name)
   "Return the prompt text for the builtin skill NAME, or #f if not builtin."
diff --git a/src/jcode/tool/apply-patch.ss b/src/jcode/tool/apply-patch.ss
index 9df665a..159c8b8 100644
--- a/src/jcode/tool/apply-patch.ss
+++ b/src/jcode/tool/apply-patch.ss
@@ -27,6 +27,7 @@
         :std/misc/ports
         :std/os/path
         :jcode/core/log
+        :jcode/core/agent-defs
         :jcode/tool/registry)
 
 (def logger (make-logger "tool.apply-patch"))
@@ -173,11 +174,16 @@
 ;;; ----- Op application ------------------------------------------------
 
 (def (apply-op op)
-  (case (op-kind op)
-    ((add)    (apply-add    (op-path op) (op-body op)))
-    ((delete) (apply-delete (op-path op)))
-    ((update) (apply-update (op-path op) (op-body op)))
-    (else "unknown op kind")))
+  ;; Write-scope gate (sub-agent sandboxing) covers every op kind —
+  ;; delete is a mutation too.
+  (let ((scope-err (write-scope-error "apply_patch" (op-path op))))
+    (if scope-err
+      scope-err
+      (case (op-kind op)
+        ((add)    (apply-add    (op-path op) (op-body op)))
+        ((delete) (apply-delete (op-path op)))
+        ((update) (apply-update (op-path op) (op-body op)))
+        (else "unknown op kind")))))
 
 (def (apply-add path body)
   (cond
diff --git a/src/jcode/tool/file.ss b/src/jcode/tool/file.ss
index 81701a2..17d2961 100644
--- a/src/jcode/tool/file.ss
+++ b/src/jcode/tool/file.ss
@@ -8,6 +8,7 @@
         :std/misc/ports
         :std/misc/string
         :jcode/core/log
+        :jcode/core/agent-defs
         :jcode/tool/registry)
 
 (def logger (make-logger "tool.file"))
@@ -190,6 +191,7 @@
     (cond
       ((sensitive-path? path) (sensitive-path-error "write" path))
       ((generated-artifact-error "write" path) => (lambda (e) e))
+      ((write-scope-error "write" path) => (lambda (e) e))
       (else
         (let ((dir (path-directory path)))
           (when (and dir (not (equal? dir "")) (not (file-exists? dir)))
@@ -209,6 +211,7 @@
     (cond
       ((sensitive-path? path) (sensitive-path-error "edit" path))
       ((generated-artifact-error "edit" path) => (lambda (e) e))
+      ((write-scope-error "edit" path) => (lambda (e) e))
       ((not (file-exists? path))
        (format "Error: File not found: ~a" path))
       (else
@@ -249,6 +252,7 @@
     (cond
       ((sensitive-path? path) (sensitive-path-error "edit_block" path))
       ((generated-artifact-error "edit_block" path) => (lambda (e) e))
+      ((write-scope-error "edit_block" path) => (lambda (e) e))
       ((not (file-exists? path))
        (format "Error: File not found: ~a" path))
       (else
@@ -399,6 +403,7 @@
     (cond
       ((sensitive-path? path) (sensitive-path-error "multi-edit" path))
       ((generated-artifact-error "multi-edit" path) => (lambda (e) e))
+      ((write-scope-error "multi-edit" path) => (lambda (e) e))
       ((not (file-exists? path))
        (format "Error: File not found: ~a" path))
       (#t
@@ -463,6 +468,7 @@
            (result  (apply-unified-patch content patch-str)))
       (cond
         ((generated-artifact-error "patch" path) => (lambda (e) e))
+        ((write-scope-error "patch" path) => (lambda (e) e))
         ((string? result)
          (write-file-string path result)
          (format "Successfully applied patch to ~a" path))
diff --git a/src/jcode/tool/task.ss b/src/jcode/tool/task.ss
index 5344de5..6dcaeb8 100644
--- a/src/jcode/tool/task.ss
+++ b/src/jcode/tool/task.ss
@@ -7,67 +7,193 @@
 ;;; tool result. This is the lever you use to delegate noisy
 ;;; research/exploration without polluting the main context.
 ;;;
+;;; Named agents (core/agent-defs.ss): pass agent="delegate" /
+;;; "doc-explorer" / "implementer" (or a jcode.json variant) to give the
+;;; sub-agent a role prompt, an enforced file-write scope, and optional
+;;; per-agent provider/model routing.
+;;;
+;;; Resumable tasks: pass a task_id to persist the sub-agent's transcript
+;;; in memory; a later call with the same task_id continues that
+;;; conversation. This enables the gated implementer protocol — call 1
+;;; (MODE: BLUEPRINT) plans, call 2 (MODE: EXECUTE) implements with the
+;;; full planning context still in the sub-agent's head.
+;;;
 ;;; Args:
 ;;;   description : short label for logging
 ;;;   prompt      : the actual task to give the sub-agent
 ;;;   system      : optional system-prompt override (skill-style)
+;;;   agent       : optional named agent definition
+;;;   task_id     : optional stable id for a resumable sub-agent session
 ;;;
 ;;; Returns the sub-agent's final assistant content.
 
-(export init-task-tool)
+(export init-task-tool
+        task-session-store
+        task-session-put!
+        task-session-clear!)
 
 (import :std/misc/string
         :jerboa/core
         :jcode/core/log
         :jcode/core/agent
+        :jcode/core/agent-defs
+        :jcode/core/config
+        :jcode/core/message
         :jcode/core/skill
+        :jcode/core/builtin-skills
         :jcode/tool/registry)
 
 (def logger (make-logger "tool.task"))
 
+;; ── Resumable task sessions ──────────────────────────────────────────
+;; task_id -> (vector seq agent-name messages). In-memory only — task
+;; sessions live for the jcode process, which matches their purpose
+;; (blueprint -> execute within one parent turn or session).
+
+(def *task-sessions* (make-hash-table))
+(def *task-session-seq* 0)
+(def *task-session-cap* 32)
+
+(def (task-session-store) *task-sessions*)
+
+(def (task-session-clear!)
+  (set! *task-sessions* (make-hash-table))
+  (set! *task-session-seq* 0))
+
+(def (task-session-put! id agent-name messages)
+  (set! *task-session-seq* (+ *task-session-seq* 1))
+  (hash-put! *task-sessions* id (vector *task-session-seq* agent-name messages))
+  (evict-oldest-if-over-cap!))
+
+(def (evict-oldest-if-over-cap!)
+  (let ((ids (hash-keys *task-sessions*)))
+    (when (> (length ids) *task-session-cap*)
+      (let loop ((ids ids) (oldest #f) (oldest-seq #f))
+        (cond
+          ((null? ids)
+           (when oldest (hash-remove! *task-sessions* oldest)))
+          (else
+            (let ((seq (vector-ref (hash-get *task-sessions* (car ids)) 0)))
+              (if (or (not oldest-seq) (< seq oldest-seq))
+                (loop (cdr ids) (car ids) seq)
+                (loop (cdr ids) oldest oldest-seq)))))))))
+
+;; ── Tool registration ────────────────────────────────────────────────
+
 (def (init-task-tool)
   (register-tool! "task"
-    "Spawn a subordinate agent to perform a focused task with its own context. Use for research, codebase exploration, or any work that would otherwise bloat the main conversation. Returns the sub-agent's final answer as a string. The sub-agent has access to the same tools as the parent."
-    (make-schema '(("description" "string" "Short label describing the sub-task (5-15 words)" #t)
-                   ("prompt"      "string" "The full prompt for the sub-agent. Self-contained — sub-agent does not see parent context." #t)
-                   ("system"      "string" "Optional system-prompt override or name of a skill (e.g. 'review')" #f)))
+    (task-tool-description)
+    (make-schema
+      `(("description" "string" "Short label describing the sub-task (5-15 words)" #t)
+        ("prompt"      "string" "The full prompt for the sub-agent. Self-contained — sub-agent does not see parent context." #t)
+        ("system"      "string" "Optional system-prompt override or name of a skill (e.g. 'review')" #f)
+        ("agent"       "string" ,(format "Optional named agent role: ~a. Each has a role prompt and an enforced file-write scope; variants in jcode.json may pin a different model." (string-join (agent-def-names) ", ")) #f)
+        ("task_id"     "string" "Optional stable id making the sub-agent resumable: a later call with the same task_id continues the same conversation (prompt becomes the next user message). Required for the implementer BLUEPRINT->EXECUTE protocol." #f)))
     handle-task))
 
+(def (task-tool-description)
+  (string-append
+    "Spawn a subordinate agent to perform a focused task with its own context. "
+    "Use for research, codebase exploration, or any work that would otherwise bloat the main conversation. "
+    "Returns the sub-agent's final answer as a string. The sub-agent has access to the same tools as the parent. "
+    "Named agents (agent parameter): "
+    "delegate = read-only exploration/research; "
+    "doc-explorer = writes only under docs/ and plans/; "
+    "implementer = code changes via gated protocol — first call with task_id and a prompt starting 'MODE: BLUEPRINT' to get a step plan (no edits), review it, then call again with the SAME task_id and 'MODE: EXECUTE' plus your approval to implement and verify."))
+
 (def (handle-task args)
-  (let ((desc   (hash-ref args "description" "task"))
-        (prompt (hash-ref args "prompt" #f))
-        (system (let ((s (hash-ref args "system" #f)))
-                  (if (eq? s (void)) #f s))))
+  (let ((desc    (hash-ref args "description" "task"))
+        (prompt  (hash-ref args "prompt" #f))
+        (system  (opt-string args "system"))
+        (agent   (opt-string args "agent"))
+        (task-id (opt-string args "task_id")))
     (unless prompt (error 'task "Missing required parameter: prompt"))
-    (log-info logger "spawn" `((desc . ,desc)))
-    (let* ((wrapped (compose-prompt prompt system))
-           ;; Sever streaming so sub-agent output is captured silently.
-           (result
-             (parameterize ((current-stream-cb #f)
-                            (current-tool-cb   #f)
-                            (current-usage-cb  #f))
-               (try (agent-chat wrapped)
-                    (catch (e)
-                      (format "Sub-agent error: ~a" (err->string e)))))))
-      (cond
-        ((not result) "(sub-agent produced no content)")
-        ((string=? (string-trim result) "")
-         "(sub-agent produced no content)")
-        (else result)))))
-
-(def (compose-prompt prompt system)
-  ;; If `system` looks like a skill name (single word, no whitespace),
-  ;; try to resolve it; otherwise treat it as literal system text.
-  (cond
-    ((not system) prompt)
-    ((string=? system "") prompt)
-    ((and (not (string-contains system " "))
-          (not (string-contains system "\n"))
-          (skill-load system))
-     => (lambda (body)
-          (string-append body "\n\n---\n\n" prompt)))
-    (else
-     (string-append system "\n\n---\n\n" prompt))))
+    (let* ((existing (and task-id (hash-get *task-sessions* task-id)))
+           ;; On resume, the stored agent name wins so the conversation
+           ;; keeps its role/scope/model — a different `agent` arg on a
+           ;; resume call is ignored.
+           (agent-name (if existing (vector-ref existing 1) agent))
+           (adef (and agent-name (agent-def-lookup agent-name))))
+      (when (and agent-name (not adef))
+        (error 'task (format "Unknown agent '~a'. Available: ~a"
+                             agent-name (string-join (agent-def-names) ", "))))
+      (log-info logger "spawn"
+        `((desc . ,desc)
+          (agent . ,(or agent-name "-"))
+          (task-id . ,(or task-id "-"))
+          (resume . ,(and existing #t))))
+      (let ((result
+              (with-agent-routing adef
+                (lambda ()
+                  (if existing
+                    (run-resumed-task task-id existing prompt)
+                    (run-fresh-task task-id agent-name adef prompt system))))))
+        (cond
+          ((not result) "(sub-agent produced no content)")
+          ((string=? (string-trim result) "")
+           "(sub-agent produced no content)")
+          (else result))))))
+
+(def (opt-string args key)
+  (let ((v (hash-ref args key #f)))
+    (if (or (eq? v (void)) (not (string? v)) (string=? v "")) #f v)))
+
+(def (with-agent-routing adef thunk)
+  "Run THUNK with provider/model/write-scope routed per ADEF (silently
+   capturing output — no streaming callbacks). The write scope can only
+   narrow: a scoped sub-agent spawning another task cannot widen access."
+  (let* ((aprov  (and adef (agent-def-provider adef)))
+         (amodel (and adef (agent-def-model adef)))
+         (scope  (write-scope-intersect
+                   (current-write-scope)
+                   (and adef (agent-def-write-scope adef)))))
+    (parameterize ((current-stream-cb #f)
+                   (current-tool-cb   #f)
+                   (current-usage-cb  #f)
+                   (current-provider-override
+                     (or aprov (current-provider-override)))
+                   (current-model-override
+                     (or amodel