Prose, blocks and the other bullets
A task line is one sentence, and one sentence is not where a design goes. What it carries
instead is a pointer to a section in the rationale file, and this family owns both halves:
section add writes the prose, and the line that points at it is refused by the gate if the
section is not there.
That split is deliberate. The line is what a reader scans and what a query returns; the section is what somebody opens once, when they are about to do the work. Keeping the argument out of the line is what stops a backlog from becoming a document — the failure this tool was written after measuring, and the reason the model treats the line and its section as two halves of one write.
The rest of the family writes the other things a file holds. Blocks are the grouping every
line belongs to, declared rather than invented by a write. Non-goals are the roadmap’s
other bullet: a constraint, not a task, printed with every brief so a suggestion that
violates one is refused before it is worked on. Criteria are the positive twin — what must
be true for a block to be finished. Priority is the one ordering a project declares rather
than derives, and it outranks the id order that pick otherwise follows.
section find goes the direction a pointer does not: an id resolves to a section, and this
resolves a sentence back to the id that carries it — which for an agent is the difference
between one call and a file printed into a context window per guess.
section
A group of the verbs under it.
The prose files are paragraphs, not lines, so their unit is a section: an anchor a pointer can resolve, a word budget, and a place derived from the task's block. `ship` calls `drop` for the first of its three edits.
section add
Writes. Served to an agent as section_add.
Write one rationale section: the prose in `--body`, within the word budget, filled to the configured width, and placed under the task's own block or beneath the section it extends. A table or a list is inserted as written.
| Argument | Notes | What it is |
|---|---|---|
anchor | required | the anchor, e.g. RK9 or S:VIII.2 (no §) |
--title TITLE | required | the heading text |
--body BODY | — | the prose; omitted or '-' reads stdin, which is how a paragraph gets in |
--body-file PATH | — | read the prose from this file instead — a refusal on a short field then costs the corrected field alone and never the paragraph again |
--role ROLE | default "improvements" | which prose file (default: improvements) |
--level LEVEL | — | heading depth; derived where omitted — a subsection at 3, and a new top level at the depth this file already writes one at |
--json | default false | machine-readable form |
section amend
Writes. Served to an agent as section_amend.
Rewrite one section's own prose, or its heading text, without deleting it. The door that was missing: `drop` refuses while an open line points at the anchor, `add` refuses the duplicate and the guard denies the hand edit, so a design was write-once until it shipped — which is the opposite of when it changes. The subtree is not touched: a subsection is amended by its own anchor. Neither is the anchor itself: that is `section move` under an outline, and `renumber` where the address is the task's id. Nor is the heading line, unless --title asks for it: a body-only amend leaves those bytes alone. An address that is not an anchor is read as a heading text, which is how the two regions carrying no anchor — the file's opening, and a table of contents — are reached; neither is charged the section word limit, which is a rationale's.
| Argument | Notes | What it is |
|---|---|---|
anchor | required | the anchor, e.g. RK9 (no §), or an unanchored heading's own text |
--title TITLE | — | replace the heading text |
--body BODY | — | the replacement prose; '-' reads stdin, and stdin is the default unless --title is the only thing being changed |
--body-file PATH | — | read the replacement prose from this file instead — a refusal on a short field then costs the corrected field alone and never the paragraph again |
--replace OLD | — | the text to correct, edited in the prose already on disk — refused unless it occurs exactly once, so the edit's reach is visible in the call |
--with NEW | — | what --replace becomes; the empty string deletes it |
--role ROLE | default "improvements" | which prose file (default: improvements) |
--json | default false | machine-readable form |
section move
Writes. Served to an agent as section_move.
Move one section to a free address, keeping its prose exactly where it is. The verb an outline had none of: `renumber` moves an id and leaves the pointer as typed under any other scheme, so a doubled address — what `lint` calls `section.ambiguous` and `add` refuses to create — was repairable only by the hand edit the guard denies. The heading, every nested anchor that extends it and the `→ §<anchor>` on every line naming one of them move together, or none of them do. The destination takes every refusal `add` computes, and stays under the parent the address already had: this write changes the address, not the place.
| Argument | Notes | What it is |
|---|---|---|
anchor | required | the anchor to move, e.g. I.2 (no §) |
--to TO | required | the free address to move it to — `anchors` names one |
--role ROLE | default "improvements" | which prose file (default: improvements) |
--json | default false | machine-readable form |
section show
Reads.
Print one section whole, with the word count the budget is measured in. An address that is not an anchor is read as a heading text, so the file's opening and a table of contents answer too. `--own` prints what `section amend --body` takes, so the two extents are one on the section that has children. Reads; never writes.
| Argument | Notes | What it is |
|---|---|---|
anchor | required | the anchor, e.g. RK9, or an unanchored heading's own text |
--own | default false | print this section's own prose alone, which is the extent `section amend --body` replaces — the round-trip on a section that has subsections |
--role ROLE | default "improvements" | which prose file |
--json | default false | machine-readable form |
section find
Reads. Served to an agent as section_find.
Report the anchors whose own prose holds a string, with a count each, so a `section amend --replace` is addressed rather than guessed. The count is what decides the next call: one is what `--replace` accepts, and more is the ambiguity it refuses. The prose itself is `section show`'s. Reads; never writes.
| Argument | Notes | What it is |
|---|---|---|
text | required | the string to look for, exactly as the prose spells it |
--role ROLE | one of improvements, strategy, decisions | one prose file (default: every one this project declares) |
--json | default false | machine-readable form |
section drop
Writes. Served to an agent as section_drop.
Delete one section and everything under it. Subsections included, because one left behind is orphaned prose under the next task's heading — which reads as that task's design and is the outcome worse than deleting too much. Refused when an open line points at the anchor or at anything under it: the section a live pointer names is `ship`'s to remove, and this verb's job is the orphan.
| Argument | Notes | What it is |
|---|---|---|
anchor | required | the anchor, e.g. RK9 |
--role ROLE | default "improvements" | which prose file |
--json | default false | machine-readable form |
block
A group of the verbs under it.
A block is declared by a heading and by nothing else, so every write refuses an undeclared one — and the guard denies the hand-edit that would declare it. Both refusals are right and the pair is a deadlock; this is the key. `list` is the read the other four assume has happened: which labels exist to file under.
block list
Reads. Served to an agent as block_list.
Where a task may go, at the moment one is placed. `add --block <x>` is the first flag on the first write of any new task and nothing said what `<x>` could be: `stats` prints letters and counts and never a title, `list --block` and `delivered <block>` both demand the letter they cannot enumerate. In file order, because a reader takes the sequence for the shape of the plan. A label the roadmap has lost keeps its ledger heading and is named as such — that is the row an `add` still refuses, and `block add` re-declares it.
| Argument | Notes | What it is |
|---|---|---|
--json | default false | the rows, with every file each label is in |
block add
Writes. Served to an agent as block_add.
The label and the title are yours; everything else is derived per file. It goes after the last block's subtree — never at the end, where the roadmap's Non-goals live — or after the block `--after` names, which is a neighbour and not an index, so each file places it after its own copy of that heading. It is spelled at the level and with the separator that file's own first block heading uses. All of the files, or none of them.
| Argument | Notes | What it is |
|---|---|---|
label | required | the block label, e.g. G |
--title TITLE | required | what the block is for |
--after AFTER | — | open it after this block instead of last, e.g. C; refused where a file that wants the heading declares no such neighbour |
--organise ROLE | repeatable; default [] | also write the first block heading into this file, e.g. changelog; a file organised by nothing is skipped without it, and every ship there refuses |
--json | default false | machine-readable form |
block amend
Writes. Served to an agent as block_amend.
The words on a heading the other three cannot change. `drop` plus `add` was the repair and it is refused the moment anything is filed under the label, so a title was write-once from the first `add` on. Narrow: the label is the identity and does not move, the subtree is untouched, and each file keeps its own level and separator. Every file that declares the label or none — a title corrected in one and left in another is the defect this closes.
| Argument | Notes | What it is |
|---|---|---|
label | required | the block label, e.g. G |
--title TITLE | required | the words it should read |
--json | default false | machine-readable form |
block drop
Writes. Served to an agent as block_drop.
The inverse, and narrow in the one way that matters: a heading over work is not an empty heading. It is removed only where its whole subtree is blank, and refused by name where anything is filed under the label — open lines, paused ones, rationale sections. The ledger is the exception, left alone and said so, because history keeps the heading it was filed under.
| Argument | Notes | What it is |
|---|---|---|
label | required | the block label, e.g. G |
--prose | default false | take the heading's note with it — loose prose only, never work |
--json | default false | machine-readable form |
block merge
Writes. Served to an agent as block_merge.
The key RK391 named. Two headings under one label is a state the gate reports and every write refuses with `merge the two regions by hand` — which the guard denies. This is that merge, done by the tool: the first heading stays, every later one's entries move under it, and the emptied duplicates go. The ledger is included, not skipped, because history stays under a heading of the same label. All of the files, or none of them. A nested section is `section move`'s to place and refused here; loose prose is dropped only under --prose.
| Argument | Notes | What it is |
|---|---|---|
label | required | the block label, e.g. B |
--prose | default false | drop a duplicate heading's loose prose as it is folded — never an entry |
--json | default false | machine-readable form |
non-goal
A group of the verbs under it.
The one content of the roadmap that is not a task line, and until RK70 the one thing nothing governed: `Edit` denied and offered five commands that all write task lines, `lint` said nothing because a bullet with no marker is prose, and `sed` through `Bash` was the route left. Opt in with `[non_goals]`.
non-goal add
Writes. Served to an agent as non_goal_add.
Compose, validate and insert one non-goal. Addressed by its lead — unique and checked — because an id would buy a lifecycle for a list of eight lines that changes once a year. No marker, no dep and no pointer: a constraint has no status to state.
| Argument | Notes | What it is |
|---|---|---|
--lead LEAD | required | what is not built — the bolded head a brief prints and a duplicate is judged on |
--why WHY | required | the reason it is not, in this file's own limit; '-' reads stdin, which is how an apostrophe or a backtick survives a shell |
--json | default false | the bullet, with the file and line it landed on |
non-goal amend
Writes. Served to an agent as non_goal_amend.
Correct a constraint's reason in place, keeping the bullet's position. The door `record amend` and `section amend` already are one grammar over: without it the only route was drop-and-re-add, and `add` inserts after the last bullet — so a constraint that sat fifth of eight moved to eighth, and a reviewer read a deletion where a word changed. The lead is not a field: it is the address, so a changed one is a `drop` and an `add`.
| Argument | Notes | What it is |
|---|---|---|
lead | required | the lead, as the file reads it; the trailing stop and case do not matter |
--why WHY | required | the corrected reason, in this file's own limit; '-' reads stdin, which is how an apostrophe or a backtick survives a shell |
--json | default false | machine-readable form |
non-goal list
Reads. Served to an agent as non_goal_list.
The list that binds an `add`, at the moment one is *proposed* rather than the moment a task starts: until RK69 only `brief <id>` printed it, so the rule was carried by a sentence in a file. Presence, not enforcement — whether a proposal violates a constraint is a judgement about meaning, and this tool has no model (L4). Reading is never refused, so an ungoverned list prints and says so.
| Argument | Notes | What it is |
|---|---|---|
--json | default false | the leads, with the file and what was left |
non-goal drop
Writes. Served to an agent as non_goal_drop.
Delete one non-goal whole. The half a correction needs: a lead is the address, so a constraint whose lead changes is one retired and one written rather than an edit to an address. Where two bullets carry one lead the later goes, which makes this the door for `lint`'s non-goal.duplicate as well.
| Argument | Notes | What it is |
|---|---|---|
lead | required | the lead, as the file reads it; the trailing stop and case do not matter |
--json | default false | machine-readable form |
criterion
A group of the verbs under it.
The roadmap's third list. A non-goal says what is not built; nothing said what would make a block done, so the only test left was a line count reaching zero — measured where a block was declared closed and reopened six times. One list per block, addressed by its lead, under a `## Done when — Block X` heading `ship` never touches. Opt in with `[criteria]`.
criterion add
Writes. Served to an agent as criterion_add.
Compose, validate and insert one criterion, addressed by its lead within its block. It **opens the block's list** where there is none, as `priority add` writes its own heading — but never the block: a label the roadmap does not declare is refused, so a typo opens nothing.
| Argument | Notes | What it is |
|---|---|---|
--block BLOCK | — | the block label this finishes, e.g. B |
--task TASK | — | the id this finishes, e.g. RK42 |
--lead LEAD | required | what must be true — the bolded head a brief prints, unique in its list |
--why WHY | required | how it is checked, in this file's own limit; '-' reads stdin, which is how an apostrophe or a backtick survives a shell |
--json | default false | the bullet, with the file and line it landed on |
criterion amend
Writes. Served to an agent as criterion_amend.
Correct a criterion's reason where it sits. `add` appends, so drop-and-re-add moves a line in a list read as the shape of what finishing means. The lead is not a field — it is the address, so a changed one is a `drop` and an `add`.
| Argument | Notes | What it is |
|---|---|---|
lead | required | the lead, as the file reads it; the trailing stop and case do not matter |
--block BLOCK | — | which block's list it is in (default: the one that carries the lead) |
--task TASK | — | which task's list it is in, by id |
--why WHY | required | the corrected reason, in this file's own limit; '-' reads stdin, which is how an apostrophe or a backtick survives a shell |
--json | default false | machine-readable form |
criterion list
Reads. Served to an agent as criterion_list.
Print what a block has to satisfy. Presence, not enforcement — whether the work satisfies a criterion is a judgement this tool has no model for (L4). Never refused, and it says which empty it found: ungoverned, unasked, or all dropped.
| Argument | Notes | What it is |
|---|---|---|
--block BLOCK | — | one block's list (default: everything that declares one) |
--task TASK | — | one task's list, by id |
--json | default false | the criteria, with the file and their lines |
criterion drop
Writes. Served to an agent as criterion_drop.
Delete one criterion whole — the half a changed lead needs, the lead being the address. The heading stays: a block whose criteria all went is one somebody asked the question about, which is not a block nobody asked.
| Argument | Notes | What it is |
|---|---|---|
lead | required | the lead, as the file reads it; the trailing stop and case do not matter |
--block BLOCK | — | which block's list it is in (default: the one that carries the lead) |
--task TASK | — | which task's list it is in, by id |
--json | default false | machine-readable form |
priority
A group of the verbs under it.
The one tier of `pick` a project declares rather than derives, moved out of `roadkeep.toml` and into a `## Priority` section of the roadmap (RK325). Every token in it names work, and work leaves — so unlike the prefix, the paths and the limits, the queue stops being true by itself, and the config was the one file nothing governs. The section wins wherever both are declared.
priority add
Writes. Served to an agent as priority_add.
Insert one entry under the priority heading. Appended by default, because a queue grows at the end and 'everything new is most urgent' is the order nobody meant; --first and --after are the two places that are not the end, and moving work up the order is the act the config file made unavailable.
| Argument | Notes | What it is |
|---|---|---|
token | required | an id of this project, or 'Block X' |
--first | default false | ahead of everything already queued |
--after TOKEN | — | directly after this entry, which must be queued |
--json | default false | the entry, its place in the order, and the line |
priority list
Reads. Served to an agent as priority_list.
The queue `pick` applies, in order, with the file it came from — because a project that wrote a section and is still being ordered by its config has a fact to learn and no other way to learn it. Reading is never refused.
| Argument | Notes | What it is |
|---|---|---|
--json | default false | machine-readable form |
priority drop
Writes. Served to an agent as priority_drop.
Delete one entry. There is no correction verb between the two: an entry carries a token and nothing else, so a token that changed is a different entry and a move is a drop and an insert.
| Argument | Notes | What it is |
|---|---|---|
token | required | the entry to remove, as the file spells it |
--json | default false | machine-readable form |
priority migrate
Writes. Served to an agent as priority_migrate.
RK325 moved the queue into the roadmap and the gate still reads the old declaration, which is right — a project that has not migrated has a real order. This is the door between them. The config line is left alone, because nothing here writes `roadkeep.toml`: the section wins from the moment this returns, and `lint` names the leftover as `priority.config`.
| Argument | Notes | What it is |
|---|---|---|
--json | default false | machine-readable form |
refs
Writes.
Write `[refs] <role>` and re-address that file's own citations in the same transaction (RK1168). Declaring the key alone re-addresses every heading at once and carries none of the prose citing them: measured on one adoption, 7 citations dangled and 21 kept resolving into the other prose file's section of the same address, where nothing reports them. Both writes land or neither does. Only a declaration: a role that already has a namespace is a re-addressing, whose citations carry the old prefix and whose answer is a different transaction.
| Argument | Notes | What it is |
|---|---|---|
role | required | the prose role, e.g. strategy |
--as NS | required | the namespace, e.g. S — the letters before the colon of an address like S:I.2 |
--json | default false | machine-readable form |