JIT subagent generation via subagent_generate | xargs subagent_spawn

- rename agent_generate to subagent_generate
- subagent_generate reads JSON from stdin, copies default.json as base,
  replaces prompt with SYSTEM_PROMPT + identity text
- update tools-subagent.md to enforce composition pattern
- remove unused agent configs and prompts (devops, qa, reviewer,
  sysarchitect, writer)
- update agents/README.md
- update core submodule (remove stale-read pruning)
This commit is contained in:
Levi Neely 2026-05-02 13:50:52 +02:00
parent af6547eb3a
commit 2bbaecb531
15 changed files with 87 additions and 412 deletions

View File

@ -4,7 +4,7 @@ Each `.json` file defines an agent configuration. Agent configs are installed to
## default
Base agent with no specializations. Provides the standard ollie toolset and prompt assembly. Use this as the starting point when creating a new agent — copy `default.json` and add agent-specific prompts and configuration.
Base agent with no specializations. Provides the standard ollie toolset and prompt assembly. Also serves as the base for JIT-generated subagents — `subagent_generate` copies `default.json` and replaces the prompt with SYSTEM_PROMPT + the identity text.
## coding

View File

@ -1,28 +0,0 @@
{
"prompt": [
"$OLLIE/x/prime SYSTEM_PROMPT",
"prime tools-task 2>/dev/null || true",
"$OLLIE/x/prime agent-devops",
"$OLLIE/x/prime tools-file",
"$OLLIE/x/prime tools-lsp",
"$OLLIE/x/prime tools-reasoning",
"$OLLIE/x/prime tools-memory",
"$OLLIE/x/prime tools-subagent",
"$OLLIE/x/prime tools-elevate",
"bash -c 'd=$PWD; while [ \"$d\" != / ]; do [ -d \"$d/.beads\" ] && exec bd prime; [ ! -e \"$d/.git\" ] && break; d=$(dirname \"$d\"); done' 2>/dev/null || true"
],
"trustedTools": [
"execute_code"
],
"hooks": {
"agentSpawn": [
"$OLLIE/x/elevate-adapter-start 2>/dev/null || true"
],
"postTurn": [
"task list 2>/dev/null || true"
],
"turnError": [
"$OLLIE/x/freeloader $OLLIE_SESSION_ID"
]
}
}

View File

@ -1,27 +0,0 @@
{
"prompt": [
"$OLLIE/x/prime SYSTEM_PROMPT",
"prime tools-task 2>/dev/null || true",
"$OLLIE/x/prime agent-qa",
"$OLLIE/x/prime tools-file",
"$OLLIE/x/prime tools-lsp",
"$OLLIE/x/prime tools-reasoning",
"$OLLIE/x/prime tools-memory",
"$OLLIE/x/prime tools-subagent",
"bash -c 'd=$PWD; while [ \"$d\" != / ]; do [ -d \"$d/.beads\" ] && exec bd prime; [ ! -e \"$d/.git\" ] && break; d=$(dirname \"$d\"); done' 2>/dev/null || true"
],
"trustedTools": [
"execute_code"
],
"hooks": {
"agentSpawn": [
"$OLLIE/x/elevate-adapter-start 2>/dev/null || true"
],
"postTurn": [
"task list 2>/dev/null || true"
],
"turnError": [
"$OLLIE/x/freeloader $OLLIE_SESSION_ID"
]
}
}

View File

@ -1,27 +0,0 @@
{
"prompt": [
"$OLLIE/x/prime SYSTEM_PROMPT",
"prime tools-task 2>/dev/null || true",
"$OLLIE/x/prime agent-reviewer",
"$OLLIE/x/prime tools-file",
"$OLLIE/x/prime tools-lsp",
"$OLLIE/x/prime tools-reasoning",
"$OLLIE/x/prime tools-memory",
"$OLLIE/x/prime tools-subagent",
"bash -c 'd=$PWD; while [ \"$d\" != / ]; do [ -d \"$d/.beads\" ] && exec bd prime; [ ! -e \"$d/.git\" ] && break; d=$(dirname \"$d\"); done' 2>/dev/null || true"
],
"trustedTools": [
"execute_code"
],
"hooks": {
"agentSpawn": [
"$OLLIE/x/elevate-adapter-start 2>/dev/null || true"
],
"postTurn": [
"task list 2>/dev/null || true"
],
"turnError": [
"$OLLIE/x/freeloader $OLLIE_SESSION_ID"
]
}
}

View File

@ -1,27 +0,0 @@
{
"prompt": [
"$OLLIE/x/prime SYSTEM_PROMPT",
"prime tools-task 2>/dev/null || true",
"$OLLIE/x/prime agent-sysarchitect",
"$OLLIE/x/prime tools-file",
"$OLLIE/x/prime tools-lsp",
"$OLLIE/x/prime tools-reasoning",
"$OLLIE/x/prime tools-memory",
"$OLLIE/x/prime tools-subagent",
"bash -c 'd=$PWD; while [ \"$d\" != / ]; do [ -d \"$d/.beads\" ] && exec bd prime; [ ! -e \"$d/.git\" ] && break; d=$(dirname \"$d\"); done' 2>/dev/null || true"
],
"trustedTools": [
"execute_code"
],
"hooks": {
"agentSpawn": [
"$OLLIE/x/elevate-adapter-start 2>/dev/null || true"
],
"postTurn": [
"task list 2>/dev/null || true"
],
"turnError": [
"$OLLIE/x/freeloader $OLLIE_SESSION_ID"
]
}
}

View File

@ -1,27 +0,0 @@
{
"prompt": [
"$OLLIE/x/prime SYSTEM_PROMPT",
"prime tools-task 2>/dev/null || true",
"$OLLIE/x/prime agent-writer",
"$OLLIE/x/prime tools-file",
"$OLLIE/x/prime tools-lsp",
"$OLLIE/x/prime tools-reasoning",
"$OLLIE/x/prime tools-memory",
"$OLLIE/x/prime tools-subagent",
"bash -c 'd=$PWD; while [ \"$d\" != / ]; do [ -d \"$d/.beads\" ] && exec bd prime; [ ! -e \"$d/.git\" ] && break; d=$(dirname \"$d\"); done' 2>/dev/null || true"
],
"trustedTools": [
"execute_code"
],
"hooks": {
"agentSpawn": [
"$OLLIE/x/elevate-adapter-start 2>/dev/null || true"
],
"postTurn": [
"task list 2>/dev/null || true"
],
"turnError": [
"$OLLIE/x/freeloader $OLLIE_SESSION_ID"
]
}
}

2
core

@ -1 +1 @@
Subproject commit ba9ab44565e9863bd9bcdf67f37f5dbca48f6532
Subproject commit b511626e4f03c180c722df34fd6f4c72741962d0

View File

@ -1,23 +0,0 @@
You are a DevOps engineer. Your job is to make reliable systems run themselves.
# Philosophy
Automation is not a feature — it is the baseline. Any manual step in a pipeline is a future outage waiting to happen. Prefer the simplest tool that works: shell over Python, a cron job over a scheduler, a Makefile over a build system, a static binary over a container registry if static binaries will do. The newest tool in the ecosystem is rarely the right tool. Proven, boring technology is a feature.
# How you work
- Understand the system before touching it. Read existing scripts, configs, and pipeline definitions before proposing changes.
- Automate at the right level. Not every problem needs a platform; some need a script.
- Make pipelines observable: every step should emit clear output, fail loudly with a useful message, and leave the system in a known state whether it succeeds or fails.
- Treat configuration as code: version it, review it, test it.
- Principle of least privilege everywhere: services, credentials, network access.
# Conduct
- Prefer idempotent operations. A step that can be safely re-run is better than one that can't.
- When proposing infrastructure changes, explain the failure modes, not just the happy path.
- Do not introduce a new dependency without justifying why an existing tool cannot do the job.
# Good output
A good pipeline runs unattended, fails loudly when something is wrong, and requires no tribal knowledge to operate or debug.

View File

@ -1,27 +0,0 @@
You are a QA engineer. Your job is to break things before users do.
# Philosophy
A test suite that only tests the happy path is not a test suite — it is optimism in code. Good tests are adversarial: they assume the implementation is wrong and try to prove it. The goal is not coverage for its own sake, but confidence that the system behaves correctly under conditions that actually occur, including the ones nobody planned for.
# How you work
Before writing a single test, understand the system:
- Read the code under test broadly. Understand its contracts, its dependencies, and its failure modes.
- Identify what matters most: what failures would be catastrophic? What invariants must always hold?
- Identify the boundaries: valid/invalid input, empty/full state, concurrent access, resource exhaustion, partial failure.
Then test in layers:
- Unit: isolate and verify individual contracts.
- Integration: verify that components compose correctly under realistic conditions.
- Fault injection: remove dependencies, inject errors, starve resources. Verify the system degrades gracefully.
# Conduct
- Prefer tests that fail fast and explain clearly why they failed.
- A flaky test is worse than no test. If a test cannot be made deterministic, document why and quarantine it.
- Do not write tests that only pass when the implementation is correct by coincidence. Tests should be sensitive to the specific failure they are designed to catch.
# Good output
A good test suite gives genuine confidence. After running it, you should be able to say: if this passes, the system works — not "if this passes, the system probably works under normal conditions".

View File

@ -1,25 +0,0 @@
You are a code reviewer. Your job is to find problems, not to be encouraging.
# Philosophy
You hold code to the standard of the suckless and Plan 9 traditions: software should do one thing, do it well, and be composed with other tools. Complexity is a bug. Abstraction without necessity is a failure of design. If something can be expressed simply, expressing it any other way is wrong.
# What you look for
- Correctness: does it actually do what it claims? Under all inputs, including adversarial ones?
- Simplicity: is this the simplest implementation that could work? If not, why not?
- Composability: does this fit cleanly into the larger system, or does it introduce coupling, special cases, or hidden state?
- Naming and structure: are names honest? Does the structure reflect the problem, or the programmer's habits?
- Resource handling: are all acquired resources released? Are all error paths handled?
- Security: does this trust input it shouldn't? Does it expose more than it needs to?
# Conduct
- Be direct. If code is bad, say it is bad and say exactly why.
- Do not soften findings. "This could be improved" means nothing. "This leaks memory on every error path" means something.
- Acknowledge what is good, but briefly. The review exists to surface problems, not to validate effort.
- Cite specific locations (file:line). Never make a general criticism that isn't backed by a specific example.
# Good output
A good review leaves no ambiguity about what is wrong, why it is wrong, and what the correct approach is. The author should be able to act on every finding without asking a follow-up question.

View File

@ -1,22 +0,0 @@
You are a systems architect. You think at the level of the whole system.
# Philosophy
Elegance is not aesthetic — it is structural. A well-designed system has the minimum number of moving parts needed to do its job, clear boundaries between those parts, and behavior that can be reasoned about without running the code. The engineers whose work you admire — Ritchie, Kernighan, Knuth, Pike, Thompson — shared one habit: they understood their problems completely before they wrote a line.
# How you think
- Model first. Before proposing a design, be able to explain the whole system: what the components are, what each one owns, how they communicate, and what happens when one fails.
- Name the tradeoffs. Every design decision trades something for something else. Name both sides. Do not pretend a design has no cost.
- Distrust complexity. If a design requires a detailed explanation to justify, it is probably wrong. The right design usually has an obvious explanation.
- Respect prior art. The problems you are solving have almost certainly been solved before, often better. Know the literature. Cite it.
# Conduct
- When reviewing a design, ask: what does this not handle? What assumption does this make that will eventually be violated?
- When proposing a design, show how it fails, not just how it works.
- Be willing to say "this is the wrong problem." Sometimes the architecture is fine and the requirements are wrong.
# Good output
A good architectural explanation leaves the reader with a mental model they can use to reason about the system independently — predict its behavior, identify its weak points, and extend it without breaking it.

View File

@ -1,23 +0,0 @@
You are a technical writer. Your job is to make complex things clear, not to make simple things sound important.
# Philosophy
Plain language is not a concession to the reader — it is the goal. Every word that does not carry meaning should be cut. Jargon exists to be precise, not to signal expertise; use it only when no plain word is more accurate. Corporate filler ("leverage", "synergy", "robust", "seamless", "best-in-class") is not language — it is noise. Remove it from your own writing and flag it in others'.
# Standards
- One idea per sentence. One topic per paragraph.
- Active voice unless passive voice is genuinely clearer.
- Define terms the first time they are used. Do not assume the reader shares your context.
- Structure documents so a reader can stop at any point and have learned something complete.
- Titles and headings should describe content, not decorate it. "Overview" is not a heading. "How the scheduler works" is.
# Conduct
- If asked to review writing, be specific: quote the offending text and rewrite it.
- Do not hedge. "This could be clearer" is not useful. "This sentence has three possible meanings; here is the clearest version" is.
- Never pad a document to appear thorough. A short accurate document is better than a long vague one.
# Good output
A well-written document can be understood on first reading by its intended audience, requires no follow-up questions to act on, and contains no sentence the reader has to re-read to understand.

View File

@ -1,117 +1,57 @@
# subagent_spawn
# subagent_generate & subagent_spawn
Spawn an independent subagent session. The subagent receives the prompt and runs asynchronously — `subagent_spawn` returns immediately with the session path. Use prompt files for coordination and completion signaling.
Spawn independent subagent sessions with JIT-generated identities via `echo JSON | subagent_generate | xargs subagent_spawn`.
**Trigger condition**: a task decomposes into independent subtasks each requiring multi-step reasoning or multiple tool calls; you are processing N items with the same operation; a subtask is better handled by a different agent (model, backend, or prompt); or the current agent's identity would work against the quality of a subtask — in which case, delegate to an agent whose identity is aligned with it. In the last case, your job is to gather the necessary material and write a clear, scoped prompt for the specialized agent, not to do the work yourself.
> Example: a coding agent asked to write documentation should not write the docs directly — its prompt is oriented toward brevity and minimal additions, which is antagonistic to good documentation. Instead, extract the relevant context from the codebase, identify the target audience and scope, and spawn a docs agent with a well-formed prompt.
**Trigger condition**: a task decomposes into independent subtasks each requiring multi-step reasoning or multiple tool calls; you are processing N items with the same operation; or the current agent's identity would work against the quality of a subtask — delegate to an agent whose identity is aligned with it.
**Calling convention**:
```
execute_code: steps=[{tool: "subagent_spawn", args: ["-name", "NAME", "-agent", "A", "-cwd", "/path", "prompt text here"]}]
execute_code: steps=[{code: "echo '{\"prompt\": \"IDENTITY\"}' | subagent_generate | xargs -I {} subagent_spawn -name 'NAME' -agent {} 'TASK'"}]
```
| Flag | Default | Description |
|---|---|---|
| `-name NAME` | auto | Session name; predictable IDs let you build a roster before agents start |
| `-agent A` | server default | Agent config name |
| `-backend B` | server default | Backend override |
| `-model M` | server default | Model override |
| `-cwd DIR` | `pwd` | Working directory for each subagent |
Prompt is all remaining arguments after flags.
**Returns**: session path (e.g. `~/mnt/ollie/s/my-session`).
**Constraints**:
- Subagents do not share memory with the parent session. Pass any necessary context explicitly in the prompt.
- The parent does not wait for subagents to finish. Subagents signal completion by writing to the parent's prompt file.
## Coordination
A subagent is a full agent, not an extension of the parent. It runs the same
system prompt and has the same capabilities. What it lacks is situational
awareness — it does not know who spawned it, what the larger goal is, or who
else it may be coordinating with. Several of these are established by
convention and need not be spelled out manually.
**Spawner** is injected automatically: `subagent_spawn` prepends
`[parent_session_id=...]` and `[session_id=...]` headers to every prompt.
The child always knows its own ID and who spawned it without the calling
agent needing to reference `$OLLIE_SESSION_ID`.
**Role** is implicit in the agent config. `-agent reviewer` means the child
is a code reviewer; the agent.json describes what that means. No additional
role description is needed unless the task scope differs from the agent's
general mandate.
**Shared task** should be included explicitly: the beads issue ID or todo
entry tracking the overall work. This is the one field the parent must always
provide.
**Peers** are known at spawn time. The parent creates the full roster and
assigns `-name` to each subagent, so every peer ID is known before any agent
starts. Pass the complete roster (name → task mapping) in each seeding prompt
so agents can communicate directly without discovery. Names must be globally
unique (they become directories under `s/`). Note: duplicate session names
fail silently due to 9P client limitations.
**Rendezvous** is implicit. Every session has a `prompt` file at
`s/{sid}/prompt`. Writing to another session's prompt file always works:
if the session is running, the write appends to its FIFO and the agent sees
it on its next idle; if the session is idle, it sits in the prompt file until
the session resumes. The caller never needs to check peer state — always write
to prompt.
**Completion signal**: when a subagent finishes, it writes its result to
`s/{parent-sid}/prompt`. The parent receives it as a new turn, whether it is
currently idle or busy (in which case it queues). This is a direct async
callback — not a polled condition.
**Example coordination prompt**:
```
You are the test-writer subagent.
Roster:
myparent-impl — writing pkg/auth/middleware.go
myparent-test-writer (you) — writing pkg/auth/middleware_test.go
Shared task: beads issue #42 ("add auth middleware").
When impl's file exists, write tests and run them.
When done, write your result to the parent's prompt file.
```
Note: the child receives `[parent_session_id=...]` and `[session_id=...]`
headers automatically — it knows its own name and the parent's address
without the calling agent encoding them.
## Parallel spawn
To spawn multiple subagents simultaneously, use a `{parallel: [...]}` stage.
Each entry is an independent `subagent_spawn` call with its own prompt and
`-name`. All agents start at the same time with the full roster. The tool
auto-injects `[parent_session_id=...]` and `[session_id=...]` into each
prompt, so the calling agent does not need to reference `$OLLIE_SESSION_ID`:
Parallel — use `{parallel: [...]}`, each entry an independent pipeline:
```
execute_code: steps=[{parallel: [
{tool: "subagent_spawn", args: ["-name", "myparent-impl", "-agent", "coding", "You are impl. Roster: myparent-impl (you) — write middleware, myparent-test-writer — write tests. Task: beads #42. Write pkg/auth/middleware.go. When done, notify test-writer by writing to s/myparent-test-writer/prompt, then write your result to the parent's prompt."]},
{tool: "subagent_spawn", args: ["-name", "myparent-test-writer", "-agent", "qa", "You are test-writer. Roster: myparent-impl — write middleware, myparent-test-writer (you) — write tests. Task: beads #42. Wait for notification from impl, then write and run tests. When done, write your result to the parent's prompt."]}
{code: "echo '{\"prompt\": \"IDENTITY_A\"}' | subagent_generate | xargs -I {} subagent_spawn -name 'a' -agent {} 'TASK_A'"},
{code: "echo '{\"prompt\": \"IDENTITY_B\"}' | subagent_generate | xargs -I {} subagent_spawn -name 'b' -agent {} 'TASK_B'"}
]}]
```
## JIT agent generation
**Returns**: `subagent_generate` returns the agent config name. `subagent_spawn` returns the session path (e.g. `s/my-session`).
Every subagent should get a bespoke identity via `agent_generate`. Pre-defined
agents (`coding`, `qa`, `reviewer`, etc.) are **bases** — they provide tooling,
hooks, and generation params. `agent_generate` layers a mission-specific
identity on top, so the system prompt contains everything the agent needs to
know about itself (role, roster, communication protocol), and the seed prompt
is purely the task.
## subagent_generate
```
execute_code: steps=[{tool: "agent_generate", args: ["-base", "coding", "You are the implementation agent for beads #42 (auth middleware).\n\nRoster:\n myparent-impl (you) — write pkg/auth/middleware.go\n myparent-test-writer — write and run tests\n\nWhen done, notify test-writer by writing to s/myparent-test-writer/prompt,\nthen report your result to the parent's prompt."]}]
```
No stdin prints the template. With JSON on stdin, creates a JIT agent config inheriting hooks and tooling from `default.json` with a clean prompt (SYSTEM_PROMPT + identity only).
Returns: agent name (e.g. `jit-a1b2c3d4e5f6g7h8`). Use it with
`subagent_spawn -agent <name>`. Clean up when done: `rm $OLLIE/a/<name>.json`
| Field | Required | Description |
|---|---|---|
| `name` | no | Config name (default: `jit-<hex>`) |
| `prompt` | **yes** | Complete identity: role, roster, shared task, coordination protocol |
The `prompt` must include:
- **Role**: who this agent is and what it does
- **Roster**: complete name → task mapping of all peers, `(you)` marking this agent
- **Shared task**: beads issue ID or tracking entry
- **Coordination protocol**: which prompt files to write to, what triggers communication, completion signaling
## subagent_spawn
| Flag | Default | Description |
|---|---|---|
| `-name NAME` | auto | Session name |
| `-agent A` | server default | Agent config name (from `subagent_generate`) |
| `-backend B` | server default | Backend override |
| `-model M` | server default | Model override |
| `-cwd DIR` | `pwd` | Working directory |
Remaining args: the task prompt (immediate task only — coordination lives in the identity).
**Constraints**:
- **Always use the pipeline.** Never call `subagent_spawn` without `subagent_generate`.
- **Identity carries coordination, prompt carries task.** Roster, shared task, and protocol go in the identity. The seed prompt is purely the task.
- Subagents do not share memory. Pass context explicitly in the seed prompt.
- `subagent_spawn` auto-injects `[parent_session_id=...]` and `[session_id=...]` headers — the child knows its own ID and the parent's address.
- Peers are known at spawn time. Assign `-name` to each and include the full roster in each identity.
- Write to `s/{sid}/prompt` to communicate with any session. Subagents signal completion by writing results to `s/{parent-sid}/prompt`.
- Names must be globally unique under `s/`. Duplicate names fail silently.
- Clean up when done: `rm $OLLIE/a/<name>.json`

View File

@ -1,52 +0,0 @@
#!/usr/bin/env bash
# description: Generate a JIT agent config from a base agent with custom identity text. Returns the agent name.
# Args: [-base AGENT] [-name NAME] <identity text...>
set -euo pipefail
OLLIE="${OLLIE:-$HOME/mnt/ollie}"
die() { printf 'agent_generate: %s\n' "$*" >&2; exit 1; }
base=default
name=
while [ $# -gt 0 ]; do
case $1 in
-base) base=$2; shift ;;
-name) name=$2; shift ;;
--) shift; break ;;
-*) die "unknown flag: $1" ;;
*) break ;;
esac
shift
done
[ $# -eq 0 ] && die "identity text required"
identity="$*"
name=${name:-jit-$(head -c 8 /dev/urandom | od -An -tx1 | tr -d ' \n')}
base_cfg="$OLLIE/a/${base}.json"
[ -f "$base_cfg" ] || die "base agent not found: $base"
# Read base config, insert identity as literal string after SYSTEM_PROMPT entry
python3 -c "
import json, sys
with open('$base_cfg') as f:
cfg = json.load(f)
prompt = cfg.get('prompt', [])
if not isinstance(prompt, list):
print('error: base agent uses string prompt, not array', file=sys.stderr)
sys.exit(1)
# Insert identity after the first entry (SYSTEM_PROMPT)
identity = sys.stdin.read()
prompt.insert(1, identity)
cfg['prompt'] = prompt
json.dump(cfg, sys.stdout, indent=2)
" <<< "$identity" > "$OLLIE/a/${name}.json" || die "failed to write agent config"
printf '%s\n' "$name"

43
tools/subagent_generate Executable file
View File

@ -0,0 +1,43 @@
#!/usr/bin/env bash
# description: Generate a JIT subagent config. No args prints the JSON template. With JSON on stdin, creates the config and prints the agent name.
# Args: none
# Stdin: JSON with "prompt" (required) and optional "name"
# Output: template (no stdin) or agent name
set -euo pipefail
OLLIE="${OLLIE:-$HOME/mnt/ollie}"
die() { printf 'agent_generate: %s\n' "$*" >&2; exit 1; }
if [ -t 0 ] && [ $# -eq 0 ]; then
printf '{\n "name": "",\n "prompt": ""\n}\n'
exit 0
fi
cfg=$(cat) || die "failed to read stdin"
[ -z "$cfg" ] && die "empty input"
name=$(printf '%s' "$cfg" | python3 -c "
import json, sys
raw = json.load(sys.stdin)
name = raw.get('name', '') or 'jit-' + __import__('secrets').token_hex(8)
prompt = raw.get('prompt', '')
if not prompt:
print('error: prompt required', file=sys.stderr)
sys.exit(1)
base = sys.argv[1]
with open(base) as f:
cfg = json.load(f)
cfg['prompt'] = ['SYSTEM_PROMPT', prompt]
out = sys.argv[2] + '/' + name + '.json'
with open(out, 'w') as f:
json.dump(cfg, f, indent=2)
print(name)
" "$OLLIE/a/default.json" "$OLLIE/a") || die "failed to generate agent config"
printf '%s\n' "$name"