§1 — Where this came from

Every item below was found by running the project-state migration on a live repository rather than by reading reckon's own code for defects. That matters for priority: these are the things that bite an operator mid-session, measured on a project carrying 22 plans, 11 sprints and 149 flat resources.

The migration itself succeeded and is not in question — 13 resources, source b9fc8216ed3b, composed parity 46b93fe68018, byte-identical snapshot, marker published last. What it exposed is the surrounding surface.

§2 — A sprint cannot span repositories, and the fix is narrow

The asymmetry is exact. Dependency refs are already project-qualified. _schema.py:172-190 defines _PLAN_REF_RE with an optional project: segment and states the contract plainly: a bare slug is local, a qualified ref is external into another mounted project, both travel in the same comma-separated meta lists, resolution of external refs is the server's job, and files stay portable because nothing in the HTML depends on another checkout being present. read_plan already resolves depends_on into a deps list carrying scope, found, status and impl per ref.

Sprint items got none of it. A migrated sprint item is {slug, why_now, done_when} with a bare slug and no project segment. So a sprint — the unit that means "this is the work we are doing now" — is confined to one repository, while the dependency graph it orders is not. That is backwards: a topic routinely splits across repositories, and the sprint is precisely the object that should hold it together.

What already works, so it is not rebuilt. /reckon-ship <project>:S1 selects which project's sprint to execute, and the external-prerequisite stop already refuses to implement another project's plan in this checkout — it surfaces /reckon-ship <project>:<slug> instead. The execution side understands cross-repository work. Only sprint membership does not.

The requirement is bidirectional, and that is the part a qualified ref alone does not give. Executing another project's sprint already works. What is missing is two things together: attaching plans owned by different repositories to one sprint, and reaching that sprint from any participating repository. A sprint is the object that says "this is the work we are doing now", so a person working in either repository must arrive at the same sprint and see all of its members — not a local subset with the rest invisible. A one-way outward ref from a single owning repository fails the second half: the other repository has no path to the sprint at all.

Deliverable, and it is two changes rather than one. First, a sprint item's slug accepts the same optional project: qualifier as every other ref, resolved through the existing external-ref machinery rather than a second implementation. Second, plan linking gains the return path: a plan states the sprint it belongs to in a form that resolves across repositories, so the sprint is discoverable from the member rather than only the member from the sprint. Today plan-sprint holds a bare id (S9) whose meaning is local by construction, which is exactly why a shared sprint cannot be reached from a second checkout.

Done when a sprint holding items from two mounted projects reports per-item scope, found, status and impl the way deps already does; roadmap orders across both; the sprint resolves from either repository with the same membership; a qualified item whose project equals the owner reads as local; and an unmounted or dangling qualified item is an audit finding rather than a silent omission.

Where the two instances land, and why this is issued once. There are two reckon deployments — the ITER host and WSL — but plan state is version-controlled by the repositories themselves, not by the server, so a link or tag change is a commit like any other and propagates by pull. The instances read; they do not own. So this is authored once and both see it. The only per-instance state is mounts.json and the state-dir symlink, which reckon-sync owns exclusively and which decide which repositories a given instance can resolve — meaning a cross-repository sprint renders fully only where both members are mounted. That is a mount question, not a content question, and it is why an unresolved member must report explicitly rather than vanish.

Three design questions this raises, recorded as decisions rather than assumed: whether a cross-repository sprint resource lives in one repository or is composed from a sprint resource in each; what a sprint's completion means when one member repository is not mounted; and what shape the return link takes so that a plan naming a shared sprint stays portable in a checkout where the other member is absent.

§3 — The layout migration destroys grouping with nowhere to put it

Measured on the live project. migrate-layout --check reports 149 moves, 113 of them out of a nested source directory, and zero colliding targets — so it is mechanically safe. It is semantically lossy:

MeasureCount
Standard-names documents flattened73
Of those, whose slug already carries the topic2
Grouping irrecoverable from the slug71

The slugs are bare numbered sequences — 09-llm-compose, 42-polling-workers, DD-03-multiplicity-unit, 37-docs-generation-design. After flattening, 09-llm-compose.html sits in one flat research/ beside gpu-cluster-scoping.html with nothing marking it as standard-names work. Worse, three independent numbered sequences — from research/standard-names/, archive/standard-names/ and archive/research/standard-names/ — each run their own 01..46 and interleave into meaninglessness.

The flattening is not arbitrary. Resource identity is {project, type, id} with id the slug, so a flat typed root makes slug-to-path a pure function. Nested directories would either make it ambiguous or force the subdirectory into the identity. The design is right; the migration simply has no destination for the information it discards.

The published schema has 41 properties and no tag field. The only grouping axes are sprint, milestone, tier and north_star — all single-valued and lifecycle-shaped. None expresses "this is standard-names work". So directories are currently the only topical grouping in the system, and the layout migration deletes them.

Deliverable, in order: add a multivalued tag field following the established comma-separated meta pattern that depends_on and informs already use; backfill it mechanically from the pre-migration paths, which need no judgement because the source directory is the tag; then run the layout migration. A document can carry two tags and cannot live in two directories, so the result is strictly better than what it replaces. Done when the layout migration runs with zero grouping loss provable by comparing tags against the saved move pre-image.

The pre-image is already preserved at ~/.local/share/imas-codex/receipts/reckon-migration/layout-moves-preimage.txt, so this is recoverable whenever it is taken.

The migration breaks 119 links, measured, and the checker cannot prove otherwise yet

This is the constraint that governs the whole section. Scanning every project-absolute href in the live corpus: 526 internal links across 175 files, of which 119 links to 48 distinct targets point at a document the migration moves. Worked examples:

RefsLinkMove
10/imas-codex/sn-search-facility.htmlplans/sn-search-facility.html
7/imas-codex/sn-quality-parity.htmlplans/sn-quality-parity.html
5/imas-codex/sn-quality-parity (no extension)plans/sn-quality-parity.html
4/imas-codex/archive/standard-names/11-rich-compose.htmlplans/archive/11-rich-compose.html

Note both link forms occur — with and without the .html suffix — so a rewriter that handles one and not the other silently fixes 7 references and breaks 5.

And the existing link checker cannot establish a baseline. audit-doc --check-links raises ResourceIdentity.validate_for_write failed: slug: must be a single safe path segment on the pre-migration corpus, because a nested resource such as research/standard-names/foo.html has no single-segment slug. So the tool that would prove the migration safe requires the migration to have already happened. That circularity has to be broken before anything moves, not after.

Sequenced deliverable, and the migration itself is a stage in this plan rather than something run beforehand:

Until that chain is green the layout migration does not run. The project-state migration is already done and is independent — the typed resolver reads flat resources for compatibility — so nothing is blocked by waiting.

§4 — Wiring findings the migration surfaced, and one that predates it

With state split into typed resources, roadmap reports more than it could against a single index:

SeverityFinding
WARNplan-sprint-missing-item — a plan declares a sprint but is absent from that sprint's items
ERRORsprint-order-inversion — a plan is scheduled before a prerequisite, twice

The membership warning is a straightforward scoped write now that sprints are independently versioned. The two inversions share one root cause and are the more interesting finding: both name the same prerequisite, a plan sitting active at 96.5% with every followup resolved, no blockers and no open decisions. It is finished and unclosed, and because it is unclosed it holds two downstream sprints in an inverted order. A plan that is done but not marked done is not a bookkeeping nit — it propagates into the graph as an error.

Deliverable: repair the three findings, and separately ask whether roadmap should distinguish a prerequisite that is genuinely incomplete from a prerequisite whose own state says it is finished. The second is a lifecycle-hygiene signal and reporting it as a sprint-order error sends the reader to the wrong repair.

§5 — The SPA: the tokens are good, the shell and the cascade are not

Read before recommending. The design foundation is genuinely considered and should not be redesigned. foundation.css establishes semantic tokens — --ink, --muted, --faint, --line — with oklch accents, a single hairline border primitive, a restrained shadow, Geist and Geist Mono, and a complete light and dark pair. That is a deliberate system, not templated defaults, and it is the part worth keeping.

What needs work is structure, in this order:

FileLinesProblem
shell.jsx1,54420 top-level components in one file
styles.css2,256498 selectors, no stated layering contract
graph.jsx742the most valuable view, the least legible file
styles-base.css1,163200 selectors; ownership versus styles.css undefined

shell.jsx holds the route parser, the top bar, the filter column, the list column with its own sort model, the status menu, the title bar, the app root, the command palette, the cockpit body and two modals. It is the file every change touches, which makes it a merge magnet — and concurrent sessions are the normal operating mode here, so that is a practical hazard rather than an aesthetic one.

Across four stylesheets there are 1,239 selectors in 5,990 lines, and dashboard.css exists twice — once under docs/_shared/ and again under skills/reckon-sync/assets/. Without a layering contract stating which file owns tokens, primitives, layout and view-specific rules, the cascade is doing work nobody can reason about.

Recommendation for the design pass. Do not spend it on colour and type; that layer is done. Spend it on information architecture — what earns primary space. The present layout is a conventional three-column dashboard with a hash router, and the content most specific to this tool, the dependency graph and sprint DAG, is compressed into a strip component while filters and list chrome take the width. The question worth designing against is which view a person opens when they want to know what to do next, and whether that view is the graph rather than a filtered list.

Sequencing constraint: decompose shell.jsx and state the CSS layering contract before the visual redesign lands, or the design work arrives as a diff to a 1,544-line file and a 2,256-line stylesheet that no one can review.

§5b — A tag nobody can enumerate is not a vocabulary

Yes, and without it the tag field is worse than no tag field. An author who cannot see the existing tags invents one, and within a week the corpus carries standard-names, standard_names, sn and standardnames as four unrelated groupings. That failure is not hypothetical here: the same shape has already been paid for twice in a consuming project — a closed grammar vocabulary that agents had to derive at runtime rather than restate, and an identity-key convention that varied by label and produced four silent-zero query failures in a single session. Both were fixed the same way: make the vocabulary discoverable from the authority rather than remembered.

Both surfaces, because they serve different moments.

One design position worth stating rather than discovering later: the tag list should be reported as observed usage with counts, not as a curated allow-list. A closed list needs governance nobody will do, and it blocks a legitimately new topic at exactly the wrong moment. Counts give the author the signal to reuse without forbidding invention — and they let an audit surface singletons for review, which is the cheap version of governance.

Done when the discovery response carries every tag in use across plans, research and evidence with its count; a test asserts the inventory is derived from the corpus rather than from any literal list, so a new tag appears without a code change; and reckon-create shows the inventory at authoring time.

§6 — Make the surface designable against a real URL

The capability already exists and is one step from being useful. reckon build docs is documented as bundling the UI and generating a portable static site for CI and GitHub Pages: it copies JSX and CSS into docs/_ui/ and docs/_shared/, writes an index.html with relative asset paths, and writes a complete index.json with live-discovered inventory, sprints and milestones so the SPA works without a running reckon server.

And CI already runs it — then throws the result away. .github/workflows/reckon-pages.yml is named "Validate reckon static build", runs uv run --frozen reckon build docs on every push and pull request to main, and declares permissions: contents: read with no pages: write and no deploy step. So the artifact is built, proven to build, and discarded.

Deliverable: add a deploy job to that existing workflow, publishing to GitHub Pages on main. That yields a public URL a browser-based design tool can fetch directly, with no exposure of the host that currently serves the private instance — the static build reads its own index.json and needs no server, so nothing internal is reachable through it. Reckon's own docs/ becomes the example, which is the honest form of a demo: the tool's own planning state, not a fixture.

The guard is not "is it private", and that is measurable

Publication is per-repository opt-in, never a central filter. The lead's rule is that private information stays private and a fork of an upstream repository counts as personal. A predicate of "publish unless private" satisfies the letter and breaks the rule, measured on this machine:

RepositoryisPrivateisForkGitHub visibilityPublish?
Simon-McIntosh/reckonfalsefalsePUBLICyes
Simon-McIntosh/imas-codexfalsetruePUBLICno — personal fork

A guard keyed on isPrivate publishes that fork and reports itself correct. That is a disclosure failure produced by a plausible predicate — the same shape as every silent-null finding that motivated this plan, with a worse blast radius, because the output is a public URL rather than a wrong count. isFork narrows it but is still a proxy: the durable rule is that a repository publishes only by declaring so in its own tree, so a repository that never opted in cannot be published by any central misconfiguration.

That settles the topology question, and the trigger with it. Each repository's own CI runs reckon build docs and deploys its own Pages site, triggered by changes to its own docs/. Reckon publishes a thin composed index over the set that has published. Nothing needs cross-repository credentials, no central list can leak a non-participant, and the common-case trigger is local rather than a cross-repo dispatch — only a change in the set of published projects needs reckon's index rebuilt, which is rare enough for a manual or dispatched run.

Three costs to state honestly rather than discover. The SPA must read several index.json files cross-origin, which needs the Pages CORS behaviour verified rather than assumed — if it does not permit it, the composed index degrades to links out instead of a unified view, and that is the fallback rather than a blocker. Cross-repository plan links become cross-origin URLs, which is arguably an improvement since they become real addresses. And a federated view can show a stale member if one repository has not rebuilt, so each published site must carry its build time and the composed view must show it rather than implying simultaneity.

Two things to settle before publishing rather than after. Reckon's own plans would become public, so their content is a disclosure decision, not a technical one. And a static build must be a genuine snapshot of the same SPA rather than a divergent second path, or the surface being designed is not the surface being shipped — done when the deployed build renders from the same JSX and CSS sources as the served instance, asserted by a test rather than by inspection.

§ Decisions

Where does a sprint spanning repositories live, given it must be reachable from every member?

Locked by the lead 2026-08-24. The requirement is navigation from any member, and a single owning repository makes one participant privileged: if that repository is not mounted the sprint does not exist for anyone else. Composed-per-repo means each repository carries the members it owns and the view unions them, so the sprint is reachable symmetrically from every side. Cost accepted: composition needs a stable sprint identity across repositories, and disagreement on that identity becomes an audit finding rather than a silent divergence.

What does a cross-repository sprint report when a member repository is not mounted?

Locked by the lead 2026-08-24. Absence must never read as completion or as permission. A hard audit error blocking closure was rejected as too strict: an unmounted repository is a local condition, not a defect in the sprint, and it would make a sprint uncloseable on a machine that legitimately lacks the other checkout. An explicit unresolved row that is never counted complete and never silently omitted gets the safety without the false blocker.

Must the tag field and its backfill precede the layout migration?

Locked by the lead 2026-08-24. 71 documents lose their programme irrecoverably, and the backfill needs no judgement because the source directory IS the tag. Doing it afterwards means reconstructing from git history rather than from a live pre-image. Nothing is blocked by waiting: the project-state migration is already complete and independent, and the typed resolver reads flat resources for compatibility.

Do reckon's own plans go public as the GitHub Pages example?

Locked by the lead 2026-08-24 WITH A GUARD: private information stays private, and a fork of an upstream repository counts as personal regardless of its GitHub visibility. Publication is therefore per-repository opt-in, never a central filter. The guard is not isPrivate - measured, Simon-McIntosh/imas-codex reports isPrivate false, isFork true, visibility PUBLIC, so a predicate of 'publish unless private' would publish a personal fork while reporting itself correct. That is a disclosure failure produced by a plausible predicate, the same shape as every silent-null finding that motivated this plan.

Does the shell decomposition precede the visual redesign or land with it?

Locked by the lead 2026-08-24. shell.jsx holds 20 components in 1,544 lines and styles.css carries 498 selectors with no stated ownership boundary, so a visual redesign landing as a diff to those is unreviewable. Concurrent sessions are the normal operating mode here, which already makes that file a merge magnet. Decomposition is also the cheaper half and independently valuable if the redesign never happens.

Is the tag inventory observed usage with counts, or a curated allow-list?

An author who cannot enumerate existing tags invents one, and the corpus fragments into standard-names, standard_names, sn and standardnames as four unrelated groupings. A closed list needs governance nobody will do and blocks a legitimately new topic at the worst moment; counts give the reuse signal without forbidding invention, and let an audit surface singletons as the cheap form of governance.

Locked by the lead 2026-08-24. A curated allow-list needs governance nobody will perform and blocks a legitimately new topic at the exact moment someone is authoring. Counts give the reuse signal without forbidding invention, and an audit surfacing singletons is governance at near-zero cost. Same shape as the closed-vocabulary problem in a consuming project, where the lesson was to derive from the authority rather than curate by hand.

What shape does a plan's sprint reference take, so a shared sprint resolves from any member repository?

plan-sprint holds a bare id whose meaning is local by construction, so qualifying sprint ITEMS alone leaves a second checkout with no path to the sprint. The return path is the half the first draft missed, and it must stay portable in a checkout where the other member repository is not mounted.

Locked by the lead 2026-08-24, and coherent with composed-per-repo which nearly implies it. Each direction fails alone: items-only leaves a second checkout with no path to the sprint, and sprint-declares-members-only means a plan cannot state its own sprint portably when a plan should be readable in isolation. Both, each INDEPENDENTLY resolvable, means either half working is enough to render - which matters precisely when one repository is not mounted. Accepted risk: two mechanisms to keep consistent, so consistency between them is itself an audit surface.

How are the one-shot migration scripts disposed of?

A script written to perform or verify a single migration becomes an undocumented tool someone runs again later against a corpus it no longer describes. The receipt is the durable artifact; the script is not.

Locked by the lead 2026-08-24. A one-shot kept for reuse becomes an undocumented tool run later against a corpus it no longer describes, and this particular script rewrites 119 links. The receipt is the durable artifact; the script is a means. Retaining it under a scripts directory would also violate the consuming repository's own rule against migration-named files persisting in the tree.

How do plans from several repositories reach one published site?

reckon's CI only checks out reckon. mounts.json is per-instance local state, so CI cannot see which repositories the ITER host resolves. Three shapes were considered. Vendoring or symlinking linked repositories into reckon needs a committed repository list plus a commit per plan edit, duplicating content and making the publish decision a central filter. Central sourcing - reckon CI checking out declared repositories with a repository_dispatch trigger from each - works but makes reckon's CI the thing that decides what is published, so one misconfiguration publishes a personal fork, and it needs credentials to reach anything non-public which is precisely the capability that should not exist. Per-repository publication with a federated index means each repository's own CI runs reckon build and deploys its own Pages site, and reckon publishes a thin composed index over the published ones.

Locked by the lead 2026-08-24, after verifying the property it depends on rather than assuming it: an unregistered project with no mounts entry, no state directory and no running server built cleanly at 496K with relative asset paths and a .nojekyll marker, so reckon build is genuinely self-contained. The decisive argument is that the guard becomes structural rather than configured - a repository that never opts in has no build job, so no central list can misconfigure a personal fork into publication - and no cross-repository credentials are needed at all. Central sourcing was rejected despite being operationally simpler (one CI instead of N) precisely because it relocates the disclosure risk into a list. Accepted cost: each consumer pins reckon, which is why pypi-release is sequenced first in S12.

§ Followups

Qualify sprint items, give grouping a destination, then design the surface

Order matters and each step removes risk from the next. The three wiring findings first, since two are ERROR-level and one traces to a plan that is finished and unclosed. Then the tag field and its mechanical backfill from the saved move pre-image, so the layout migration can run with provable zero grouping loss rather than discarding 71 documents' programme. Then sprint items accepting the same project: qualifier every other ref already takes, resolved through the existing external-ref machinery. Then the GitHub Pages deploy job, which turns an already-validated static build into a URL a browser design tool can reach. The shell.jsx decomposition and the CSS layering contract precede the visual redesign, or the design lands as a diff to a 1,544-line file nobody can review.

/reckon-ship sprint-scope-and-surface

The link risk is measured, not estimated, and it is the reason the migration is a staged deliverable rather than a command to run. Across the live corpus: 526 project-absolute internal links in 175 files, of which 119 links to 48 distinct targets resolve to a document the migration moves. Both link forms are in use — /imas-codex/sn-quality-parity.html appears 7 times and the extensionless /imas-codex/sn-quality-parity 5 times — so a rewriter handling one form silently repairs 7 references and breaks 5.

And the checker cannot currently establish the baseline. audit-doc --check-links raises slug: must be a single safe path segment on the pre-migration tree, because a nested resource has no single-segment slug. A read-only audit is asserting a write-time identity invariant, so the one tool that could prove the migration safe requires the migration to have happened first. Breaking that circularity is the first deliverable of the section, ahead of any file move.

Scope corrected by the lead before any work started. The first draft of this section read the requirement as project-qualified sprint selection — running another repository's sprint — which already works and was never the gap. The actual requirement is bidirectional: attach plans owned by different repositories to one sprint, AND reach that sprint from any participating repository. A one-way outward ref from a single owning repository satisfies the first half and fails the second, leaving the other checkout with no path to the sprint at all.

That makes it two changes rather than one, and the second is the one the draft missed: plan linking needs the return path. plan-sprint holds a bare id whose meaning is local by construction, so a shared sprint is unreachable from a second checkout no matter how its items are qualified.

Per-repository publication is verified workable, by building an unregistered project rather than by reasoning about it. A throwaway project with one plan, no mounts.json entry, no state directory and no running server built cleanly: copied _shared/, wrote index.html with relative asset paths, created .nojekyll, and wrote state/<project>/index.json reporting 1 plan. So reckon build is genuinely self-contained and needs neither registration nor the server — which is the property the federated shape depends on.

Four measurements from that build. Payload is 496K, trivial for Pages. index.json already carries an updated timestamp and the project key, so the per-member staleness display the federated view needs is supported rather than new work — though it carries no git revision, so revision provenance would be an addition. Asset paths are relative, confirming Pages compatibility by observation. And the git HEAD lookup failed harmlessly outside a repository and the build continued, so it degrades rather than aborting; in CI that lookup succeeds.

One dependency worth naming: index.html links Google Fonts at fonts.googleapis.com and fonts.gstatic.com, so a published site is not fully self-contained and its typography degrades on a locked-down network. Acceptable for public Pages, and a self-hosted font would remove it.

And one cost of reckon not being on PyPI: reckon-plans resolves to no distribution, so every consuming repository's CI pins a git ref. Unpinned, a reckon change can break a consumer's build with no signal; pinned, there are N repositories to bump. That is the real recurring maintenance cost of the federated shape and it should be chosen with open eyes rather than discovered at the first breakage.