The ubc agent command¶
ubc agent is the engine behind Pharaoh: a set of subcommands that read your
graph and your [workflow] config and report what to do next, what the graph
looks like, and whether it is ready to release. All of them are read-only with
two exceptions: run drives your AI, and install / update scaffold
config — nothing else changes your requirements.
Caution
ubc agent is experimental and intentionally hidden from ubc --help.
It works today, but the subcommand set and the JSON shapes may change between
releases. List the subcommands with ubc agent --help and a specific one with
ubc agent <command> --help.
Three conventions worth knowing
Every verb pins its project and its need the same way. -p <PATH>
(--project) names the project root — a repository can hold several
ubproject.toml files, and without it a verb runs against the current directory.
The verbs that read a single need all take it as --id <NEED_ID>; the inspect verbs
(audit, context, impact) also accept the id positionally.
Everything is JSON. Every verb prints a JSON object on stdout, designed to
be parsed by an AI agent rather than read as prose. The reports (next,
status, review-brief) always exit 0 on a readable graph; the gates
(gaps, trace, release-check, verdict-check, config-validate,
doctor, update) exit non-zero exactly when their check fails; the inspect
verbs (audit, context, impact) exit non-zero when the requested id does
not exist; and the two write verbs (run, verdict-submit) exit non-zero when
they cannot produce their artefact.
The loop verbs act on the “active stream”, not the whole graph. next and
run derive the stream you are working on from git diff
(dirty-vs-HEAD locally, branch-vs-merge-base on a feature branch), or from the
stream anchored on --id <NEED_ID>. An --id that is not a need exits non-zero
rather than silently falling back to the whole graph, so a typo can never be mistaken
for progress. Pre-existing gaps elsewhere are surfaced as
non-blocking open_streams so one stream’s work is not drowned out by another’s.
Driving the loop¶
nextReport the single next actionable stage (
0or1, never a list) for the active stream, as JSON. A report, not a gate — always exits0. When there is nothing to do it returnsstage: nullwith areason(empty/done/blocked). This is the heartbeat you poll to drive authoring. Pass--stage <STAGE_ID>to brief an explicit stage — for example a globalrisks/decisionsarm — instead of the computed recommendation; an unknown stage id exits non-zero.runExecute the recommended stage through the configured runner, then rebuild and report what was produced.
--id <NEED_ID>picks a stream (required on a clean tree);--skill <NAME>runs a named skill instead of the stage’s default — the execution surface for targeted intents such as thechange-requestcascade. The produced artefact is left uncommitted for human or UI approval.statusThe full heartbeat across the whole graph (it takes no stream anchor): one row per stage with its
state(done/ready/blocked), coverage counts, failing gates, and route. Always exits0.
Inspecting the graph¶
audit --id <ID>A read briefing for one need: its
id,title,type,status,body, andsourcepointer, plus atracesub-object and the full map of every named link on it. Exits non-zero if the id is absent (it never substitutes a stand-in).context --id <ID>One need’s neighbourhood: the anchor plus every directly linked need (forward and
_back), each with its own body and the relation it came in on. This is the briefing the authoring skills read.--no-code(alias--graph-only) returns the graph briefing without attempting to resolve related source.impact --id <ID>The blast radius of one need: the multi-hop, up-and-down trace closure of everything that depends on it, to a
--depthbound, each entry tagged with its depth and direction and carrying its code/test URLs. Also listsstale_gapsalready attributed to the anchor or anything it reaches. Used by the change-request cascade to scope an edit.
Gating and coverage¶
gapsThe full structural gap list (
{gaps, by_category, summary, ok}). Exits non-zero when any gap is found — the whole-graph structural gate.--scope <NEED_ID>narrows it to the one stream that need anchors — its entry root and the whole trace-subtree beneath — and recomputes the tally and exit code over that subset. Scoping is by the trace graph, not aspecs/path; an--scopeid that resolves to no need exits non-zero rather than gating nothing.traceThe bidirectional trace-coverage matrix: one row per declared trace edge with forward and backward covered/total counts and the surviving gap ids. Exits non-zero if any edge is uncovered in either direction.
release-checkThe release gate. By default every open structural gap fails it. With
--baseline <PATH>it gates only new gaps against a baseline of accepted debt: a baselined gap is reported as non-blocking, a gap absent from the baseline fails. Add--update-baselineto ratchet the baseline down on a passing run — fixed gaps are dropped so they can never be readmitted; without the flag the gate never mutates state.--with-verdictsfolds the substance gate in: the combined gate is green only when both the structural gaps and the AI-review verdicts are clear.verdict-checkThe AI-review substance gate. Reads the review verdicts from
.pharaoh/verdicts/and reports which review-required needs block a release:missing(no valid verdict),failing(a recorded failure),malformed(an unusable file),outdated(the need changed after its verdict was recorded), andunverifiable. Exits non-zero on any of those; a leftoverstaleverdict (for a need no longer in the graph) warns but never blocks.
Reviewing needs¶
See Reviews and quality analysis for the model these two verbs serve; the entries here are the reference.
review-briefThe reviewer’s contract, as JSON — the review-arm counterpart to
next.review-brief <TYPE>(or--ids <ID…>) emits, per reviewed type: thereview_skillto run, the absoluteverdicts_dir, thereview_needsstill needing a verdict (already-fresh needs are listed underfresh_skippedand excluded unless--all), each need’s current one-hop contentfingerprint, the resolvedcriteriapack (itsguidanceand scoredaxes), the derivedverdict_schema, and thesubmit_commandtemplate. A report — exits0even when everything is already fresh.verdict-submitWrite one need’s verdict.
verdict-submit <NEED_ID> --file <PATH|->reads a scored verdict (-= stdin) and writes it atomically to.pharaoh/verdicts/<NEED_ID>.json. The engine stamps the provenance (schema,need,criteria, and both fingerprints), validates the scores against the need type’s resolved pack, and refuses an in-repo draft path — so a reviewer cannot forge a verdict’s identity.--fingerprintand--criteria-fingerprintarm race guards that reject a verdict scored against content or a rubric that has since changed. Exits non-zero, with a machine-readable reason, on any rejection.
Setup and health¶
config-validateCross-check
[workflow]against theubproject.tomlontology as JSON ({ok, errors, warnings}): every type a stage produces or traces against must be a declared type, every referenced link a declared link, every transition state a declared lifecycle state. It also validates the[quality]gate against the loaded criteria packs — a floor above an axis’smax_score, or a floor naming an axis no pack declares, is an error. Reads config only — never builds the graph.doctorThe pre-flight an agent runs before driving the loop, so it fails on a misconfiguration before spending tokens. Composes
config-validateand then probes only the tools the resolved config actually requires (a diagram renderer when the diagram gate is on; codelinks when a code-trace gate is configured). A required-but-missing tool is a blocking error with a remediation hint.install/updateBootstrap a project into the loop from a profile, and reconcile a previously installed project against the current profile. Covered in the quick start (install, update).
See also
Quick start shows these verbs in the order you actually use
them; Configuring the workflow explains the [workflow]
keys the gates enforce.