| name | roster-orchestrator |
| description | Bootstraps roster workspaces. On chat session start, surfaces unread decisions (HITL) as a single banner pointing at /inbox. On a scheduled fire, verifies the schedule is registered, resolves the agent's merged env, dispatches the named agent via the host tool's native subagent primitive, writes a run log + state.md entry, and exits. Reads roster/<function>/schedules.yaml plus pending items at both roster/<function>/pending/ (error class) and <function>/<agent>/pending/ (lesson class). Subscription-billed primitives only — never invokes the Claude CLI in headless print or API modes, nor the Anthropic SDK. |
| version | 1.4.0 |
| trigger_conditions | ["Session start in a legacy roster workspace (identified by config/project.yaml at cwd)","A scheduled fire prompt names a roster agent (e.g., 'Run sdr cold-outreach')","User invokes /roster-orchestrator"] |
roster-orchestrator
The bootstrap entry point for every fresh CLI session in a roster workspace. Two modes:
- Chat-session bootstrap — surface a single banner if there are any unread decisions (HITL items).
- Scheduled fire — verify the fire matches a registered schedule, resolve the agent's merged env, dispatch the named agent, log the run, exit.
The skill is stateless. It re-reads disk on every invocation so /clear and fresh fires both work identically.
Working directory
Operate from the workspace root and classify it before choosing a mode:
- If both
roster.yaml and config/project.yaml exist, stop. This is a mixed workspace; preserve both markers and use the v2 migration flow when #363 lands.
- If
roster.yaml exists, stop with: Roster v2 is host-activated through ROSTER.md. The legacy roster-orchestrator scheduler and pending bootstrap do not apply; do not invoke legacy schedule, pending, review, or workspace scripts. Thin shared host activation lands in #349.
- Otherwise,
config/project.yaml alone marks a legacy v1 workspace. The roster/ directory (the scheduler/queue namespace) is created lazily by roster schedule install / roster pending sync, so it is absent on a fresh legacy init and that is normal — do not require it for chat-session bootstrap.
- If neither identity file exists, abort with:
Run roster-orchestrator from a legacy Roster workspace root (must contain config/project.yaml); Roster v2 uses roster.yaml and ROSTER.md instead.
A missing roster/ simply means zero error-class pending items (see Mode 1). The stricter requirement on roster/<function>/schedules.yaml applies only to scheduled-fire mode (Mode 2).
.roster/ is not roster/. .roster/ (dotted) holds scaffold/schedule-spec metadata written by roster init / roster schedule install; roster/ (undotted) is the runtime queue + state tree. They are different directories — never treat the presence of .roster/ as the runtime roster/, or vice-versa.
Mode detection
Inspect the initial prompt:
- If it matches a scheduled-fire shape (
Run <agent> <plan>, Use the <agent> skill to <plan>, etc.) → scheduled-fire mode.
- Otherwise → chat-session-bootstrap mode.
When ambiguous, default to chat-session-bootstrap (it is the safe no-op when no fire is happening).
Mode 1 — Chat-session bootstrap
- Walk both decision surfaces:
- Error class —
roster/<function>/pending/*.md across all functions (synthesized by roster pending sync from non-zero cron exit codes / STALE detection). If roster/ does not exist yet (fresh init), this surface is simply empty — count it as zero and continue; never abort.
- Lesson class —
<function>/<agent>/pending/*.md across all agents (drafted by the dreamer skill).
- Count files matching
*.md in each surface. Sum the counts (no dedupe — error and lesson namespaces are disjoint).
- If sum == 0 → print nothing, exit silently.
- If sum > 0 → print one banner line and stop (pluralize
decision):
⚠ You have N unread decision(s) awaiting — run /inbox
(Single-line surface. /inbox reviews them conversationally; roster review is the CLI backend.)
No other side effects. Do not read item bodies. Do not modify any file.
Mode 2 — Scheduled fire
-
Parse the fire prompt for <function>, <agent>, <plan>, and — when present — the schedule name.
- Installed prompts carry a function-qualified agent AND a
(schedule <name>) suffix (e.g., Use the roster-orchestrator skill to run plan cold-outreach for agent gtm/sdr (schedule sdr-cold-outreach)). Parse <schedule> from the suffix whenever present.
- Preferred agent shape:
<function>/<agent> (e.g., gtm/sdr) — every current install renders it. Split it ONCE into <function> and the bare <agent> (e.g., gtm + sdr) and use those consistently below: the function names which registry file to load; the bare agent is what the registry stores.
- Bare-agent shape (e.g.,
sdr — a legacy installed prompt): resolve the function by scanning <function>/<agent>/ for exactly one matching directory. If zero or more than one match (the same bare agent exists under two functions), abort with the parsed fields and the candidate functions — that is true ambiguity; a current install avoids it by qualifying the agent in the prompt.
- Refuse if
<agent> or <plan> is missing — list which one. A missing (schedule …) suffix is NOT an error — it means a legacy install (see step 3's fallback).
-
Load roster/<function>/schedules.yaml using the resolved function from step 1. Scheduled-fire is strict about this file — if roster/ or roster/<function>/schedules.yaml is missing or unreadable, abort immediately (the Mode 1 tolerance of a missing roster/ does NOT apply here):
Schedule registry not found: roster//schedules.yaml. Install the schedule first with roster schedule install, or run roster schedule list to see what is registered.
-
Verify a matching entry exists — by schedule name first; two schedules in one function may legitimately share the same (agent, plan), so the name is the only unambiguous key. The registry stores the BARE agent (agent: sdr, kebab — the file is already function-scoped by its path, so entries never embed the function): always compare the registry's bare agent against the prompt's bare (function prefix stripped in step 1) — never against :
Env resolution
The dispatched subagent needs workspace-wide secrets plus any agent-specific overrides. v1 ships a pure loader for this:
import { resolveAgentEnv } from '<roster-internal>';
const env = resolveAgentEnv(workspaceRoot, "<function>/<agent>");
Precedence (each key resolved independently):
<function>/<agent>/.env — if the key is defined, use that value. Empty string = explicit unset (does NOT fall through).
/.env (workspace) — if the key is defined, use that value.
- Otherwise the key is unset.
The orchestrator must ensure the merged env is materialized in the dispatch primitive's environment before the subagent runs — apply via the host's env-application mechanism (Claude Task env hand-off, Codex agent env, Gemini equivalent). Subscription-safety: only .env values are loaded; never inherit API-key shell exports from the user's interactive session. For scheduled fires this is reinforced upstream by the cron wrap (env -i).
Subagent dispatch
The skill body is tool-agnostic. Use the dispatch idiom that matches the host CLI:
When an ops backend is configured (see "Run-ledger correlation"), the parent has
already emitted roster run start --run <run-id> … BEFORE dispatch — with the
--schedule <schedule-name> --function <function> clause ONLY when
ROSTER_FIRE_ID is present in the environment (a cron-wrapper-hosted fire; the
flag also stamps the fire sidecar) — and passes <run-id> into the subagent
prompt so the subagent records its own outputs against it. When no backend is
configured, drop the <run-id> clause — the dispatch idiom is otherwise
identical.
On Claude Code
Use the Task tool with run_in_background: false:
Task(
subagent_type="<agent>",
prompt="Run plan <plan>. Record your work to the run ledger under run id <run-id>: pipe your final report to `roster run report --run <run-id> --stdin`, and for each produced/used artifact call `roster run declare-artifact --run <run-id> --agent <function>.<agent> …`.",
run_in_background=false,
)
The subagent runs in isolated context. The return value is a short status string (~30 tokens). No other tools should be invoked in this turn. After it returns, close the run: roster run end --run <run-id> (success) — see "Run-ledger correlation".
On Codex CLI
Invoke the subagent via natural language. Codex resolves the agent name against ~/.codex/agents/<agent>.toml:
Use the <agent> subagent to run plan <plan>. Record your work to the run ledger under run id <run-id>: pipe your final report to roster run report --run <run-id> --stdin, and call roster run declare-artifact --run <run-id> --agent <function>.<agent> … for each artifact.
Wait for the subagent to return its status, then close the run (roster run end --run <run-id>) and proceed to the state.md write.
On Gemini CLI
Dispatch by prefixing the prompt with @<agent> — Gemini resolves it against ~/.gemini/agents/<agent>.md and forces that subagent (an in-session prompt prefix, not a shell command, so it stays on the user's subscription):
@ run plan . Record your work to the run ledger under run id <run-id>: pipe your final report to roster run report --run <run-id> --stdin, and call roster run declare-artifact --run <run-id> --agent <function>.<agent> … for each artifact.
Wait for the subagent to return its status, then close the run (roster run end --run <run-id>) and proceed to the state.md write.
Delegated helper subagents (non-scheduled)
Some subagents are dispatched on demand by a skill rather than by a scheduled fire — e.g. the brain skill delegates the brain-organizer subagent for its on-demand corpus pass. Same primitives, same subscription guarantee:
| Subagent | Claude Code | Codex CLI | Gemini CLI |
|---|
brain-organizer | Task(subagent_type="brain-organizer", prompt="Organize this corpus into the brain", run_in_background=false) | "Use the brain-organizer subagent to organize this corpus into the brain." | @brain-organizer organize this corpus into the brain |
Run-ledger correlation
Opt-in — no-op unless an ops backend is configured. When (and only when) the
workspace has an ops backend, wrap the scheduled dispatch with roster run
lifecycle events so the run is queryable and reconstructable from any machine
sharing the backend. Detect this first and skip silently on not-configured:
state = `roster run list --json` exits 0? (or resolveOpsBackend state ≠ not-configured)
if not configured → do nothing; legacy workspaces are unaffected
The correlation is parent-authoritative — the parent (this skill) owns the
run id and the lifecycle boundary; the subagent owns its own outputs. The parent
never reads the subagent's markdown (it only ever holds the short status string):
- Before dispatch (step 5), the parent generates a run id and emits the
run-start event. Check the environment first: pass
--schedule ONLY when
ROSTER_FIRE_ID is set. roster run start --schedule REQUIRES a fire id,
and only the codex cron wrapper mints one — a UI-hosted fire (Claude Desktop
Scheduled Task, Codex app Automation) has no ROSTER_FIRE_ID, and passing
--schedule there fails the run-start and leaves NO ledger record at all.
ROSTER_FIRE_ID present (cron-wrapper fire):
roster run start --run <run-id> --agent <function>.<agent> --trigger schedule --origin-task <plan> --schedule <schedule-name> --function <function>
That single call both records the run-start AND stamps a PER-FIRE, function-
scoped run-id sidecar (logs/cron/<function>/<schedule-name>/<fireId>.run-id,
JSON {runId, firedAt, fireId}), so a LATER session can correlate a crashed
fire back to THIS exact run — even when fires overlap and even when two
functions own a same-named schedule.
ROSTER_FIRE_ID absent (UI-hosted fire, manual invocation):
roster run start --run <run-id> --agent <function>.<agent> --trigger schedule --origin-task <plan>
The run is fully recorded in the ledger, but there is no sidecar and no
fire correlation — crash correlation is cron-wrapper-only; a UI-hosted
fire that dies before run end surfaces via stale detection instead.
The fire id comes from ROSTER_FIRE_ID, which the cron wrapper minted and
exported to this process (pass --fire-id only for manual/test invocation). The
wrapper writes the matching <fireId>.exit in the SAME dir, so the two pair by
an EXACT token — never a timestamp guess. Do NOT hand-write the sidecar
(printf … > …run-id): the per-fire pairing is what makes overlapping fires
correlate correctly. Use a . (or :) between function and agent — e.g.
gtm.sdr — NOT /: the agent handle is projected into the safe run_index
view (charset [A-Za-z0-9._:-]), so a slash is rejected.
- Pass the run id into the dispatched subagent (shown in the dispatch idioms
above). The subagent — which owns its run log — records its own outputs against
that id:
roster run report --run <run-id> --stdin # its final report (source=agent, unverified)
roster run declare-artifact --run <run-id> … # each produced/used artifact
roster run is a subscription-safe local CLI (it only writes to the configured
Postgres/S3 ops backend — no model calls), so it is exempt from the banned-list
below.
Subscription-billing guarantee
This skill — and every subagent it dispatches — must run on the user's interactive Claude Pro/Max or ChatGPT Plus/Pro subscription. Banned primitives (any occurrence is a release blocker, enforced by roster doctor and CI audit):
claude -p
claude --prompt
claude api
@anthropic-ai/sdk
from anthropic
- Any wrapper that re-routes calls through the Agent SDK billing pool
Scope of the guarantee. The static audit blocks the above literals in roster's shipped source (skills/, src/) at build/install time. It is a source guard, not a runtime sandbox — nothing prevents a host LLM from inventing a banned invocation while following these instructions. Runtime compliance depends on the LLM honoring this section.
One sanctioned exception (ADR-0002, ROS-155). The second-opinion claude adapter (src/lib/second-opinion/adapters.ts) may spawn claude print mode — human-invoked only, never from a schedule or this orchestrator — because a fail-closed preflight refuses to spawn unless the child is provably on the user's subscription (no API keys, no apiKeyHelper, no Bedrock/Vertex, OAuth credential present). That single marked line does not weaken this list: any other occurrence anywhere in shipped source is still a release blocker.
If you encounter a workflow that seems to require one of the above, stop and surface it as a HITL item. Do not attempt to bypass.
What this skill does NOT do
- Execute business logic (SDR outreach, content drafts, design generation). That's the dispatched subagent's job.
- Read or modify HITL item bodies. That's
roster review.
- Write the full run log. That's the dispatched subagent.
- Touch git. Roster never auto-commits.
- Invoke any model billing path other than the host tool's native subscription.
Failure modes
- No
config/project.yaml at cwd → not a roster workspace; abort with the Working-directory message.
- Fire prompt missing agent or plan → abort, list the parsed fields.
roster/<function>/schedules.yaml missing or unreadable (scheduled fire) → abort with the registry-not-found message; the missing-roster/ tolerance does NOT apply to Mode 2.
- Schedule registered file present but no matching entry → abort with the
roster schedule list pointer.
- Subagent dispatch fails → write
status=failed to state.md, do not retry. Failure-class HITL items are created by the next session-start (ROS-42 / failure observability).
roster/ directory missing (chat-session bootstrap) → first run on a fresh init; treat error-class pending as zero, continue to lesson-class checks, exit cleanly. Never abort for this in Mode 1.