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.
Verify the record anytime, or open the local overview:
$ bafgo manifest --verify # chain + fingerprints
$ bafgo serve --port 4000 # local web app (localhost-only): overview + review gate
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
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, change, or decision: 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. Evidence is never existence-checked — it may cite files in another repository. Only link targets must exist in this repo.
- Decisions are a standalone log of how a requirement came about. A decision has a question, a recommendation, and a decision, plus decided_by/decided_on, implemented, and raised. It starts open, moves to decided, and can be superseded. Open decisions appear in status by priority.
Prose, comparison & legacy import
- Prose: a rule can carry its requirement as free text instead of structured when → then — use --prose (or --prose-file <path>) on rule new 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.
- Confidence & notes: --confidence takes confirmed, decided, or inferred; --note appends a dated free-text note. Both live on rules and decisions.
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: ["e2e/*.spec.ts"]
cmd: npx
args: ["playwright", "test", "{file}", "-c", "e2e/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 Epheme, 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. 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.
Check & report
- bafgo check validates the whole .bafgo/ state and exits non-zero (for CI) when it finds duplicate IDs, a dangling reference, an enum value outside its list, an executable rule with no when/then/prose, a defect or needs-decision rule that is untriaged, an open decision that is untriaged, a fixed rule with no linked test, or an event log that is unparseable, carries merge-conflict markers, or has a broken chain.
- bafgo report --format md prints a deterministic Markdown report (same input → same bytes): open items by priority, per-area counts, then rules without tests. Commit it beside the code so Product can read it in pull requests.
- bafgo manifest refuses to write over an event log that doesn't parse or chain, so a corrupt log is never re-fingerprinted and stamped as healthy.
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.
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.
- Run bafgo manifest, then bafgo manifest --verify to confirm the chain and fingerprints 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 status [id] [--json]
bafgo rule new <id> --title <t> [--track executable|exploratory] [--when "a,b"] [--then "c,d"] [--prose "text"|--prose-file <path>] [--source s] [--reason r] [--evidence "file:line,..."] [--priority p] [--tags "a,b"] [--needed-by d] [--fix f] [--compare status] [--confidence confirmed|decided|inferred] [--note "text"] [--filter f] [--by name]
bafgo rule specify <id> [--by name]
bafgo rule set <id> [--title t] [--track x] [--when "a,b"] [--then "c,d"] [--prose "text"|--prose-file <path>] [--source s] [--reason r] [--evidence "file:line,..."] [--priority p] [--tags "a,b"] [--needed-by d] [--fix f] [--compare status] [--confidence confirmed|decided|inferred] [--note "text"] [--filter f] [--by name]
bafgo link <id> --code a,b [--unit c,d] [--e2e e,f] [--demo url]
bafgo triage <change-id|rule-id|decision-id> --disposition accepted|declined|deferred [--priority p] [--tags "a,b"] [--needed-by d] [--fix f] [--reason text] [--by name]
bafgo stage <change-id> --demo <url>
bafgo request-review <change-id>
bafgo approve <change-id|rule-id> [--by name] [--reason text]
bafgo reject <change-id|rule-id> [--by name] [--reason text]
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 [--question q] [--recommendation r] [--decision d] [--rule a,b] [--change c,d] [--evidence "file:line,..."] [--confidence confirmed|decided|inferred] [--note "text"] [--by name]
bafgo decision decide <id> --decision "text" [--by name]
bafgo decision supersede <id> --with D-OTHER [--by name]
bafgo decisions [--json]
bafgo regen [--json]
bafgo graph [id] [--json]
bafgo manifest [--verify]
bafgo check
bafgo report [--format md]
bafgo chain
bafgo serve [--port 4000] [--host 127.0.0.1] [--as name] [--read-only]
Windows PowerShell 5.1
Epheme runs offline on Windows with Windows PowerShell 5.1. Two known pitfalls and how the CLI avoids them:
- Embedded quotes get mangled by native-command quoting. For long prose or text with quotes, put it in a file and pass --prose-file <path> (on rule new, rule set). The file is read verbatim — newlines, double quotes, and non-ASCII included — so nothing gets mangled.
- Non-ASCII in .ps1 files is read as ANSI. Keep any .ps1 wrapper scripts ASCII-only; if you need non-ASCII rule text, put it in a UTF-8 .md/.txt file and reference it with --prose-file.
- Line endings. Git for Windows defaults to core.autocrlf=true, which rewrites LF to CRLF on checkout and would silently change hashes. bafgo init adds .bafgo/** text eol=lf to .gitattributes, and every hash is computed on normalised LF — so a checkout can never change a fingerprint or break the chain.
Installing from the private registry works from PowerShell 5.1; quote both arguments with npm config set:
npm config set "@bafgo:registry" "https://npm.bafgo.com/"
npm install -g @bafgo/cli
No network calls at runtime: every command is file-first and works against the local .bafgo/ directory. The only command that touches the network is bafgo update, and only when you ask for it. bafgo serve binds 127.0.0.1 by default (pass --host <addr> to widen it) and makes no outbound calls.
Serve (local web UI)
bafgo serve starts a read-only web app on 127.0.0.1 by default. To enable approve/reject from the browser, start it with an identity:
bafgo serve --as dana
- Every mutation (approve, reject, verify, rule create/edit, upload) requires a session token that serve prints on startup and embeds in the page it serves — so another website open in the same browser can't drive it. Requests with an unexpected Host header are refused too, so a DNS-rebinding page can't read the token.
- Without --as, serve is read-only. The approver's name always comes from --as, never from the request.
- Pass --host <addr> only when you want it reachable beyond localhost.
- A rule's page shows its prose (or when/then), compare, confidence, area, triage, legacy and implementation evidence, verified_by, notes, and links.
- The editor and upload tabs are hidden; rules are written with the CLI (or by the agent). serve stays overview + review gate.
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.epheme.org/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 (question, recommendation, decision).
- Triage before you build: bafgo triage <id> --disposition accepted.
- Commit .bafgo/ changes alongside the code.
- Never invent a test result — verify runs the real tests.
- Don't edit events.jsonl or links.json by hand; use the commands.