Skip to content

Config

Everything lives in prodc.toml. Only [prodc].needs is required.

The EARS / solution-verb text lint fires only on a need that has not stated its claim (the warrant — what delivering it would establish). State the claim and the lint goes quiet: one board serves a product PM and a research lab head with no mode, because "When a roaster opens the queue, she can see today's orders" and "Prove that π is irrational" are the same claim under uncertainty. Source-hygiene warnings (about evidence, not phrasing) always run.

[prodc]
needs = ["needs/*.yaml"]

[proofs.web]
kind = "gherkin"
paths = ["features/*.feature"]
delivered = ["@implemented"]
pending = ["@pending"]

[sources.issues]
kind = "frontmatter"
paths = ["docs/issues/*.md"]

[importance]
levels = [[3, "critical", "blocks launch"], [2, "needed", "first quarter"], [1, "nice", "someday"]]

[lint]
duplicate_threshold = 0.8
stale_after_days = 90
importance_revisit_days = 60

[add]
default_draft = false

[proofs.<name>]

A proof is <name>:<payload>.

kind payload DELIVERED when extra keys
gherkin file.feature#Scenario title or tag:@X results report passes, else a delivered tag paths, delivered, pending, results
pytest tests/test_x.py::test_y junit passes, else the test exists (declared) root, results
files path/to/script file exists (declared) root
accept who@date always (a human sign-off, declared) —
prototype ref@who@date always (validated with a user, declared) —
citation ref@author@year always (a cited published result, declared) — records what settles it, vs accept's who signed off —
coverage category category is in catalog catalog
field field name field is surfaced in schema schema
external observable-outcome key key recorded passing in results (a JSON {key: bool}); implementation-independent results
evalgate metric>=threshold (>=/<=/>/<) the recorded score meets the gate; score in results as {metric: {score: N}} or {metric: N} results
command anything your hook understands your hook prints {"state": "delivered"} command, timeout
need other-need-id or other-need-id@yes\|no\|inconclusive the referenced need is settled (DELIVERED); the @answer form also requires its authored answer to match — (built in; not configured)

Any adapter takes repo = "../sibling" to resolve against a sibling checkout; absent sibling → UNRESOLVED, not BROKEN. results (cucumber-json or junit XML) turns declared states into real ones.

need is a need→need edge for the question→question DAG ("B can't be posed until A settles"): a proof whose payload is another need id. It is resolved by prodc itself, not a configured adapter, so it needs no [proofs.need] block (one would be shadowed). Cycles (A→B→A) resolve to BROKEN, not an infinite loop. B does not inherit A's answer — the dependency only requires A settled, optionally in a given direction.

command — the generic hook

Resolve a proof against any system with a script, with no per-provider code in prodc. command is an argv list (no shell); the need's locator is substituted for every {locator} in it, or appended as the final argument when there is none. The hook prints a JSON object to stdout:

[proofs.jira]
kind = "command"
command = ["./hooks/jira-proof.sh"]   # invoked as: ./hooks/jira-proof.sh <locator>
timeout = 10                           # seconds, optional (default 10)
{"state": "delivered", "detail": "PROJ-7 closed"}

state is delivered / pending / broken / unresolved; detail is optional. The command comes only from this config (trusted); the locator is passed as data, never executed. A non-zero exit, a timeout, unparseable output, or an unknown state resolves to UNRESOLVED ("could not read") — never a false DELIVERED. The hook inherits the environment and owns any network/secrets itself.

[sources.<name>]

kind checks keys
frontmatter a source locator of file.md#heading exists paths
gitlab a source locator of #123 (or group/project#123) is a real GitLab issue — read-only, token from token_env; missing token/offline → unverified, 404 → broken base_url, project, token_env
command a source locator your hook resolves — prints {"exists": true, "who": ..., "date": ..., "quote": ...} command, timeout

command works the same as the proof hook (argv, {locator} or appended, trusted config). exists: true resolves the source; exists: false flags it BROKEN; a non-zero exit, a timeout, unparseable output, or a missing exists → unverified (never a false BROKEN).

[importance]

Key Means Default
levels rubric: list of [n, "label", "meaning"]. Declared → importance must be one of these n; set importance= accepts n or the label; lint flags off-scale values none (any int accepted)

[lint]

Key Means Default
duplicate_threshold text similarity in (0, 1] that flags a pair in dupes, add, recurrence --suggest and lint 0.8
stale_after_days lint warns when a need's newest observed_at is older than this off
importance_revisit_days lint warns when importance_set_at is older than this off

[add]

Key Means Default
default_draft add need stages as draft: true unless told otherwise false

[callbacks] — lifecycle callbacks (#71)

prodc sync runs a command when a need's state changes (e.g. a proof turns green). Same trust model as the command hook: the command is from this config, never from need data; the changed need + board context are piped to it as JSON on stdin, never executed.

[callbacks]
state_file = ".prodc/state.json"   # the state ledger (default); gitignore it — it is local state

[[callbacks.hooks]]
on = "state_change"                # the only event today (capture/report/pre_gate are follow-ups)
to = ["delivered"]                 # fire only on transitions INTO these states (optional; empty = any)
from = ["pending", "hole"]         # fire only on transitions OUT OF these states (optional; empty = any)
command = ["./hooks/notify.sh"]    # argv (no shell); receives the JSON payload on stdin
timeout = 30                       # seconds (optional, default 30)

The stdin payload is {"event", "from", "to", "need": {id, text, state, who, client, must, goal, release, warrant}, "board": {coverage_pct, delivered, total}}. A need is DELIVERED-green when to == "delivered"; use that to update the external tracker, notify the owner, or append to a changelog.

sync resolves the board, compares it to the ledger, fires the matching callbacks, and writes the ledger forward. The first run records a baseline and fires nothing (everything would look new). A callback that fails (non-zero exit, timeout, or spawn failure) is reported, makes sync exit non-zero, and its need is left un-advanced in the ledger so the next sync retries it. --dry-run shows which transitions would fire without running anything or writing the ledger.