Install and set up
Prerequisite: Node.js 18 or newer.
# add to the repo's .npmrc (or your user .npmrc):
@bafgo:registry=https://npm.bafgo.com/
$ npm install -g @bafgo/cli
$ bafgo version # verify
$ bafgo init # create .bafgo/ (run at the repo root)
If you prefer npm config set, quote both arguments on Windows/PowerShell: npm config set "@bafgo:registry" "https://npm.bafgo.com/".
Run commands from anywhere inside the repo; the CLI walks up to find .bafgo/. Use --root <dir> to point at a specific repo instead.
Signing keys (optional, recommended):
$ bafgo keygen --name <you> # private key → ~/.bafgo/keys/<you>
$ bafgo keys
$ bafgo keys rotate --name <you>
Add the printed public key to .bafgo/keys.yaml, then approvals sign automatically. Verify the record anytime with bafgo manifest --verify, and open the local overview + review gate (approve/reject) with bafgo serve --port 4000.
Your role
When a ticket is handed to you, keep the record as you work:
- Draft the requirement as a rule.
- Specify it so it can be checked (when → then).
- Implement, then link the code and tests to the rule.
- Stage a demo and request review.
- A human approves; then the tests are verified.
You draft, specify, implement, link, and stage. You never impersonate the human: record the approver's name with --by, but the human is the one who approves.
The files
Everything lives in a .bafgo/ folder committed with the code:
- rules/<id>.yaml — one requirement per file
- changes/<id>.yaml — a unit of work touching one or more rules
- decisions/<id>.yaml — decision log: how a requirement came about and what was decided
- runners.yaml — optional test-runner registry (Playwright, custom filters)
- events.jsonl — append-only, hash-chained record of what happened
- links.json — the trace, regenerated
- manifest.json — fingerprints of rules, changes, decisions, and linked files
- keys.yaml — trusted public keys for signed approvals
Rules
A rule is one requirement written so it can be checked. For an executable rule, every when must hold and every then must pass. Design taste that can't be tested is --track exploratory: it is approved, never verified, and you don't generate tests for it.
Changes
A change is one unit of work touching one or more rules. Create one with claim and reference its tracker id with --tracker (Jira / Azure DevOps).
Triage, evidence & decisions
Record where a rule came from and why it exists — not just what it says.
- Triage is the initial disposition of a rule or change: bafgo triage <id> --disposition accepted|declined|deferred records who, why, and when. Untriaged is the default.
- Evidence is a list of file:line (or URL) references to where a requirement came from — e.g. docs/vision.md:42. Set it with --evidence on rule new, rule set, or claim.
- Decisions are a standalone log of how a requirement came about. bafgo decide records the context, decision, rationale, and evidence, and links it to rules and changes.
Prose, comparison & legacy import
- Prose: a rule can carry its requirement as free text instead of structured when → then — use --prose on rule new, rule import, or rule set. An executable rule needs when+then or prose.
- Comparison status: how a ported rule lines up with the legacy system. --compare takes matches, fixed, defect, not-ported, differs-on-purpose, or needs-decision — it sits alongside the state machine, not in place of it.
- Extra fields: --priority, --tags, --needed-by, --fix.
- Legacy import: bafgo rule import creates a rule that already exists in a legacy system. It lands in implemented (code exists, not approved) and records one honest rule.imported event — it never fabricates approval history.
Test runners & filtering
verify picks a runner for each linked test file. Built-ins: node (*.js/*.mjs/*.cjs) and dotnet (*.csproj/*.fsproj/*.vbproj/*.sln). Add or override runners in .bafgo/runners.yaml:
# .bafgo/runners.yaml
runners:
playwright:
match: ["journeys/*.spec.ts"]
cmd: npx
args: ["playwright", "test", "{file}", "-c", "journeys/playwright.config.ts"]
filter: "--grep {filter}"
{file} is the linked test path; {filter} is the value passed to bafgo verify ... --filter. The flag is defined by the runner, not BafGo, so no xUnit/Playwright contract is baked in — --filter "Rule=R-1" for dotnet, --filter "@BR-SENT-001" for Playwright.
Store a rule's filter with --filter on rule new / rule set / rule import. Then bafgo verify CH-1 on a change gathers every rule's filter, dedupes shared test files, and runs each file once with the filters OR-combined (Rule=R-1|Rule=R-2) — one dotnet test build covers all the change's rules instead of one per rule. If any rule links a file without a filter, that file runs unfiltered. An explicit --filter on bafgo verify overrides the stored filters.
State machines
Rule: draft → specified → implemented → review → approved → verified
Change: open → in-review → approved → verified
- specify works only from draft; an executable rule needs a when and a then.
- link with a code file moves a rule specified → implemented.
- A rule reaches review by stage <rule-id> --demo <url>, or automatically when its change is requested for review.
- stage on a change only sets the demo (it stays open); request-review moves it open → in-review and pushes its rules implemented → review.
- approve moves review → approved (rule) or in-review → approved (change).
- reject sends it back: review → implemented, in-review → open.
- verify runs the linked tests and only reaches verified if every one passes. Run it on the change (it cascades to its rules) or on a rule already in approved — not on a rule in implemented.
- How verify finds tests: it runs each linked --unit/--e2e file with the runner that matches its path (node, dotnet test, or a .bafgo/runners.yaml entry). Exit 0 is pass, non-zero is fail. --filter restricts a run to tagged tests.
Parallel branches & merging
The event log is one linear hash chain — every event points at the hash of the one before it — while state lives in per-rule and per-change files. Two consequences:
- Parallel work is fine. Different rules and changes advance independently; their events interleave on the one chain in append order.
- Approvals are content-signed. A signature covers the event's semantic fields (type, rule/change, by, reason) — never time, prev, or sig. So an approval stays valid wherever it lands in the log after a merge.
When two git branches both append to .bafgo/events.jsonl, git reports a conflict on that file. Resolve it like this (never hand-edit the log otherwise):
- Merge the per-rule / per-change YAML files with git as usual. If two branches changed the same rule or change, resolve the state conflict explicitly, like a code conflict — the merged file is the source of truth.
- For events.jsonl, keep every event from both branches. Don't drop, rewrite, or reorder by hand — just concatenate both branches' new events in chronological order (by time).
- Run bafgo chain to re-link each prev hash onto the merged order. This never invalidates a signature.
- Run bafgo manifest, then bafgo manifest --verify to confirm the chain and signatures are intact.
Don't approve the same rule or change concurrently from two branches — serialize approvals for a single entity. If it happens anyway, the log records both events honestly: resolve the YAML state conflict, and never edit the log to hide one.
Command reference
bafgo init
bafgo keygen --name <you>
bafgo keys [rotate --name <you>]
bafgo status [id] [--json]
bafgo rule new <id> --title <t> [--track executable|exploratory] [--when "a,b"] [--then "c,d"] [--prose "text"] [--source s] [--reason r] [--evidence "file:line,..."] [--priority p] [--tags "a,b"] [--needed-by d] [--fix f] [--compare status] [--filter f] [--by name] [--key path]
bafgo rule import <id> --title <t> [--track executable|exploratory] [--when "a,b"] [--then "c,d"] [--prose "text"] [--compare status] [--filter f] [--state implemented] [--by name] [--key path]
bafgo rule specify <id> [--by name] [--key path]
bafgo rule set <id> [--title t] [--track x] [--when "a,b"] [--then "c,d"] [--prose "text"] [--source s] [--reason r] [--evidence "file:line,..."] [--priority p] [--tags "a,b"] [--needed-by d] [--fix f] [--compare status] [--filter f] [--by name] [--key path]
bafgo link <id> --code a,b [--unit c,d] [--e2e e,f] [--demo url]
bafgo triage <change-id|rule-id> --disposition accepted|declined|deferred [--reason text] [--by name] [--key path]
bafgo stage <change-id> --demo <url>
bafgo request-review <change-id>
bafgo approve <change-id|rule-id> [--by name] [--reason text] [--key path]
bafgo reject <change-id|rule-id> [--by name] [--reason text] [--key path]
bafgo verify <change-id|rule-id> [--filter f]
bafgo claim <change-id> --rule a,b [--tracker ref] [--evidence "file:line,..."]
bafgo decide <id> --title t --decision d [--context c] [--rationale r] [--rule a,b] [--change c,d] [--evidence "file:line,..."] [--by name] [--key path]
bafgo decisions [--json]
bafgo regen [--json]
bafgo graph [id] [--json]
bafgo manifest [--verify] [--strict]
bafgo chain
bafgo serve [--port 4000]
Worked example — one ticket, end to end
The ticket: “Timer keeps running while the screen sleeps.”
$ bafgo rule new R-123 --title "Timer keeps running while the screen sleeps" --when "a timer is active,the display sleeps" --then "the countdown continues,the chime still fires at zero" --source "Jira BAFGO-42"
$ bafgo rule specify R-123
$ bafgo claim CH-42 --rule R-123 --tracker BAFGO-42
$ bafgo link R-123 --code timer.html --unit timer-core.test.js
$ bafgo stage CH-42 --demo https://preview.bafgo.com/r-123
$ bafgo request-review CH-42
# the human approves:
$ bafgo approve CH-42 --by dana --reason "fixes the chime"
$ bafgo verify CH-42
→ verified · tests green
Rules for you
- Link only files that actually exist in the repo.
- Record why a rule exists: --source, --reason, and --evidence (file:line refs).
- Record a non-obvious requirement's origin with bafgo decide.
- Triage before you build: bafgo triage <id> --disposition accepted.
- Commit .bafgo/ changes alongside the code.
- Sign approvals when a key is available: approve ... --key <path>.
- Never invent a test result — verify runs the real tests.
- Don't edit events.jsonl or links.json by hand; use the commands.