Changing what is already specified¶
CLI VS Code Experimental
Added in version 0.38.0.
Writing new needs is the easy half of a living project. The usual case is that someone changes a requirement that already exists, and then three questions follow: what does that edit reach, who agreed to the work it causes, and was that work ever finished? A change request answers all three. It is a need of its own, written in your documents beside the needs it changes, and the engine computes its impact, records who approved it, and later checks what it can of the close-out.
A change request holds only what git cannot. Git already records who changed which line and when. The change request records the impact analysis as it stood when it was assessed, who approved it and under which decision, and a status log of every step it has been through. Whether the approved work was done is read off the diff you review.
flowchart LR
plan["1 · Plan<br/>the engine"] --> assess["2 · Assess<br/>AI agent"]
assess --> decide{"3 · Decide<br/>a person"}
decide -->|approved| execute["4 · Execute<br/>AI agent, then a person reads the diff"]
decide -->|rejected or deferred| stop["kept as the record of the decision"]
execute --> close["5 · Close out<br/>the engine"]
The five steps¶
Step |
Who |
What happens |
Then it reads |
|---|---|---|---|
1 · Plan |
the engine |
You name the need or needs the change is aimed at, the anchors.
The engine walks the graph from them, mints an id (or takes the one you give with |
|
2 · Assess |
your AI agent |
The agent rules on every reached need: the change modifies it, removes it, or deliberately excludes it, with a written reason for each exclusion. It edits the change request and nothing else. |
|
3 · Decide |
a person |
Approve, reject or defer. An approval is refused until the assessment is complete, and on approval the engine stamps who decided and when into the record’s status log. |
|
4 · Execute |
your AI agent, then you |
The agent reconciles every downstream stage, held to the approved scope.
Its edits stay uncommitted, and you read them as a diff before they become history.
When every stage succeeded, the engine appends |
|
5 · Close out |
the engine |
The engine checks that what the change removed is gone and that no question is left open,
and only when every check holds appends |
|
Two of the steps belong to a person on purpose:
an approval only means something if a person gave it,
and an edit only becomes history after a person read it.
The close-out carries a name too (--by), so the status log says who closed the record.
No verb commits, and the AI agent is told never to approve on your behalf.
A rejected or deferred change request is kept. A rejection produces no commits, so the record is the only trace that the change was ever proposed and refused.
The status flow¶
Every change request moves through the same states, and where it stands is always read off the record itself:
flowchart LR
draft(["draft"]) -->|"the assessment rules<br/>on every reached need"| assessed(["assessed"])
assessed -->|approve| approved(["approved"])
approved -->|execute| executed(["executed"])
executed -->|close| closed(["closed"])
approved -->|close| closed
draft -->|reject or defer| declined(["rejected or deferred"])
assessed -->|reject or defer| declined
approved -. assess again .-> draft
assessed -. assess again .-> draft
declined -. assess again .-> draft
State |
How a change request gets there |
What it means |
|---|---|---|
|
|
Planned. The assessment has not yet ruled on every reached need. |
|
The assessment’s three impact links cover every reached need. |
Ready for a decision. Nobody has decided yet. |
|
A person approves it ( |
Authorized: the work may proceed. |
|
A person rejects it ( |
Refused. No commit will ever carry the change, and the record is kept. |
|
A person defers it ( |
Put down without a verdict either way, to be decided later. |
|
|
The work is done and waits for the close-out. |
|
|
Finished. Nothing follows. |
draft and assessed are read from what the record holds.
Every other move is written into the record’s status log, cr_status,
as one line per step: the state, when, who, and why.
After a change request has been approved, executed and closed, its log reads:
:cr_status:
approved | 2026-10-14T21:57:08Z | Jane Doe | approved by Jane Doe
executed | 2026-10-15T08:03:11Z | Jane Doe | recorded with --done, without running the cascade
closed | 2026-10-16T09:12:40Z | Jane Doe | closed by Jane Doe
The engine writes the log, and you never edit it by hand. It only grows, so every decision and every step stays on the record, including the ones that were later revised. The rules for what may follow what:
executedfollowsapproved, and itself whenexecuteruns again.closedfollowsapprovedorexecuted, and nothing followsclosed.A decision can be recorded again over a
draft,assessed,approved,rejectedordeferredrecord, but never over anexecutedorclosedone:approverefuses it withtransition_not_allowed.Assess again takes an
assessed,approved,rejectedordeferredrecord back todraftand appends adraftline saying who asked for it. From there the state follows the record’s content again. Anexecutedorclosedrecord cannot go back: that work is history, and a follow-up change request covers what changed since.A
rejectedordeferredrecord is never executed or closed.
A change request that is approved or executed still owes work,
so it shows up as a gap until it is closed.
ubc agent change list --status <STATE> lists the change requests in one state.
Setting up a project¶
Change requests need three things: a ubc that knows them, the configuration that declares them,
and the agent skills that do the two AI steps.
A project installed with ubc agent install from this release already has the configuration and the skills.
For an existing project:
Upgrade ubc and the extension for everyone.
[workflow.change_request]is new in this release. Aubcof version 0.37.0 or earlier refuses the whole configuration over a[workflow]key it does not know, soubc build,ubc checkand the language server all fail on a project that declares the table. Later releases validate[workflow]only inubc agent. Upgrade everyone working on the project, and any CI that runsubc, together.Add the configuration below to your
ubproject.toml.ubc agent updatenever changes yourubproject.toml, so this step is done by hand.Install the skills. Run
ubc agent update -p <project>. It adds thechange-request-assessanddrive-changeskills besidechange-requestfor every AI agent the project was installed for. Commit what it writes.Check the setup.
ubc checkpasses,ubc agent doctorreports no warning aboutchange_request, andubc agent change listanswers with an empty list. Until the configuration is complete,listandplanrefuse and name what is missing.ubc agent config-validate, and with itdoctor, reports an error when[workflow.change_request]is present but thecr_anchorlink is not declared.
In VS Code, nothing else is needed to run the AI steps:
Assess the impact and Execute hand their work to the AI agent you already use in the editor
(the ubcode.agent.id setting), and it works in its own session.
Only on the command line do assess and execute
start a separate, headless agent, the one [agent.runner] names.
The configuration¶
Part |
Why the feature needs it |
|---|---|
|
A change request is a need of this type, in a project that also declares the |
The second |
|
The six |
Where the decision, who made it and when, and the status log are written,
plus two that you may fill: who asked for the change, and its id in an external tracker.
|
The six |
The anchor, the needs the impact analysis reached, the ones it found potentially affected,
and the assessment’s three rulings.
Links validate their ids, and |
|
Where each change request is written, one file per change request.
Without it |
|
No workflow stage produces a change request, so it traces up to nothing, and the orphan gate must not report it. |
Every name starts with cr_. Fields and links are declared for the whole project,
so the prefix keeps them apart from a project’s own anchor or requester.
Each field says nullable = true: it stays absent until the engine writes it,
and Sphinx-Needs warns about a field that declares nothing at all.
Each link says allow_dead_links = true: a change request records what was true when it was written,
so a later change that removes or renames a need it names raises no warning on it.
[needs]
# Keep your own rule as the first branch, and add the ids `plan` mints.
id_regex = "^([A-Z]+_[A-Z][A-Z0-9_]*|CR_[0-9]{8}_[0-9A-F]{4})$"
[[needs.types]]
directive = "change_request"
title = "Change Request"
prefix = "CR_"
[needs.fields.cr_requester]
nullable = true
[needs.fields.cr_external_id]
nullable = true
[needs.fields.cr_approval_decision]
nullable = true
[needs.fields.cr_approval_by]
nullable = true
[needs.fields.cr_approval_at]
nullable = true
[needs.fields.cr_status]
nullable = true
[needs.links.cr_anchor]
incoming = "anchor_of"
outgoing = "anchored_on"
allow_dead_links = true
[needs.links.cr_computed_impact]
incoming = "impact_computed_by"
outgoing = "reaches"
allow_dead_links = true
[needs.links.cr_potentially_affected]
incoming = "potentially_affected_by"
outgoing = "may_affect"
allow_dead_links = true
[needs.links.cr_impact_modify]
incoming = "impact_modified_by"
outgoing = "modifies"
allow_dead_links = true
[needs.links.cr_impact_remove]
incoming = "impact_removed_by"
outgoing = "removes"
allow_dead_links = true
[needs.links.cr_impact_excluded]
incoming = "impact_excluded_by"
outgoing = "excludes"
allow_dead_links = true
[workflow]
# Add "change_request" to the list your project already has.
orphan_exempt_types = ["user_story", "req", "arch", "change_request"]
[workflow.change_request.route]
granularity = "per-need"
path = "specs/_global/change_requests/{id}.rst"
scope = "global"
Each change request is written to its own file on that route, as a page of its own:
it opens with :orphan: and a heading that repeats the change request’s title.
A freshly planned change request therefore raises neither toctree.not_included nor toctree.no_title,
and no toctree has to reach the folder.
Add a :glob: entry such as specs/_global/change_requests/* if you want the change requests in the built site,
and expect toctree.empty_glob from it until the first change request exists.
Step by step: one change¶
This walk-through uses the Pharaoh Changes view in VS Code, and every step names the command that does the same from a terminal.
Plan the change. Click + New change request and pick the need or needs you are changing, the anchors. Then give the change request an id. The box starts with
CR_and the first anchor’s id without its type prefix, soREQ_PIN_FORMATsuggestsCR_PIN_FORMAT. Edit it, leave it empty to have one minted, or press Escape to cancel. If the project cannot take the id, the message says why and nothing is written. A new card appears, readingdraft. Open the record and write what the change is for under Purpose, in place of its TODO. Every later step quotes it, and assessing again keeps it.$ ubc agent change plan --id REQ_PIN_FORMAT --cr-id CR_PIN_FORMAT --purpose "PINs grow from six to eight digits"
Let your AI agent assess the impact. Click Assess the impact. Your AI agent rules on every need the change reaches, modify, remove or excluded, writes why each excluded need is left alone, and edits the change request and nothing else. When every reached need has a ruling, the card reads
assessed. Where the agent could not decide, it left[NEEDS CLARIFICATION: <question>]in the record: answer the question there before you go on.$ ubc agent change assess --id CR_PIN_FORMAT
Review and decide. Open the record and read the rulings, the Planned changes and the Exclusion rationale. Click Decide to approve, or open Other decisions to reject or defer. A box asks why (optional), and the decision is recorded under your git identity. An approval is refused, with every reason at once, until every reached need has a ruling and no clarification marker is left. The card then reads
approved. Commit the change request: the decision is part of the record.$ ubc agent change approve --id CR_PIN_FORMAT --by "Jane Doe" --decision approved
Let your AI agent execute it. Click Execute. Your AI agent reconciles every stage below the anchors, one step at a time, held to the approved scope, and records
executedafter the last step.$ ubc agent change execute --id CR_PIN_FORMAT
Review the diff and commit it. Nothing is ever committed for you. Read the edits as a diff, correct what is wrong, and commit them together with the change request.
Close it out. Click Close. The panel runs the close-out checks first, and if one fails, the card lists the failing checks and nothing is recorded. When they all hold, a box asks who is closing the change request out, with git’s name filled in, and a confirmation names that person before anything is written. The card reads
closed, with who closed it and when. Commit the record.$ ubc agent change close --id CR_PIN_FORMAT --dry-run # what is left? $ ubc agent change close --id CR_PIN_FORMAT --by "Jane Doe"
If the graph moved or the assessment was wrong before the work ran, click Assess again on the card,
or run ubc agent change reassess --id CR_PIN_FORMAT --by "Jane Doe".
The change request goes back to draft with its Purpose kept,
as Assessing again describes, and you continue at step 2.
The Pharaoh Changes view¶
Open the Pharaoh Changes view in the sidebar.
Each change request is a card:
its id and title, its state as a coloured label, and one button for the step it is waiting on.
Open record opens the change request’s reStructuredText at its directive.
That is where what the change request says, and who decided what and when, is written, so the card does not copy it.
Under the buttons, the needs the change was assessed over are listed,
each with what happens to it: modify, remove or excluded.
Click a need to open it.
A closed change request has no button, and its card says who closed it and when.
A rejected one has no button either.
A card that was assessed, approved, rejected or deferred also offers Assess again,
which asks who wants it, confirms, and takes the change request back to draft
before your AI agent assesses it again.
If the text of a change request never mentions a need it excludes,
the card carries one warning line naming those needs.
It is only a warning: the excluded links are the ruling, and the text is for the person who reads it.
Refresh in the view’s title bar reads everything again,
and its expand and collapse buttons show or hide the needs of every card at once.
From the command line¶
The view drives the ubc agent change verbs, and they work the same without it:
$ ubc agent change plan --id REQ_PIN_FORMAT --id REQ_LOCKOUT_THRESHOLD \
--cr-id CR_KEYPAD_SIX_DIGIT_PIN \
--purpose "PINs grow to six digits, and the lockout drops to three attempts" # draft, from two anchors
$ ubc agent change assess --id CR_KEYPAD_SIX_DIGIT_PIN # the AI agent rules on each need
$ ubc agent change reassess --id CR_KEYPAD_SIX_DIGIT_PIN --by "Jane Doe" # back to square one, if the graph moved
$ ubc agent change approve --id CR_KEYPAD_SIX_DIGIT_PIN --by "Jane Doe" --decision approved
$ ubc agent change execute --id CR_KEYPAD_SIX_DIGIT_PIN # the AI agent does the work
$ ubc agent change close --id CR_KEYPAD_SIX_DIGIT_PIN --dry-run # what is left before it can close?
$ ubc agent change close --id CR_KEYPAD_SIX_DIGIT_PIN --by "Jane Doe" # close it out
$ ubc agent change list # every change request and its state
Run on their own, assess and execute start the AI agent configured under [agent.runner],
headless, the same way ubc agent run does.
Note
The headless runner can be any agent CLI that reads its prompt on standard input,
for example command = "opencode run --auto".
The [agent.runner] reference has the keys and the UBC_AGENT_RUNNER* overrides.
Inside an AI agent session that would be a second, hidden agent,
so both have a form for the agent that is already there.
The drive-change skill uses those forms, takes exactly one step,
reports what comes next and who owns it, and stops.
The verbs below has the details.
What you commit¶
The change request, in its own file on the configured route, at every step: the draft, the assessment, the stamped decision, and a rejection too.
The edits the work produced, after you have read them as a diff.
Nothing else is stored. There is no ledger beside the documents: the change request is the record.
How it works¶
The change request¶
A change request is an ordinary need of type change_request.
Here is one after approval, anchored on two requirements of a door keypad:
.. change_request:: Change to REQ_LOCKOUT_THRESHOLD, REQ_PIN_FORMAT
:id: CR_20261014_B48C
:cr_anchor: REQ_LOCKOUT_THRESHOLD, REQ_PIN_FORMAT
:cr_computed_impact: ARCH_KEYPAD, IMPL_KEYPAD_LOCKOUT, IMPL_KEYPAD_PIN
:cr_potentially_affected: REQ_LOCKOUT_RESET, REQ_PIN_CONSTANT_TIME
:cr_impact_modify: ARCH_KEYPAD, IMPL_KEYPAD_PIN
:cr_impact_excluded: IMPL_KEYPAD_LOCKOUT
:cr_approval_decision: approved
:cr_approval_by: Jane Doe
:cr_approval_at: 2026-10-14T21:57:08Z
:cr_status: approved | 2026-10-14T21:57:08Z | Jane Doe | approved by Jane Doe
**Purpose**
PINs grow from four to six digits, and the lockout threshold drops from five to three attempts.
**Planned changes**
- ARCH_KEYPAD: the keypad takes six-digit PINs and locks out after three failed attempts.
- IMPL_KEYPAD_PIN: the PIN check accepts exactly six digits.
**Exclusion rationale**
- IMPL_KEYPAD_LOCKOUT: the lockout code reads the threshold from the requirement's constant, so it needs no edit.
Who writes which part:
Fields |
Written by |
Meaning |
|---|---|---|
|
|
The needs the change is aimed at, and what the impact analysis found from them. |
|
the assessment |
The ruling on every reached need, as links rather than prose. |
Purpose |
you, or |
Why the change is made, in markup over as many lines as you like. Every step of the work quotes the change request’s text, and assessing again keeps it. |
Planned changes |
the assessment |
What changes in each modified or removed need, one bullet per need, and |
Exclusion rationale |
the assessment |
Why each excluded need is left alone. Assessing again drops it with the rulings it explains. |
|
you |
Optional: who asked for the change, and its id in an external tracker. |
|
|
The decision. Overwritten at every approval, never written by hand. |
|
|
The append-only status log, one |
The anchor, the two computed tiers and the three impact links are real links,
so ubc build html shows them as back-links on the needs they name
(anchor_of, impact_modified_by and so on),
you can follow them from the change request in the editor, and they can be queried like any other link.
The editor’s Need view of a need they name leaves them out, since there they are the history of changes against it.
They are deliberately not trace links: each records what a change request said on a date,
not that one need derives from another, so no impact walk passes through a change request,
and a need that only a change request names still counts as unwired.
What makes a link a record is that it starts at a change request, not its name,
so a project’s own link stays a trace link whatever it is called.
A need of type change_request is a change request only in a project that declares the cr_anchor link.
In the editor, a change request is nobody’s parent or child in the trace lenses,
and it offers no Run QA: it is the record of a change, not specification text to review.
When a later change removes or renames a need one of them names, including the anchor a change deleted itself, the record raises no warning and no gap, because each allows dead links (see the configuration). A trace link keeps reporting a dead target, because there it is a real error.
The impact analysis¶
plan walks your trace links from the anchors and sorts what it finds into three tiers.
Tier |
What it holds |
|---|---|
Reached |
Every need above an anchor (its parents, their parents, and so on up the V), and every need below an anchor, all the way down. This is the set the assessment must rule on in full. |
Potentially affected |
Needs linked to something below an anchor that the walk did not reach, typically a second requirement satisfied by the same architecture element. Advisory: listed so a person sees them, never required to be covered. |
Bound |
Only when |
The walk never goes down again from a need it reached by going up. Without that rule, one edit to a requirement reaches its siblings through their shared parent, then everything below them, and soon a fifth of the graph. With it, a single-anchor change reaches 4 to 6 needs on average on the graphs measured so far (ubcode’s own, Eclipse S-CORE, sphinx-needs-demo), and about twice that when a verification arm is present. Several anchors reach the union of their single reaches.
In the keypad example, the two requirements reach their architecture element and the two implementations below it,
while REQ_LOCKOUT_RESET and REQ_PIN_CONSTANT_TIME, which the same element satisfies,
come out as potentially affected.
How a need was written makes no difference to the walk. A need materialised from a one-line code marker is reached exactly like one written in a document.
plan refuses, writing nothing, when an anchor does not exist or is itself a change request,
when the project declares no route, when the type declares no id prefix,
or when --cr-id names an id the project cannot take (see the verbs).
One missing anchor refuses the whole plan:
dropping it would produce a draft that looks complete and is missing an entire branch of the V.
The assessment¶
assess hands the draft to your AI agent with the change-request-assess skill.
The prompt carries every reached need with the file it is written in,
so the agent never has to search for anything.
The agent names every reached need in exactly one of the three impact links.
In the body it always writes Planned changes, one bullet per modified or removed need saying what changes in it,
and says why each excluded need is left alone, one bullet per excluded need under Exclusion rationale.
Nothing parses either section: the links are the ruling,
and the body is prose for the person who approves and for every step of the work, which quotes it.
When the agent cannot decide on a need, it writes
[NEEDS CLARIFICATION: <question>]into the change request and stops on that need. Approval refuses while such a marker stands, so a person answers the question first.New work the change creates, such as a test that now has to exist, is written as a real need with a link back to the change request, never as a line of prose, so the graph records what the change brought with it.
To pull a potentially affected need into scope, the agent adds it to
cr_computed_impactas well, so widening the scope is a visible edit.An anchor is not in the reached set, because the walk starts from it, so it needs no ruling. When the change rewrites the anchor, the agent may name it on
cr_impact_modify, and when the change deletes it, oncr_impact_remove:executethen deletes the anchor after the last step, once everything below it is reconciled. An anchor is never excluded, and an approval refuses one that is.
assess rebuilds the project afterwards and reports assessment_complete from what the record now holds.
It exits with no_assessment_written when the agent did not edit the record at all.
Assessing again¶
A change request is never assessed again on its own when the graph moves after its assessment.
When you want a fresh assessment, reassess (Assess again in the view)
takes the change request back to square one, as if it were new:
The impact analysis runs again from the same anchors, so
cr_computed_impactandcr_potentially_affectedare recomputed.The three impact links and any decision are withdrawn, and so is every section of the text but Purpose: the Planned changes, the Exclusion rationale and whatever else the assessment wrote.
The Purpose is kept as you wrote it, and so are the id, the title and the anchors.
The status log is kept whole, and a
draftline naming who asked for it (--by) is appended.
The change request is then assessed and decided like any draft.
reassess refuses an executed or closed change request:
that work is history, and a follow-up change request covers what changed since.
The decision¶
approve records a decision: approved, rejected or deferred.
--by is required and must name the person deciding.
An approval refuses unless all of these hold, and names every failing one at once:
the impact analysis names at least one need,
the three impact links cover
cr_computed_impactexactly, nothing missing and nothing extra,no
[NEEDS CLARIFICATION]marker stands anywhere the change request reaches.
A rejection or deferral owes none of that: declining a change must never require finishing its analysis first.
The body’s prose gates nothing, so editing a paragraph can never block an approval.
If an excluded need is not mentioned anywhere in the body,
the approval still goes through and its result lists the need in excluded_without_mention,
which show reports too, so you can see which exclusions have no reason on record.
It is a warning only, and a rejection or deferral never carries one.
On approval the engine writes the decision, who made it and when into the record,
overwriting whatever was typed in those fields, and appends approved to the status log.
The record names the needs and the ruling on each, and git holds what the needs said at the time.
Whether the approved work was done is read off the uncommitted diff by the person who reviews it.
Executing¶
execute reconciles the stages below the anchors with the change-request skill,
one pass per downstream stage, one anchor after the other.
Every pass receives the approved scope as a boundary, not a hint:
the needs of that stage with their ruling, every need the change removes, and where a new need goes.
All anchors are checked before the first agent starts, so one bad anchor cannot leave half a cascade behind.
When an anchor sits at the bottom of the workflow, nothing lies below it.
If everything the approval authorized has the anchor’s own type, the work is already done by hand
and execute answers nothing_to_cascade.
If the approval authorized work elsewhere, execute refuses with no_downstream_stages:
the change was anchored on the wrong need.
Every edit is left uncommitted for you to read as a diff.
When every pass succeeded, or there was nothing to cascade, execute appends executed to the status log.
--by names who ran it, and without it the entry carries your git identity.
A pass that failed appends nothing, so the record still reads approved.
When the approval names an anchor on cr_impact_remove, that anchor is deleted just before executed is appended,
so everything below it was reconciled while it still existed,
and a deletion that fails appends nothing.
execute can run again from executed, and each run that succeeds appends another entry.
In an agent session: print, then carry out¶
assess and execute hand their work to a separate headless process.
Inside an editor agent session that is a hidden second agent for every stage:
the editor shows none of its progress,
the shell timeout of one tool call can kill it halfway through a stage,
and a project with no [agent.runner] cannot run them at all.
So each has a form for the agent that is already in the session,
the same relation ubc agent prompt has to ubc agent run.
assess --print prints, as plain text, the prompt the headless run would have handed its runner,
after a === WORKING DIRECTORY === header that names the project root.
The agent carries it out itself by editing the change request,
which is also how a headless assessment is recorded, so there is no --done for it.
execute --print takes every decision the run takes before it would start anything,
then prints ONE step of the cascade:
the prompt the run would have piped to its runner for one stage of one anchor’s cascade,
counted in run order across the anchors.
--step <N> picks the step, and without it the first is printed.
The step is followed by the exact command to run next:
=== WORKING DIRECTORY ===
/work/keypad
Run every command below FROM that directory, and resolve every relative path below AGAINST it.
=== STEP 1/2: archs (anchor REQ_PIN_FORMAT) ===
You are reconciling ONE stage of a spec-driven V-model after an UPSTREAM change. ...
=== NEXT COMMAND ===
This is step 1 of 2. Carry it out in full, then run this command to print step 2. ...
ubc agent change execute --id CR_KEYPAD_SIX_DIGIT_PIN --print --step 2 -p /work/keypad --config-name ubproject.toml
The headless run rebuilds the project between passes, so each pass briefs from the edits of the one before it,
and a print cannot do that for a step it hands out in advance.
So each call builds its step from the project as it stands when the call runs,
and the agent prints the next step only after it has carried out the current one.
A step printed then quotes the edits the earlier steps made,
where a print of every step up front would have quoted need bodies from before the first.
The command in the footer carries the project and the flags of the call that printed it,
so every step of one cascade is built the same way.
After the last step the footer names execute --done instead, with <WHO> left for the person to fill in.
--done appends executed to the status log, the entry a successful run would have appended,
by --by or else your git identity.
It runs nothing and cannot check the steps were carried out, so an agent that could not finish one does not run it.
Like a run, it refuses unless the record stands at approved or executed.
The steps keep their numbers while they are carried out. The list comes from the anchors, the type of each anchor and the workflow, and never from the text of a need, so rewriting, adding or removing needs does not renumber it, and
NinSTEP <n>/<N>is the same on every call.A
--stepoutside 1 to the number of steps is a usage error that names how many there are.When there is nothing to cascade, there is no step to print.
--printprints the lineNO ACTION (nothing_to_cascade)followed by the--donecommand. It writes nothing, so the record still readsapproveduntil--donerecords it.A refusal is the JSON the run prints, with the run’s exit code, and nothing is printed as a prompt.
--printand--donetogether are a usage error, and so are--bywith--print,--skillwith--doneand--stepwithout--print: each would be a flag that is accepted and does nothing.--printresolves no runner, spawns nothing and writes nothing.
Removing needs¶
ubc agent change remove --id <NEED_ID> deletes a need from the document it is written in.
It checks the whole set before touching a line,
refuses a need whose text contains another need unless both are named,
and refuses a need materialised from a code marker:
the marker says where the need starts, but nothing records where the code it stands for ends.
A document left empty is kept, because deleting it could break a toctree.
Needs that still link to a removed need are reported by the close-out.
The close-out¶
close runs four checks over the change request and reports every one of them, passing or not,
so what is left is seen at once:
Check |
Holds when |
|---|---|
|
The record stands at |
|
Every need on |
|
No need still links at a removed need. |
|
No |
When every check holds, close appends closed to the status log and exits 0.
When any check fails it writes nothing and exits non-zero.
--by names who closes the record out and is required, unless --dry-run is given:
a dry run reports the same checks, writes nothing, and exits the way a real run would.
{
"id": "CR_20261014_B48C",
"anchors": ["REQ_LOCKOUT_THRESHOLD", "REQ_PIN_FORMAT"],
"state": "closed",
"checks": [
{"name": "state", "ok": true, "detail": "the record stands at `executed`"},
{"name": "removed_ids_absent", "ok": true, "detail": "the change removes no need"},
{"name": "no_links_to_removed", "ok": true, "detail": "no need links at a removed id"},
{"name": "no_clarification_markers", "ok": true, "detail": "no [NEEDS CLARIFICATION] marker is left"}
],
"dry_run": false,
"closed": true,
"path": "/work/keypad/specs/_global/change_requests/CR_20261014_B48C.rst",
"by": "Jane Doe",
"at": "2026-10-16T09:12:40Z",
"transitions": 3,
"ok": true
}
state is where the record stands after the run, so a dry run keeps the state it found.
closed says whether the entry was written, and is always false for a dry run.
path, by, at and transitions (the length of the status log) are null unless it was written.
close measures nothing about a need that stayed:
whether the approved work was done is a question for the person reading the diff.
Nothing follows closed.
Unfinished work shows up in gaps¶
Without a check, a half-done change could sit in the graph forever.
ubc agent gaps and the Pharaoh Gaps view therefore report every change request
that is approved or executed and has not been closed,
as a change_incomplete gap on the change request itself, naming the state it stands at.
The needs it scopes are not flagged:
they are in a fine state, it is the authorization over them that was never closed out.
Drafts, rejected, deferred and closed change requests owe no work and raise no gap.
The status flow has every state and what may follow it.
The verbs¶
Every verb prints JSON, takes -p <project>, and accepts -c and --config-name like the other ubc agent verbs.
None of them commits, and none stages a file.
Every key a verb’s output declares is always there: a value that does not exist is null, and an empty set is [].
A change request’s anchors are always the list anchors, and a refusal lists in anchors the ones it is about.
plan --id <NEED_ID>…Drafts a change request.
--idis repeatable and names the need the change is aimed at, never a change request.--titlesets its title, which otherwise names the anchors.--bound <HOPS>limits how far the walk goes down. It is off by default, and the result says so only when the bound actually dropped a need.--cr-id <ID>gives the change request that id instead of a minted one. It must start with the id prefix the project declares for the type, carry no whitespace or path separator, match[needs] id_regexand be no existing need’s id. Otherwiseplanrefuses withcr_id_wrong_prefix,cr_id_unsafe_characters,cr_id_not_matching_id_regexorcr_id_exists, and writes nothing.--purpose <TEXT>writes what the change is for under the draft’s Purpose heading, where it otherwise asks for it with a TODO. Refuses when the destination file already exists.assess --id <CR_ID> [--print]Runs the AI agent with
change-request-assess(or--skill) over one change request. With--printit starts nothing and prints the prompt the run would have handed its runner, for an agent that is already in a session, and needs no[agent.runner].reassess --id <CR_ID> --by <WHO> [--rationale <WHY>]Takes the change request back to square one, as described under Assessing again. Refuses a blank
--by, anexecutedorclosedrecord, and an anchor that is gone.approve --id <CR_ID> --by <WHO> --decision approved|rejected|deferredRecords the decision and stamps it into the record.
--rationalesays why. Refuses a blank--by, and a record that was already executed or closed.execute --id <CR_ID> [--by <WHO>] [--print [--step <N>] | --done]Runs the AI agent with
change-request(or--skill) over every stage below the anchors, bounded by the approved scope. Refuses anything that is notapprovedorexecuted. Appendsexecutedto the status log when every pass succeeded.--printstarts nothing and prints the prompt of one pass, step<N>(the first by default), as a=== STEP <n>/<N> ===section followed by the command that continues: the next step, or--doneafter the last.--doneruns nothing and appends the sameexecutedentry, for steps carried out in the session.close --id <CR_ID> --by <WHO> [--dry-run]Runs the close-out checks above and, when every one holds, appends
closedto the status log.--bymay be left out only with--dry-run, which reports and never writes.show --id <CR_ID>One change request whole: its fields, status log, impact sets, and the source location of every need in scope.
list [--status <STATE>]Every change request and its state, rejected ones included. An unknown state is refused rather than answered with an empty list.
remove --id <NEED_ID>…Removes needs from their documents, as described above.
In VS Code, the language server answers agent.changeList, agent.changeShow, agent.changeClose
and agent.changeApprove for the Pharaoh Changes view.
agent.changeClose only reports the checks, like --dry-run, and never writes.
The view records a close by running ubc agent change close --by,
once the checks hold and the person has named themselves and confirmed.
assess and execute run for minutes, so the view sends them to your AI agent instead.
Known limits¶
A link that points down the V, such as one from a test to its result, is invisible to the walk: the walk learns direction from parent links, not from each link’s configuration.
On a project that uses its satisfaction link to group requirements under a feature, every requirement inherits the feature’s whole subtree and the reach grows accordingly.
statusdrives no gate, so a superseded decision reads like a live one.The view asks for each change request separately, which is fine for tens and slow for hundreds.