Bachkator reads Bachfile from the current directory by default. The file is HCL with a Vim-friendly modeline often used at the top:
# vim: set ft=hcl :bach list
bach run <target> [target ...]
bach affected [path ...]
bach init [--provider opencode]
bach factory submit <factory> --title <title>
bach factory list <factory>
bach factory inspect <factory> <work-item-id>
bach factory cancel <factory> <work-item-id> --reason <text>
bach factory approve <factory> <work-item-id> --phase <phase>
bach factory start <factory>
bach factory status <factory>
bach plan status <plan-file> [plan-file ...]
bach plan implement <plan-file>
bach backend sqlite
bach provenance <path> [path ...]
bach artifacts [run-id]
bach explain <target>
bach graph
bach quality [summary|reports|metrics|findings|gates|slow-targets|failing-tests]
bach report init --role <role> [--name <name>] [--summary <text>] [--path <file>]
bach report finding --kind <kind> [--stdin] [--file <path>] [--line <n>] [--duration-ms <n>]
bach report metric --name <name> --value <number> [--scope <scope>] [--unit <unit>]
bach report status <success|failed> [--summary <text>]
bach validate
bach reference [topic]
bach runs list
bach runs inspect <run-id>
bach logs <run-id>Supported flags:
-f, --file <path>: load a different Bachfile. Withinit, choose the Bachfile path to create and writeAGENTS.mdbeside it. Default isBachfile.-a, --aliases: withlist, include target aliases and their canonical targets.--generated: withlist, include generated policy fan-out nodes.-v, --verbose: withlist, include cost and risk metadata; withrun, stream quiet target output.--runs-limit <n>: maximum runs or artifacts to list. Defaults to10; use0for all.--target <target>: filterruns listorartifactsby target address.--status <status>: filterruns listorartifactsby run status; withfactory list, filter Work Items by lifecycle. Factory list defaults topendingandwaiting_approval; useallfor every lifecycle.--since <duration|time>: filterruns listorartifactsby age or RFC3339 timestamp.--artifact <path>: filterruns listorartifactsby artifact path substring.--failed: withlogs, show only failed target logs.--last <n>: withlogs, show only the last N log lines.--errors: withlogs, show only likely error/failure lines.-d, --dry-run: print the planned operations without executing them.-p, --provider opencode: withinit, delegate starter Bachfile and agent-instruction generation to OpenCode.--force: run cacheable targets even when their fingerprints are fresh.-y, --yes: confirm execution of targets markedrequires_confirmation = true, including generatedbach plan implementagent targets.--env-file <path>: load target operation environment from this file after project.env.--profile <name>: select an environment profile. May be repeated; later profiles win.--log-only: suppress command stdout/stderr in the terminal while keeping Bach progress and quality progress visible; full command output is still written to.bach/runs/.../*.logfiles. Provider adapters may redact normal progress logs and keep complete provider telemetry in private run artifacts.-j <n>: maximum number of targets to run in parallel.--var name=value: set a Bachkator variable. May be repeated.--json: withrun --dry-run, print a machine-readable execution plan; withruns inspect, print a machine-readable run export for failures, agent reports, policy evidence, and control-plane ingestion; withprovenance, print path provenance records; withvalidate, print machine-readable diagnostics; withfactory, print command-specific Factory JSON output; withplan status, print Plan status JSON; withplan implement, print Plan implementation JSON.--format <mermaid|json>: choose thegraphoutput format.--version: print the Bachkator version.--workflow <name>: withfactory submit, select a workflow when a Factory has more than one workflow; withfactory list, filter by workflow.--title <text>: withfactory submit, set the Work Item title.--body <text>: withfactory submit, set the Work Item body.--body-file <path>: withfactory submit, read the Work Item body from a project-relative file.--priority <value>: withfactory submit, set the priority. Defaults tonormal.--label <value>: withfactory submit, attach a label. May be repeated.--dedupe-key <value>: withfactory submit, reuse an existing open item with the same factory, workflow, and key.--plan <value>: withfactory submit, store an opaque submitted plan reference.--reason <text>: withfactory cancel, record the cancellation reason; withfactory approve, record the approval reason.--phase <phase>: withfactory approve, select the gated phase to approve, such asplanordeploy.production.--poll-interval <duration>: withfactory start, set the queue poll interval. Defaults to5s.--renew-interval <duration>: withfactory start, set the daemon lease renewal interval. Defaults to10s.--lease-ttl <duration>: withfactory start, set the daemon lease expiration time. Defaults to30s.
Global flags (--file, --var, --profile, --json, --verbose, --version) may appear before the subcommand. Command-specific flags must appear after the subcommand they apply to, for example bach run --dry-run shell/test and bach plan implement --yes plans/example.md.
0: success.1: general failure, including target failures, validation errors, and uncaught runtime errors.2: invalid arguments or command usage, such as missing required positional arguments.
bach report writes public bach.agent_report.v1 quality report artifacts for review agents and scripts. It does not load a Bachfile and does not write the State Store directly; normal quality declarations ingest the artifact after the target finishes.
bach backend sqlite starts the bundled SQLite Backend Provider as a low-level JSON-RPC stdio process.
It is launched by Bach from resolved project.backend configuration for Backend writes, is not a Target,
is not listed by bach list, and waits for bach.backend.v1 protocol messages on stdin when run directly.
Destination resolution for every bach report subcommand is --path, then BACH_AGENT_QUALITY_REPORT_PATH, then $BACH_RUN_DIRECTORY/agent-report-v1.json. Report paths must stay under the current workspace or BACH_RUN_DIRECTORY unless --allow-external-path is set. BACH_AGENT_REPORT_PATH is reserved for Agent Target completion and merge reports and is not used by bach report.
Identity flags are shared across init, finding, and status: --role, --name, and --summary. report metric --name is the metric name; use BACH_REPORT_AGENT_NAME when metric-only calls need agent identity. Missing reports are auto-created with status = "success", summary Report initialized by bach report., agent.role from --role, BACH_REPORT_AGENT_ROLE, BACH_AGENT_ROLE, or reporter, and agent.name from --name or BACH_REPORT_AGENT_NAME. Subject metadata is populated from BACH_AGENT_TARGET, BACH_AGENT_SUBJECT_WORKSPACE, and BACH_AGENT_SUBJECT_COMMIT when present.
Use bach report finding --stdin to read one strict JSON finding object from stdin. Unknown fields are rejected. Adding a blocking finding records evidence but does not change final status; use bach report status failed or a quality gate over parsed policy metrics when the target should fail.
bach run accepts one or more target addresses such as shell/test, image/app,
pipeline/release, or agent/example. Multiple requested targets are planned as one deduplicated run, so shared
dependencies execute once. Requested targets are not ordered left-to-right; use a pipeline target
when strict sequencing is required.
bach run also accepts generated policy nodes such as policy.merge@agent.checkout_refactor.
Policy nodes must be run separately from normal targets. They fan out their required_targets in
the subject workspace and write policy evaluation JSON under that workspace's .bach/artifacts.
With --dry-run --json, the plan includes requested_targets with the canonical target addresses
that seeded the run. The legacy target field is the space-joined requested target list.
bach init creates starter Bach adoption files in a repository that does not already have them. Plain
bach init writes a minimal valid Bachfile and an AGENTS.md beside it with Bach-first operating
guidance. It refuses to overwrite either file. bach init --provider opencode writes a provider prompt
under .bach/init/, runs opencode run <prompt>, verifies staged provider files under
.bach/init/outputs/, then installs them without overwriting existing files. Use bach init --dry-run
to print planned writes without creating files, or bach init --dry-run --provider opencode to print the
provider command without writing the prompt or invoking OpenCode. Dry-run still reports overwrite
preflight failures.
bach provenance <path> [path ...] explains whether each path is generated by declared target outputs
or consumed by declared target inputs. Paths are relative to the project root unless absolute. Missing
declared outputs still map to their producer. Unknown paths succeed with empty producers and consumers.
Use bach provenance --json <path> for strict JSON output.
bach quality reads parsed quality reports from the state database. summary shows recent reports,
quality gates, slow targets, and top failing tests. Use metrics for normalized values such as
coverage.line.percent, findings for parsed Checkstyle/JUnit-style findings, and gates for threshold
results.
bach validate parses, decodes, and validates the configured Bachfile without planning or running
targets. It also skips graph plugin execution so editor and agent checks are fast and side-effect free.
Successful human output summarizes configured targets, aliases, inputs, and profiles:
Bachfile valid: 14 targets, 2 aliases, 3 inputs, 2 profiles
Invalid human output uses problem-matcher-friendly file:line:column: message lines and exits non-zero.
With --json, bach validate writes strict JSON to stdout and exits non-zero when diagnostics contain
errors:
{
"valid": false,
"files": ["Bachfile"],
"summary": {
"targets": 0,
"aliases": 0,
"inputs": 0,
"profiles": 0
},
"diagnostics": [
{
"severity": "error",
"file": "Bachfile",
"range": {
"start": {"line": 2, "column": 13},
"end": {"line": 2, "column": 25}
},
"message": "obsolete target reference \"shell/test\": use type.name, for example shell.test",
"code": "obsolete-target-reference"
}
]
}Diagnostic severities are strings and may be error, warning, info, or hint; current Bachfile
validation emits error and HCL warning severities. Diagnostic codes are stable identifiers such as
hcl-parse-error, hcl-decode-error, obsolete-target-reference, unknown-target-reference, and
duplicate-target.
bach affected [path ...] suggests the smallest useful configured targets for changed files. It is read-only and never runs targets.
With explicit paths, Bachkator matches those paths against each target's resolved inputs, including named inputs and plugin-provided inputs:
bach affected packages/api/src/foo.tsWith no paths, Bachkator uses the current Git staged, unstaged, and untracked files:
bach affectedOutput is sorted by target name. Each line includes the target name, the number of matching inputs, and up to the first three matching inputs.
Use bach provenance <path> [path ...] to explain which declared targets generate or consume files.
bach provenance docs/reference.md
bach provenance --json internal/runner/plan.goPaths are interpreted relative to the project root unless they are absolute. Missing files can still report provenance when they match declared target outputs.
Human output is optimized for agents deciding whether to edit a file directly or regenerate it from sources. For broad source declarations, generated files may also list consumers; this example is abridged:
docs/reference.md
generated: true
source: false
generated_by:
- shell/docs-generate
operation: go run ./cmd/bach-docs-gen
regenerate: bach run shell/docs-generate
consumed_by:
- shell/build
operation: sh -c mkdir -p dist && go build -ldflags '-X main.version=dev' -o dist/bach ./cmd/bach
status: unknown
JSON output emits one record per queried path:
{
"paths": [
{
"path": "docs/reference.md",
"generated": true,
"source": false,
"producers": [
{
"target": "shell/docs-generate",
"operation": "go run ./cmd/bach-docs-gen",
"regenerate_command": "bach run shell/docs-generate",
"outputs": ["docs/reference.md"],
"inputs": ["cmd/bach-docs-gen", "docs/reference"]
}
],
"consumers": [
{
"target": "shell/build",
"operation": "sh -c mkdir -p dist && go build -ldflags '-X main.version=dev' -o dist/bach ./cmd/bach",
"regenerate_command": "bach run shell/build",
"outputs": ["dist/bach"],
"inputs": ["cmd", "docs", "internal"]
}
],
"status": "unknown",
"reasons": []
}
]
}Directory inputs and outputs match files beneath them. Unknown paths return success with empty producers and consumers.
Use top-level alias blocks to preserve old command names while directing users and agents to canonical targets.
alias "staging-kristiyan-deploy" {
target = "pipeline.deploy-kristiyan"
deprecated = "Use pipeline.deploy-kristiyan."
}Aliases resolve before planning and execution, so dry-runs, locks, cache keys, logs, and run history use canonical target names. Aliases do not create executable target nodes.
deprecated is optional. When present, Bachkator prints the message when the alias is used and includes it in bach explain <alias>.
List aliases with:
bach list --aliasesAlias targets must point directly to real targets. Alias-to-alias chains are rejected at load time.
bach explain <target> prints target guidance without running anything. It includes description, when to use the target, cost, risk flags, dependencies, pipeline steps, inputs, outputs, and produced resources. If <target> is an alias, explain also prints the alias name, canonical target, and optional deprecation message. If <target> is a generated policy node, explain prints generated: true, the policy subject, subject workspace/commit scope, and required targets.
bach explain shell/test-api
bach explain policy.merge@agent.checkout_refactorUse explain before high-cost, remote, destructive, or unfamiliar targets.
Targets support optional guidance metadata:
shell "deploy-staging" {
description = "Deploy staging API"
when = "after image publish"
cost = "high"
remote = true
destructive = true
requires_confirmation = true
}Fields:
description: shown bybach listandbach explain.when: guidance for when humans or agents should run the target.cost: expected cost. Valid values arelow,medium, orhigh.remote: set totruewhen the target talks to external services.destructive: set totruewhen the target can delete, overwrite, or irreversibly change state.requires_confirmation: set totruewhen operators should confirm intent before running.
Risk metadata is inherited through depends_on and pipeline steps, so aggregate targets show and enforce the risk of the targets they run. Dry-runs are always allowed. Real execution of a target whose effective risk includes requires_confirmation must use --yes.
Bachkator loads .env from the project root into target operation environments when the file exists. Use --env-file .env.local to overlay another file on top of .env.
Environment precedence for operations is:
- parent process environment.
- project
.envvalues. --env-filevalues.- top-level Bachfile
envvalues. - selected
profileenv values, in CLI order. - Bachkator runtime values such as
BACH_GIT_COMMIT. - target
enventries.
Environment files support blank lines, # comments, optional export, KEY=value, and single- or double-quoted values.
Top-level env blocks define project-wide operation environment. Entries are stored and fingerprinted in sorted key order.
var bla {}
var "foo" {
default = "foo"
}
var foobar {
default = "${var.foo}bar"
}
env {
ENV_1 = "b"
ENV_2 = "${ENV_1} b ${var.bla}"
}Environment values can reference variables with var.name and earlier resolvable environment keys directly by name. HCL string literals use double quotes.
Profiles are named environment overlays for staging variants and operator-specific settings:
profile "staging" {
env {
NAMESPACE = "atelier-staging"
AWS_PROFILE = "atelier-staging"
PUBLIC_HOST = "staging.example.com"
}
}
profile "staging-kristiyan" {
env {
NAMESPACE = "atelier-staging-kristiyan"
}
}Select profiles with --profile. The flag may be repeated, and later profiles override earlier profile values:
bach --profile staging --profile staging-kristiyan run shell/renderSelected profile values overlay after top-level env and before target env. Unknown selected profiles are errors. Selected profile names and resolved profile values are included in target fingerprints.
Bachkator embeds the docs/ reference into the binary.
bach reference
bach reference project
bach reference pluginsbach reference lists all headings. bach reference <query> prints matching sections. It does not require a Bachfile, so agents can use it before project loading succeeds.
Use top-level import "path" declarations to split a Bachfile into local reusable fragments.
project "example" {
default = "shell.test"
}
import "./bach/go.bach"
import "./bach/docs.bach"Import paths are string literals resolved relative to the file containing the import. Imported files share the root project's scope, and their declarations behave as if pasted at the import site.
Imported fragments can contain variables, environment blocks, profiles, inputs, resources, plugins, aliases, policy blocks, quality blocks, and targets. They must not introduce a second effective project block.
Each canonical local file is loaded at most once. Re-importing the same file is allowed and deduped, while duplicate declarations from different files remain errors. Import cycles, missing files, and invalid imported files fail project loading with diagnostics that include the importing location or imported file path.
Remote imports, glob imports, environment-expanded paths, registries, and version pinning are not supported.
Every Bachfile needs one project block:
project "example" {
root = "."
default = "shell.test"
}Fields:
root: project working directory. Defaults to the Bachfile directory.default: target to run when no target is provided.
project.state is no longer supported. Omit backend to use the default bundled SQLite Backend Provider at .bach/state.db.
The omitted backend is equivalent to:
project "example" {
root = "."
backend {
type = "stdio"
command = ["bach", "backend", "sqlite"]
config = {
path = ".bach/state.db"
}
}
}Backend fields:
type: Backend transport. Phase 1 supports onlystdio.command: argv array for the Backend Provider. Phase 1 supports only["bach", "backend", "sqlite"].config: provider-owned object. The SQLite provider supportspath, resolved relative to the project root.
Root Bachfiles can import local Bachfile fragments to keep reusable target packs in separate files:
import "./bach/go.bach"
import "./bach/docs.bach"
project "example" {}Import paths must be string literals and are resolved relative to the file that contains the import declaration. Imported files are local files only; HTTP, Git, registry, glob, environment-expanded, and computed import paths are not supported.
Imported files share the same project scope as the root Bachfile. Targets, aliases, policies, variables, inputs, resources, plugins, profiles, and quality blocks from imported files participate in the same validation rules as root declarations, so duplicate declarations are errors. The root Bachfile must own the project block; imported files must not declare one.
The same canonical file can be imported more than once and is loaded only once. Import cycles fail during project loading with a diagnostic that includes the cycle path.
Bach Backend Providers speak bach.backend.v1 over stdio JSON-RPC with Content-Length framing. Headers are capped at 16 KiB and payloads are capped at 8 MiB.
The bundled provider entrypoint is:
bach backend sqliteProvider stdin and stdout are protocol-only. Diagnostics belong on stderr.
Initialization uses backend.initialize with the protocol version, Project name/root, and provider config. The bundled SQLite provider supports the capabilities runs, evidence_refs, quality_reports, findings, factory_queue, and plan_ledger.
Current method names are:
backend.initializebackend.shutdownruns.createruns.startTargetruns.finishTargetruns.finishruns.getruns.listevidence.recordRefevidence.listRefsquality.recordReportquality.recordReportsfindings.recordObservationfindings.getfindings.listCurrentfindings.listEventsfactory.enqueueWorkItemfactory.getWorkItemfactory.listWorkItemsfactory.cancelWorkItemfactory.acquireDaemonLeasefactory.renewDaemonLeasefactory.releaseDaemonLeasefactory.claimWorkItemfactory.updateWorkItemPhasefactory.completeWorkItemfactory.failWorkItemfactory.getDaemonStatusplans.recordLedgerplans.getLedger
runs.finish accepts a run finish payload with:
run: the final Run record.targets: changed target fingerprint records keyed by Target Address.target_runs: per-target execution records keyed by Target Address.evidence: evidence references associated with the completed Run.
Evidence references used by runs.finish, evidence.recordRef, and evidence.listRefs follow
bach.backend.evidence_ref.v1. They may include created_at as an RFC3339Nano timestamp; providers stamp
omitted values at write time.
quality.recordReports accepts a batch payload with reports and gates arrays. quality.recordReport accepts a single report.
factory.enqueueWorkItem accepts a Work Item, initial attempt, submitted event, and optional dedupe event.
factory.getWorkItem and factory.cancelWorkItem operate by factory name and Work Item ID.
factory.listWorkItems operates by factory name with an optional lifecycle status filter.
Daemon methods acquire and renew Factory leases, claim pending Work Items, record phase status, complete or
fail Work Items, and read daemon status without exposing table-level writes.
Work Item JSON follows docs/schemas/backend-factory-work-item-v1.schema.json.
plans.recordLedger appends one immutable Plan ledger and its evidence entries transactionally. Repeating the same ledger_id with an identical payload is idempotent; repeating it with a different payload returns conflict. plans.getLedger takes plan_id and returns the latest ledger by recorded_at, then ledger_id, or not_found when no ledger exists. Plan ledger JSON follows docs/schemas/backend-plan-ledger-v1.schema.json.
The approvals capability name is reserved for future approval operations; the current bundled provider does
not advertise it.
Public provider DTOs and helpers are available from:
github.com/applauselab/bachkator/pkg/backendprotocolgithub.com/applauselab/bachkator/pkg/jsonrpcstdio
Machine-readable schemas live under docs/schemas/backend-*.schema.json.
Domain errors are JSON-RPC errors with Bach error codes in error.data.code, including invalid_request, not_initialized, unsupported_capability, not_found, conflict, validation_failed, and internal.
Variables are declared with var blocks and referenced as var.name:
var "release_version" {
default = ""
}
shell "release" {
command = ["gh", "release", "create", var.release_version]
}Variable defaults can derive values with built-in computed functions:
var "image_tag" {
default = "${git_short_sha()}${git_dirty_suffix()}"
}
var "deps_tag" {
default = "deps-${file_hash("bun.lock", "package.json")}"
}Computed functions:
git_short_sha(): first 12 hex characters of the current project-root Git commit.git_dirty_suffix():-dirtywhen the project-root Git worktree has changes, otherwise an empty string.file_hash(paths...): first 12 hex characters of a deterministic content hash for files, globs, or directories under the project root.
Value precedence:
--var release_version=v0.1.0BACH_VAR_release_version=v0.1.0BACH_VAR_RELEASE_VERSION=v0.1.0defaultin thevarblock- empty string when no default is set
Variable defaults can call built-in functions:
git_short_sha(): first 12 hex characters of the project-root Git commit.git_dirty_suffix():-dirtywhen the project-root Git worktree has changes, otherwise an empty string.file_hash(paths...): first 12 hex characters of a deterministic content hash for files, globs, or directories under the project root.
var "image_tag" {
default = "${git_short_sha()}${git_dirty_suffix()}"
}
var "deps_tag" {
default = "deps-${file_hash("bun.lock", "package.json")}"
}Inputs name reusable file sets:
input "file" "go_sources" {
srcs = ["go.mod", "go.sum", "cmd", "internal"]
}Fields:
src: one file, directory, or glob.srcs: multiple files, directories, globs, or input references.
Targets can use raw paths or named inputs:
shell "test" {
command = ["go", "test", "./..."]
inputs = [input.file.go_sources]
}Directories are walked and content-hashed. Resources are not hashed directly.
Top-level prompt blocks register prompt files that agent/provider workflows can reference by name:
prompt "implementer" {
path = "prompts/agents/implementer.md"
description = "Default implementation-agent prompt"
version = "v1"
}path is required and must be project-relative. It must point to an existing regular file inside the project root after symlink resolution. description and version are passive metadata for operators and future provider integrations.
Prompt blocks do not execute anything and do not affect target fingerprints by themselves.
Resources model produced capabilities or artifacts without hashing large output trees:
resource "workspace_deps" {}
shell "install" {
command = ["bun", "install"]
produces = [resource.workspace_deps]
outputs = ["node_modules"]
}
shell "test" {
command = ["bun", "test"]
inputs = [resource.workspace_deps]
}When a target consumes a produced resource, Bachkator automatically adds an implicit dependency on the producer.
Shell targets run commands:
shell "build" {
description = "Build the binary"
when = "after tests pass, before packaging"
cost = "medium"
remote = false
destructive = false
requires_confirmation = false
depends_on = [shell.test]
quiet = true
lock = "container-builder"
timeout = "5m"
retry {
attempts = 3
backoff = "2s"
}
env {
CGO_ENABLED = "0"
GOARCH = "arm64"
GOOS = "linux"
}
command = ["go", "build", "-o", "dist/bach", "./cmd/bach"]
tools = [{ name = "go", command = ["go", "version"], version = "1.24+" }]
preflights = [{ name = "package registry session", command = ["bun", "pm", "whoami"], fix = "Run bun login." }]
inputs = [input.file.go_sources]
outputs = {
binary = "dist/bach"
}
success_when {
file_exists = "dist/bach"
}
}Fields:
description: shown bybach listandbach explain.when: guidance for when humans or agents should run the target. This is metadata only; usedepends_onor pipelinestepsto enforce order.cost: expected cost. Valid values arelow,medium, orhigh.remote: set totruewhen the target talks to external services.destructive: set totruewhen the target can delete, overwrite, or irreversibly change state.requires_confirmation: set totruewhen operators should confirm intent before running. Real execution then requires--yes; dry-run is still allowed.depends_on: explicit target dependencies.lock: optional in-run named lock. Ready targets with the same lock do not run concurrently in one Bachkator invocation.timeout: optional Go-style duration such as30s,5m, or1h. The timeout bounds target operation execution and completion-contract checks.retry: optional operation retry policy.attemptsis the total number of attempts;backoffis an optional duration to wait between failed attempts.command: argv array executed directly.shell: shell string executed via/bin/sh -c.tools: required host tools checked before execution. Each entry hasnameplus optionalcommand,version, andfixfields.commandis an exact probe command; Bachkator does not parse versions yet.preflights: credential or session probes checked after required tools and before target execution. Each entry hascommandplusnameorkind, and optionalfix. Bachkator treats these as generic host checks and does not hardcode provider-specific auth behavior.quiet: whentrue, write this target's progress and operation output only to.bach/runs/.../*.logunless--verboseis set.workdir: working directory relative to project root.env { ... }: sorted target environment block. Values can reference top-level env entries, earlier resolvable target env entries, process env, andvar.name. Commands reference target env at runtime with$NAMEor${NAME}.inputs: file paths, input references, plugin input references, or resources.outputs: paths expected after execution. Use a list for unnamed cache evidence or an object for named outputs such as{ junit = "reports/junit.xml" }.produces: inputs or resources produced by this target.success_when { ... }: optional post-execution checks. When omitted, command exit code is the success signal.fail_when { ... }: optional post-execution failure checks. Matching checks make the target fail even when the command exits successfully.
Use either command or shell, not both.
Targets with only depends_on are aggregate targets.
Dry-run reports required tools and preflights but does not fail when they are missing locally. Normal runs fail before starting target operations when a required tool is missing, a required tool probe exits non-zero, or a preflight exits non-zero. Preflight failures use preflight-failed in the run summary so expired sessions are distinct from target operation failures. Aggregate and pipeline targets require the union of child target tools and preflights.
Retry applies only to operation failures and completion-contract failures. Required tools, preflight failures, and quality gate failures are not retried.
Quality reports and gates are configured in top-level quality blocks so shell targets stay focused on commands.
outputs are concrete file paths. produces are logical input/resource identities made available to other targets. Quality reports consume outputs, not produces.
Use $(RUN_DIRECTORY) for per-run generated files such as test reports. Bach expands it before executing the operation and also exposes the value as BACH_RUN_DIRECTORY and RUN_DIRECTORY.
Use locks for shared local or remote resources while keeping high overall parallelism:
shell "test-db" {
lock = "postgres"
command = ["bun", "test", "db"]
}Locks are process-local to a single Bachkator run. They do not coordinate separate bach processes.
See bach reference completion-contracts for generic post-execution checks using success_when and fail_when.
Completion contracts are generic execution evidence. They do not replace outputs, which remain cache evidence. Contracts are evaluated only after the target operation exits successfully. When both success_when and fail_when are omitted for a shell target, Bach relies on the command exit code exactly as before.
shell "deploy" {
command = ["./deploy.sh"]
fail_when {
output_contains = "ROLLBACK"
}
success_when {
output_contains = "Deployment complete"
}
success_when {
command = ["./scripts/smoke.sh"]
}
}Supported completion checks are:
output_contains: match text in the target log after operation execution.file_exists: require a file path under the project root to exist.command: run a verification command from the target workdir with the target environment.
Each success_when or fail_when block must set exactly one check field.
Group targets name an unordered collection of existing targets. Use a group when several targets should run as one logical step but may execute in parallel subject to dependencies, locks, cache state, and preflights:
group "ci" {
targets = [shell.lint, shell.test, shell.build]
}Running bach run group/ci walks one deduplicated execution graph. Shared dependencies and
shared group members run at most once per run.
Groups can be used as pipeline steps:
group "checks" {
targets = [shell.lint, shell.test]
}
pipeline "release" {
steps = [group.checks, shell.build-release, shell.github-release]
}In that example, shell.build-release cannot start until all targets in group.checks and
their dependency closures complete. Inside the group, normal dependency and lock rules decide
what can run concurrently.
Fields:
description: shown bybach list.targets: existing target references to include in the unordered group.timeout: optional Go-style duration that bounds the whole group invocation, including all members.lock: optional lock held for the whole group invocation.
Groups are scoped completion targets. They do not run their own command and do not have
independent cache inputs or outputs; cache and fingerprints remain attached to the member
targets. Group-level timeout and lock wrap member execution, while member-level runtime
fields still apply to each individual target.
Pipeline targets run existing targets in a declared sequence. Use them for deploy flows where dependency concurrency is unsafe:
pipeline "deploy-staging" {
timeout = "15m"
steps = [
shell.render-staging,
shell.apply-staging,
shell.rollout-staging,
shell.smoke-staging,
]
}Pipeline steps may reference shell, image, or pipeline targets. Nested pipelines let you name reusable ordered lanes and compose them into a higher-level program:
pipeline "build-lane" {
steps = [shell.build-a, shell.build-b]
}
pipeline "merge-lane" {
steps = [shell.merge-a, shell.merge-b]
}
pipeline "delivery-program" {
steps = [pipeline.build-lane, pipeline.merge-lane, shell.regression]
}Pipeline cycles are rejected during config loading.
Fields:
description: shown bybach list.steps: existing target names to run in order.timeout: optional Go-style duration that bounds the whole pipeline invocation, including all steps.
bach run --dry-run pipeline/deploy-staging prints the pipeline and then each step in execution order. bach run pipeline/deploy-staging stops at the first failed step, so later steps do not run. Step targets remain runnable directly for debugging.
Pipeline targets inherit risk metadata from their steps. If any step has requires_confirmation = true, running the pipeline requires --yes; --dry-run still works without confirmation.
Pipeline retry is not enabled by default; use retry on individual shell steps that are safe to repeat.
Image targets synthesize OCI build commands:
resource "base_image" {}
image "base" {
builder = "container"
image = "example/base"
tags = ["local"]
dockerfile = "Dockerfile.base"
context = "."
platform = "linux/amd64"
produces = [resource.base_image]
}
image "app" {
builder = "container"
image = "example/app"
tags = ["latest"]
dockerfile = "Dockerfile"
push = true
build_args = {
BASE_IMAGE = image.base.tag
}
inputs = [resource.base_image, "Dockerfile", "cmd"]
}Fields:
builder: build executable. Defaults toOCI_BUILDER, thendocker.image: image name. Defaults to the image target name withoutimage/.tags: tags to apply.dockerfile: Dockerfile path. Defaults toDockerfile.context: build context. Defaults to..platform: optional platform.push: push every resolved tag after a successful build. Docker-compatible builders run<builder> push <tag>; Applecontainerrunscontainer image push <tag>.build_args: map of build args.build_args_list: list of already-renderedKEY=valuebuild args.lock: optional in-run named lock. Useful for builders such as Applecontainerthat share host resources.inputs,depends_on,produces: same behavior as shell targets.
image.name.tag resolves to the first full image tag for use in build args.
Bachkator does not need a special release target type. Use a shell target with the GitHub CLI:
var "github_repo" {
default = "owner/repo"
}
var "release_version" {
default = ""
}
shell "github-release" {
depends_on = [shell.build]
command = [
"gh",
"release",
"create",
var.release_version,
"dist/app",
"--repo",
var.github_repo,
"--target",
"$BACH_GIT_COMMIT",
"--title",
"App ${var.release_version}",
"--generate-notes",
"--latest",
]
}Run it with:
bach --var release_version=v0.1.0 run shell/github-releasegh release create <tag> creates the Git tag when it does not exist. --target "$BACH_GIT_COMMIT" pins the tag to the current commit.
policy blocks define generated policy nodes that run required targets against a subject workspace.
The generated node address is policy.<name>@<subject>.
policy "merge" {
subject = "agent.checkout_refactor"
subject_workspace = ".bach/agents/checkout_refactor"
subject_commit = "0123456789abcdef0123456789abcdef01234567"
required_targets = [group.gate]
}Fields:
subject: required subject identifier used in the generated address.subject_workspace: optional workspace where required targets run. Relative paths resolve from the project root.subject_commit: optional Git commit that the subject workspace must have checked out before policy evaluation starts.required_targets: required target references such asgroup.gateorshell.test.
Generated policy nodes are hidden from the default target list. Use:
bach list --generated
bach explain policy.merge@agent.checkout_refactor
bach run --dry-run policy.merge@agent.checkout_refactor
bach run policy.merge@agent.checkout_refactorPolicy nodes can be run directly. Implementer agents that reference a policy invoke a generated
policy/<name>@agent.<subject> target after implementation evidence passes, so required targets,
reviewers, policy logs, quality parsing, and applied-policy verdicts are recorded under a visible policy
target run instead of hidden inside the implementation agent. Required targets keep their normal target
identity in run records and logs, but execute with the subject workspace as project root. State and
artifacts for standalone subject-policy fan-out are written under the subject workspace's .bach
directory, so cached results from the main checkout cannot satisfy subject policy checks.
When subject_commit is set, required targets receive these environment variables:
BACH_POLICY_SUBJECTBACH_POLICY_SUBJECT_COMMITBACH_POLICY_NODE
Standalone subject-policy fan-out writes evaluation JSON to:
<subject_workspace>/.bach/artifacts/<policy-node>.json
The JSON includes policy, subject, subject_workspace, subject_commit, status,
required_targets, findings, and created_at. Failed required targets add
required_target_error. If required targets mutate files outside .bach and declared target outputs,
the policy fails with policy-required-target-mutated-workspace. If subject_commit does not match
the subject workspace HEAD, the policy fails with policy-subject-commit-mismatch.
Quality-enabled policy runs, including generated policy targets invoked by implementer agents, also write subject-keyed applied-policy verdict artifacts in the project root:
.bach/artifacts/policies/<run-id>/<sanitized-target>.json
Sanitization replaces /, :, and spaces with -.
Merge agents consume the latest matching applied-policy artifact only when the artifact verdict passed, its
subject_workspace matches the merge subject workspace, its subject_commit matches the subject
workspace HEAD, its policy_target names the generated policy target for the subject's current
policy, and that exact policy target succeeded in the recorded run.
Plugins are typed external executables in any language. The plugin type determines when Bachkator runs the executable and which stdout contract it must satisfy.
Existing plugins default to type = "graph". Graph plugins run while loading the Project and emit graph evidence JSON to stdout.
plugin "ts_imports" {
type = "graph"
command = ["bun", "examples/plugins/ts-import-graph.ts"]
sources = {
api_tests = ["packages/api/tests/**/*.test.ts"]
}
}
shell "test-api" {
command = ["bun", "test", "packages/api/tests"]
inputs = [plugin.ts_imports.api_tests]
}Graph plugin environment:
BACH_PLUGIN_NAME: plugin name.BACH_PROJECT_ROOT: project root.BACH_PLUGIN_INPUTS: resolved plugin input paths, newline-separated.BACH_PLUGIN_SOURCES: JSON-encodedsourcesmap.
Graph plugin stdout contract:
{
"inputs": {
"api_tests": ["packages/api/src/main.ts"]
},
"targets": {
"shell.test-api": {
"depends_on": ["shell.generated"],
"inputs": ["generated.ts"]
}
}
}Bachkator merges graph plugin-provided depends_on and inputs into existing targets before validation, fingerprinting, and scheduling.
Graph plugins should not perform side effects. They run on graph load, so bach list also runs graph plugins.
Quality plugins use type = "quality". They do not run while loading the graph. They run only after a target command succeeds and a quality report declaration references them with parser = plugin.<name>.
plugin "eslint_quality" {
type = "quality"
command = ["bun", "scripts/bach/parse-eslint-quality.ts"]
timeout = "10s"
env = ["MODE=strict"]
}Quality plugins receive the report path as the first command argument and through environment metadata such as BACH_QUALITY_REPORT_ABS_PATH, BACH_QUALITY_KIND, BACH_TARGET, and BACH_RUN_ID. Stdout must contain only normalized quality JSON; stderr is diagnostics and is copied to the target log.
See examples/plugins/quality-parser for a complete quality parser plugin example. The normalized stdout schema lives at docs/schemas/quality-plugin-report.schema.json.
Quality plugin fields:
commandorshell: parser executable. One is required; they are mutually exclusive.timeout: optional parser timeout. Defaults to30s.env: optional environment entries layered onto Bach's runtime environment.
Quality plugins do not support sources or inputs. Graph plugins do not support timeout.
Quality plugin environment:
BACH_PLUGIN_NAME: plugin name.BACH_PLUGIN_TYPE:quality.BACH_PROJECT_ROOT: project root.BACH_RUN_ID: current run ID.BACH_RUN_DIRECTORY: target run directory.BACH_TARGET: target address being parsed, such asshell/lint.BACH_QUALITY_KIND: report kind, such aslintortests.BACH_QUALITY_REPORT_PATH: report path as declared in the Bachfile after env expansion.BACH_QUALITY_REPORT_ABS_PATH: absolute report path.
examples/plugins/ts-import-graph.ts is self-contained and only needs Bun. It does not require the typescript package.
It supports:
- glob source entries.
- relative imports.
.jsspecifiers resolving to.tsand.tsxfiles.- JSON leaf inputs.
@app/*workspace imports throughpackage.jsonexports.- TSX parsing via Bun's
tsxloader. - shebang stripping before scanning imports.
examples/plugins/bun-package-graph.ts is an example workspace-level plugin for Bun monorepos that use @app/* packages.
plugin "bun_packages" {
command = ["bun", "examples/plugins/bun-package-graph.ts"]
sources = {
api = ["packages/api"]
}
}
shell "test-api" {
command = ["bun", "test", "packages/api"]
inputs = [plugin.bun_packages.api]
}The plugin reads root package.json workspaces, package names, exports, dependencies, and dev dependencies. For each configured source package, it emits a deterministic plugin input containing that package and its transitive workspace dependency closure, so changes in shared packages affect dependent target suggestions through bach affected.
A target is cacheable when it declares inputs or outputs.
The fingerprint includes:
- operation configuration.
- target environment.
- dependency fingerprints.
- resolved input file contents.
- output existence.
Fingerprints are stored in SQLite table target_state in .bach/state.db.
When a cacheable target is stale, Bach prints the cache invalidation reasons before the operation. Dry-run JSON includes the same values in targets[].cache.reasons so agents can inspect why a target will run without executing it.
Stale reasons include changed inputs, changed environment, changed operation configuration, dependency fingerprint changes, missing outputs, forced runs, missing cache records, and legacy fingerprint changes from older cache state.
Every non-dry-run invocation records a run in SQLite table runs. Every non-dry-run target execution records a row in target_runs. Dry-runs are read-only with respect to persistent state: they may read existing cache records to explain freshness or staleness, but they do not create .bach/state.db and do not persist runs, target runs, artifacts, fingerprints, or cache state.
Output is streamed to both the terminal and per-target log files:
.bach/runs/<run-id>/<target>.log
Use bach runs list to list the 10 most recent prior non-dry-run runs, statuses, timestamps, and log directories. Use bach runs list --runs-limit 0 to list all recorded non-dry-run runs.
Use bach runs inspect <run-id> to inspect one completed run. The human output highlights failed targets, exit codes, log paths, parsed quality reports, failed quality gates, and declared preflight/tool fixes. Use --json for an agent-readable run export:
bach runs inspect --json 20260608T120000.000000000ZThe JSON object includes:
run_id,requested_target,status,started_at,finished_at, andlog_dir.targets[]with each target execution's name, status, operation, log path, artifact paths, quality report summaries, agent reports, applied-policy summaries, provider metadata, subject metadata, and merge evidence when present.failed_targets[]withtarget,status, optionalexit_code,operation,log_path,artifacts,quality, preflight/tool diagnostics, and a boundedlog_excerpt.quality.reports[]with report path, parser status, metric count, finding count, and parser message when present.quality.failed_gates[]with metric, operator, threshold, actual value, and message.suggested_fixes[]from declared tool or preflightfixfields.
The JSON export intentionally links to local artifact and log paths instead of embedding every raw finding or full log line. Treat those artifacts as local evidence that may contain provider output or sensitive project details.
Use bach logs <run-id> for concise log slices without opening large files manually:
bach logs 20260608T120000.000000000Z --failed --last 80
bach logs 20260608T120000.000000000Z --target shell/test --errors--failed limits output to failed target logs. --target selects one target. --last N bounds output. --errors keeps lines containing likely error/failure terms.
Each run ends with a concise terminal summary, including the run ID, status, requested target or targets, duration, log directory, and target status counts. A multi-target invocation stores the requested target list as a space-joined value in run history:
run 20260608T120000.000000000Z success target=shell/test duration=1.2s logs=.bach/runs/20260608T120000.000000000Z
targets: success=3 cached=2 failed=0 dry-run=0 running=0
run 20260608T120500.000000000Z success target=shell/lint shell/test duration=2.4s logs=.bach/runs/20260608T120500.000000000Z
targets: success=4 cached=1 failed=0 dry-run=0 running=0
--log-only suppresses command stdout/stderr in the terminal while keeping Bach progress, quality progress, and the final summary visible. quiet = true targets suppress their command output and target progress unless --verbose is set. Full command target output is still written to the target log so agents can report the outcome and log location. Provider adapters may redact normal progress logs and keep complete provider telemetry in private run artifacts. Failed runs include the last 20 non-empty lines from the first failed target log; successful runs do not print log excerpts.
Every target operation receives Git context from the project root:
BACH_GIT_BRANCH
BACH_GIT_COMMIT
BACH_GIT_DIRTY
BACH_GIT_DIRTY_SUFFIX
BACH_GIT_STAGED_FILES
BACH_GIT_UNSTAGED_FILES
BACH_GIT_UNTRACKED_FILES
BACH_GIT_CHANGED_FILES
Command arrays and shell strings expand $NAME and ${NAME} from the runtime environment before execution.
Targets publish concrete report files as named outputs. Quality blocks attach parsers and thresholds to those outputs without bloating the target command definition:
shell "test-api" {
command = ["bun", "test", "--reporter", "junit", "--junit-path", "$(RUN_DIRECTORY)/junit.xml", "--coverage"]
outputs = {
junit = "$(RUN_DIRECTORY)/junit.xml"
lcov = "$(RUN_DIRECTORY)/lcov.info"
lint = "$(RUN_DIRECTORY)/checkstyle.xml"
}
}
quality "test-api" {
junit {
path = shell.test-api.outputs.junit
}
cov {
path = shell.test-api.outputs.lcov
}
lint {
path = shell.test-api.outputs.lint
}
quality_gate {
metric = "tests.failed"
max = 0
}
quality_gate {
metric = "coverage.line.percent"
min = 80
}
}For linters that can emit Checkstyle, let Bachkator own the failure decision by forcing the linter command to exit zero and enforcing the report through a quality gate:
shell "lint" {
command = [
"golangci-lint",
"run",
"--issues-exit-code=0",
"--output.checkstyle.path=$(RUN_DIRECTORY)/checkstyle.xml",
"--output.text.path=stdout",
]
outputs = {
checkstyle = "$(RUN_DIRECTORY)/checkstyle.xml"
}
}
quality "lint" {
lint {
path = shell.lint.outputs.checkstyle
format = "checkstyle-xml"
}
quality_gate {
metric = "issues.total.count"
max = 0
}
}Unqualified quality targets default to shell targets, so quality "test-api" attaches to shell.test-api. Use quality "image.build" or quality "pipeline.release" when targeting other target types.
After the target operation exits successfully, Bachkator parses declared reports into the SQLite state database, then evaluates gates. A failing gate marks the target and run as quality-failed.
Quality blocks can also attach Rego policies that evaluate already-normalized report evidence:
quality "shell.security-scan" {
reports = [
{ kind = "security", format = "agent-report-v1", path = "$(RUN_DIRECTORY)/security.json" },
]
rego_policy {
path = "policies/no-critical-security.rego"
package = "bach.policy"
}
}rego_policy.path is required and is project-relative. package is optional, but when set Bach defaults allow to data.<package>.allow and findings to data.<package>.findings. If package is omitted, set explicit allow and findings queries. Multiple rego_policy blocks may attach to one quality block; every policy must allow the target.
Bach evaluates Rego in-process after parsing declared reports and before final quality status. Rego receives normalized JSON only; raw report file contents are not included:
{
"schema": "bach.rego_input.v1",
"run": { "id": "20260612T150325.460845000Z" },
"target": { "name": "shell/security-scan", "kind": "shell" },
"git": { "commit": "abc123", "branch": "main", "dirty": false },
"reports": [
{
"kind": "security",
"format": "agent-report-v1",
"path": ".bach/runs/.../security.json",
"status": "success",
"message": "",
"metrics": [],
"findings": [
{
"kind": "security",
"severity": "critical",
"rule": "CVE-2026-1234",
"message": "openssl has critical vulnerability",
"file": "go.sum",
"line": 42,
"duration_ms": 0
}
]
}
],
"metrics": { "policy.security.blocking_findings.count": 1 },
"findings": [
{
"kind": "security",
"severity": "critical",
"rule": "CVE-2026-1234",
"message": "openssl has critical vulnerability",
"file": "go.sum",
"line": 42,
"duration_ms": 0
}
]
}Policy output must expose a single boolean allow. findings may be undefined, empty, or a collection of normalized finding objects with at least kind:
package bach.policy
default allow := true
allow := false if {
some i
f := input.findings[i]
f.kind == "security"
f.severity == "critical"
}
findings := [finding |
some i
f := input.findings[i]
f.kind == "security"
f.severity == "critical"
finding := {
"kind": "security-policy",
"severity": "error",
"rule": "no-critical-security-findings",
"message": sprintf("Critical security finding: %s", [f.message]),
"file": f.file,
"line": f.line,
}
]Bach stores each Rego decision as a synthetic quality report with kind = "policy" and format = "rego-policy-v1". Compile errors, query errors, non-boolean allow, invalid findings, and allow = false mark the synthetic report failed and make the target quality-failed when the target command otherwise succeeded. Rego-emitted findings are visible through bach quality findings and bach runs inspect --json <run-id>. Rego path values resolve from the project root, not a shell target's work_dir. Rego source files and query configuration (package, allow, and findings) participate in target fingerprints, so policy edits and query changes invalidate cached targets. Bach evaluates policies with network and runtime-introspection builtins such as http.send and opa.runtime disabled.
Use quality plugins to convert raw project-specific report files into normalized metrics and findings. Use Rego policies to decide whether the normalized evidence across one or more reports is acceptable.
If the target command exits non-zero but declared report files already exist, Bachkator still attempts best-effort parsing before finalizing the target as failed. Parsed reports and gate results are saved as diagnostic evidence, so commands such as bach quality failing-tests and bach runs inspect --json <run-id> can help diagnose the failed command. Missing report files are tolerated on this path because the command failure may have prevented report creation. Parser and gate failures after a command failure are recorded as secondary diagnostics; they do not change the primary target status from failed to quality-failed.
$(RUN_DIRECTORY) is a Bach runtime placeholder expanded before operation execution. It points at a per-target directory under the current run, for example .bach/runs/<run-id>/shell__test-api. Bach also exposes the same value as BACH_RUN_DIRECTORY and RUN_DIRECTORY environment variables.
Supported initial formats:
junit-xml: test counts, durations, and failing tests.checkstyle-xml: lint/static-analysis findings and severity counts.lcov: line coverage, common in JavaScript/Bun projects.cobertura-xml: generic line and branch coverage.jacoco-xml: JVM coverage and complexity counters.codecov-json: coverage JSON with top-level ortotalscoverage values.go-cover: Go coverage profiles, used by Bachkator's own test target.gocyclo: optional complexity reports.agent-report-v1: Bach Agent Report evidence from implementation or reviewer agents.
Use the generic reports list when a target emits multiple evidence files or a format does not fit one of the shorthand blocks:
quality "shell.agent-fixture" {
reports = [
{ kind = "agent", format = "agent-report-v1", path = ".bach/artifacts/agents/implementer.json" },
{ kind = "docs", format = "agent-report-v1", path = ".bach/artifacts/agents/docs.json" },
{ kind = "org-policy", parser = "org_policy", path = ".bach/artifacts/org-policy.txt" },
]
}Each report entry must set kind, path, and exactly one of format or parser. In list form, parser is the plugin name as a string. In shorthand blocks such as lint {}, use parser = plugin.<name>.
Project-specific report formats can use a quality plugin:
plugin "atelier_lint" {
type = "quality"
command = ["bun", "scripts/bach/parse-lint-quality.ts"]
}
quality "shell.lint" {
lint {
path = ".bach/artifacts/lint.json"
parser = plugin.atelier_lint
}
quality_gate {
metric = "issues.total.count"
max = 0
}
}Quality plugin stdout must be normalized JSON:
{
"metrics": [
{ "name": "issues.total.count", "value": 0, "unit": "count" }
],
"findings": []
}The JSON must include metrics, findings, or both. Unknown top-level fields are rejected. Metric name and value are required; finding kind is required.
The JSON Schema lives at docs/schemas/quality-plugin-report.schema.json and is embedded in the bach binary. Agents can read it with bach reference quality-plugin-report-schema.
Use format = "junit-xml" and other built-in format strings for built-in parsers. Use parser = plugin.<name> for quality plugins. A report declaration must not set both format and parser.
agent-report-v1 expects this JSON envelope:
{
"schema": "bach.agent_report.v1",
"agent": { "role": "docs-sweeper", "name": "fixture" },
"subject": { "target": "shell/api" },
"status": "success",
"summary": "docs are current",
"metrics": [{ "name": "documentation.changed_files.count", "value": 1, "unit": "count" }],
"findings": []
}schema must be bach.agent_report.v1, agent.role is required, and finding kind is required. status may be omitted in hand-authored compatibility artifacts and ingests as success; reports written by bach report always use explicit success or failed. Agent-supplied custom metrics must have non-empty names and must not use the reserved agent.* or policy.* namespaces. Bach emits standard metrics such as agent.<role>.status.success, agent.<role>.report.count, agent.<role>.findings.count, policy.<role>.findings.count, and policy.<role>.blocking_findings.count. Roles are normalized by replacing /, spaces, and - with . or _ as needed for metric names.
Agent report findings with severity blocker, blocking, critical, error, failure, or failed count as blocking findings. A non-success agent report status marks the parsed evidence as failed and makes the target quality-failed unless the target had already failed for another reason.
Agents can create and update this envelope without hand-writing JSON:
export BACH_AGENT_QUALITY_REPORT_PATH=.bach/artifacts/agents/docs.json
bach report init --role docs-sweeper --name opencode --summary "Review started"
bach report finding --kind docs --severity error --rule stale-reference --message "CLI docs are stale"
bach report metric --name review.docs.checked_files.count --value 12 --unit count
bach report status success --summary "Docs review passed"bach report finding --stdin accepts one strict JSON finding object with fields such as kind, severity, rule, message, file, line, and duration_ms; unknown fields and trailing JSON are rejected. Destination resolution is --path, then BACH_AGENT_QUALITY_REPORT_PATH, then $BACH_RUN_DIRECTORY/agent-report-v1.json. Report paths must stay under the current workspace or BACH_RUN_DIRECTORY unless --allow-external-path is set. The helper writes artifacts only; the target must still declare the file with format = "agent-report-v1" for quality ingestion and State Store persistence.
Reviewer policy aggregation uses the internal agent-report-json format. It is distinct from the
public agent-report-v1 quality envelope: reviewers write mode, role, status, subject,
metrics, findings, and message fields to the path injected by BACH_AGENT_REPORT_PATH. Bach
generates the exact reviewer report schema in each reviewer prompt.
Metric names are policy-owned and must be unambiguous. Duplicate metric names within one report fail with policy-metric-collision. Duplicate metric names across reports also fail, except repeated agent-report-v1 count metrics ending in .count, which Bach aggregates for repeated same-role evidence. Use organization-specific plugin metrics under a unique namespace such as policy.org.*.
Bach writes an applied policy artifact for each quality-enabled target at:
.bach/artifacts/policies/<run-id>/<sanitized-target>.json
Sanitization replaces /, :, and spaces with -.
The artifact uses schema bach.applied_policy.v1 and records the run id, target, producing policy_target when applicable, passed or failed verdict, minimized report evidence, gate results, and creation time. For generated policy targets, target is the reviewed subject target while policy_target is the generated policy target that produced the verdict; merge agents require that exact policy_target to have succeeded in the recorded run. It intentionally stores counts and statuses rather than full finding messages to avoid creating an extra broad evidence export surface.
Quality parser failures and quality gate failures mark the target quality-failed. Cached targets do not rerun quality parsers or gates; use --force when fresh quality evidence is required.
Targets can opt into retrying gate failures:
retry {
attempts = 3
backoff = "5s"
retry_on_quality_gate_failure = true
}Only gate failures retry. Parser failures do not retry because they usually indicate a broken parser/report contract.
Query stored quality data:
bach quality summary
bach quality metrics
bach quality findings
bach quality gates
bach quality slow-targets
bach quality failing-testsUseful gate metrics include tests.failed, tests.duration.ms, coverage.line.percent, coverage.branch.percent, issues.total.count, issues.error.count, issues.warning.count, complexity.max, and complexity.avg.
Agent Targets run implementation and merge providers through generated prompt and context artifacts.
Implementer agents can attach reviewer policies that run mode = "review" agents and publish
quality-gated policy evidence. Merge agents consume that passing policy evidence before invoking a
serialized provider.
provider "opencode" {
type = "opencode"
}
provider "generic_opencode" {
type = "agent"
command = ["opencode", "run"]
}
prompt "implementer" {
path = "prompts/agents/implementer.md"
description = "Default implementation instructions"
version = "v1"
}
prompt "architecture_review" {
path = "prompts/agents/architecture-review.md"
}
prompt "merge" {
path = "prompts/agents/merge.md"
}
agent_template "feature_implementer" {
mode = "implement"
provider = provider.opencode
role = "implementer"
prompt = prompt.implementer
workspace {
path = ".bach/agents/feature-implementer"
}
git {
branch = "bach/agents/feature-implementer"
}
}
agent "checkout_refactor" {
template = agent_template.feature_implementer
plan = "plans/checkout-refactor.md"
workspace {
mode = "clone"
path = ".bach/agents/checkout_refactor"
}
git {
branch = "bach/agents/checkout_refactor"
commit = "required"
}
}
agent "architecture_review" {
mode = "review"
provider = provider.generic_opencode
role = "architecture-reviewer"
prompt = prompt.architecture_review
}
agent "merge_checkout_refactor" {
mode = "merge"
provider = provider.generic_opencode
prompt = prompt.merge
subject = agent.checkout_refactor
}
policy "merge_review" {
reviewers = [agent.architecture_review]
quality_gate {
metric = "findings.error.open.count"
max = 0
}
}Provider blocks:
type:opencodefor the first-class OpenCode provider, oragentfor a generic command provider.command: required only for generictype = "agent"providers. Bach expands environment variables and appends the generated prompt path as the final argument.
First-class OpenCode providers do not accept command and are supported for mode = "implement" Agent Targets. Bach owns the argv and invokes opencode run --format json "Follow the attached Bach agent prompt." --file <generated-prompt> so it can capture JSONL provider evidence and session IDs while ensuring OpenCode receives the generated Bach contract. On improvement attempts after attempt 1, Bach resumes the captured session with opencode run --format json --session <sessionID> "Follow the attached Bach agent prompt." --file <generated-prompt>. Review and merge agents that should run OpenCode directly can use a generic type = "agent" provider with command = ["opencode", "run"].
OpenCode JSONL is provider evidence, not target success evidence. Malformed JSONL or a successful OpenCode process that emits no sessionID fails the provider attempt. Bach mirrors OpenCode assistant text events and tool names to the target log/stdout as readable progress while preserving the complete provider event stream, including tool inputs, outputs, titles, and descriptions, in raw JSONL. Normal progress output does not include raw tool arguments.
Raw OpenCode JSONL capture is capped at 10 MiB per attempt, individual JSONL events are capped at 1 MiB, and mirrored provider text/tool call summaries are capped at 1 MiB. Exceeding a cap fails the provider attempt after Bach drains the provider process output.
Prompt blocks:
path: project-relative Markdown instructions file. Agent Targets that reference the prompt treat the file as an implicit input.description: optional passive metadata.version: optional passive metadata.
Prompt files provide reusable task guidance only. Bach always appends generated context and the required
structured report schema to the prompt passed to the provider. If an agent omits prompt, Bach uses a
baked-in default task prompt and still injects the same report schema.
Agent Template blocks:
agent_template "<name>"declares reusable Agent Target defaults; it is not a Target, cannot run, and is hidden frombach list.provider: required typed reference such asprovider.opencode.mode: optionalimplement,review, ormerge; defaults toimplement.role: optional report/log identity metadata.prompt: optional typed reference such asprompt.implementer.policy: optional typed reference supported only whenmode = "implement".workspaceandgituse the same block shape as Agent Targets.planandsubjectare not valid on templates because concrete agents or future Factory Work Items provide concrete context.- Template placeholders are valid only in
workspace.pathandgit.branch:${work_item.id},${work_item.slug},${plan.id},${workstream.id},${factory.name}, and${workflow.name}. - Concrete
agentblocks can settemplate = agent_template.<name>; explicit fields and blocks on the agent override inherited template defaults. - Runnable Agent Targets must be concrete.
bach validaterejects an agent that still has unresolved template placeholders after inheritance.
Provider processes are untrusted with respect to Bach-owned policy evidence. During provider execution,
Bach fails the attempt if .bach/artifacts/policies, target cache state, policy-target run state, Plan ledger/evidence rows, or Factory approval records change outside Bach's own writes.
Implementer and reviewer providers require the main checkout to be clean before invocation and fail if
the provider changes main checkout HEAD, branch, Git metadata, ignored files, or non-.bach status.
Agent fields:
template: optional typed reference such asagent_template.feature_implementer; inherited values are applied before validation.mode:implement,review, ormerge. Managed workspaces and git evidence apply toimplementagents.provider: required typed reference such asprovider.opencode.role: optional report/log identity metadata.prompt: optional typed reference such asprompt.implementer.plan: required project-relative implementation plan path formode = "implement"; not used by review or merge agents. The plan file is an implicit input.subject: required formode = "merge"; must be an explicitagent.<name>reference to the implementer being merged.policy: optional typed reference such aspolicy.merge_review; supported on implementer agents.workspace.mode: defaults tocloneand must beclonein v1.workspace.path: defaults to.bach/agents/<agent-name>and must stay under.bach/agents.git.branch: defaults tobach/agents/<agent-name>.git.commit: defaults torequired; allowed values arerequiredandoptional.improve.max_attempts: optional number of policy-informed attempts for implementer agents.improve.until: currently supportspolicy.passed.
Agent Targets always execute when requested; prompt and plan inputs still participate in bach affected.
Bach creates or reuses the workspace clone for each run. New workspaces are cloned from the project root and
checked out on the configured branch. Existing workspaces must be clean git clones at run start, and the
project HEAD must already be an ancestor of the workspace HEAD; Bach fails the target instead of resetting or
cleaning a dirty or stale workspace.
Bach writes implementation attempt artifacts under .bach/runs/<run-id>/<agent-target>/attempt-N/:
agent-prompt.md: provider prompt combining the optional prompt file, required plan, workspace path, context path, report path, and commit instructions.agent-context.json: structured metadata including target, mode, attempt, provider, prompt, plan, workspace, branch, report path, and context path.agent-report.json: provider completion report path.provider-events.raw.jsonl: raw OpenCode JSONL events whentype = "opencode".provider-session.json: OpenCode session evidence including session ID, workspace path, dirty status, raw event path, and executed argv whentype = "opencode"and a session ID was observed.provider-summary.json: normalized OpenCode provider telemetry whentype = "opencode".finish_reason,tokens, andcostare present only when OpenCode emits them;tokenspreserves provider-shaped token payloads.
Provider session and summary artifacts use the machine-readable contracts in docs/schemas/provider-session.schema.json and docs/schemas/provider-summary.schema.json.
Merge artifacts are written directly under .bach/runs/<run-id>/<merge-agent-target>/ as
merge-prompt.md, merge-context.json, and merge-report.json.
Implementation and reviewer provider commands run in the managed workspace. Merge provider commands run in the main project checkout. Agent providers should report the provider base command, such as ["opencode", "run"]; Bach-managed OpenCode flags such as --format json, --session, and the generated prompt path are captured in provider session evidence instead.
Bach also exposes convenience environment variables:
BACH_AGENT_PROMPT_PATHBACH_AGENT_CONTEXT_PATHBACH_AGENT_REPORT_PATHBACH_AGENT_WORKSPACEBACH_AGENT_TARGETBACH_AGENT_ATTEMPTBACH_AGENT_MAX_ATTEMPTSBACH_AGENT_FEEDBACK_BUNDLEBACH_AGENT_ATTEMPT_DIRECTORYBACH_AGENT_MODEBACH_AGENT_ROLEBACH_PROJECT_ROOT
The prompt file remains the primary invocation contract; environment variables are convenience helpers.
Reviewer policies:
policyblocks declarereviewers, a list ofagent.<name>references whose targets must usemode = "review".quality_gateblocks in a policy are enforced by the generated policy target for implementer agents that reference the policy.- After implementation evidence passes, Bach invokes a generated policy target named
policy/<name>@agent.<subject>so policy work has its own target run, log, quality report, and applied-policy artifact. - Attached policy
required_targetsrun in the subject workspace after implementation evidence passes and before reviewer fan-out. - Required-target failures are converted into policy findings, and Bach validates the subject workspace commit, branch, and cleanliness before reviewers run.
- After an implementer creates valid git evidence, reviewers run in parallel inside the managed workspace.
- Reviewers receive
BACH_AGENT_MODE=review,BACH_AGENT_ROLE,BACH_AGENT_REPORT_PATH, andBACH_AGENT_SUBJECT_*environment variables. - Reviewer providers receive a generated reviewer prompt that combines the optional reviewer prompt file, subject metadata, and the required reviewer quality-evidence JSON schema.
- Bach aggregates reviewer findings into the generated policy target's
policy-report.jsonusing theagent-report-jsonquality format. - Policy reports provide
findings.open.countandfindings.error.open.countmetrics for quality gates.
Default reviewer prompt files are provided under prompts/agents/architecture-review.md,
prompts/agents/docs-sweeper.md, and prompts/agents/security-review.md. Docs-sweeper and security
reviewers should emit blocking findings when user-visible/agent-visible docs or security expectations are
not met.
Improvement loops:
improve { max_attempts = N, until = "policy.passed" }starts another provider attempt after failed policy evidence.- Failed attempts write
feedback-bundle.jsonunder the attempt directory with verdict, findings, failed gates, reviewer summaries, and evidence paths. - OpenCode improvement attempts resume the previous captured session by default. Before resuming, Bach verifies the previous session evidence, target, workspace path, feedback bundle, and current workspace branch. Bach records whether the workspace is dirty before invoking OpenCode and never cleans or resets dirty workspace contents.
attempt-history.jsonrecords each attempt, provider session ID, and provider artifact paths while the final policy verdict is based on the latest attempt.retryremains separate fromimprove: retry repeats the same failed target execution, while improve starts a new policy-informed agent attempt.
Merge agents:
mode = "merge"targets default tolock = "merge-lane"unless a lock is set explicitly.- Bach refuses to invoke the merge provider until the subject's latest matching applied policy artifact under
.bach/artifacts/policies/<run-id>/<sanitized-subject>.jsonhas a passing verdict, whosesubject_workspacematches the merge subject workspace, whosesubject_commitmatches the subject workspace HEAD, and whosepolicy_targetnames the generated policy target that succeeded in the recorded run. For example,agent/checkout_refactoris written asagent-checkout_refactor.json. - Before provider invocation, Bach verifies the subject workspace is on the configured subject branch and is clean.
- Merge providers run in the main project checkout, receive
BACH_AGENT_SUBJECT_TARGET,BACH_AGENT_SUBJECT_BRANCH,BACH_AGENT_SUBJECT_COMMIT,BACH_AGENT_SUBJECT_WORKSPACE, andBACH_AGENT_POLICY_EVIDENCE, and get the generated merge prompt as the final argv. - The generated merge context includes subject branch, commit, workspace, plan, provider metadata, and policy evidence.
- A successful merge report must include
pr_url,target_branch_commit, ormerge_commitevidence. PR URLs must be valid absolute URLs and must be paired withtarget_branch_commitormerge_commit; commit evidence must name a commit reachable from the main checkout and must have the reviewed subject commit as an ancestor. - Merge providers receive a generated merge prompt that combines the optional merge prompt file, subject metadata, policy evidence, and the required merge completion report JSON schema.
bach runs inspect --json <run-id>exports all target executions intargets, including log paths, artifact paths, quality report summaries, agent reports, applied policy summaries, provider metadata, subject metadata, and merge evidence for control-plane ingestion.
Providers must write an Agent Report JSON file to BACH_AGENT_REPORT_PATH. The minimal v1 envelope is:
{
"target": "agent/checkout_refactor",
"provider_name": "opencode",
"provider_type": "opencode",
"provider_command": ["opencode", "run"],
"mode": "implement",
"status": "passed",
"attempt": 1,
"workspace": "/absolute/project/.bach/agents/checkout_refactor",
"branch": "bach/agents/checkout_refactor",
"commit": "abc123",
"changed_files": ["src/checkout.go"],
"summary": "Refactored checkout validation."
}Valid statuses are passed, failed, blocked, and partial; only passed succeeds the Agent Target.
Bach validates the report target, provider evidence, mode, attempt, workspace, branch, commit, and summary.
Missing or malformed reports fail the Agent Target before later policy or reviewer phases. When
git.commit = "required", the provider must create a new descendant commit in the configured branch during
the attempt or the target fails. The workspace must also be clean after provider execution.
Merge providers write a merge Agent Report JSON file to BACH_AGENT_REPORT_PATH:
{
"target": "agent/merge_checkout_refactor",
"provider_name": "opencode",
"provider_type": "agent",
"provider_command": ["opencode", "run"],
"mode": "merge",
"status": "passed",
"subject": {
"target": "agent/checkout_refactor",
"workspace": "/absolute/project/.bach/agents/checkout_refactor",
"commit": "abc123"
},
"pr_url": "https://github.com/example/repo/pull/123",
"summary": "Opened merge PR for checkout refactor."
}Factories declare durable queues for plan-first work. A Factory is not a Target and is hidden from
bach list.
factory "delivery" {
workflow "ship" {
plan {
agent_template = agent_template.planner
path = "plans/factory/${work_item.id}.md"
}
implement {
agent_template = agent_template.implementer
}
merge {
target = "pipeline.merge_ready"
}
deploy "staging" {
target = "shell.deploy_staging"
}
verify "staging" {
target = "shell.verify_staging"
}
deploy "production" {
target = "pipeline.deploy_production"
requires_approval = true
}
verify "production" {
target = "group.production_gate"
}
}
triggers {
manual {}
provider "github_issues" {
command = ["bach-github-issue-trigger"]
poll_interval = "5m"
config = {
repo = "example/repo"
token_env = "GITHUB_TOKEN"
labels = "factory:ship"
}
route {
label = "factory:ship"
workflow = "ship"
}
}
}
}Factory fields:
factory "<name>": declares a queue namespace.workflow "<name>": declares a route for submitted Work Items. A single workflow is selected by default; multiple workflows require--workflowon submit.plan { agent_template = agent_template.<name>, path = "...", requires_approval = true }: runs a planning Agent Target and copies exactly one Plan file from the agent workspace to the configured project-relative path.${work_item.id},${factory.name}, and${workflow.name}are supported inpath.requires_approvaldefaults totrue; set it tofalseto allow unattended planning.implement { agent_template = agent_template.<name> }: runsbach plan implementinternally with this implementation template override. The daemon does not mutate the planner-authored Plan file.merge { target = <target> }: runs one normal Bach target after implementation succeeds.deploy "<name>" { target = <target>, requires_approval = true }andverify "<name>" { target = <target> }: run named deployment and verification targets in declaration order. Deploy approvals default tofalse; setrequires_approval = trueto pause before the deploy target runs.triggers { manual {} }: enablesbach factory submitfor that Factory.triggers { provider "<name>" { ... } }: enables an external trigger provider process that the Factory daemon polls for new Work Items.
Provider trigger fields:
command: required argv array for the provider process. The first element may bebachto resolve to the current Bach executable.poll_interval: optional duration string; defaults to5mand is clamped to at least1s.config: optional map of string keys to string values passed to the provider during handshake and poll.route { label = "...", workflow = "..." }: optional routing rule. Items with the matching label are routed to the named workflow. When a Factory has multiple workflows, at least one route is required; with a single workflow, omitted routes default to that workflow.
The bach-github-issue-trigger provider reads GitHub Issues through the trigger provider protocol. It accepts these config keys:
repo: required GitHub repository inowner/nameform.token_env: optional environment variable name containing a GitHub token; defaults toGITHUB_TOKENwhen the provider process receives that variable.bach factory startonly forwards the variable named bytoken_env, so settoken_env = "GITHUB_TOKEN"when the daemon should use GitHub token auth. The token value must stay out of the Bachfile.api_url: optional GitHub API base URL; defaults tohttps://api.github.com.labels: optional comma-separated GitHub label filter passed to the Issues API.state: optional issue state, one ofopen,closed, orall; defaults toopen.since: optional RFC3339 timestamp used as the initial cursor when no Bach cursor exists.per_page: optional GitHub page size from1to100; defaults to100.max_pages: optional positive page limit per poll; defaults to5.priority_label_prefix: optional label prefix for Work Item priority extraction; defaults topriority:.
GitHub Issue labels are preserved as Work Item labels for route matching. Pull requests returned by the GitHub Issues API are ignored. The provider advances its cursor from GitHub updated_at timestamps, suppresses issues at or before the stored cursor to avoid duplicate delivery, and treats priority:critical, priority:urgent, priority:high, priority:normal, and priority:low labels as Bach priorities.
Validation rules:
- Factory and workflow names must start with an ASCII letter, digit, or
_, and may then contain ASCII letters, digits,_,., or-. - Factory names must be unique within a Bachfile.
- A Factory must declare at least one workflow.
- Workflow names must be unique within a Factory.
- Daemon-executable workflows require exactly one
planblock and exactly oneimplementblock. merge, nameddeploy, and namedverifyphases are optional. Each phase block accepts singulartarget.requires_approvalis accepted only onplananddeployblocks.implement,merge, andverifyreject the field.- A Factory may declare at most one
triggersblock, at most onemanualtrigger block, and any number of namedprovidertrigger blocks. - Provider trigger names must be unique within a Factory and must be simple identifiers.
- Provider triggers require a non-empty
commandarray. - Provider triggers require at least one
routeblock when the Factory declares multiple workflows. - Unknown Factory fields are rejected by Bachfile validation.
Submit a Work Item:
bach factory submit delivery \
--title "Ship billing webhook" \
--body "Implement the webhook and tests." \
--label billing \
--dedupe-key billing-webhookSubmission creates a UUIDv7 Work Item with lifecycle pending, current_phase = "plan", one pending
attempt, and an immutable intake snapshot at .bach/artifacts/factory/<work-item-id>/intake.json.
The Work Item is persisted through the configured Backend Provider using the factory_queue capability.
The submitted --plan value is stored as an opaque reference and is not parsed or used for file I/O in
this slice.
If --dedupe-key matches an existing pending item for the same factory and workflow, submit returns that
existing item. JSON output reports "created": false for this case.
Inspect and manage the queue:
bach factory list delivery
bach factory inspect delivery <work-item-id>
bach factory cancel delivery <work-item-id> --reason "no longer needed"
bach factory approve delivery <work-item-id> --phase plan
bach factory approve delivery <work-item-id> --phase deploy.production --reason "change approved"
bach factory status deliverybach factory approve records durable approval evidence for a Work Item that is currently waiting at the
specified phase. The command accepts --phase <phase> and an optional --reason <text>. It returns the
existing approval idempotently when the same Work Item, attempt, and phase were already approved. Approval
phase strings use dot form such as deploy.production. The Backend Provider DTO schema is
docs/schemas/backend-factory-approval-v1.schema.json.
Start a long-running Factory daemon:
bach factory start delivery --yes
bach factory start delivery --yes --poll-interval 10s --renew-interval 1m --lease-ttl 2mThe daemon acquires a Backend lease, polls for pending Work Items, claims one item at a time, and executes
the workflow spine plan -> implement -> merge -> deploy[*] -> verify[*]. Empty queues do not stop the
daemon; use SIGINT or SIGTERM to stop it and release the lease. --poll-interval controls how often the
daemon checks the queue and defaults to 5s. --renew-interval controls how often the daemon renews its
lease and defaults to 10s. --lease-ttl controls how long a lease remains valid without renewal and
defaults to 30s.
When a plan or deploy phase requires approval and no matching approval exists, the daemon sets the Work
Item lifecycle to waiting_approval, keeps current_phase at the gated phase, clears active daemon
ownership, and continues polling other eligible work. After an operator records approval with
bach factory approve, the daemon resumes the Work Item on a later poll or after a restart. Plan approval
evidence stores the Plan path and hash; if the Plan file changes after approval but before implementation
resumes, the Work Item fails with a stale-approval message instead of silently implementing different text.
When a Factory declares provider triggers, bach factory start also starts a long-running JSON-RPC session with each provider process. The daemon polls each provider on its configured interval, routes returned items to workflows using labels, and enqueues or updates pending Work Items. If any item in a polled batch fails intake validation, the entire batch is nacked so the provider can redeliver; successfully processed batches are acked and the trigger cursor is advanced. Provider trigger protocol messages conform to docs/schemas/trigger-provider-v1.schema.json. Provider intake failures do not fail Work Items that are already queued or active.
Provider subprocesses receive only PATH, temp directory variables, and the environment variable named by config.token_env when present. Configure token env names explicitly instead of relying on the daemon to pass through the full shell environment.
Use --json with any Factory command for machine-readable output. factory submit returns
{"item": <work-item>, "created": true|false}. factory list returns {"items": [<work-item>, ...]}.
factory inspect and factory cancel return a single Work Item object. factory inspect includes an
approvals array. factory status returns the active daemon lease, optional active Work Item ID, and
lifecycle counts. factory start --json returns the daemon ID and acquired lease record after the command
stops. CLI Work Item JSON omits raw body text; use the intake evidence URI for private submission details.
Failed Work Items include failure_phase and failure_message. The Backend Provider Work Item DTO schema is
docs/schemas/backend-factory-work-item-v1.schema.json.
factory list defaults to pending and waiting_approval items; pass --status all to include every
lifecycle. --status accepts pending, claimed, running, waiting_approval, completed, failed,
cancelled, or all. Pass --workflow to filter list output by workflow.
Current lifecycle values are:
pending: queued for future planning/execution phases.claimed: claimed by a daemon lease before phase execution starts.running: executing the current workflow phase.waiting_approval: paused at a gated phase until an approval is recorded.completed: all configured workflow phases succeeded.failed: a workflow phase failed; the Work Item records the failed phase and message.cancelled: manually cancelled before execution.
Deferred Factory behavior includes retries, review queues, and replan loops.
bach plan status <plan-file> [plan-file ...] loads Markdown Plans, validates the selected Plan graph, reads Backend Plan ledgers, and prints deterministic status and planned wave output. It does not execute agents, generate targets, mutate Factory Work Items, or write Plan evidence.
bach plan implement <plan-file> [plan-file ...] executes one or more Markdown Plans. When a single Plan file is supplied it behaves like the focused single-Plan executor. When multiple Plan files are supplied it runs them as a batch: it validates the selected Plan graph, computes ready waves, executes ready Plans with bounded parallelism, applies the configured stop mode, and writes lifecycle Plan ledger records to the Backend for every executed Plan. Generated targets are not written to a Bachfile and are not shown by bach list.
bach plan review <plan-file> [plan-file ...] groups the selected Plans by decision state without executing anything. It derives the review queue from Plan ledgers, status, and diagnostics.
Plans do not require frontmatter. Bach infers the Plan ID from the project-relative file path and the title from the first Markdown heading:
# Checkout refactorOptional YAML frontmatter supplies stable overrides and execution hints:
---
id: phase-14-checkout
title: Checkout refactor
depends_on: [phase-13-runtime-model]
agent_template: feature_implementer
policy: standard_feature
required_targets: [shell/test]
labels: [factory]
metadata:
owner: platform
---
# Checkout refactorSupported frontmatter fields are schema, id, title, description, depends_on, agent_template, policy, required_targets, labels, and metadata. schema is optional; when present it must be bach.plan.v1. Unknown fields fail validation. workstreams is rejected in v1 because one Plan is one future implementer unit.
Statuses are:
ready: no ledger exists and the Plan has no dependencies, or all selected dependencies are implemented.planned: no ledger exists and at least one selected dependency is not implemented yet.blocked: no ledger exists and at least one selected dependency is stale, invalid, or blocked.pending: latest Backend ledger says execution has been queued but has not started.in_progress: latest Backend ledger says execution is currently active.implemented: latest Backend ledger is implemented and matches the current Plan hash.failed: latest Backend ledger says execution failed.stale: latest Backend ledger is implemented but the Plan hash changed.invalid_ledger: latest Backend ledger fails validation.
Use --json for machine-readable output with schema_version, plans, waves, and diagnostics.
bach plan implement writes pending, then in_progress, then either implemented or failed ledger records for the current Plan hash. Generated Plan implementer targets are remote/destructive agent targets and require --yes to execute. If the latest Backend ledger is already implemented for the same Plan hash, the command skips execution and reports skipped; use --force to run the generated target anyway. Plan implementation validates required_targets references but does not run them as a separate Plan phase.
bach plan status requires every depends_on Plan to be included in the selected Plan set so it can render waves. bach plan implement <plan-file> executes exactly one Plan and treats depends_on as external preconditions: every dependency must have a latest Backend ledger with status implemented before execution starts.
Batch execution supports:
--parallelism <n>: maximum Plans to execute concurrently within a ready wave. Default1.--stop-on <mode>:failurestops starting new Plans after any Plan fails;nevercontinues independent ready Plans. Defaultfailure.--dry-run,--force,--yes,--env-file,--log-only,--verbose, and--jobsare passed through to each generated target run.
A Plan is ready for a batch wave when all selected dependencies were implemented in earlier waves and all external dependencies already have an implemented Backend ledger. A Plan is blocked when a dependency failed, is blocked, or has no implemented ledger. A Plan is skipped when the batch stopped before it could start and it was not already blocked.
Human batch output summarizes selected Plan count, wave count, per-state counts, and a per-Plan table with state, run, target, and reason. JSON batch output uses schema_version: "bach.plan_batch.v1" with plans, waves, started_at, and ended_at.
bach plan review groups Plans into:
implemented: completed cleanly.needs_review: implemented but has diagnostics that may need human inspection.failed: execution failed.blocked: dependency or precondition blocked execution.skipped: skipped due to the batch stop mode or already fresh evidence.
JSON review output uses schema_version: "bach.plan_review.v1" with implemented, needs_review, failed, blocked, skipped, and optional diagnostics.
Human output summarizes the Plan, generated target, template, run ID, result, and written ledgers. JSON output for single-Plan execution uses schema_version: "bach.plan_implement.v1" with plan, result, target, optional template, optional run_id, latest ledger, written_ledgers, and optional diagnostics.