Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,11 @@

### Added

- Added the `task-list` prepare hook for replacing assignment tasks from
repo-local `.task-runner/tasks.yml` files after host-side prepare steps;
replacement tasks are frozen into the manifest/reset seed and are not
re-read on resume or reset.
([#134](https://github.com/kcosr/task-runner/pull/134))
- Added run-group-aware runtime interpolation for fresh cwd values and
launcher command/args, plus `TASK_RUNNER_RUN_ID`,
`TASK_RUNNER_RUN_GROUP_ID`, and `TASK_RUNNER_CWD` backend wrapper env.
Expand Down
62 changes: 53 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -241,27 +241,71 @@ Launchers apply only to subprocess-backed execution (`claude`, `cursor`,
the built-in `direct` launcher.

Launcher command and args are runtime-interpolated before they are frozen
into the manifest. A short-term persistent-container workflow can combine
a run-group workspace path with a launcher wrapper:
into the manifest. Container-backed workflows should keep the host
workspace cwd and container cwd explicit: prepare hooks run from the host
cwd, while the launcher receives the container cwd it should enter.
Task-runner does not manage container lifecycle, startup, shutdown, or
cleanup.

```yaml
# assignment.md
cwd: "/home/kevin/agent-workspaces/{{run_group_id}}/repo"
cwd: "{{host_workspace_root}}/{{run_group_id}}/repo" # host cwd
vars:
repo_url:
type: string
required: true
sources: [cli, web]
branch:
type: string
required: true
sources: [cli, web]
host_workspace_root:
type: string
default: /srv/agent-workspaces
sources: [cli, web]
container_workspace_root:
type: string
default: /workspace/agent-workspaces
sources: [cli, web]
image:
type: string
default: agent-dev
sources: [cli, web]
hooks:
prepare:
- builtin: command
with:
mode: status
command: bash
cwd: /
args:
- -lc
- |
set -euo pipefail
target="{{cwd}}"
mkdir -p "$(dirname "$target")"
if [ -d "$target/.git" ]; then
git -C "$target" fetch origin "{{branch}}" --prune
git -C "$target" checkout "{{branch}}"
git -C "$target" reset --hard "origin/{{branch}}"
else
git clone --branch "{{branch}}" "{{repo_url}}" "$target"
fi
```

```yaml
# agent.md
launcher:
command: aw-tr-launch
args:
- agent-dev
- "{{cwd}}"
- "{{run_group_id}}"
- "{{image}}"
- "{{container_workspace_root}}/{{run_group_id}}/repo"
```

Task-runner does not manage the container lifecycle or cleanup. The
wrapper receives the frozen cwd and run group id so it can enter a
workspace that another process prepared.
In this pattern the clone/update happens on the host at `{{cwd}}`. The
launcher wrapper receives the matching container cwd so it can enter an
already-running container that sees the same repository at its own mount
path.

Agents may also author backend-owned argv tokens:

Expand Down
72 changes: 69 additions & 3 deletions docs/agents-and-assignments.md
Original file line number Diff line number Diff line change
Expand Up @@ -312,6 +312,65 @@ Assignments may also mix reusable task refs with inline task objects:
as explicit task file paths
- inline objects stay local to the assignment

Repo-local task lists can also be loaded at prepare time with the
first-party `task-list` hook. This is for workflows where an earlier
prepare hook clones or updates a repository on the host and the actual
task list lives inside that repository:

```yaml
hooks:
prepare:
- builtin: task-list
with:
path: "{{cwd}}/.task-runner/tasks.yml"
mode: replace
missing: continue
empty: keep-existing
```

The hook config is strict. The only supported keys are `path`, `mode`,
`missing`, and `empty`; the only supported values are
`mode: replace`, `missing: continue`, and `empty: keep-existing`.
`path` is runtime-interpolated like other hook config strings. A missing
repo-local task-list file continues with the assignment-authored tasks,
and an existing empty task list also keeps those tasks. An existing file
with invalid YAML, unsupported schema keys, missing task refs, duplicate
ids, invalid task shapes, or invalid task-local hooks fails prepare.

The repo-local task-list file is YAML:

```yaml
schemaVersion: 1
tasks:
- orient
- ./tasks/review.md
- id: local-check
title: Check local behavior
body: |
Inspect the repository-specific state.
hooks:
- path: ./hooks/local-guard.mjs
```

Task-list entries use the same task entry shape as assignment `tasks`:
inline task objects, named task refs, and explicit relative or absolute
task file paths. Relative file refs resolve from the task-list file's
directory; named refs still resolve from `${TASK_RUNNER_CONFIG_DIR}/tasks`.
Task-local path hooks inside loaded tasks are made absolute relative to
the task-list file before they are frozen into the run.

Security: a repo-local `tasks.yml` may declare task-local `hooks[]`
entries with `path: ./...` references. Those hook modules are loaded
and executed by task-runner on task transitions. Treat any repository
used with `task-list` as trusted code, or avoid task-local path hooks in
repo-local task lists.

If the hook replaces tasks, the replacement list becomes the frozen
manifest task set. Resume, reset, ready-start, initialized reconfigure,
and recurring reset/clone do not re-read the repo-local task-list file.
Changing or deleting `.task-runner/tasks.yml` after init affects only a
new fresh run or reinit, not the existing run.

Named task definitions are markdown files under
`${TASK_RUNNER_CONFIG_DIR}/tasks/<task-id>.md`:

Expand Down Expand Up @@ -491,10 +550,12 @@ Hook mutation boundaries:

- `prepare` may mutate run config (`cwd`, backend/model/effort,
timeout/unrestricted, prompts, locked fields), runtime vars, hook
state, note/pin metadata, task patches, and attachments. Backend args
are resolved from the final selected backend after prepare changes.
state, note/pin metadata, task patches, full task replacement through
`setTasks`, and attachments. Backend args are resolved from the final
selected backend after prepare changes.
- non-prepare phases may mutate run config, hook state, note/pin
metadata, task patches, and attachments, but not runtime vars.
metadata, task patches, and attachments, but not runtime vars or
`setTasks`.
- task-transition hooks run transactionally around `task set`,
`task append-notes`, `task add`, and the run loop's own task writes.
If a task-transition hook rejects, the requested task edit rolls back,
Expand All @@ -506,6 +567,11 @@ Built-in hooks:
- `git-worktree` runs in `prepare` and `beforeAttempt`. It ensures a git
worktree, switches the run `cwd` to that path, and in `prepare` also
projects `worktree_path` into runtime vars.
- `task-list` runs in `prepare`. It reads a repo-local YAML task list and
replaces the run task set when the file exists and is non-empty. Missing
files continue, empty lists keep the assignment-authored tasks, invalid
existing files fail prepare, and the resulting task list is frozen for
resume/reset.
- `command` runs in every phase. `mode: status` treats exit code `0` as
success and a non-zero exit code as block/reject. `mode: json`
requires exit code `0` and parses a full hook result from stdout;
Expand Down
40 changes: 33 additions & 7 deletions docs/design.md
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,15 @@ plain task shape before run creation; runtime still performs later
`{{var}}` interpolation against the resolved tasks during brief
construction.

Assignments can also replace the authored task list during `prepare`
through a hook `setTasks` mutation. The first-party `task-list` hook uses
that mutation to load a repo-local YAML file, typically
`.task-runner/tasks.yml`, after earlier prepare hooks have cloned or
updated the host cwd. The repo-local file uses `schemaVersion: 1` and a
strict `tasks:` array with the same inline/ref task entries as
assignment `tasks`. Missing repo-local files continue with the authored
assignment tasks; invalid existing files fail prepare.

Canonical definition identity comes from the on-disk key:

- agents: slash-relative directory under `agents/`
Expand Down Expand Up @@ -240,17 +249,21 @@ Fresh `run` / `init`:
3. run `prepare` hooks before the first manifest write
4. freeze the resolved descriptors plus any prepare-time mutations into
`manifest.resolvedHooks`, `manifest.runtimeVars`, `manifest.cwd`,
`manifest.hookState`, prompt state, attachments, and reset seed
`manifest.hookState`, prompt state, attachments, final task list, and
reset seed

Resume and reset do not re-run prepare hooks from current source files.
They reuse the frozen manifest descriptor/config and the prepare outputs
captured at first write.
Resume, reset, ready-start, initialized reconfigure, and recurring
reset/clone do not re-run prepare hooks from current source files. They
reuse the frozen manifest descriptor/config and the prepare outputs
captured at first write, including replacement task lists.

Phase behavior:

- `prepare` may mutate runtime vars and all other hook-owned run state.
- `prepare` may mutate runtime vars, replace the full task list with
`setTasks`, and mutate all other hook-owned run state.
- `beforeAttempt`, `afterAttempt`, and `afterExit` may continue, block,
or request a follow-up prompt reinvocation.
or request a follow-up prompt reinvocation. Non-prepare `setTasks`
mutations are rejected.
- `taskTransition` wraps all task mutations from the run loop and task
command surfaces. Task-local `tasks[].hooks[]` run before root
`hooks.taskTransition[]`, and rejections roll back the requested task
Expand All @@ -273,6 +286,16 @@ The built-in `git-worktree` hook runs in `prepare` and `beforeAttempt`.
It creates or reuses a worktree, switches the run cwd to that path, and
in `prepare` also projects `worktree_path` into runtime vars.

The built-in `task-list` hook runs in `prepare` only. Its config is
strictly `{ path, mode: "replace", missing: "continue",
empty: "keep-existing" }`. When the file is missing it continues without
mutating tasks. When the file exists and is empty it keeps the
assignment-authored tasks. When the file exists and is invalid, prepare
fails. When the file exists and contains tasks, it emits `setTasks`; the
resolved replacement task list, task-local hooks, manifest
`finalTasks`, `tasksTotal`, `tasksCompleted`, `resetSeed.finalTasks`,
`assignment-seed.md`, and `brief` are all built from that replacement.

Declarative `when` support remains narrow: attempt-phase hooks support
`when.sessionIndex` and `when.attemptIndexInSession`, while task-transition
hooks support `when.taskId`, `when.taskIds`, `when.fromStatus`,
Expand Down Expand Up @@ -343,7 +366,7 @@ blocked with the rest completed or blocked → `blocked`; otherwise
runtime-interpolates prefix launcher command/args
11. builds the provisional prepare manifest
12. runs prepare hooks, then freezes final cwd/runtime vars/backend
outputs, task text, and launcher values
outputs, task list, task text, task-local hooks, and launcher values
13. composes and stores `brief`
14. imports complete backend-owned history when `--backend-session-id`
is present and the backend supports history reads
Expand Down Expand Up @@ -452,6 +475,9 @@ It uses the frozen agent, assignment, hooks, launcher, tasks, cwd,
schedule, selected backend args, and backend-specific config already
stored on the manifest, rerenders the brief/reset seed, and commits the
replacement manifest only after validation and prepare/rendering succeed.
If the initialized run was created from a repo-local task-list
replacement, reconfigure uses the frozen replacement tasks and does not
re-read the repo-local task-list file.
It appends
`run.reconfigured` with changed var keys and a message-changed boolean,
not secret values or message text.
Expand Down
119 changes: 119 additions & 0 deletions docs/examples.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,125 @@ separate `backendArgs.<name>.extraArgs` tokens. Custom backend modules
live under `${TASK_RUNNER_CONFIG_DIR}/backends/<backend-name>/` and are
trusted local code, not sandboxed plugin packages.

## Containerized exploratory codebase assistant

This pattern keeps repository preparation on the host while invoking the
agent inside a pre-existing container. Task-runner does not create,
start, stop, or clean up the container; the launcher is only a subprocess
prefix.

```yaml
# ~/.config/task-runner/launchers/container-agent.yaml
schemaVersion: 1
name: container-agent
command: aw-tr-launch
args:
- "{{image}}"
- "{{container_workspace_root}}/{{run_group_id}}/repo"
```

```md
<!-- ~/.config/task-runner/agents/container-codex/agent.md -->
---
schemaVersion: 1
name: container-codex
backend: codex
launcher: container-agent
---
Explore the target codebase and keep task notes concrete.
```

```md
<!-- ~/.config/task-runner/assignments/explore-container/assignment.md -->
---
schemaVersion: 1
name: explore-container
cwd: "{{host_workspace_root}}/{{run_group_id}}/repo"
vars:
repo_url:
type: enum
required: true
sources: [cli, web]
values:
- git@github.com:org/repo-one.git
- git@github.com:org/repo-two.git
branch:
type: string
required: true
sources: [cli, web]
host_workspace_root:
type: string
default: /home/kevin/agent-workspaces
sources: [cli, web]
container_workspace_root:
type: string
default: /workspace/agent-workspaces
sources: [cli, web]
image:
type: enum
default: agent-dev
sources: [cli, web]
values: [agent-dev]
hooks:
prepare:
- builtin: command
with:
mode: status
command: bash
cwd: /
args:
- -lc
- |
set -euo pipefail
target="{{cwd}}"
mkdir -p "$(dirname "$target")"
if [ -d "$target/.git" ]; then
git -C "$target" fetch origin "{{branch}}" --prune
git -C "$target" checkout "{{branch}}"
git -C "$target" reset --hard "origin/{{branch}}"
else
git clone --branch "{{branch}}" "{{repo_url}}" "$target"
fi
- builtin: task-list
with:
path: "{{cwd}}/.task-runner/tasks.yml"
mode: replace
missing: continue
empty: keep-existing
tasks:
- id: orient
title: Orient to the repository
---
Inspect the repository at the prepared cwd and report what matters.
```

Run it with separate host and container roots:

```bash
task-runner run \
--agent container-codex \
--assignment explore-container \
--var repo_url=git@github.com:org/repo-one.git \
--var branch=main
```

The prepare hooks clone or update the repository at
`${host_workspace_root}/<run-group-id>/repo` on the host. The launcher
receives the matching container cwd
`${container_workspace_root}/<run-group-id>/repo`, which must already be
mounted into the container by external lifecycle tooling. `aw-tr-launch`
is expected to append the backend command and backend args after the
launcher args:

```bash
aw-tr-launch <image> <container-cwd> <backend-command> <backend-args...>
```

If the prepared repo does not contain `.task-runner/tasks.yml`, the
assignment falls back to its authored `orient` task. If the file exists
but is invalid, prepare fails. Once a run is initialized, resume and
reset use the frozen task list and do not re-read the repo-local file.

## Bundled agents

### `implementer`
Expand Down
Loading