Agent controller roadmap
Post-decoupling features to build on the typed API + event model that Chunk 3
of agent-dispatch-unification delivers. Each is “new fields + a new
producer/consumer,” never new IRC-command scraping — that leverage is the whole
payoff of the decoupling.
Sequencing: build AFTER the Chunk 3 cutover (a clean, ergo-free, API-driven controller), except where noted. #4 (governance / judge / auto-approve scoping) is deliberately deferred — not in this roadmap.
Reference: turnstone (k0s/turnstone) already implements all of these —
config.yaml, scheduled-tasks.toml, personas.toml, skills/. Borrow the
shapes; the scheduled-tasks.toml header is essentially a spec sheet for 1–3.
1. Scheduled tasks — cron-created tasks ✅ BUILT 2026-08-25
What. The controller creates tasks on a schedule (nightly repo digest,
recurring checks), not only on-demand. Replaces ad-hoc crons with first-class,
git-tracked recurring work.
Shape. A scheduled_tasks table {name, schedule_type (cron|at), cron_expr/at_time, repo, model, persona, skill, initial_message (the task prompt), notify_targets, enabled} + a scheduler goroutine that fires due entries
by calling the SAME domain op the API does (createTask / POST /v1/tasks). New
endpoints POST/GET/DELETE /v1/schedules; reconciled from a git-tracked toml
exactly like turnstone’s scheduled-tasks-import.
Depends. Nothing IRC — cleanly separable, could even be an early standalone
win. Highest value.
Built (7d2d07a1 engine, 0c75beb3 import+deploy). internal/schedule — a
dependency-free 5-field cron evaluator (Vixie dom/dow rule, Sunday 0|7), the Sched
type, Validate, and the Scheduler runner (re-reads each tick; fires at-most-once
per due schedule, marking the fire clock BEFORE firing so a failure skips rather
than double-spawns). store migration #3 = scheduled_tasks; RunScheduled
(create+ready) is the fire hook; /v1/schedules CRUD behind a separate
ScheduleStore dep. Declarative import = a git-tracked YAML (scheduled-tasks.yaml,
AGENT_SCHEDULES_PATH) reconciled (upsert-only) at startup, delivered as the
name-hashed agent-controller-schedules ConfigMap. Shipped INERT (empty list). We
used YAML not TOML — no local Go to hand-manage a TOML dep’s go.sum, and yaml.v3
was already in the module graph. Deferred within #1: persona/skill/notify are
persisted but not yet consumed (they are features #2/#5/#3).
2. Personas — reusable behavior configs ✅ BUILT 2026-08-25
What. A named persona (system prompt / voice / defaults) a task references, so
setup is DRY and consistent — turnstone’s glados-slack.
Shape. A personas store/config {name, system_prompt, default_model?, …} +
a persona field on task-create, injected into the worker env (system prompt).
Endpoints /v1/personas (CRUD or git-tracked import). The worker applies the
persona as its system prompt.
Depends. Additive: a field on task-create + worker env. The IRC adapter
exposes it (!persona) only if wanted.
Built (23620a85 core, 09aacef4 import+deploy). internal/persona leaf
package (type + Validate + YAML loader). store migration #4 = tasks.persona
column (mirrored like the other task fields, reconstructs on restart), #5 =
personas table + CRUD + ResolvePersona. The persona name lives on the task;
its system prompt resolves FRESH at each spawn → SpawnSpec.SystemPrompt →
LLM_APPENDED_SYSTEM_PROMPT (append, not replace — the popquiz/rrroster
pattern, keeps the base safety prompt). A persona’s default MODEL applies at
create via the model path (right provider) only if the task didn’t pin one.
/v1/personas CRUD + declarative personas.yaml, folded with the schedules file
into one agent-controller-config ConfigMap. The bot stays decoupled (primitive
PersonaResolver). Shipped INERT. Not done: the !persona IRC command in the
adapter (deferred — set via api/schedule for now).
3. Notify targets — deliver results anywhere ✅ BUILT 2026-08-26
What. On completion, deliver a task’s final result to configured destinations
(a Slack channel, a webhook, a DM) — not just DM the IRC requester. Turnstone’s
notify_targets.
Shape. notify_targets on the task [{channel_type, channel_id | webhook_url}]
- a delivery consumer that subscribes to
task_completedon/v1/eventsand posts to each target. SHAPES 3c NOW. Delivery is a client/adapter concern (the controller emits; a deliverer sends) — the same seam as the IRC render. So design the extracted adapter to own delivery from the start, rather than baking requester-DMs into the controller. Build the general notify-target delivery after the cutover, but let this principle steer the 3c adapter design. Depends. The event stream (done) + the adapter (3c). Built (c9f17c00).internal/notifyleaf (Target type — schedule.NotifyTarget now aliases it — + RenderCompletion); store migration #7 =tasks.notify_targets; task_completed carries title+status+notify_targets (so a deliverer needs only the event); RunScheduled threads a schedule’s targets. The deliverer is a CLIENT (internal/delivery, theagent-controller notifysubcommand): tails /v1/events, posts task_completed to Slack (chat.postMessage) / webhook.clients-notifyDeployment (replicas 0, staged like clients-irc), SLACK_BOT_TOKEN from the slack-bot-pi-mom Secret, :9101 opened to it. MVP scope: delivers a COMPLETION NOTICE (title/status), not the worker’s full output — capturing a task’s final result text is a follow-up (needs a worker-reported result field). Undeployed.
5. Skills — materialized capability scripts
What. Named skills (reusable scripts/tools) a task applies — turnstone’s
kuvasz, prometheus — materialized onto the worker’s PATH so a task can call
them.
Shape. A skills registry {name, scripts, activation} + a skill field on
task-create; the worker materializes the skill’s scripts at startup. Distribution
is the hard part (a skills ConfigMap, or an OCI/Garage bundle the worker pulls).
Depends. Worker-side changes; biggest lift, lowest priority — build last.
Order
Chunk 3 cutover → (1) scheduled tasks ✅ → (2) personas ✅ →
(3) notify targets ✅ → (5) skills (last, biggest lift). Governance/judge
(#4) deferred. Also open: Phase 4 (pi-mom → translator, calls /v1/ask) —
blocked on block_actions in pi-mom; and result-text capture (a worker-reported
final result, so #3 delivers output not just a completion notice).
Progress 2026-08-25: #1 + #2 built (
7d2d07a1,0c75beb3,23620a85,09aacef4) — the “features first” pair, both ahead of the Chunk 3 cutover and safe because neither touches the IRC seam. Next: the Slack side (Phase 3 ofagent-dispatch-unification— registry split +/v1/ask+ pi-mom as translator), with #3 notify-targets built as part of it. #5 skills last.