zirvDocs

DocsWorkflows and skills

Development Workflows

README.md, lines 2093–2803View source

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.

bash
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 stats

Built-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#

bash
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 alone

Outcome 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 >= 2one step heavier
first-pass rate >= 95% and mean review rounds <= 0.2one step lighter
anything elseno change
fewer than N samplesinsufficient 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_fields throughout) the operator points plan/run at -- anywhere on disk, most naturally under docs/benchmarks/<project>/campaigns/. It names a baseline commit, a protected evaluator set, a corpus split, a backend (a real command or a scripted fixture), 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/holdout each name a corpus split and a repetition count, and may optionally narrow to specific corpus class values with classes = [...]. screen and validate must use different splits, and neither may use holdout (reserved for the single final confirmation) -- a manifest violating either is refused at load time. plan also refuses a validate/holdout stage that could never reach criteria.min_pairs given its split, classes filter, and reps, 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 immutable lock.json (resolved manifest + hash, baseline sha, evaluator file hashes, zirv version) written once, an append-only ledger.jsonl (every dispatch, finish, retry, stage decision, and stop reason), and per-trial spec.json/trial.json under trials/.
  • Budgets: max_spend_usd, max_wall_secs, max_calls, max_trials, max_retries, concurrency are 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. --resume replays 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.toml or proposal/candidate.patch plus proposal/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:

  1. Built-ins. Versioned Zirv packs compiled into the binary (src/commands/workflow/packs/*.toml) -- today feature, bugfix, refactor, spike, and review, reproducing the same step ids, phases, skills, conditions, approvals, and artifacts the pre-#542 engine used.
  2. Operator-global (~/.zirv/workflows/*.{toml,yaml,yml}). Trusted: may replace a built-in id, but only when the file itself sets override = true; otherwise a colliding id is ignored with a warning, never silently dropped or silently active.
  3. Repository (<repo>/.zirv/workflows/*.{toml,yaml,yml}, gated by workflow.repo_workflows_enabled, default false). Untrusted: may only ADD a non-colliding id, and additionally can never widen authority beyond what some built-in pack already exercises -- effects = "external", a repo.write/shell.exec/network.access/agent.spawn capability 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)#

idgroupeffectsoutput
featuresoftwarerepositorychange
bugfixsoftwarerepositorychange
refactorsoftwarerepositorychange
spikesoftwarerepositoryfindings
reviewsoftwarenonedisposition
adaptive-work(generic fallback)noneresult
pm-requirementspmnonebrief
pm-status-reportpmnoneupdate
pm-backlog-triagepmnonebacklog-update
pm-cycle-planningpmnonecommitted-plan
pm-risk-reviewpmnonerisk-register
pm-retrospectivepmnoneretro-summary
data-question-to-reportdatanonereport
data-quality-investigationdatanonefindings
data-anomaly-investigationdatanonefindings
data-recurring-kpi-reviewdatanonekpi-summary
architecture-decision-recordarchitecturenoneadr
architecture-design-reviewarchitecturenonedisposition
architecture-discoveryarchitecturenonediscovery-report
architecture-migration-roadmaparchitecturenoneroadmap
architecture-threat-scale-cost-reviewarchitecturenoneassessment
sre-incident-triagesrenonetriage-report
sre-deploy-or-rollbacksreexternaldecision-receipt
sre-postmortemsrenonepostmortem
sre-capacity-reliability-reviewsrenonereview-summary
devops-ci-cd-changedevopsrepositorychange
devops-infrastructure-changedevopsexternalchange-receipt
dependency-upgradesoftwarerepositorychange
security-remediationsoftware, securityrepositorychange
schema-data-migrationsoftwarerepositorychange
performance-investigationsoftwarerepositorychange
documentation-runbook-changesoftwarerepositorychange

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. Bare zirv 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 own zirv workflow start in their own worktrees never collide or clobber one another's active pointer.
  • The Test/Verify evidence gate's relatedness check. A workflow records the branch it gates (WorkflowState.branch: --branch <name> at start, or the checkout's own current branch when not given), and every zirv test changed/zirv verify run records the branch it was produced on. zirv test changed always 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:

TierDeploy step
developmentAuto-advances once test/review/verify evidence passes
stagingRequires an explicit zirv workflow approve on the deploy step
productionRequires 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:

LevelBehavior
offNo detection, no nudge, no gate.
adviseA 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.
enforceThe 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:

idteam rolewrite posturedeliverable
implementerimplementerwritablebounded implementation unit + evidence
reviewerreviewerread-onlyindependent structured findings
doc-keeperimplementerwritablesynchronized documentation
security-scannerreviewerread-onlysecurity/trust-boundary findings
explorerresearcherread-onlybounded investigation findings
researcherresearcherread-onlysourced external/repo research
plannerplannerread-onlydependency-ordered task breakdown
architectplannerread-onlyADR-quality decision record
debuggerimplementerwritablereproduction test + root-cause note
testertesterread-onlyindependent test results + triage
data-analystresearcherread-onlyreproducible data/query analysis
devops-sreimplementerwritableCI/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:

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.