<< All versions

Skill v1.0.0

currentAutomated scan100/100
openreachtech/hora-core/hora-plan
──Details
PublishedOctober 21, 2026 at 11:22 AM
Content Hashsha256:bf0f08ed4e7ea563...
Git SHAfb1aa38a49a9
──Files
Files (1 file, 64.1 KB)
SKILL.md64.1 KBactive
SKILL.md · 853 lines · 64.1 KB

version: "1.0.0" name: hora-plan description: Plan one version from its spec. Fixes which version is being built, verifies the spec for holes and contradictions in conversation, and writes the feature-level task list. Runs at the root of the hora repository. Invoked by /hora, or directly as /hora-plan.


hora-plan

The planner. Decide which version is being built, get its spec into a state that can actually be built, and write the list of features to build.

Read ../hora/references/structure.md first — the layout, the invariants and the language rule. `../hora/references/asking.md` fixes how anything here is put to a person, and it is the same file /hora-spec reads.

The three things this skill does

1. Fix the version being implemented
2. Verify the spec for holes and contradictions, and resolve them in conversation
3. Write the task list — one entry per feature, plus the acceptance tasks

The task list is feature-level, never implementation-level. "Build the attendance feature" is an entry. "Write the RpaFlow model" is a checkpoint inside /hora-build, and this skill does not decide it.

This skill may write into specs/. Only /hora-spec may too

Planning is a conversation with whoever wrote the spec, and asking that person to hand-edit twenty separate holes defeats the point of having it. So this skill may write into specs/, under ../hora/references/structure.md, invariant 1:

1. state the hole or contradiction found
2. propose the exact edit, in full
3. wait for that person to approve THAT edit
4. write it

Approval is per edit, never blanket. What is protected is that no requirement enters `specs/` without a human having read the exact words first.

Step 2 is a proposal, and it is said as one — "I suggest this edit; it is yours to decide". Where the finding is instead that this skill may have misread the document, that is a check — "I read this as X; is that right?" — settled before any edit is proposed (../hora/references/asking.md). Half of what looks like a hole in a spec is a hole in the reading of it.

Step 3 stays in prose. The edit's exact words have to be read, and an option labelled "approve" is what lets somebody not read them. Everything around it defaults to the question tool.

A finding that needs design work goes to `/hora-spec` instead, at the stage that owns it. The split is by what the fix is, not by how large it looks:

The findingWhere it is fixed
a missing annotation, a target that names no repository, a typo, a section number that driftedhere, one edit at a time
a missing use case, a use case the design cannot serve, an operation with no kind or no caller, a scope split nobody has made, a contradiction between two designed things`/hora-spec`, at the stage ../hora-spec/references/stages.md names

Why it is not all done here. A use case written into specs/ by the planner is a use case no stage ever walked against a data model or a screen.

Never write into a past version's directory. A fix that belongs to an already-released version goes into the version currently being planned, as a full replacement of that section.


1. Fix the version

A version directory is one under `specs/` whose name is a semver version, and nothing else. specs/skeleton/ holds the blank spec — never planned, never implemented, never counted as unfinished. Any other non-semver directory is skipped, and reported once so nobody assumes it was read.

The target version: among those directories, the lowest one whose .hora/tasks/<version>/ has not been generated or still holds unfinished features. If all are finished, the lowest version that exists under specs/ but not under .hora/tasks/. If there is no such version either, report that every version is complete.

Only the directory name counts. If the version written inside spec.md contradicts it, have a human fix it.

If the target version's `spec.md` is empty or missing, hand the run to `/hora-spec` and stop. Never write the first spec of a version here — writing it without those stages means writing use cases nothing ever walked against a design.

A version whose `.hora/spec/<version>/_stages.md` holds a split handoff not marked consumed is handed over the same way. A hand-written spec satisfies the test above while the moved criteria sit unread; the handoff's presence is as mechanical a test as the emptiness (../hora-spec-horizon/SKILL.md, "Splitting a version under way").

Resolve the diffs first

Sort the version directories in ascending semver order and apply them in turn, each overwriting the last. The lowest version is complete; every one after it is a diff against the version immediately before it.

1.0.0 full
1.0.1 overwrites 1.0.0
1.1.0 overwrites 1.0.1 ← the previous version is the base, not the lowest one

The key for overwriting is `id`. A section that does not appear in the diff is carried over unchanged.

What the diff wroteResult
heading and annotations onlyannotations are overwritten one by one; the body carries over from the previous version
heading, annotations and bodythe whole body is replaced (partial patches of prose are not supported)

Because of this rule, reviving a section takes three lines.

markdown
# Payroll
<!-- id: payroll -->
<!-- kicked: no -->

No target, no depends, no body. Only kicked is overwritten.

Deferring is expressed as "`kicked: yes` in this version, `kicked: no` in the next". kicked: yes carries over through diffs.

`rows` is the one annotation that does not carry over. Read it from the section as this version writes it, never from the resolved document: a version that does not write it runs every row target names, where one that inherited an earlier version's rows would silently skip a row nobody chose to skip.

Files of past versions are never rewritten. Carrying bodies over works precisely because past versions are frozen.

A gap in the versions (1.0.0 → 1.5.0) does not break the chain. Only the versions that exist are applied, in ascending order.

Annex material is not diffed in parts. Prose cannot be patched, so the version that wants to change it places the whole text.

Scanning, digests and the judgment about "a section that disappeared" all happen against the resolved document. Handled per file, every feature would be flagged "the spec changed" on every version bump.

Judge whether the version number is valid

The first version (1.0.0) is not judged. From the second on, the diff in .hora/contracts/ is the primary evidence.

Difference in the contractThe valid bump
nonepatch (if nothing was added)
fields or types only addedminor
removed, renamed, retyped, or a required field addedmajor

Changes that do not appear in a contract (wording fixes, internal refactors) are patch. Something that does not appear in a contract but is visible to users (a new screen) is minor.

Also detect: skipped versions (1.0.0 → 1.5.0; report only, non-blocking), and versions that go backwards or repeat (blocking).

A version number becomes three directory names and a tag in release.yml, so questions about versioning are `blocking: yes`.

How much may be added to a version

The line is not the kind of change but whether the version has been released. Judge by the tag in the hora repository.

bash
git fetch --tags && git tag -l '<version>' # empty = not released

A version is an attribute of the spec, not of the code. app's merge into main comes after every declared repository's merge into main, so app's tag is evidence that all of them have been released.

State of the versionTreatment
not releasedadditions, changes and deletions are all accepted. The version number does not change
releasedleave it alone. Do it in the next version

An unreleased version has no users, so changing a contract breaks nobody. What happens is rework, not broken compatibility. A spec change or a withdrawn feature just before release is entirely normal, and this must not be closed off.

While unreleased, a migration can be rebuilt with db:refresh, so editing the existing migration directly is fine. Once released, add a new migration that undoes it.

A spec change after a child repository has already landed on main becomes an additional PR to that child's main. The version itself is still unreleased, so it may be accepted.


2. Verify the spec, in conversation

What is read

Reached by a link from `spec.md`, and reached by a link alone — not by name or folder.

specs/1.0.0/spec.md the entry point
specs/1.0.0/attendance/spec.md a feature file. Linked from spec.md
specs/1.0.0/attendance/monthly/spec.md nesting is allowed
specs/1.0.0/spec/00-overview.md a declared Source. Any name, any location
specs/1.0.0/docs/RPA_CORE_SPEC.md linked, but not declared. Interpretation material only
specs/1.0.0/request/csv-export.md what somebody asked for. NOT read here, and not an orphan

A file that is none of `spec.md` / a feature file / a declared Source, and that nothing links to at all, is never read — and raises a question (orphan, blocking: no).

`specs/<version>/request/` is the one directory this does not apply to. /hora-spec reads it and drafts sections from it, and what it produced is in `spec.md` by the time planning starts. Reading it here would extract tasks from a wish list nobody was held to.

Declared under Sources, or a <feature>/spec.mdReached, but not declared
Read forextraction — id/target/depends, features, contractsinterpretation only
Ever produces a taskyesnever

A file listed under a `Sources` section acts as a feature file, even if it is not named `spec.md`.

Never invent an id

id is the reference key from .hora/tasks/, and once given it never changes.

PlaceHow it is decided
the H1 of a feature filejoin the path segments relative to `specs/<version>/` with `--`. Deterministic and unique
a ## sectionsomebody states it and it is written. If it is not written, do not infer it
a ## with no idtie that section's content to the H1's `id`. The reference stays stable
attendance/monthly/spec.md → id: attendance--monthly -- separates folders
attendance-monthly/spec.md → id: attendance-monthly - separates words

Folder and file names must not contain `--`. A ## section's id joins to its feature's id with a single `-` (attendance--monthly-data-model).

`id` is unique across the whole version. On a collision, ask with blocking: yes.

`id`/`target` may come from the spec's own existing ID scheme instead of being written as annotations. Where the entry point's Document information declares an Annotation source row, <!-- id: -->/<!-- target: --> are not required.

  • `id` is then the element's own existing identifier, taken as written
  • `target` is looked up from the declared prefix table, mechanically. A prefix the table does not cover is treated as an unstated target: infer from content and report inferred-annotation (blocking: no)

The annotations

markdown
## 6. Data model
<!-- id: data-model -->
<!-- target: backend -->
<!-- depends: none -->
AnnotationContent
ida stable identifier (kebab-case, unique in the document). References use `id`, never a section number
targetwhich repositories this feature touches. The repository name with the project prefix removed (myproject-frontend-admin → frontend-admin; the backend is a single repository, so always backend). Also app and none. Several are comma-separated
rowswhich of `target`'s rows this version runs. Omitted means every row target names. Never carried over from an earlier version (below)
dependsthe id of the sections it depends on. State none explicitly when there are none
kickedyes means withdrawn. Shown in an annotation rather than by deleting the section
builthow far this feature was implemented before Hora Kit was adopted — spec / backend / frontend. Absent for anything built under the kit. Never inferred
baselineinventoried says this feature is listed: not specified, and not accepted. Admissible only where Existing assets declared Baseline: inventoried. It requires `built:`, which is then recorded and acted on nowhere. Never inferred, and never recommended

Subsections inherit from their parent. State it to override.

`target` decides which checkpoints apply to a feature, and nothing else. One feature is one file, whatever it touches. A feature whose target is backend alone skips the frontend gate entirely, so getting target wrong changes what gets built.

`rows` narrows a version's run and nothing else. target stays the permanent classification, and a version that touches one new row of a feature names that row in rows, so the rows an earlier version accepted are not run again (../hora/references/spec-format.md, "rows"). Check each value against `target`, and each row it leaves out against the released task files (section 5).

Check `target`'s value against the repository layout declaration. Pointing at a repository that does not exist is a typo, so ask (blocking: yes).

Where it is unstated, infer from the content. Never treat it as `none`. Record the inference as inferred-annotation (blocking: no).

`target: none` does not mean "do not read". Non-functional requirements become constraints on every feature, the implementation plan decides the order, terminology becomes the source of the glossary.

The required sections never need `<!-- id: -->`/`<!-- target: -->`/`<!-- depends: -->` at all. Recognize each one by its role, the same way its content is already read for meaning. A required role may be satisfied by a declared Source — except the project name and the repository layout, which must be written directly in `spec.md`. Both are decisions, not facts to locate.

What to verify, and what stops the run

Work through the resolved document and check every one of these.

CheckMissing meansblocking
Use cases per feature — except a section carrying <!-- baseline: inventoried -->checkpoints 2, 9 and 11 have nothing to verify againstyes
Acceptance criteria per feature — the same exception, and only those two"what counts as done" would have to be inventedyes
The kind of each API operation — query / mutation / subscription / REST renderercheckpoints 3, 6 and 14 cannot choose which convention to followyes
A stated caller per operation, and an actors table to state it againstthe operation gets whatever filter its neighbours hadyes
A listed section carrying a usecases block, an acceptance block, a screen section or a data-model table of its ownit is specified and listed at once, and nothing decides which half the checkpoints run againstyes
A feature's use cases and acceptance criteria reaching no further than that feature and its `depends`four separate runs act on a block that reaches forward (below)yes
The version's own acceptance criteria — the section present, none or every criterion carrying spans:the whole-version sweep has nothing to check the product againstyes
An order that puts every feature after the features it depends on/hora-build silently builds them in a different order than the document statesyes
The implementation scope, split into "for now" and "permanently"the design cannot tell an extension point from a dead abstractionyes
Whether existing assets may be used"reimplement" is implied, but whether the code is visible is unknownyes
Unknown fields in an API schema or a REST payloadit would mean inventing the shape of an APIyes
A contradiction in the textthere is no way to choose between themyes
baseline: inventoried under Baseline: verifiedthe permission was never grantedyes
baseline: inventoried with no built:nothing makes "this code exists" checkableyes
baseline: inventoried with authority: to-specto-spec runs every checkpoint against the existing code; listing says none of them runsyes
baseline: inventoried on a section added after the version that declared Baseline: inventoriednew work is not inherited codeyes
A `depends` naming the listed section, where a feature's own tables or operations sit on those of a section carrying <!-- baseline: inventoried -->the dependent gets no Rests on: line and stays outside the transitive set when the debt is paidyes
A `rows` value that `target` does not name, or a row it leaves out that no released task file records passed (contradiction)the version would skip work that nothing acceptedyes
A missing target / dependsit classifies content, so it can be derivedno
A missing id on a ##it ties to the H1's id, so references holdno
An orphaned filenotice that something will not be readno

Use cases and acceptance criteria are not the same thing, and a spec that has one still needs the other.

StatesVerified at
a use casewho does what, for what purpose, end to endcheckpoints 2 (does the spec support it), 9 (does the built API support it), 11 (does the screen support it), 18 (does the product support it)
an acceptance criterionan observable behavior that is either present or absentthe tests written alongside the code, and checkpoint 18
a version acceptance criterionan observable behavior that spans several featuresthe whole-version sweep, and nothing else. No feature gate reads it

A feature with acceptance criteria but no use cases builds a set of operations that are each correct and together unreachable, and nobody finds out until acceptance — the most expensive place to find it.

A section carrying `<!-- baseline: inventoried -->` is the one exception to the first two rows, and it suspends exactly those two. It is listed rather than specified, so there is nothing for either block to be checked against. Nothing else is lifted — undefined-api-kind and missing-authorization are raised over a listed feature's operations exactly as over any other's, because those rows describe code that is already running and already reachable.

The emptiness is checked in the other direction too. A listed section is a heading, its annotations and one line of prose; one that also carries a usecases block, an acceptance block, a screen section or a data-model table is claiming both states at once. Stop with contradiction (blocking: yes) rather than pick. What the feature still owes is a row, not a section: its tables and operations are a row each in the version's data model and operation list, justified by the feature's name in place of a use case.

A block that reaches forward is a stop, not a note

Every gate that reads a feature's blocks runs at that feature's own position in the order, so a criterion or a use case naming a feature built afterwards cannot be met wherever it is read (../hora/references/spec-format.md, "A criterion is checked at its own feature's gate"). Four runs act on one anyway: checkpoint 1 builds from the criteria, 6 and 16 write a test for each one for /hora-build to run, hora-verifier reports the untestable one as specIssues, and 18 fails the feature by construction.

Detect it by walking the order once, carrying what is built so far, and reading each feature's two blocks against that set plus the feature itself. A depends on a listed feature is satisfied by the running code and orders nothing, so it counts as already built.

The fix is a design decision and it belongs to `/hora-spec`, at stage 2. The order changes, or the behavior moves to the version's own criteria. Raise `forward-reference` (`blocking: yes`) and route it there. Never move the criterion here, and never reorder _plan.md to make it fit: the order comes from the spec's implementation plan.

Where the order itself contradicts a `depends`, the same category and the same destination. The walk above cannot even run until it is settled.

Resolving what was found

Resolve it here, in conversation, whenever the person who can answer is present. For each finding: state it, propose the exact edit, wait for approval, write it.

Batch the deciding, not the approving. Which findings are real, and which of several fixes to take, go out through the question tool four at a time; the exact wording of each edit is then shown and approved on its own (../hora/references/asking.md).

Two things still go to `.hora/questions/<version>/open.md` instead:

Why
anything the person present cannot answer now (it needs another team, a client decision, a measurement)a decision nobody has made cannot be made by conversation either
every finding that was resolved, recorded after the factthe question file is the record of what was decided and why. A conversation is not
markdown
## Q1. #scope says nothing about what is out of scope for now
<!-- spec: scope -->
<!-- blocking: yes -->
<!-- category: scope -->
There is a "permanently out of scope" part, but no section for "out of scope
for now (to be built later)". Without that distinction there is no way to decide
whether an extension point should be left in place.
-[x] resolved
Added "Out of scope for now" to #scope, listing payroll, in this session.
  • The file is append-only. Existing questions are never removed, and resolved ones stay as - [x]
  • While a `blocking: yes` is unresolved, `/hora-build` is entered only where `Whether the run may go on past an open blocking question`, later in this file, says yes. With only no left, warn and continue
  • A human may also answer by editing specs/ between runs; on re-entry, re-read specs/ and tick what is now resolved

[wing] Whether the run may go on past an open blocking question

No. If even one blocking: yes is unresolved, /hora-build is not entered, and /hora stops at its step 4 with what to fix.

[wing] How to categorize a question

Every question takes one category from this table.

categoryContentDefault blocking
versioningwhether the version number is validyes
scopeconfirming the implementation scopeyes
missing-usecasea feature with no stated use casesyes
missing-acceptancemissing acceptance criteria, or a version with no Version acceptance criteria section and no noneyes
forward-referencea feature's use case or acceptance criterion reaches a feature built after it, or the written order contradicts a depends. Fixed at `/hora-spec`, stage 2 — never hereyes
undefined-api-kindan operation whose kind (query / mutation / subscription / REST) is not statedyes
missing-authorizationan operation, a screen or a spec that does not say who may reach ityes
unmet-usecasea stated use case that the design as written cannot completeyes
spec-proposalan improvement /hora-spec proposed and whoever decided declined or deferred it. Recorded so it is not proposed again every runno
existing-assetswhether existing code may be used, which side is authoritative when it and the spec disagree, and how much of the inherited product this version's tag claimsyes
undeclared-behaviorthe code does something no spec states, under to-spec. Both readings offered, neither recommendedno
contradictiona contradiction in the textyes
dependency-installa declared dependency failed to install, or a conflict-proof change failed to applyyes
lacked-environmentsomething failed for a reason no code change could fixyes
undefined-detailundefined types, schema or validation definitions, seed values and the likedepends
common-fileundocumented handwritten content mixed into a file several features sharedepends
inferred-annotationreporting that id / target / depends was inferredno
spec-assumptionan ambiguous criterion was still meetable under some reading; one was assumed and judged againstno
reinventionchecking whether an existing package already does what is about to be writtenno
upstream-defecta defect in a framework or a package, worked around in this project's own code rather than by editing the dependency, and what would let the workaround be removed againno
orphana file that nothing links to from spec.mdno
addon-precedencetwo active add-ons changed one [wing] section differently, nothing a person stated chose between them, and the more precise one was judged and taken (../hora-addon/SKILL.md)no
hotfix-debta /hora-hotfix run shipped a fix to main without the acceptance review, and that debt is still openno, but fail-loud
eslint-exceptionan adhoc/ branch disabled one rule of a genuine rule contradiction for one fileno, but fail-loud
acceptance-findingan acceptance review found something that is not a spec defect and not yet fixeddepends
audit-findinga checkpoint 8 finding accepted instead of fixed (../hora-build/SKILL.md, "Whether an audit finding may be accepted without a person")no, but fail-loud

`no, but fail-loud` is not the same as an ordinary `blocking: no`. State it by name, on its own, every time a closing report is written.


3. Derive the contracts

Write them into .hora/contracts/<version>/.

The largest risk of having split into repositories is contract drift. Let each repository derive its schema from the spec independently and they will disagree. Derive once before implementing, pin it, and have every side read that.

The spec's API tables usually already carry schema names, inputs and results. When there is no actual schema definition:

RpaFlowsInput(pagination) the contents are indicated in parentheses
→ derive it after the shape of an existing schema. blocking: no
record in a question what was derived, and how
RpaFlowsInput the fields are unknown
→ this would mean inventing the shape of an API. blocking: yes

Every operation's kind belongs in the contract, not only its shape. The contract is where checkpoints 3, 6 and 14 each read it from.

Contracts are cut per server

Not per repository. One backend repository holds several servers, and each has its own contract.

.hora/contracts/1.0.0/
employee-api.<ext>
admin-api.<ext>
public-rest.<ext>

The file form of each contract — its extension and what it holds — is the stack handbook's (docs/stack/artifacts.md, "The contract a server's consumers read"), decided by the server's protocol.

A contract is only made for a server whose consumer is in another repository or outside. The declaration's consumer column decides it.

ServerConsumerContract
employee-apifrontend-employee (another repository)needed
public-restthe phone app (outside)needed
workeran API server in the same repositorynot needed

A Worker's Job payload and the DB schema are not contracts. Both are closed inside the repository. A contract is only for what another implementer reads.

A server with no consumer, and a frontend with no server to match it, are both errors in the declaration, so ask (blocking: no).

Walk every use case through the contracts

Once the contracts are pinned, walk each use case through them as a sequence of calls, field by field. /hora-spec-review and checkpoint 2 ask whether a step has an operation behind it. This walk asks whether the caller can make that call with what it holds at that step.

At each stepLook for
every input the operation requiresa value no earlier step returned, no seed row holds and the actor cannot enter
the result the next step readsa field the contract does not return
a record the use case comes back to, after a filter, a status change or a deletiona query whose input cannot select it

A step on a server with no contract is read from the spec's API table for that server.

A gap is `unmet-usecase` (`blocking: yes`), fixed at `/hora-spec`, stage 4. The contract is derived from the spec, so a field added to the contract alone sets the two apart.

This walk is what lets `/hora-fast` open a dependent before its dependency is finished. A field found missing in the middle of the dependency's backend gate reaches every dependent already built on the contract without it.


4. Write the glossary

.hora/glossary.md (not split per version, append-only). It stops one concept from acquiring two names. A contract pins the type names on an API's surface, but not class names, method names or internal variable names.

Check names against `@openreachtech/eslint-config`'s naming rules as they are written — read them from the package itself, under the backend row's node_modules/@openreachtech/eslint-config/ (../hora/references/structure.md, "The division of labor"). Skip the check and implementation walks into lint errors, each of which invents its own local workaround name.

markdown
| Term | Not called | Identifier | Kind | Used in | Notes |
|---|---|---|---|---|---|
| Flow | workflow, job | `RpaFlow` | entity | backend / frontend | table: `rpa_flows` |
| Random string | | `RandomTextGenerator` | existing package | backend | `<the package the handbook names for it>`. Do not reimplement |
## Names avoided, and why
| The naive name | Why it fails | What was used |
|---|---|---|
| `flowList` | `~List` / `list` are on the denylist | `flows` |

Recording what was avoided is the point. Without the reason, somebody later restores the naive name and lint fails.

`Not called` is copied from the spec's Terminology, never decided here. It holds the domain words a person rejected, and Names avoided holds the identifiers lint rejects, so the two tables never merge.

Do not write a change log (git holds that).


5. Write the plan and the feature files

_plan.md — the order

markdown
# 1.0.0
## Features
1.[ ] #attendance backend, frontend-employee
2.[ ] #attendance--monthly backend, frontend-employee depends: attendance
3.[ ] #payroll backend, frontend-admin depends: attendance--monthly, billing
Rests on: #billing (not accepted)
## Acceptance
-[ ] Sweep the whole version, once every feature above is done
Version criteria: 4 (#version-acceptance-1-0-0), 1 resting on #billing
## Not accepted
-#billing listed since 1.0.0, runs in `admin-console` built: frontend — recorded, not acted on
## Withdrawn
-#year-end kicked in 1.0.0

The order comes from the spec's implementation plan and from `depends`. Never derive an order of your own.

A section revived with `kicked: no` may have `depends` pointing into a past version. Look back through past versions in .hora/tasks/ and treat it as satisfied if it was finished there.

Acceptance appears twice, and the two are different tasks. Every feature carries its own acceptance as checkpoint 18, covering everything implemented so far; the ## Acceptance entry is the whole-version sweep that runs once, before the merge into main. Write both.

The sweep entry names the version's own criteria, because it is the only run that checks them. The entry carries three things — how many criteria, the section's `id`, and how many of them rest on a feature under `## Not accepted` — so that whoever opens the plan can see what the sweep will be judged against.

It is a derivation, re-read off the resolved document on every run, and never carried over. Count the criteria, take the id, count the rests on: lines against this version's ## Not accepted. A version whose section reads none gets Version criteria: none, written rather than left out.

The count going up is worth reading. Nine version criteria against eleven features says most of this version's verification has moved to a single run at the end, which is the shape the feature-at-a-time design exists to avoid (../hora-build/SKILL.md, "One feature at a time, never two").

`## Not accepted` is `## Withdrawn`'s shape applied to the opposite case. ## Withdrawn holds a feature that should not exist and was dropped; this holds one that exists, runs, and has never been specified or accepted. One line each, saying three things: where it runs, which version has been listing it, and its `built:` value, marked as recorded and not acted on.

No checkbox, for the same reason `## Withdrawn` has none. A checkbox would have to mean something, and both meanings are wrong: [ ] puts a feature nobody intends to build in front of /hora-build, and [x] claims a pass over eighteen checkpoints not one of which was ever marked.

`built:` is written on the line and acted on nowhere. The version that pays the debt restates the value and has it confirmed first (section 6).

A feature may depend on a listed one, and its entry says what that costs. New work on an adopted product almost always sits on inherited behavior. The dependent keeps its ordinary depends and adds Rests on: #<id> (not accepted); its own feature file carries the same line, and the acceptance record repeats it beside the id in its scope line. A pass resting on unstated behavior is allowed to exist; a pass that hides what it rests on is not.

That kind of `depends` is satisfied by the running code, never by a checkbox. Nothing is scheduled ahead of a listed feature and nothing is blocked behind one.

`Rests on:` is derived from more than `depends`, because `depends` is a line the kit is allowed to infer. An inference reading prose misses what a table states plainly: a feature whose data model reads a table stage 4 justified by a listed feature's name rests on that listing whether or not anybody wrote depends: payroll. So read both — every `depends` edge, and every data-model or operation row a listed feature justifies by name that this feature's own tables or operations sit on — and write `Rests on:` from the union. Where that second reading finds one the annotation does not name, the omitted depends is a stop (existing-assets, blocking: yes, section 2).

Derive the section again on every run, from the resolved document's annotations. Never carry it over. baseline is an annotation, so it is excluded from the digest (below): a feature that gained inventoried changes no digest at all, and reconciliation watching only digests would never see one move between ## Features and here.

Nothing in the section is declared. All of it follows from two lines in `specs/` — Baseline: inventoried in Existing assets, and the per-feature annotation (../hora/references/structure.md, "Where a lever lives"). Delete the section and the next run rebuilds it identically; hand-edit it and the next run overwrites it.

A version whose every specified feature carries built: collapses to one sweep

The normal shape of an `as-built` adoption is twenty features with checkpoints 1–17 not applicable and 18 open — twenty per-feature acceptance runs over an ever-growing cumulative scope, each finding mostly what the one before it found. The per-feature gate exists to catch a feature breaking its predecessors while the change is one commit old; here nothing is changing.

The qualifying test is specified and built, never built alone.

The featureWhat it gets
specified, and `built:` up to some gatecollapses. Its entry goes under the heading below with a [ ] box, and the adoption sweep closes it
listed — `built:` and `<!-- baseline: inventoried -->`does not qualify, and never had a gate. It keeps its ## Not accepted entry, its absent checkbox and its eighteen [ ]
specified, with no `built:` — a to-spec exception, a new feature riding alongdoes not qualify. It keeps its own open checkpoints and its own gate-18 run, and the sweep entry stays as well

The gate could not be keyed on `built:` alone, because a listed feature carries it by requirement. That test cannot tell the two states apart — and the adoption sweep is the one lever that deliberately overrides the box-state rule, taking every entry under a collapsed version's feature section whatever its box reads (../hora-accept/SKILL.md, "What is in scope"). Write a listed feature in there and it goes to the review skills with no use cases and no acceptance criteria.

Twenty sections carry built: and three of them are listed, so seventeen entries stand under the heading and three sit below it with no box:

markdown
## Features — adopted as built
1.[ ] #attendance built: frontend ← 1–17 n/a, 18 open until the sweep below passes
...
17.[ ] #payroll built: frontend
## Acceptance
-[ ] Sweep the whole version — the adoption sweep. Covers checkpoint 18 of every entry above
Version criteria: 2 (#version-acceptance-1-0-0), 0 resting on a not-accepted feature
## Not accepted
-#billing listed since 1.0.0, runs in `admin-console` built: frontend — recorded, not acted on

Every entry under that heading stays `[ ]` until the adoption sweep passes, and then they are set together. An entry is [x] only once every checkpoint of that feature is, and checkpoint 18 always stays [ ] here whatever built: says. `_plan.md` derives its checkboxes from the checkpoints; it does not announce results ahead of them.

This skill is what sets them, and it is a reconciliation row like every other one in section 6. The trigger is a state, not an invocation: .hora/acceptance/<version>/_sweep.md exists and its newest block reads a passing verdict over tips: that still stand (../hora-accept/SKILL.md, "Recording the result"), and entries under the collapsed heading still stand [ ]. On finding it, set checkpoint 18 in each of those features' files and their entries in `_plan.md` in the same write, off that one record — 18 first, so no entry ever claims more than its own file does.

markdown
-[x] 18. Acceptance (E2E and unit both) <!-- the adoption sweep: .hora/acceptance/1.0.0/_sweep.md -->

The writer has to be named here, because neither skill a reader would expect can do it. /hora-accept writes acceptance records and never _plan.md; /hora-build's own "set the feature's entry to [x]" step never fires, because step 2 of its "Where to start" skips exactly the entries a sweep entry covers. With no writer named, twenty entries stand [ ] over a sweep that passed, and the version can never be finished.

It stays a derivation, and that is why it waits for the record rather than for the run. A pass the record does not carry sets nothing, however certain the run that produced it was — and since /hora enters this skill on every invocation, the record and the boxes are never more than one invocation out of step.

So those entries are `[ ]`, and the entry that closes them is the sweep's. Two things read those boxes and would otherwise draw opposite conclusions:

Reads the boxWhat an unticked entry under ## Features — adopted as built means
whatever selects the next feature to buildnot a candidate. Its checkpoint 18 is covered by the ## Acceptance entry (../hora-build/SKILL.md, "Where to start")
the acceptance sweep, deciding its scopein scope. A collapsed version's sweep covers every entry under the heading, whatever its box reads (../hora-accept/SKILL.md, "What is in scope")

The heading keeps the suffix: a collapsed version's feature section is written `## Features — adopted as built`, here and in every other line of this file that names it. The suffix is the only place in _plan.md where the collapse is written down. And every reader takes the version's feature section whatever its heading reads — a run matching the string ## Features literally finds no section in a collapsed version, sweeps nothing, and reports that nothing failed.

An unticked box and no box at all are different states, and the difference is the whole of both mechanisms. [ ] says a run is going to close this and has not yet; no box says no run will.

Every feature file is still written, in full. The n/a marks, the reasons and the spec digests are what a later version reopens a checkpoint against; collapsing the runs must not collapse the records.

The collapse reaches the features that qualify and stops at each one that does not. A specified feature without built: keeps its own gate-18 run alongside the sweep entry; a listed feature is not written into the section at all. A collapsed version is an ordinary version with fewer acceptance runs in it, never one where the sweep stands in for everything.

One file per feature

markdown
# #attendance Recording and listing attendance
<!-- spec: attendance @ sha256:abc123... -->
<!-- repositories: backend, frontend-employee -->
Constraint: this will be reindexed into a search platform later (#search-infra).
leave room for a hook when a record is saved
Conflict: appends to scalars/index.js. Two other features carry the same mark
## Spec gate
-[ ] 1. Draft or confirm the specification
-[ ] 2. Verify the use cases can be met
## Backend gate
-[ ] 3. DB and API schemas
-[ ] 4. Stub API
-[ ] 5. The modules the implementation needs
-[ ] 6. Actual API
-[ ] 7. Worker
-[ ] 8. Security audit
-[ ] 9. Verify the use cases again, against the built API
## Frontend gate
-[ ] 10. Open the frontend
-[ ] 11. Reconfirm UI/UX and the use cases
-[ ] 12. Component design
-[ ] 13. The frontend modules the implementation needs
-[ ] 14. API client
-[ ] 15. UI
-[ ] 16. Wire the data-fetching logic in
-[ ] 17. Local test environment
## Acceptance gate
-[ ] 18. Acceptance (E2E and unit both)

The header's `repositories:` line is the feature's `target` as it stood when the file was last written, in stripped names, and every file carries it. Reconciliation compares it against the resolved document's target (section 6). A `rows:` line sits beside it only where the section writes `rows`, and is compared against rows as the section writes it, never against the resolved document.

A feature whose `target` names two or more rows the frontend gate takes writes that gate once for each row, in target order, copied from ../hora-build/references/checkpoints.md, "A feature that names several frontend rows carries one frontend gate per row". A feature with one such row keeps the single ## Frontend gate.

A row, or `backend`, that the version's `rows` leaves out is written in full with every checkpoint of its gate marked `accepted in <version>`. Take the version from the released task file that records that gate's last checkpoint [x] (9 for backend, 17 for a row). A released file written before the gate was split per row has one `## Frontend gate`, and its 17 counts for every frontend row its `repositories:` line names, unless a note in the file says that pass was for one row only, and then ask. Where no released file records the pass, stop and ask, since marking it would claim work nobody did.

markdown
## Frontend gate (frontend-admin)
-[x] 10. Open the frontend <!-- n/a: accepted in 1.1.0; this version's rows leave out frontend-admin -->
← 11 to 17 the same, each with the reason
## Frontend gate (frontend-employee)
-[ ] 10. Open the frontend
← 11 to 17 written out in full

A feature carrying `built:` starts with that much already marked not applicable.

builtCheckpoints written [x] <!-- n/a: built before Hora Kit was adopted -->
spec1–2
backend1–9
frontend1–17

Checkpoint 18 always stays `[ ]`. No reading of an existing repository can stand in for an acceptance review.

A listed feature gets the same file, and not one line of it is dropped. What changes is the header and the marks: two non-checkbox lines say what the listing recorded, and all eighteen checkpoints stay [ ].

markdown
# #billing Invoicing and payment collection
<!-- spec: billing @ sha256:def456... -->
<!-- repositories: backend, frontend-admin -->
Listed, not specified: carries `baseline: inventoried`, listed since 1.0.0, and
sits in _plan.md's `## Not accepted`. Runs in
`admin-console`. Nothing below has been marked
Built (recorded, not acted on): frontend. The version that specifies this
feature restates the value and has it confirmed, and
only then does anything below get marked (section 6)
## Spec gate
-[ ] 1. Draft or confirm the specification
-[ ] 2. Verify the use cases can be met
← and the remaining sixteen, written out in
full and verbatim, every one of them [ ]

Eighteen `[ ]`, and not one of them marked not applicable. Not [x], and not n/a either — a listed feature's checkpoints are marked as nothing at all (../hora/references/spec-format.md, "baseline").

The header is what stops eighteen empty boxes being read as "never started". A feature sitting at checkpoint 1 with a screen already in production invites exactly one action: build it. The header says the code exists, says nothing about it has been verified, and says which version listed it.

That file is written into every version's `.hora/tasks/<version>/` for as long as the feature stays listed. Leave it out on the grounds that nothing is being built and reconciliation fires on entry to the next version, appending the section under ## Features with a checkbox.

A dependent's file carries `Rests on:` beside its constraints.

markdown
Rests on: #billing (not accepted). Its behavior is listed, never specified — a
pass here claims nothing about it

It is not a constraint, and it does not come off when the dependent passes. A Constraint: line tells an implementer what to leave room for; Rests on: tells whoever reads an acceptance record what that pass did not cover. It stays until the debt is paid and the dependent's own checkpoint 18 has been re-earned.

Do not infer `built` from the repository. A feature nobody declared is planned from checkpoint 1, however finished its code looks.

Write every checkpoint, including the ones that will obviously not apply. A checkpoint this skill leaves out is indistinguishable from one that was forgotten. ../hora-build/references/checkpoints.md is the authority on the list and its wording — copy it from there, do not paraphrase it.

Digests are taken per section, and annotation comments are excluded from the digest — id, target, depends and every other one. A section runs "from its heading to the next heading at the same level or above". Where a spec is built around a table of individually-identified requirements, the row is the unit.

So a change to `built:`, `baseline:`, `target` or `rows` is invisible to a digest, and is caught by re-reading the resolved document instead (section 6). Each changes what runs rather than what is built.

Mark what overlaps

Mark the features that touch the same file.

What overlapsHow it is detected
an aggregation filefrom how registration works, seen by /hora-setup. Nothing overlaps if scanning is automatic
the same tableseveral sections name the same table
a module a dependent importsthe dependent's section uses an operation, a job or a module its depends owns, in the spec's key file map

Features are built one at a time, so the mark is a signal to re-read the real file before writing, not a lock. If several features add columns to the same table, there is an order — where depends is not written, infer it and report through inferred-annotation.

A module a dependent imports is marked in the dependent's file as `Imports: <module> from #<id>`. /hora-fast builds it in the foundation, so the dependent's backend opens without waiting for the dependency's.

A mark here is about two features, and the concurrent case lives elsewhere. Inside one checkpoint, a file two units would both write is assigned to one of them there (../hora-build/SKILL.md, "Step 5 — splitting a checkpoint into units").

Carry both kinds of "out of scope" as design constraints

Confusing them wrecks the design.

What the spec saysWhat the feature file must reflect
out of scope for now (to be built later)leave an extension point. Keep it replaceable
permanently out of scopedo not abstract it. Exclude it from the design
markdown
Constraint: getting past a CAPTCHA is permanently out of scope (#scope).
stop when one is detected. Build no bypass layer

If the spec does not let you tell them apart, ask with scope (blocking: yes).


6. Reconcile on re-entry

This skill runs every time `/hora` runs. Skip it and sections added to specs/ after the list was settled are never read at all.

Reconcile the set of sections in the resolved document against the feature files in .hora/tasks/<version>/.

StateAction
a section with no feature filecreate one. Append it to `_plan.md`'s end (do not disturb the existing order). One carrying `<!-- baseline: inventoried -->` is appended to `## Not accepted` instead, with no checkbox, and its file gets the provenance header and all eighteen [ ] (section 5)
a section whose digest does not matchclear the checkpoints its change invalidates, and say which (below)
a section whose target gained a row, against the file's repositories: lineclear that row's gate from its first checkpoint, in that feature, and checkpoint 18 with its `_plan.md` entry. A frontend row gets a ## Frontend gate (<row>) section of its own, written [ ] at its position in target order, and the file's single ## Frontend gate takes the name of the row the repositories: line already held, with its boxes untouched. backend clears from 3. Nothing else clears. Name the row and the cleared checkpoints in the report
a section whose target lost a rowraise a removal task for that row's work, as kicked: yes does, and drop the row's section once it is done
a section whose rows gained a row, against the file's rows: linethat row's accepted in marks come off, its gate reopens from its first checkpoint, and checkpoint 18 clears with its _plan.md entry. A rows that lost a row is a stop (blocking: yes) where that row's gate holds an [x] that recorded a run, because no mark is ever taken off a checkpoint that ran, and otherwise goes through the check in section 2
a section that gained kicked: yesmove its entry to _plan.md's ## Withdrawn. Raise a removal task if it was implemented
a section that gained baseline: inventoriedmove its entry to ## Not accepted, and bring every checkpoint back to `[ ]` — an [x] reading <!-- n/a: built before Hora Kit was adopted --> is cleared; an [x] recording a checkpoint that actually ran is a stop (below)
a section that lost baseline: inventoriedthe debt is being paid (below). Do not plan it for building until built: has been restated and confirmed, or authority: to-spec declared
the Version acceptance criteria section's digest does not matchclear the `## Acceptance` sweep entry, and nothing else (below). Re-derive the entry's Version criteria: line in the same write
a section that vanished with no annotationdo not delete anything. The intent is unknown, so ask (blocking: no)
a .hora/hotfix/<hotfix-id>.md whose debt: reads openpay it (below), then write debt: closed in that record
a collapsed version whose _sweep.md has a newest block reading a pass over tips: that still stand, over entries still standing [ ]set checkpoint 18 in each of those features' files and their entries under `## Features — adopted as built`, off that one block (section 5). Nothing else sets them
the implementation scope carries a Reconsider <version>'s scope when: line naming the version being planned, whose condition now holdsraise it once, in conversation, as a proposal to re-run stage 2 — naming the condition, what in the plan satisfies it, and the scope question that recorded the original decline — and record the outcome as a question naming the line: declined lands as spec-proposal (blocking: no, the category that exists so a declined proposal is not re-raised every run); taken hands the run to /hora-spec at stage 2. The recorded question is the record that it fired — the walk raises nothing where one already names this line

A digest only detects changes to sections an existing feature points at. A new section has no feature pointing at it, so this reconciliation is the only way to detect one.

Which checkpoints a spec change invalidates depends on what changed, and this skill decides it, not `/hora-build`.

What changed in the sectionClear from
a use casecheckpoint 2 — everything after it
the data model, or an API's shape or kindcheckpoint 3
an acceptance criterion onlycheckpoint 18
a screen or an interaction onlycheckpoint 11, in each row whose screens changed. In every row when that cannot be told apart
the version's own acceptance criteriathe `## Acceptance` sweep entry alone — not one feature's checkpoint 18
wording, with no change to any of the abovenothing. Record the new digest and move on

The version's own criteria reach no feature's checkpoint, so a change to them may not clear one. No gate ever read them, so no feature's pass was measured against them. What has gone stale is the sweep's.

When it cannot be told apart, clear from checkpoint 2. Rebuilding more than was necessary costs time; leaving a checkpoint marked passed against a spec it no longer satisfies costs correctness.

A withdrawn feature keeps its record — its entry moves to ## Withdrawn and its file stays. It carries no checkbox there, so it does not pollute the count. If it was implemented, raise a removal task and move it once that is done.

Have withdrawal stated with `kicked: yes`. Never have the section deleted. Under the diff scheme every unchanged section is "absent", so absent cannot be told from deleted.

Removing a task does not remove the code. The model, the resolver, the tests and the migration all stay.

A split needs no reconciliation rules of its own. The kicked: row above moves its entries, the Version acceptance criteria digest row re-derives the sweep, and the removal rule covers an implemented mover. Build nothing new for it (../hora-spec-horizon/SKILL.md, "Splitting a version under way").

A section that gains `baseline: inventoried` almost always arrives with checkpoints already marked — up to seventeen of them — and every one of those comes off. A not-applicable mark is cleared the moment its reason stops holding (../hora-build/references/checkpoints.md). The reason here was built: expanded into marks, and listing makes built: a value recorded and acted on nowhere.

The stop is an `[x]` recording a checkpoint that actually ran, and that mark is the one never unmarked here. A run did that work and the file is its only record of it. Ask (blocking: yes) — either the annotation is wrong or the version means to throw away a verified checkpoint, and choosing between those is a decision.

Paying a listed feature's debt

Paying it is a version's ordinary work — and it is the one reconciliation that refuses to act on what the document already says. The single fact the listing recorded, built:, was recorded precisely so that nothing would act on it, and acting on it now would mark up to seventeen checkpoints not applicable on the strength of a value nobody has confirmed since the day it was written. So restate the value, have it confirmed through the question tool with the evidence laid out (../hora/references/asking.md), and only then expand it into not-applicable marks.

Or that version declares `authority: to-spec` for the feature, and all eighteen run against the existing code. The two never appear on one feature.

Inside the paid feature there is nothing to clear. No checkpoint of it was ever marked. And its checkpoint 18, when it comes, is that feature's first acceptance ever: /hora-accept decides that run's reach itself, from the absence of any passing record together with the earlier version's ## Not accepted entry naming it.

What the payment clears lands on other features: checkpoint 18 of every feature that reaches the paid one transitively — through the same union `Rests on:` was derived from, never through `depends` alone (section 5). Each of them passed acceptance while resting on behavior nobody had stated, and stating that behavior changes what the pass was measured against.

A dependent that is itself listed is not in that set, and the exclusion belongs here, where the set is defined. Re-schedule it and it gets a fresh ## Features entry with a [ ] box, seventeen not-applicable marks and a scheduled acceptance over a feature with no use cases and no acceptance criteria — every state the listing denies. It never passed anything, so there is nothing to clear and nothing to re-earn: it keeps its ## Not accepted entry and its eighteen [ ] until its own debt is paid.

Nothing else of theirs is cleared. Checkpoints 1 to 17 stay as they were, because their code did not change — only what they were accepted against did.

Where that clearing lands is the paying version's own plan, and nowhere else. A dependent finished in 1.1.0 has its checkpoint 18 in a released version's task file, which this reconciliation never opens and which /hora never revisits. So each transitive dependent gets a fresh entry in the paying version's `_plan.md`, under `## Features` with a `[ ]` box, and its own file in `.hora/tasks/<paying version>/<id>.md` — checkpoints 1–17 marked not applicable against a stated reason, 18 left [ ], and its Rests on: line carried across.

markdown
-[x] 1. Draft or confirm the specification <!-- n/a: accepted in 1.1.0; re-accepted because #billing's debt was paid -->
← and 2 through 17 the same, each with the reason
-[ ] 18. Acceptance (E2E and unit both)

The box is `[ ]` rather than absent because a run is going to close it. The reason line is what stops 1–17 being run again over code nothing touched, and it names the earlier version so the second acceptance can be read against the first.

No released version's task files or `_plan.md` are ever rewritten (section 1). The version that caused the re-earning is the version that schedules it, which is also the version whose closing report somebody is going to read.

One payment can reopen a dozen acceptances, and that has to be visible before it happens. depends is followed transitively, so a feature three hops away is reopened as surely as a direct dependent. Name every feature the clearing will reach, and what each one now owes, before clearing anything — then clear.

Paying a hotfix's debt

A `/hora-hotfix` run put code on `main` without an acceptance review (../hora-hotfix/SKILL.md). Its record says which features it touched. Turn that into work the ordinary gates already handle.

for each id on the record's touches: line
the id has an entry in this version's _plan.md -> clear its checkpoint 18
back to [ ], AND its
_plan.md entry with it,
and say so
it has no entry here -> add what the sweep now
rests on to the
## Acceptance entry
touches: none -> the ## Acceptance entry
alone
a suites-rest-on: line reads unproven -> add the fact to what the
## Acceptance entry rests
on. The sweep's full
suites prove it
a schema-contract-debt: line stands -> it is work this version
owes. Raise it, and have
the section written
through /hora-spec
then write debt: closed in the record

Both boxes come off together. A _plan.md entry left [x] over a feature file holding an open checkpoint 18 is a feature /hora step 5 will never pick up, so the debt would never be collected.

Only checkpoint 18 is cleared, and only in the version being planned. The hotfix changed code that was already accepted, so what has gone stale is the acceptance, not the build. No released version's `_plan.md` is ever rewritten (section 1).

A hotfix that touched code no feature owns still clears something. touches: none means the sweep is the only run that can reach it, so the sweep entry is what carries it.

Name every feature this reopens before clearing anything, the same way a listed feature's payment is named.


When this skill finishes

State, in one report:

the version fixed, and why that one
how many findings were raised, and how many were resolved in conversation
every question written to the question file — its Q<n> id, its category, its
blocking value, one line of what it is, and a link to the file
(../hora/references/structure.md, "Citing a question in a report")
how many features are in the plan to build, and how many are already done
how many version acceptance criteria the sweep will be judged against, and how
many of them rest on a feature nobody accepted
every feature in ## Not accepted, BY NAME — where it runs, and which features
rest on it
what /hora will start on next

Findings resolved in conversation may be counted. Questions may not. A resolved finding is over; an open question is work somebody still has to do.

A listed feature may not be counted either. "3 not accepted" says that this version claims nothing about part of the product and not which part. They are also outside the feature count — a plan of twenty with three listed is seventeen to build.

Seventeen here and twenty in the acceptance record are not a discrepancy. This report counts what there is to build; the sweep's verdict counts what the tag claims about the product, so it keeps all twenty in its denominator (../hora-accept/SKILL.md, "Recording the result"). Two questions, two numbers.

When it stopped with a blocking: yes outstanding, put what the human has to do first.


References

FileContent
../hora/references/structure.mdthe layout, the invariants, the language rule
../hora/references/asking.mda check, a proposal or a question — and the question tool this skill defaults to
../hora/references/spec-format.mdthe authority on the format of specs/<version>/spec.md
../hora-spec/SKILL.mdwho writes a spec, and what to hand back to it
../hora-spec/references/stages.mdwhich stage a design-level finding goes back to
specs/skeleton/spec.mdthe blank spec that gets copied. /hora-spec does the copying
../hora-build/references/checkpoints.mdthe checkpoint list to write into each feature file
../hora/references/done-criteria.mdwhat "done" means for a checkpoint, a feature and a version
All versions