DocsWorkflows and skills
Development Workflows
Zirv owns the high-level development lifecycle outside model conversation memory. Only the current phase's selected skill instructions are injected; completed phases remain durable private state and are not repeated after a session restart or compaction.
zirv skill list
zirv skill list --match "production outage, paging alert" --limit 3
zirv skill show systematic-debugging --agent codex
zirv skill load incident-investigation
zirv skill export systematic-debugging --dir ./bundles
zirv skill read frontend-craft references/typography.md # a bundle resource, incl. a built-in's compiled-in references
zirv workflow classify --task "fix authentication race"
zirv workflow start bugfix --task "fix authentication race" --agent codex
zirv workflow start feature --task "use only shipped methodology" --built-in-only
zirv workflow status
zirv frontend profile
zirv frontend capabilities --agent claude
zirv frontend check
zirv frontend render
zirv frontend review --agent codex
zirv frontend benchmark
zirv test changed
zirv verify
zirv workflow statsBuilt-in workflows cover feature, bugfix, refactor, spike, and
review. Deterministic intent/complexity/risk classification selects
proportional design, approval, test, and review depth; sensitive auth/security
and database/schema changes cannot be downgraded below High risk. Optional
operator-owned jev.gates advice can only tighten gate-time classification,
add displayed workflow tags, or refuse a high-confidence template-copy
artifact; high-confidence thin artifacts warn and pin, and the exact
untouched-template check always runs first.
The full verb set#
zirv workflow list [--json] [--built-in-only] [--repo <path>] # registry ids: layer/version/hash/domains
zirv workflow show feature [--json] [--built-in-only] [--repo <path>] # one definition's steps
zirv workflow classify --task "..." # classify without starting; like start, untracked files are not part of the change
zirv workflow start feature --task "..." [--agent claude] [--built-in-only] [--brainstorm|--no-brainstorm] [--branch <name>]
zirv workflow status [id] # one instance, or the active one; shows brainstorm: on|off, Jev tags, per-step wall-clock, and pinned definition/drift
zirv workflow resume <id> # restore as the active workflow
zirv workflow context [id] # the current step's resolved skill context
zirv workflow artifacts <id> [--json] # committed work-product state
zirv workflow agents list|show <id>|dispatch <id> --adapter <name> --prompt <task>
zirv workflow team plan "<objective>" [--workflow <id>|active] [--dry-run] [--seat <id>] [--json]
zirv workflow team show [--workflow <id>|active] [--json]
zirv workflow team brief <seat-id> [--json] # Agent-tool-ready brief for one compiled seat
zirv workflow approve <id> # approve the current gated step
zirv workflow advance <id> --outcome success|failure
zirv workflow review package <id> | run <id> --agent <name> | add | record <id> --model <name> [--finding <id>]... | ...
zirv workflow maintain scan [--repo <path>] [--json]
zirv workflow stats # local bounded telemetry: per-phase timing, the implement/validate wall-clock split, approval wait, and fix-round causes (issue #699 Phase 0)
zirv workflow calibrate [--json] [--min-samples N] # read-only: outcome buckets and routing proposals (issue #757)
zirv workflow spend --state-dir <dir> [--receipts <file.jsonl>] [--campaign <id>] [--candidate <id>] [--trial <id>] [--task <id>] [--json]
# read-only: reconciles delegations.jsonl/jev-decisions.jsonl/jev-effects.jsonl/proxy-decisions.jsonl into one SpendReport (issue #800)
zirv workflow research plan <manifest> [--repo <path>] [--dir <path>] [--json]
# validate + resolve a campaign manifest; zero backend/provider calls; exit 0 valid / 2 refused (issue #802)
zirv workflow research run <manifest> [--repo <path>] [--dir <path>] [--resume] [--json]
# baseline -> per-candidate screen -> validate -> one holdout confirmation, inside explicit budgets
zirv workflow research status <id|dir> [--json] # stage, trial counts, spend used vs caps for an in-progress or finished campaign
zirv workflow research report <id|dir> [--json] # regenerate report.md/report.json/results.tsv/proposal/* from the ledger aloneOutcome calibration#
When a workflow completes, fails, or is closed, zirv appends one metadata-only
row to <state>/logs/workflow-outcomes/<day>.jsonl (daily buckets, kept 365
days; recorded only while [workflow] telemetry_enabled is on). A row holds
the schema version, workflow id, pack id, profile, complexity, risk band, seat
tier (when known), the highest review round reached, whether Test/Verify steps
passed on the first attempt and at all, the terminal state, and the duration.
It never holds task text, prompts, or paths.
zirv workflow calibrate [--json] [--min-samples N] (default N = 10) groups
rows by complexity x profile x seat tier and prints each bucket's count,
first-pass verification rate, mean review rounds, and abandon rate, plus one
proposal per bucket:
| Rule (bucket has >= N samples) | Proposal |
|---|---|
| first-pass rate < 60%, or mean review rounds >= 2 | one step heavier |
| first-pass rate >= 95% and mean review rounds <= 0.2 | one step lighter |
| anything else | no change |
| fewer than N samples | insufficient evidence |
The step moves the seat tier (cheap -> standard -> deep -> frontier)
when the bucket knows it, otherwise the complexity class the classify.rs
thresholds assign. The command is read-only: it never writes config or
changes routing; an operator decides whether to act on a proposal.
Attribution and cost accounting (issue #800)#
ZIRV_ATTR_CAMPAIGN/ZIRV_ATTR_CANDIDATE/ZIRV_ATTR_TRIAL/ZIRV_ATTR_TASK
(each a [A-Za-z0-9._:-]{1,64} opaque id; anything else is dropped) ride
along on delegations.jsonl, jev-decisions.jsonl, jev-effects.jsonl, and
proxy-decisions.jsonl rows, and on workflow-outcomes rows (schema v2, see
below) -- never a new billing ledger. The headless launch seam
(zirv ctx exec's own child-env builder) additionally exports
ZIRV_ROUTE_HARNESS/ZIRV_ROUTE_MODEL/ZIRV_ROUTE_TIER (the actual, resolved
values; unset when unknown) so a downstream reader never has to re-derive them
from argv.
zirv workflow spend --state-dir <dir> [--receipts <file.jsonl>] [--campaign <id>] [--candidate <id>] [--trial <id>] [--task <id>] [--json] folds every
existing spend-adjacent log under --state-dir (plus, optionally, externally
supplied receipt rows) into one SpendReport: execution (agent + intake +
jev + helper + worker spend) and overhead (proposer/judge spend, for a
future autoresearch campaign) as separate totals, a by_source breakdown,
token totals, and a completeness verdict (complete/partial/unknown) --
unpriced usage is never reported as free. Filtering by --campaign/
--candidate/--trial/--task means two campaigns sharing one state
directory can never cross-attribute. Exits 0 even when the report is
incomplete; a filter or missing price table is not an error.
Autoresearch campaigns (issue #802)#
zirv workflow research plan|run|status|report runs a bounded, resumable,
budgeted search over small policy changes (Jev gate floors, model/effort
routing, context/compaction/handoff knobs, or an operator-declared,
path-scoped source patch) against a baseline, and reports accept / reject /
inconclusive / tradeoff / unmeasured with evidence -- see
docs/benchmarks/autoresearch/README.md
for the full manifest reference, worked examples, and the design this
implements.
- Manifest: a TOML file (
schema = 1,deny_unknown_fieldsthroughout) the operator pointsplan/runat -- anywhere on disk, most naturally underdocs/benchmarks/<project>/campaigns/. It names a baseline commit, a protected evaluator set, a corpus split, a backend (a realcommandor a scriptedfixture), route,[budgets], per-stage repetitions, the promotion[criteria], and a[candidate_space]bounding what a[[candidates]]entry may actually vary. - Stages and splits:
screen/validate/holdouteach name a corpus split and a repetition count, and may optionally narrow to specific corpusclassvalues withclasses = [...].screenandvalidatemust use different splits, and neither may useholdout(reserved for the single final confirmation) -- a manifest violating either is refused at load time.planalso refuses avalidate/holdoutstage that could never reachcriteria.min_pairsgiven its split,classesfilter, andreps, so a campaign that can never promote anything is caught before a single real trial runs. - Campaign directory:
<ctx state dir>/research/<id>/by default (or--dir) -- an immutablelock.json(resolved manifest + hash, baseline sha, evaluator file hashes, zirv version) written once, an append-onlyledger.jsonl(every dispatch, finish, retry, stage decision, and stop reason), and per-trialspec.json/trial.jsonundertrials/. - Budgets:
max_spend_usd,max_wall_secs,max_calls,max_trials,max_retries,concurrencyare hard caps -- each trial's declared cost ceiling is reserved before it is dispatched (so in-flight work always counts against the cap), and scheduling stops the instant a reservation would exceed one, letting in-flight trials finish.--resumereplays the ledger and never re-dispatches an already-finished trial. - Nothing is applied automatically. A finished campaign writes
report.md,report.json,results.tsv, and (for the single simplest accepted candidate)proposal/overlay.tomlorproposal/candidate.patchplusproposal/ROLLBACK.md-- reviewable evidence and a proposal, never merged, applied, pushed, or turned into a pull request by the runner itself; that stays an explicit, separate, operator-taken action.
Workflow definitions v2 (issue #542)#
zirv workflow list/show/start resolve any id against a layered,
validated registry of WorkflowDefinitionV2 packs instead of a fixed Rust
enum:
- Built-ins. Versioned Zirv packs compiled into the binary
(
src/commands/workflow/packs/*.toml) -- todayfeature,bugfix,refactor,spike, andreview, reproducing the same step ids, phases, skills, conditions, approvals, and artifacts the pre-#542 engine used. - Operator-global (
~/.zirv/workflows/*.{toml,yaml,yml}). Trusted: may replace a built-in id, but only when the file itself setsoverride = true; otherwise a colliding id is ignored with a warning, never silently dropped or silently active. - Repository (
<repo>/.zirv/workflows/*.{toml,yaml,yml}, gated byworkflow.repo_workflows_enabled, defaultfalse). Untrusted: may only ADD a non-colliding id, and additionally can never widen authority beyond what some built-in pack already exercises --effects = "external", arepo.write/shell.exec/network.access/agent.spawncapability no built-in step ever declares, or dropping an approval/validation/ independent-review gate category a same-domain built-in establishes are each refused (dropped with a warning, not a hard failure of the rest of the layer). Symlinked directories/files, path escapes, and oversized (>32 KB) manifests are hard refusals, mirroring.zirv/skills/.
A definition's fields: stable id/version/title/description, free-form
domains tags and example triggers, typed inputs/outputs, a DAG of
steps (each with id, title, phase, skills, an optional agent_role,
capabilities, depends_on, an optional parallel_group, condition,
approval, artifact, max_attempts, effect, a human-readable reason,
and the domain-variant pair domains/overrides_step), gates (approval/
validation/independent-review step-id sets), limits, a failure policy, a
top-level effects ceiling, an optional idempotency note, and a
completion contract. Validation (id shape, uniqueness, cycle detection,
unreachable-step detection, unknown skill/dependency/gate-reference
detection, and the 32 KB size cap) runs before a pack is ever
registered; zirv workflow list/show --json expose the resolved shape and
each pack's stable content hash.
zirv workflow start <id> executes ANY registry id through the same v2
materialization path (issue #542 chunk 3a) -- not just the five built-in
kinds; a step whose agent_role names nothing in the (registry-aware)
AgentRegistry fails the start before any state is written.
materialize_from_definition prunes steps by condition (unchanged
semantics), resolves each surviving step's data, and orders the result by
depends_on with a stable topological sort (ties broken by declaration
order); parallel_group is carried onto the materialized step as
informational metadata for a future concurrent scheduler -- the state
machine itself is still the single current_step sequence it always was, so
two steps sharing a parallel_group tag still execute one after the other
today. effect (none/repository/external) is likewise carried onto
the materialized step for downstream effect-aware tooling.
Domain variants (frontend, for now). A step whose data should differ by
domain -- today only "frontend" -- is authored as a SEPARATE [[steps]]
entry with domains = ["frontend"] and overrides_step = "<canonical id>",
supplying that id's skills/agent_role/capabilities/approval/
artifact/max_attempts/effect when the classified profile matches. A
variant step is a data donor, not its own DAG node: it must declare no
depends_on of its own, and the materialized step keeps the CANONICAL id,
phase, depends_on, parallel_group and condition from the step it
overrides regardless of which variant supplied its data -- switching profile
(zirv workflow reclassify --profile ...) never changes a step's id. This
replaces the old hardcoded WorkflowProfile-keyed Rust match table: a new
pack adds a domains/overrides_step entry instead of an engine.rs change.
Brainstorm and deploy-tier tags. These two overlays are NOT pack fields
at all -- they are engine transforms keyed purely on WorkflowPhase, so any
pack's steps opt in automatically by using the same phase as a built-in:
a step with phase = "intent" gets the brainstorm/--no-brainstorm skill
swap (brainstorm vs write-intent); a step with phase = "deploy" gets
its approval set to deploy_tier >= staging, and if the deploy tier is
production and no step anywhere has phase = "review", a synthetic
independent-review step is inserted immediately before the first
phase = "verify" step as a production-readiness floor.
A workflow started from a registry id pins that pack's id/version/hash
(and, for a non-built-in pack, the full definition inline) on the running
WorkflowState, so update, resume, and orchestrator rollover cannot change
the run's meaning silently; zirv workflow status prints the pin and a
definition drifted from registry note when the registry's current copy no
longer hashes the same.
Built-in packs (issue #542 chunks 4-5)#
| id | group | effects | output |
|---|---|---|---|
feature | software | repository | change |
bugfix | software | repository | change |
refactor | software | repository | change |
spike | software | repository | findings |
review | software | none | disposition |
adaptive-work | (generic fallback) | none | result |
pm-requirements | pm | none | brief |
pm-status-report | pm | none | update |
pm-backlog-triage | pm | none | backlog-update |
pm-cycle-planning | pm | none | committed-plan |
pm-risk-review | pm | none | risk-register |
pm-retrospective | pm | none | retro-summary |
data-question-to-report | data | none | report |
data-quality-investigation | data | none | findings |
data-anomaly-investigation | data | none | findings |
data-recurring-kpi-review | data | none | kpi-summary |
architecture-decision-record | architecture | none | adr |
architecture-design-review | architecture | none | disposition |
architecture-discovery | architecture | none | discovery-report |
architecture-migration-roadmap | architecture | none | roadmap |
architecture-threat-scale-cost-review | architecture | none | assessment |
sre-incident-triage | sre | none | triage-report |
sre-deploy-or-rollback | sre | external | decision-receipt |
sre-postmortem | sre | none | postmortem |
sre-capacity-reliability-review | sre | none | review-summary |
devops-ci-cd-change | devops | repository | change |
devops-infrastructure-change | devops | external | change-receipt |
dependency-upgrade | software | repository | change |
security-remediation | software, security | repository | change |
schema-data-migration | software | repository | change |
performance-investigation | software | repository | change |
documentation-runbook-change | software | repository | change |
Every pack that pairs a test step with a review step (feature, bugfix,
refactor, dependency-upgrade, devops-ci-cd-change,
performance-investigation, schema-data-migration, security-remediation)
also carries a simplify step between them, depending on test and gated by
the same condition as review (review depends on simplify in turn): a
code review round is always preceded by a simplify pass, so zirv-wrapped
agents reuse existing code instead of re-implementing it. Packs without a
test step carry none.
Every pack's effects is none or repository, except
devops-infrastructure-change and sre-deploy-or-rollback (issue #542
chunk 5), which declare external as their ceiling for the day #539 ships
typed cloud/deployment-ops tools -- neither one actually mutates anything
outside this checkout TODAY: every step stays effect = "none", the
workflow starts read-only, and every mutating step sits behind an explicit
operator approval whose reason names the missing integration rather than
acting through raw/untyped access. A pack that would otherwise need an
integration (Kibana logs for sre-incident-triage's diagnosis, a Linear/Jira
ticket for pm-requirements/pm-backlog-triage/pm-retrospective, a live
metrics/BI pull for data-recurring-kpi-review/
sre-capacity-reliability-review) says so in the relevant step's reason
and stops at an explicit approval gate instead of guessing. The repository
trust layer (above) refuses effects = "external" on ANY repository-provided
pack unconditionally, regardless of what a built-in declares -- only zirv's
own versioned built-ins may ever reach for it.
How a workflow is chosen. zirv workflow start without an id runs
selection::select_definition deterministically against the resolved
classification and --task text -- no model call. A pack scores by matching
--task text against its triggers (3 points each, as whole-word token
sequences -- "retro" never matches inside "Retrofit", and the objective's
token aligned with a trigger's last word may carry a trailing plural
s/es), a domains tag appearing in the task text (2 points), and the
classified work-domain aligning with a domains entry (1 point); scores
below a floor are dropped, and the highest-scoring survivor wins, ties broken
toward fewer external effects and then alphabetically by id -- both the score
and the tie-break are recorded in Selection::reasons/alternatives and
printed alongside the started workflow (--json adds a selection object;
workflow classify --json adds the same field as a preview with no side
effect). A legacy intent (Feature/BugFix/Refactor/Spike/Review)
that classification already produces with high confidence selects that
kind's pack by DEFAULT, skipping scoring entirely -- unless a more
specialised pack actually matched one of its own trigger phrases in the task
text AND declares effects compatible with that intent (Feature/Bugfix
need repository or external, Review needs none, Spike accepts any
effects, Refactor is never displaced), in which case the specialised pack
replaces it and both packs are recorded in reasons/alternatives. Only a
built-in or operator-global pack (the registry's own LAYER, shown by
zirv workflow list) may ever refine a legacy intent this way -- a
repository-layer pack is untrusted and may only ADD a non-colliding id
(see the three layers above), so it never displaces a legacy kind's own
pack this way; it can still be chosen outright the ordinary way, for
Intent::Other. Nothing above the floor selects
adaptive-work, a small five-step (understand/plan/execute/validate/present)
fallback pack with no domain/trigger tags of its own (so it never competes
for another pack's task) that prunes itself down to three steps
(understand/execute/present) for a trivial task. An explicit id
(zirv workflow start <id> --task ..., or the native /workflow <id> <task>
slash command) always wins outright with no selection performed at all, and
that choice survives resume/reclassify as the pinned definition on the
running state -- matched case-insensitively (zirv workflow start Bugfix
resolves exactly like bugfix; zirv workflow show the same way), since
registry ids are themselves always lowercase. A bounded model tie-break for
a close, ambiguous score is deferred -- the deterministic algorithm's every
decision is already explainable from reasons alone, which a model call
would only obscure for the near-certain-floor cases it would actually apply
to; see the design note for the full reasoning.
zirv workflow start never refuses because another workflow is already
active for this repository -- multiple workflows per repository are
legitimate, and zirv workflow resume <id> restores any of them. When the
new start silently changes which workflow this repository's active pointer
names -- a DIFFERENT, still-running (Running/AwaitingApproval) workflow
was active -- it prints one best-effort note to STDERR after the new
workflow is saved: note: workflow <old-instance-id> (<old definition id>) is no longer this repository's active workflow; restore it with: zirv workflow resume <old-instance-id>. Stdout/--json output is unaffected
either way.
Lifecycle and artifacts#
A workflow instance moves through intent → spec → plan → implement → test → review → verify → deploy. Ceremony is proportional to classification: a
trivial bugfix skips straight to the debug/test/verify spine, while
substantial or high-risk feature/refactor work gains intent, plan (and, for
substantial/high-risk work, spec) artifact gates plus an approval-gated design
step. Frontend work overlays the same engine rather than running a separate
one, selected automatically from task language and changed frontend paths.
Artifact steps name a fixed template path under .zirv/work/<workflow-id>/
(intent.md, spec.md, plan.md, ...) for the agent to write itself — zirv workflow start/advance/resume never pre-create the file or touch the git
index (issue F6: a blind reviewer flagged the untouched, unfilled file as a
stray addition), so nothing appears in the worktree until the agent actually
fills it in. zirv workflow status/context print both the path and the
template text for a step whose artifact does not exist yet, so it stays
discoverable either way. zirv workflow approve treats a still-missing file
exactly like an untouched template and refuses it the same way, then pins the
accepted file's SHA-256 digest and timestamp in private state once it is
genuinely filled in; a later step folds only accepted, hash-matching artifacts
into prompt context. Editing or deleting an accepted file after the fact
reopens its acceptance gate and invalidates later completed steps, so
implementation can never silently proceed against a plan that changed
underneath it — zirv workflow artifacts <id> shows pending/accepted/drifted/
missing state directly.
Classification is re-measured (never downgraded) whenever a workflow advances
into a review or verify step, so a review/verify gate the initial workflow start measurement missed (an empty tree, before any code existed) still gets
added once the real change exists.
Implementation and review#
When a behavior-focused test is possible, the implement skill asks for the
same test-first loop the standalone tdd skill describes: write the
smallest test first, confirm it fails for the missing behavior rather than
setup noise, implement the minimum change that makes it pass, then rerun
that test before broadening verification, keeping each red/green cycle
attributable to one behavior. The same exemptions apply as in tdd:
generated files, pure configuration, exploratory spikes, or a change whose
only useful assertion sits at a broader integration boundary. implement
also runs the repository's own fast formatting and lint checks after each
meaningful edit rather than deferring them to test.
Before reporting done, implement self-checks its diff against the same
five dimensions review scores — correctness, security, data loss,
compatibility, and missing tests — and fixes what it can rather than leaving
it for a review round; review itself keeps the same rubric and bar. A
workflow step's skills list only ever materializes its first entry as the
step's running skill, so this discipline lives directly in implement
rather than in a second, unread tdd entry; tdd stays registered and
unmodified for workflow show, operator-authored packs, and a future step
that can carry more than one skill id.
Linked worktrees#
A workflow started in a repository's main checkout can be found from, and
gated against evidence in, a git worktree add-linked sibling of it (and
vice versa) -- but workflow state, and evidence, are never merged into one
shared identity: every piece of per-repository state (workflow state, the
active-workflow pointer, verification reports, crash witnesses, handoffs,
telemetry, test baselines, mail, ...) stays keyed by the LITERAL checkout a
session, process, or zirv test changed run actually used. Two independent
lookup/relatedness rules make cross-worktree orchestration work without that:
- Finding a workflow by id or by "the active one".
zirv workflow status|advance|review package <id> --repo <path>looks in<path>'s own state directory first, then in each of its sibling checkouts (git worktree list) for that id -- an explicit id is never ambiguous, so this is safe to widen to every sibling. Barezirv workflow status(no id) is different: it reads<path>'s own active-workflow pointer first, and if<path>has none, falls back ONLY to the MAIN checkout's own pointer -- never an arbitrary other sibling. This is what lets a worker worktree with no workflow of its own inherit the orchestrator's, while two workers each running their ownzirv workflow startin their own worktrees never collide or clobber one another's active pointer. - The
Test/Verifyevidence gate's relatedness check. A workflow records the branch it gates (WorkflowState.branch:--branch <name>atstart, or the checkout's own current branch when not given), and everyzirv test changed/zirv verifyrun records the branch it was produced on.zirv test changedalways writes its evidence under the literal checkout it ran in, so concurrent runs in sibling worktrees never clobber each other's evidence. The gate widens only its read: if the checkout it is evaluated from has no fresh, passing evidence of its own, it also checks every sibling checkout's own evidence against that sibling's own tree -- but ONLY accepts a sibling whose recorded branch matches the workflow's own recorded branch exactly. A sibling with fresh, passing evidence on a different branch never opens this gate, no matter how fresh or passing.
Together: start the workflow in the main checkout (--branch worker/feature-x if that main checkout is not itself on the worker's
branch), have the worker implement and run zirv test changed in
<repo>/.claude/worktrees/<name> (checked out on worker/feature-x), then
zirv workflow advance <id> --outcome success from the main checkout (or
--repo <repo>/.claude/worktrees/<name>, either finds the same workflow) --
the gate accepts the worktree's evidence because its recorded branch matches,
not merely because it happens to be fresh and passing for someone.
zirv workflow classify/start --branch <name> diffs that branch against
its own base as refs (git diff <base> <name>), not --repo's working tree
-- necessary because the checkout given as --repo need not have <name>
checked out at all. Without --branch, classification is unchanged: git diff --numstat <base> already measures whichever repository it is given, so
pointed at a worktree (--repo or plain cwd) it already saw that worktree's
own branch diff (uncommitted edits included) against its base.
The one unsupported edge case is a main checkout whose .git was relocated
with git init --separate-git-dir=...; its worktree siblings are not
resolved to it as "the main checkout" for the active-pointer fallback above.
Deploy tiers#
[workflow.deploy] tier = "development" | "staging" | "production" in
~/.zirv/ctx.toml (ZIRV_CTX_WORKFLOW_DEPLOY_TIER for the final override) is
operator-only; a repository may set only workflow.deploy.minimum_tier, and a
running workflow's resolved tier only ever ratchets upward:
| Tier | Deploy step |
|---|---|
development | Auto-advances once test/review/verify evidence passes |
staging | Requires an explicit zirv workflow approve on the deploy step |
production | Requires approval, plus at least one fresh independent reviewer-seat run and fresh final zirv verify evidence; an open finding or stale evidence blocks it outright |
Workflow auto-start#
[workflow] auto_start = "off" | "detect" | "always" in ~/.zirv/ctx.toml
(ZIRV_CTX_WORKFLOW_AUTO_START for the final override; default detect) is
operator-only. On a session's first prompt the UserPromptSubmit hook starts
a workflow itself, bound to that session and running at its first step with
no approval prompt: detect for programming or investigation work (a
feature, bugfix, refactor, spike, performance or review request, judged from
the prompt text alone, no network), always for every first prompt. Questions
and chat start none, and "no workflow" in the prompt opts out. It is skipped
for a session that already has a bound workflow, for delegated seats
(worker, sub-orchestrator, single, or a zirv agent child), and for unattended or
zirv-spawned launches (ZIRV_CTX_HEADLESS=1, or ZIRV_CTX_INTERNAL=1 on the
distiller/handoff/memory model call, supervisor consults and zirv ctx loop cycles), so a worker brief
never inherits another session's workflow. An INTERNAL or HEADLESS session also gets no per-turn [zirv] health-marker
instruction, so a helper or worker whose reply a script parses is never told to prefix it.
The seat prompt is composed before that first prompt, so the starting turn
carries the current step's resolved context (zirv workflow context, capped
at workflow.max_context_bytes), not just a pointer. After that, any prompt
that follows a step or status change of the session's bound workflow carries
the new step's context once; the launch compile records what it already
injected, so a resumed session is not shown the same step twice. An automatic
start never interviews the operator: its intent step uses write-intent, not
brainstorm. The start's git diff --numstat and untracked listing are
each bounded to 5s; past that the task is classified from its text alone and
the risk is raised one step, so a huge diff can no longer push the hook past
the harness's hook timeout.
Before starting, the hook closes (once per session) this repo's in-flight
workflows that never completed a step and are abandoned: every session bound
to them has ended, or they have been idle for 3 days. Each gets a recorded closed_reason
(abandoned: session <short> ended without advancing, stale: no advance in <n>d). zirv ctx status lists the same set, plus this session's own
workflow once it has gone an hour without advancing, as one workflows: line, and zirv workflow stats
prints progress: <a>/<s> started workflows advanced at least once; median steps completed <m> next to the adoption line.
Workflow adoption#
[workflow] adoption = "off" | "advise" | "nudge" | "enforce" in
~/.zirv/ctx.toml (ZIRV_CTX_WORKFLOW_ADOPTION for the final override) is
operator-only and detects a session that has done "substantial" edit work --
at least 12 edit-like tool calls, or at least 1 edit-like call over 25 turns --
with no active zirv workflow:
| Level | Behavior |
|---|---|
off | No detection, no nudge, no gate. |
advise | A one-time nudge rides the Stop hook's systemMessage once substantial work is detected. |
nudge (default) | The same nudge, repeated every 5 turns while the session stays substantial with no active workflow, and also surfaced on the next prompt (UserPromptSubmit) if a workflow still has not started. |
enforce | The nudge behavior, plus zirv agent (ctx::agent::run_with) refuses to dispatch for a session recorded as substantial with no active workflow, until one is started. |
The same transcript scan that detects substantial work also counts skill
loads, for a sibling "no skill loaded" nudge that rides the identical Stop
and prompt hooks ([zirv skills] substantial work (<n> edit calls over <m> turns) and no zirv skill loaded this session. Check the skill index: zirv skill list --match "<task>" then zirv skill load <id>.). It fires once a
session is substantial with zero skill loads, on its own 5-turn cadence
(never suppressing, or suppressed by, the workflow nudge above), and falls
silent the moment a load is seen. It needs workflow.adoption at advise or
above (at off the transcript is never rescanned for either signal, so
neither nudge has anything fresh to fire on) and prompt.skill_index = true
-- otherwise it is independent of workflow.adoption's level and of whether
a workflow is active: skills matter either way.
A load is counted two ways, because zirv skill load <id> run from a shell
is the PRIMARY path (the standing skill index header and the pretool
dispatch pointer above both tell an agent to run exactly that): a native/MCP
skill_load tool call (including a host-namespaced one such as
mcp__zirv__skill_load) is picked up by the same transcript scan that counts
edit-like calls, from the tool's own NAME; a shell-invoked zirv skill load
cannot be seen that way at all (no adapter's transcript parser exposes a
shell command's own argument text, only the tool name Bash/PowerShell),
so the CLI counts itself instead -- a successful load (never a refused or
unknown id) bumps the calling session's own record directly, keyed off the
same SESSION_ENV the hooks use, whenever one is set.
Intake discipline#
On a session's first UserPromptSubmit, the Claude hook classifies the
prompt with the same deterministic, text-only classifier zirv ctx proxy
uses (no Git, no network, no model call). When the prompt is substantial
or larger (a long request, or 8 or more enumerated requirements) or its risk
is high, that one turn's additionalContext also carries a note of under
400 bytes: plan ordered, verifiable steps before editing; write tests first
for behaviour changes; never modify or weaken existing or protected tests; run
the full suite before declaring done; and load the plan/tdd/verify
skills. Trivial and bounded prompts, and every later turn,
get nothing. The hook never starts a workflow. It skips sessions whose launch
already applied a proxy decision (ZIRV_CTX_PROXY_DECIDED=1, set by zirv chat for its wrapped or dashboard seat), single/worker/sub-orchestrator
seats, and delegated zirv agent runs. prompt.intake_discipline
(ZIRV_CTX_PROMPT_INTAKE_DISCIPLINE, default true) turns it off; a
repository may only narrow it to false.
Agent registry#
Workflow seats are provider-neutral data, not harness-specific plugins: a
WorkflowStep.agent addresses one by id. The prebuilt roster has twelve
built-in manifests, each mapped to the closed native team role
(ctx::team::TeamRole) whose authority it holds — a manifest's role field
is a display label, never an authorization grant:
| id | team role | write posture | deliverable |
|---|---|---|---|
implementer | implementer | writable | bounded implementation unit + evidence |
reviewer | reviewer | read-only | independent structured findings |
doc-keeper | implementer | writable | synchronized documentation |
security-scanner | reviewer | read-only | security/trust-boundary findings |
explorer | researcher | read-only | bounded investigation findings |
researcher | researcher | read-only | sourced external/repo research |
planner | planner | read-only | dependency-ordered task breakdown |
architect | planner | read-only | ADR-quality decision record |
debugger | implementer | writable | reproduction test + root-cause note |
tester | tester | read-only | independent test results + triage |
data-analyst | researcher | read-only | reproducible data/query analysis |
devops-sre | implementer | writable | CI/CD, infrastructure, deployment change |
reviewer/security-scanner/explorer are pinned read-only by their own
adapter. ~/.zirv/agents/* (operator-global) may replace a built-in seat;
.zirv/agents/* (repository) is disabled unless the operator sets
workflow.repo_agents_enabled, and even then may only add non-colliding ids —
a repository manifest can never rewrite reviewer or grant itself
capabilities it does not already have. An operator/repository manifest may
omit team_role; it is then derived from read_only alone (read-only →
researcher, writable → implementer), so a manifest written before this
field existed keeps loading unchanged. A manifest's skills list attaches
existing zirv skill ids to a seat by reference (never by copying instruction
text); an unknown or version-mismatched reference is refused by
AgentRegistry::validate_against. zirv workflow agents list|show inspects
the resolved registry and provenance; zirv workflow agents dispatch <id> --adapter <name> --prompt <task> launches that seat directly.
Every built-in seat also carries a model_tier (fast/standard/deep) —
a routing hint for how mechanical its work is, not a model choice.
doc-keeper/explorer are fast; security-scanner/architect are
deep; every other built-in seat is standard. Zirv never turns this hint
into a model id on its own; the operator may map (adapter, tier) to a real
model id under [model_tiers.<agent>] in ~/.zirv/ctx.toml:
[model_tiers.claude]
fast = "haiku"
standard = "sonnet"
deep = "opus"
[model_tiers.codex]
fast = "gpt-5.6-luna"
standard = "gpt-5.6-terra"
deep = "gpt-5.6-sol"When a seat dispatches (zirv workflow agents dispatch --model <id>, and any
workflow step that dispatches a seat under the hood), resolution order is: an
explicit per-invocation model pin always wins; otherwise a mapped (adapter, tier) pair supplies the model; otherwise no model flag is added and the
adapter's own default applies. An operator who sets nothing sees no change in
behavior. model_tiers is repo-forbidden — a repository choosing which model
a seat runs on would be a silent provider/model switch — so only the
operator's own ~/.zirv/ctx.toml or the matching
ZIRV_CTX_MODEL_TIERS_<AGENT>_<TIER> environment variable may set it.
Team composition#
zirv workflow team plan "<objective>" [--workflow <id>|active] [--dry-run] [--seat <manifest-id>] [--json] compiles the smallest capable team for a
request: it classifies the objective exactly like zirv workflow classify,
derives the minimal execution profile (intent/complexity/risk →
Direct/Bounded/Orchestrated, plus which independent-review/test/security
gates apply), and deterministically selects seats from the agent roster
above. Unless --dry-run, the compiled TeamPlan is stored on the active (or
--workflow-named) workflow; with no active workflow it prints without
storing and says so. zirv workflow team show [--workflow <id>|active] [--json] re-prints the stored plan, and zirv workflow team brief <seat-id> [--json] prints an Agent-tool-ready brief for one compiled seat — the seat
manifest's own instructions plus the bodies of only the skills that seat's
manifest references, never the whole skill catalogue. --seat <manifest-id>
bypasses the proportional rules for one explicit seat, but still passes the
same capability/team-role/route checks a compiled seat does — explicit
selection never bypasses policy.
How a team is chosen. Mechanical/trivial work (Direct execution) spawns
nobody. Bounded work gets one implementer (or the matching domain
specialist); an independent reviewer joins only when the validation profile
requires it. A bug fix gets a debugger (reproduction test + root-cause
note) then an implementer scoped to that root cause. Substantial features
add a planner; architectural ones also add an architect; implementers
split one per claim-boundary group (by TOP-LEVEL path from the
classification's own changed-path list when it has one, capped by the
profile's fan-out limit — a count-based bucket only when it does not).
Security/data/docs/dev-ops signals in the request or the diff add the
matching specialist with a concrete deliverable — never a duplicate of a
gate already covered (a security signal is satisfied by the same independent
security-scanner gate risk alone would require, not a second seat).
Independent review/test/security seats never share a writer's claim or
worktree and depend only on the writers having finished, never on reading
their transcript. An unknown manifest or a team role with no eligible route
is never invented into a plan: the seat is omitted with a stated reason, and
the plan still compiles. Fan-out (Bounded: 2, Orchestrated: 6) and
dependency-depth limits are enforced before the plan is returned — a plan
that would exceed them is refused outright, not silently truncated. The team
compiler never spawns every role; see
docs/design/2026-09-15-native-team-composition.md for the full rule set and
what is deferred to #537's full execution profile. A native coordinator or
sub-orchestrator seat compiles the same plan itself with the team_plan
tool, and the native pane's /agents, /agent and /team slash commands —
see "The native meta-orchestrator" below.
Maintain loop#
zirv workflow maintain scan is an invoked scanner, not a daemon: it runs
every operator-configured deterministic detector once, reading detector
commands only from [workflow.maintain.detectors.<id>] in the operator's own
~/.zirv/ctx.toml (repository config cannot define one). Each detector is a
bounded command judged by exit code/timeout or a stdout line-count threshold;
zirv retains only exit code, timeout state, and line/byte counts, never
detector command or output bodies. A breach parks one bounded incident
workflow at its Intent acceptance gate (.zirv/work/<id>/intent.md, committed
with detector metadata) and, when operator-only [report] repository = "owner/repo" is configured, auto-files a title-deduplicated GitHub issue. A
clean scan clears the incident marker, so a later recurrence opens fresh.