Skip to content
Merged
107 changes: 106 additions & 1 deletion docs/agents-and-skills.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ Agents are markdown files with YAML frontmatter:
- project: `.lecode/agents/*.md` (nearest from the cwd up to the git root)

The project layer wins on name collisions, and user files may override the
built-ins (`build`, `plan`, `explore`) by name. `/agents` lists them; Tab
built-ins (`build`, `plan`, `explore`, `general`) by name. `/agents` lists them; Tab
cycles the primary agents in the TUI.

```markdown
Expand Down Expand Up @@ -42,6 +42,111 @@ You are a careful code reviewer. …
`denied_tools` always deny, and the overlay's rules/mode can never grant
more than the global config.

## Persistent workers: review, validate, integrate

`build` is the primary coding agent and `plan` is the read-only primary.
`explore` is a read-only subagent; `general` is a general-purpose coding subagent
that can write, subject to all inherited permissions. It does not grant access
denied by the parent, including a read-only parent. Global/project agent files
still override built-ins by name, and hidden/primary-only agents are not eligible
for delegation. The system prompt lists eligible subagents at runtime creation.

Create a coding worker with
`task(agent='general', prompt='Implement and verify ...', run_in_background=True)`.
The result returns the actual `worker_id`. Then use
`workers(action='send', id=<returned worker_id>, text='Follow-up ...')` to send
feedback, or `workers(action='list')` to discover existing IDs. `workers` controls
existing workers only: sending to an invented ID or an agent name does not spawn
one. Omit `run_in_background` to wait for the task's answer.

Writable workers require a Git repository with a committed HEAD and an attached
branch. If the session started outside such a repository, restart it from the
repository: a shell `cd` does not change the session's runtime cwd.

The `task` tool starts persistent workers. Read-only workers share their parent's
cwd. Write workers get an isolated branch and checkout based on their immediate
parent's committed HEAD. Their sidecar pins the parent checkout, branch, and
base commit, including for nested workers. Dirty parent changes require human
confirmation because they are not copied into the child.

The supervising model can carry out the following workflow without asking for
routine review/integration approval, subject to the normal tool permissions:

1. Let the worker finish, or stop it and wait for it and its descendants to become
idle. The worker may checkpoint changes with `bash` on **its own branch**.
2. Call `workers` with `{"action":"inspect","id":"WORKER_ID"}` (`review` is
an alias). This returns the assignment, pinned parent/base, current parent and
worker HEADs, and the actual committed diff. Review that exact diff against
the assignment. A worker's completion message alone is not a review.
3. If changes are needed, use `send` to request them and review again. Inspection
of dirty workers also returns their tracked uncommitted diff and untracked
paths. Commit/checkpoint in the worker, resolve any merge, and inspect again
before integration. These controls never commit the user's root checkout.
4. After accepting the diff, the **immediate supervisor** explicitly calls
`{"action":"integrate","id":"WORKER_ID","reviewed_head":"WORKER_HASH","reviewed_parent_head":"PARENT_HASH"}`.
Both must be the exact full hashes returned by the accepted review, not fresh
lookups at integration time.
General controls can address descendants, but a grandparent model cannot
integrate a grandchild directly. Integrate the grandchild into its parent,
then review and integrate that parent separately.
5. Configured validation commands run in the child's cwd through the registered
`bash` permission gate. Fresh worker-bound permission and hook contexts remain
constrained by the supervisor. A denial, hook rewrite, execution error,
missing process exit result, timeout, or failed check blocks integration.
The integration primitive rechecks both checkouts before fast-forwarding the
pinned parent. Any changed worker or parent HEAD, including a parent rewind,
demands another review; hashes are never automatically refreshed. It never
blindly merges after a validation failure.
6. Once integrated, call `{"action":"cleanup","id":"WORKER_ID"}` if the
workspace is no longer needed. Cleanup refuses uncommitted or unmerged work.
Clean up nested child workspaces before their parents. Worker records,
transcripts, and sidecars remain available; there is no automatic discard
or push.

Configure project validation in TOML (see [configuration](configuration.md)):

```toml
[worktree]
validation = ["uv run ruff check", "uv run ruff format --check", "uv run python -m pytest"]
```

Only this configured command list is accepted. Models cannot supply replacement
checks or an `allow_unvalidated` flag. An empty list requires an explicit human
confirmation even with tool auto-approval enabled. Without a human callback,
unvalidated integration is unavailable. Confirmations identify the worker,
agent, and cwd.

### Human controls

`/agent` lists workers; use either a worker ID or its roster number:

| Command | Effect |
| --- | --- |
| `/agent ID` | Open the retained transcript/detail view |
| `/agent ID inspect` | Show assignment, pinned destination, both HEADs, and diff |
| `/agent ID integrate WORKER_HASH PARENT_HASH` | Validate and integrate using both full reviewed hashes |
| `/agent ID cleanup` | Remove a clean, integrated workspace; retain its transcript |
| `/agent ID recover` | Ask before recreating a missing checkout from committed history |
| `/agent ID send TEXT` | Queue feedback or a follow-up |
| `/agent ID stop [tree]` | Stop the worker, optionally its descendants |
| `/agent ID resume [TEXT]` | Resume a retained worker |
| `/agent ID submit` | Submit a human-started worker's result to its parent |
| `/agent ID focus` | Focus the composer on that worker |

Mutating slash controls use the same `workers` dispatch permissions as model
calls. Nested integration first passes the current root's permission gate, then
runs in its immediate supervisor's context with the complete ancestor policy.
Validation also respects current root restrictions after a root agent switch;
neither human controls nor child grants widen ancestor Deny or Ask decisions.
Cleanup has no force/discard argument. Recovery requires human confirmation and
warns that old uncommitted data is unrecoverable.

Before a retained write worker follows up or resumes, its workspace guard
reconciles committed parent progress into a clean checkout. Dirty work is
preserved; conflicts remain in the worker for resolution. Missing checkouts
fail until a human confirms recovery. Workspace maintenance reserves the idle
worker against concurrent follow-ups, resumes, and nested worker creation.

## Skills

Skills are `SKILL.md` packs:
Expand Down
57 changes: 57 additions & 0 deletions docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,63 @@ default context window and zeroed pricing.
| `enabled` | `{}` | per-tool on/off, e.g. `enabled = { bash = false }` |
| `allowlist` | `[]` | when non-empty, only these tools are registered |

## `[worktree]`

| field | default | meaning |
|---|---|---|
| `validation` | `[]` | ordered list of validation commands run in the worker checkout before integration |

```toml
[worktree]
validation = ["uv run ruff check", "uv run python -m pytest"]
```

Lists replace inherited lists, so a project can explicitly set `validation = []`.
**Empty means an explicit human gate**, not automatic approval or successful
validation. Integrating without checks requires a human decision for that operation,
passed as `allow_unvalidated=True`. Tool auto-approval never supplies that decision.
Blank commands are rejected. Configuring checks does not grant permission to run them.

### Worker lifecycle integration API

These `WorktreeManager` primitives are for the worker/tool caller to wire at an
idle boundary, after stopping or joining the worker:

- `await reconcile(name, *, recreate=False) -> WorktreeInspection` verifies the
checkout against its sidecar and its pinned immediate parent. It merges the
parent's latest committed HEAD into a clean worker for follow-up work. Dirty
or in-progress work is returned unchanged. A conflicting merge raises
`WorktreeError` and remains visible in the worker. It never resets or rebases.
Missing checkouts raise a clean error unless `recreate=True` was explicitly
requested. Recreation uses the surviving worker branch, or the recorded base
commit if the branch is gone, then merges current parent progress.
**Missing uncommitted content cannot be recovered.**
- `await integrate(name, *, reviewed_head, validation, validation_runner,
allow_unvalidated=False) -> MergeResult` requires the parent model to review
the actual diff against the pinned destination and approve the exact full
commit hash. The mechanical gate cannot judge whether that review was honest.
`ValidationRunner` is `Callable[[str, Path], Awaitable[ProcResult]]`, using
`lecode.extras.proc.ProcResult`. The caller must dispatch every command through
`ToolRegistry` and map denials/failures to nonzero results. There is no built-in
shell runner. If merging destination progress changes the candidate HEAD,
integration stops with `WorktreeError` requiring a fresh diff review and hash.
Checks validate that exact candidate. Changed HEADs, dirty checkouts, in-progress
Git operations, and replaced or switched destinations prevent integration.
Only a fast-forward of the pinned parent is performed. No push occurs.
- `await cleanup_worker(name, *, discard=False) -> WorktreeInfo` proves that the
clean worker's **current** HEAD is an ancestor of its current pinned destination,
then removes the checkout and uses safe branch deletion (`-d`). Extra commits
after integration prevent cleanup. Only explicit human `discard=True` permits
deleting dirty or unmerged work and force-deleting its branch. Checkout identity
checks still apply. Sidecars and session data are retained.

These operations serialize by canonical common Git directory and destination
branch using a cancellable, nonblocking `flock`. Lock sidecars are never unlinked.
The caller must also keep workers idle and prevent concurrent tool writes to the
worker or parent during the operation. Destination path, repository identity and
branch are pinned; the helper never silently switches branches. A nested worker
integrates into its immediate parent, not directly into the repository's main branch.

## `[ui]`

| field | default | meaning |
Expand Down
15 changes: 15 additions & 0 deletions src/lecode/agent/builder.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ def build_runtime(
agent_registry: AgentRegistry | None = None,
skill_registry: SkillRegistry | None = None,
catalog: Catalog | None = None,
worker_manager: object | None = None,
) -> Runtime:
"""Build the permission checker, tool context, registry, and system prompt.

Expand Down Expand Up @@ -93,6 +94,15 @@ def build_runtime(
from lecode.agent.tools import task as task_tool

tools.append(task_tool.make_tool())
if session is not None and store is not None:
# Local: workers imports this module to build child runtimes.
from lecode.agent.tools import workers as workers_tool
from lecode.extras.workers import WORKER_EXTRA, WorkerManager

ctx.extras[WORKER_EXTRA] = worker_manager or WorkerManager(
config, cwd=Path(cwd), root_ctx=ctx, session=session, store=store
)
tools.append(workers_tool.make_tool())
if config.memory.enabled:
memory_store = MemoryStore(memory_root(cwd), max_bytes=config.memory.max_bytes)
ctx.extras["memory"] = memory_store
Expand Down Expand Up @@ -125,6 +135,11 @@ def build_runtime(
extra_parts: list[str] = []
if agent is not None and agent.body:
extra_parts.append(agent.body)
if "task" in registry.names():
extra_parts.append(
"Available subagents for task(agent=..., prompt=...):\n"
+ "\n".join(f"- {a.name}: {a.description}" for a in agents.subagents())
)
listing = skills.render_listing()
if listing:
extra_parts.append(listing)
Expand Down
Loading
Loading