Contributing to VeriPower
August 30, 2026 · View on GitHub
VeriPower is a stage-gated, event-sourced agent pipeline. This guide covers the core contribution workflows. For architectural background, read ARCHITECTURE.md first.
Adding or modifying a stage skill
A stage skill's job is to "write result.json correctly." DAG routing and state transitions are NOT a stage skill's responsibility — those belong to the Orchestrator and the kernel (kernel.py).
Checklist for a new stage skill:
skills/<stage-skill>/SKILL.md— skill description + instructions. It must be self-sufficient — carry every mechanism, gate sequence, and threshold inline, since the plugin end-user has theSKILL.mdbut not veripower'sCLAUDE.md(the full rule is the §Scope note under Bulletproofing a skill). Frontmatter carries onlynameanddescription— noallowed-tools; subagent behavior is bound by the dispatch-time prose forbidden-actions list, not tool gating.- If needed,
skills/<stage-skill>/references/— tool manuals, checklists, prompt fragments. - If the stage is a new rule (not a replacement of an existing one), add a
Ruletoframework/scripts/rules.py:RULES— declaring itsskill,execution,workdir_rootandinputs(the producer→consumer dependency graph is derived from those: an input names a path under some stage'sworkdir_root, and that stage is its producer — there is no separate DAG to edit and no output declaration to keep in sync). An input should name what its producer delivers as a unit — a file, or the directory a stage delivers whole — never a filename pattern: a pattern can only ever be a guess at what the producer will call things, plusproofandoracle. Register its name inFORWARD_PRIORITY(and inADVISORY_ORDERonly if it needs a non-data sequencing edge).rules.pyis the SSoT — see the Cross-module SSoT identity note under Coding Conventions. Add unit tests (code-behavior); put any new cross-artifact sync/invariant check intests/contracts/. - Add scenario tests under
tests/scenarios/<stage>/— bulletproof them RED-first via the subagent ritual (see Bulletproofing a skill). - Update
skills/design-flow/SKILL.mdif the new stage introduces new scheduling semantics.
Modifying the kernel
The kernel is the deterministic core: kernel.py (the CLI and sole writer of events.jsonl), schedule.py (the decide scheduler), rules.py (the rule registry), and facts.py (event-log I/O + the freshness queries). Changes require unit tests.
Rules:
- Every scheduling decision lives in
schedule.py, never in the Orchestrator or a skill. "If upstream is X, start Y" is a decision — it is computed bykernel.py decide; the Orchestrator only executes the one action it returns. Rework-target selection is not a computation at all: the failing envelope names its ownfix_ownerandschedule.pychecks that naming against the derived input closure. The Orchestrator's only judgment channel is the ask-gatedpin/reopen/ humandiagnoseproposals — nothing else. It authors no per-dispatch content: at dispatch time every fact it could state is already a file the target reads, so it passes coordinates and the kernel resolves them intodispatch.json. - The event log is the only durable state. A state change is an append to
events.jsonlviafacts.append_event, reachable only throughkernel.py(the sole writer). There is notask.jsonand no status snapshot to keep in sync — per-stage status, freshness, and in-flight are derived on demand from the log + disk (facts.projection/proof_valid/input_available). Never add a stored status or freshness field. - Verb return values are Orchestrator prompt material — new fields need a clear consumer.
- Unit tests live under
tests/unit/:test_kernel_cli.py(verbs),test_schedule.py(decide),test_rules.py(registry + derived graph),test_facts_*.py(events / fingerprints / freshness), Cover happy path + error branches.
Validating new structured outputs
Which validation regime a new structured output needs depends on its class: kernel-enforced schema validation for verdict outputs, or a producer self-gate for advisory artifacts. The contributor obligation per class:
- Verdict output (
result.json, event payloads): do not add a field without a schema update and a coverage test (test_event_schemas.pyfor an event field, the stage'stest_<stage>_result.pyfor aresult.jsonfield). The kernel validates events at append time andresult.jsonat reap, so an unschema'd field corrupts the deterministic core. - Descriptive/advisory artifact (e.g., triage ANALYSIS, verification scaffold): ship a
scripts/validate_*.pyproducer self-gate (pattern:skills/simulation-triage/scripts/simtriage/__main__.py,skills/simulation-plan/scripts/simplan/__main__.py); the skill fixes-and-retries before emitting. Do not add akernel.pyverb for advisory validation — the kernel never validates the artifact's content.
Testing
tests/unit/— pure-Python code-behavior tests (call a framework function, assert output).tests/contracts/— deterministic artifact sync/invariant lints (read declarations & compare; run no code). Runpytest tests/unit/ tests/contracts/for the fast loop when changing the kernel (kernel.py/schedule.py/rules.py/facts.py), schemas, or any cross-artifact contract.tests/scenarios/— skill-level discipline tests under pressure. No EDA tools; uses Claude (Opus) as the system under test, run via a clean-isolationclaude -psubprocess (tests/scenarios/scenario-run.sh) — see Bulletproofing a skill below.- CI (
.github/workflows/ci.yml) is the enforcement net for the gates you run locally:pyteston Python 3.10/3.11/3.12, and thepre-commitlint gate (Coding Conventions) on 3.12 — on every push and PR; PRs must be green to merge. Keep running both locally for fast feedback; CI is the net, not the loop.tests/scenarios/is deliberately not in CI: it drives a liveclaude -psubprocess (non-deterministic, needs model access) and stays a manual gate.
Bulletproofing a skill (RED-GREEN-REFACTOR)
Testing a VeriPower skill is TDD applied to the skill document. The test subject is a fresh, isolated claude -p subprocess — not an in-session subagent. This matters: an in-session subagent inherits the project CLAUDE.md, the developer's auto-memory, and repo file-access, all of which pre-encode the invariants under test and contaminate the RED baseline — even a tools-off subagent stays compliant, carrying the auto-memory's invariant notes and reading SKILL.md straight from disk. The runner tests/scenarios/scenario-run.sh gives a clean baseline — a temp workdir (no developer auto-memory, no skill auto-load), --allowedTools "" (no file reads), and only the context it injects. Both RED and GREEN run on Opus (= production), so teeth are judged against the model that ships.
RED-first acceptance gate: keep a scenario only if it fails RED and passes GREEN. A scenario the agent gets right on RED is toothless (bare Opus already complies) — discard or re-aim it.
Per scenario (the runner does RED/GREEN; you judge + REFACTOR):
- RED —
scenario-run.sh --skill <s> --scenario <id> --mode redinjects nothing — bare Opus, no projectCLAUDE.md, noSKILL.md(baseline + isolation caveat:tests/scenarios/README.md). Read the printedDECISION:/ACTION:tag. Expected: it fails (violating option / proceeds when it should block). - GREEN —
--mode greeninjectsskills/<s>/SKILL.mdalone — exactly what a plugin end-user receives. Expected: it complies. - REFACTOR (on GREEN failure) — edit the skill: an explicit negation in the rule + a rationalization-table row (the agent's verbatim excuse → reality) + a Red-Flags entry + a
descriptionsymptom. Then meta-test (a follow-upclaude -pwith the transcript, or in-session reasoning — meta-testing is not a baseline, so contamination is harmless): "you read the skill and still chose X; how should it have been written to make the compliant option unambiguous?" Apply the answer; re-run GREEN until it passes. - Record provenance — stamp the scenario frontmatter
baseline: fail/green: pass/activated: <date>/model: opus. For a borderline tag, run 2–3 times and take the majority.
Scope (the SKILL.md-self-sufficiency rule): target what each skill's SKILL.md must carry on its own — its mechanisms, exact gate sequences, thresholds — since the plugin end-user has the SKILL.md but not veripower's CLAUDE.md. Skills whose discipline bare Opus already holds unaided get few or zero scenarios; never manufacture pressure — an empty per-skill corpus is an honest outcome, not a gap. Baseline + isolation caveat and live corpus status: tests/scenarios/README.md.
Scenario types: pressure (DECISION: A/B/C) and missing-info (ACTION: PROCEED/BLOCKED) self-report a tag the runner extracts. open (answer key in ## Expected Behavior / ## Anti-Pattern) has no tag — human/main-agent judgment only.
Regression (after editing a skill): re-run that skill's scenarios --mode green. A previously-passing scenario that now fails means the edit reopened a hole — fix before merging.
Documentation
- Script contract sync (mandatory). Each SKILL.md is the complete runtime contract for the scripts it invokes — agents run them per its documented command lines rather than reading their source. So any change to a directly-invoked script's CLI flags, exit codes, or output shape MUST update the invoking SKILL.md (and the script's
--helptext) in the same commit; a new script MUST be classified at introduction (directly-invoked: document the full command line + failure protocol; bootstrap-/make-internal or import-only: one line marking it internal). Silent drift breaks the black-box rule for every downstream run. - Paths into the plugin. The plugin installs outside the tree a run works in, so nothing in skill content may be written relative to the working directory, and no harness variable stands in for the install location —
${CLAUDE_SKILL_DIR}is a render-time substitution applied to a SKILL.md body only, so the same literal in areferences/contract reaches a sub-Task's shell and expands to nothing. Three forms, enforced bytests/contracts/test_skill_self_location.py: a shell command cites<skill>/…, defined once per file as this skill's base directory (the rendered body names it on its first line); a document citing a document goes relative to itself, asreferences/x.mdalready does throughout; a Level-1 contract takes<skill>as a named input its dispatcher hands over, since a sub-Task renders no skill. - ARCHITECTURE.md no-restatement rule. State each cross-cutting invariant once — either at a single home section (every other mention cross-refs it), or split across a rationale sentence + a
> **Contract:**box that each state only their half (the why vs. the verifiable form). A localized contract — a per-stageresult.jsonfield, a CLI flag — is never restated here; link to its SSoT (the owningresult.schema.jsondescription /kernel.py --help) instead. New architectural content picks one home before it lands; if you find yourself writing a fact this document already states, cross-reference it rather than rephrasing it. - User/contributor-facing content (architecture, contribution norms) — lives at the repo root: README.md, ARCHITECTURE.md, this file.
- Brainstorming, design proposals, review records — under
docs/superpowers/. These are uncommitted by convention (see.gitignore).
Language posture
VeriPower content lives on two surfaces: Surface 1 (runtime-LLM-consumed; English-only) and Surface 2 (user-data; bilingual, follows user language). For the full rule and the boundary criterion, see docs/language-posture-design.md. When writing skill content, use the established workflow vocabulary already present in skills/<name>/SKILL.md and skills/<name>/references/*.md as the source of truth.
Commit messages
One inclusion test decides what goes in: write only what a reader can't recover from a more authoritative source. The diff already records what changed; CI records whether it passes. A message owns only what neither does:
- Subject — imperative, intent not mechanism, with a
type:prefix (ci:,docs:,fix:,style:, …). Self-evident commits can stop here. - Body (when warranted) — the why: problem + root cause (cause only when non-obvious). No file:line evidence (the diff has it); length scales with the change.
- Verification — only for checks CI does not run: manual bring-up, a local EDA flow, a reproduced bug. Don't write "pytest passes" — CI is the authoritative pass/fail record.
- Trailers —
Co-authored-by:, issue refs.
Pull requests
A PR adds one authoritative source on top of the commits: the commit list itself. So the same inclusion test gains a clause — write only what the diff, CI, and the individual commit messages don't already give. What's left is PR-unique: the umbrella why and reviewer guidance.
- Title — like a commit subject (imperative, intent,
type:prefix), but the umbrella intent of the whole PR, not a copy of one commit. If the repo squash-merges, this becomes the merge commit's subject — keep it convention-clean. - Description — the umbrella why (what these commits deliver together) plus reviewer guidance: where to start, what's risky, what's deliberately out of scope, what to verify by hand. Link issues with
Closes #N. Don't re-list files (the diff has them), don't say "tests pass" (CI does), don't re-narrate each commit (the commit list does).
One PR, one logical change; length scales with the change. The .github/PULL_REQUEST_TEMPLATE.md prefills this shape.
Merge strategy
Rebase and merge — keeps history linear and preserves each commit's message. Squash only a PR of WIP/fixup commits not worth keeping apart. Avoid merge commits (they own nothing the commits / PR / CI don't already).
Coding Conventions
Enforced by pre-commit (ruff + shellcheck + shfmt); run pre-commit run --all-files (or pre-commit install once for the per-commit hook). Config: ruff.toml, .shellcheckrc, .pre-commit-config.yaml.
Python
- Naming:
snake_casefunctions/vars,UPPER_SNAKEconstants,_privateprefix. NocamelCase. - Formatting:
ruff format(88 cols); imports sorted by ruffI. f-strings (not%/.format, except dict-unpack"{x}".format(**d)).pathlib.Pathoveros.path. - A script that is directly runnable (
if __name__ == "__main__") starts with#!/usr/bin/env python3; import-only library modules do not. - Exit via
sys.exit(...)— neverraise SystemExit(...). Pass an int code, or a"<script>: message"string for fail-fast (Python prints it to stderr and exits 1; see the_fail()helper inspec/constraints.py). Useprint(..., file=sys.stderr)for diagnostics that aren't the exit message itself. Exit codes: 0 ok, 1 runtime failure, 2 usage. - New scripts are fully type-annotated. (Legacy partial annotations are not retrofitted.)
- JSON I/O:
json.dumps(..., ensure_ascii=False);indent=2for files written to disk; compact (no indent) only for single-line stdout payloads consumed by a caller.
Shell
#!/usr/bin/env bash+set -euo pipefailfor executable scripts. Sourced POSIX files (env.sh) carry# shellcheck shell=sh.- Tabs (
shfmt -i 0).[[ ]]tests (not[ ]).UPPER_CASEglobals,locallowercase. Quote expansions ("$VAR"); brace where needed (${VAR}). - Errors to stderr prefixed
<script>: ...>&2. Exit codes: 2 usage, 1 runtime, 0 ok.
EDA templates — three placeholder conventions:
MY_*— substituted by the bootstrap shell viased(default for shell/TCL/SDC templates).{{VAR}}— substituted by Python at scaffold-build time (the simulationrender-scaffoldverb for the simulation scaffold,emit_power_tests.pyfor the power scaffold).FILL_IN_*— a sentinel for a value the human must supply (e.g.FILL_IN_LIB_DB_PATH); the bootstrap substitutes it only when it can resolve a value, and the tool script fail-closes if the sentinel survives.
Cross-module SSoT identity — import shared SSoT modules the bare way (import rules after putting framework/scripts on sys.path, as kernel.py / schedule.py / facts.py and the tests all do), never via the package path (framework.scripts.rules); the latter creates a second module object and breaks the kernel.rules is schedule.rules identity (the dup-module bug class). Do not add re-exports for test convenience; tests read framework constants from their real home module. Guarded by tests/unit/test_kernel_cli.py::test_bare_import_single_module_identity.
File naming — each stage's Tier-1 scripts live in one package skills/<stage>/scripts/<pkg>/ (<pkg> = the short stage tool name, no hyphens, e.g. lint-cdc→lintcdc): a thin __main__.py argparse entry dispatches verb subcommands to focused <verb>.py / _<lib>.py modules (no god-file). Verbs follow the shared vocabulary (bootstrap / finalize / derive-* / materialize-* / render-* / check-*). There is no review-validating verb in any stage: every one that had one dropped it, last of all simulation, whose review is the only one no human reads before the stage routes on it. The reason generalizes past the review case. A verb that hands a verdict to the party the verdict constrains enforces nothing, and it is the step that cannot proceed without the answer, finalize, that has to read it. Put the check where the consequence is. Framework libraries keep domain-noun names (rules.py, store.py); kebab-case skill directories.
Further reading
- EDA tool environment — PATH,
LIB_DB,UVM_HOME,/bin/sh→bash, optionalVCS_CC: docs/eda-env.md. - Repository layout — per-module workspace: CLAUDE.md § Module Layout.