Skip to main content
AcademytutorialOpenSpec tutorial series — Part 5 (optional): Retrofit an existing codebase

OpenSpec tutorial series — Part 5 (optional): Retrofit an existing codebase

Optional, situational follow-up to Parts 1 + 2. Only relevant if you need to bring an older app — built before OpenSpec — up to the current spec ↔ code convention. Reference playbook for the /opsx-coverage-scan, /opsx-annotate and /opsx-reverse-spec skills.

TutorialOpenSpecSpec-firstTutorial seriesRetrofitLegacyClaude Code
21 min read

Heads up — this is an optional, situational tutorial. Most developers will never need to run a retrofit. Day-to-day work in a Conduction app — writing changes, implementing tasks, opening PRs — doesn't touch any of the commands below. You can finish Parts 1 + 2 and go straight to the Hydra series or the Claude skills series without losing anything.

This part is only relevant in one specific case: an older Conduction app needs to be brought up to date with the current OpenSpec convention after the fact. A small number of our apps were built before openspec/specs/ and the @spec-tag convention existed — they have working code, but no specs and no annotations pointing from code to requirements. If one of those apps ever has to be brought into line with the convention, this is the playbook for it. We work through the retrofit playbook end to end.

Discuss with your team lead before you start. A retrofit is real work — not a side-quest you run after lunch. The full pass on a medium-sized app easily takes hours of wall-clock time and a serious chunk of Claude tokens (often the cost of a normal feature change, sometimes more, especially if Bucket 2 turns out to be large and /opsx-reverse-spec has to run on many clusters). The retrofit playbook explicitly lists a roll-out order across our apps for exactly this reason: not every app is next, and not every app needs it now. Don't run a retrofit on your own initiative — confirm with your team lead or product owner first whether this app is the right candidate, what budget is available, and which apps are higher priority. Then start at Step 1.

Why retrofit?

Almost every Conduction app was built before the spec ↔ code annotation convention from ADR-003. That convention says: every PHP file's main docblock and every public method's docblock carries one or more @spec openspec/changes/<change-name>/tasks.md#task-N tags pointing at the task(s) it implements.

Apps you build spec-first via /opsx-apply get those tags for free — the builder writes the tag while it writes the code. Apps that already exist need a one-time retrofit pass, otherwise /opsx-verify (Part 2, Step 11) falls back to fragile keyword matching: every audit becomes a guessing game, every refactor a coin flip.

Retrofit gives you three things at once:

  1. Traceability — for every method you can answer: which requirement does this implement?
  2. An honest gap list — code without a REQ, REQs without code, and ADR violations are all surfaced in one report.
  3. An entry point for Specter — the spec dashboards across all our apps fill up properly only when the cohort flags are set right (which retrofit does).

Retrofit is not "writing the spec you should have written". It captures observed behaviour, not original intent. Lossy by design. We accept that — a noisy map is more useful than no map at all, and you can review it later.

Three skills, in order

SkillWhat it doesWrites
/opsx-coverage-scanAudit only. Buckets every code unit into one of six categories.openspec/coverage-report.md + .json sidecar
/opsx-annotateAdds @spec tags to every Bucket 1 method via a ghost change.Annotation-only PR
/opsx-reverse-specDrafts REQs for a Bucket 2 cluster via a ghost change, then annotates.One spec PR per cluster

Run them in exactly this order. The audit is the contract between the three: annotate reads its report to know what to tag, and reverse-spec reads it to know which clusters still need a home. Don't skip the audit — and don't run annotate on a stale report.

What is a ghost change?

Legacy code was never written against a change, so there is no tasks.md#task-N for @spec to point at. Retrofit bridges the gap with ghost changes.

A ghost change has the same shape as a normal OpenSpec change — proposal, spec-delta (sometimes empty), tasks, eventually archived — but it exists purely as an anchor for @spec annotations. Naming convention: retrofit-{YYYY-MM-DD}-{descriptor} so it sorts chronologically with non-retrofit changes.

Once archived, the path openspec/changes/archive/<ghost-name>/tasks.md#task-N keeps resolving as a textual reference. @spec doesn't follow live lookups, so the annotations stay valid forever.

How is a retrofit-change different from a regular feature-change? A regular change starts from intent ("I want to add full-text search") and ends in code. A retrofit ghost change starts from code ("this method already does X") and ends in a documented anchor. Same artefacts on disk, opposite direction of arrow.

Step 1: prep the app

Before the first scan:

cd /path/to/openregister
git checkout development        # or 'beta' for apps that keep specs there
git pull
git status                       # MUST be clean

Optional but recommended — create .opsx-ignore in the app root for paths you deliberately don't want scanned (vendor code, generated files, deliberately-unspec'd internal tools). One glob per line, # for comments. See openregister/.opsx-ignore for a worked example.

Specter prereq (one-time, idempotent):

python3 concurrentie-analyse/scripts/migrate_app_specs_retrofit.py

That adds the retrofit, retrofit_extensions and spec_hash columns to the app_specs table. Without those columns /opsx-reverse-spec will refuse to sync.

Step 2: scan with /opsx-coverage-scan

In Claude Code, in the app folder:

/opsx-coverage-scan openregister

This runs read-only. No @spec tags are written, no specs change, no commits. The skill walks every PHP file, identifies each public method, and tries to match it against an existing REQ in openspec/specs/. The output is two files:

  • openspec/coverage-report.md — human-readable
  • openspec/coverage-report.json — parseable sidecar, consumed by the next two skills

Open the markdown report. It contains a header, six bucket sections, and two meta-sections.

Step 3: read the report — the six buckets

The whole retrofit pivots on these buckets, so it's worth knowing them by heart.

Plus two meta-buckets the report lists but that need no retrofit action:

  • annotated — methods that already carry @spec tags. Re-runs of the scan should grow this bucket and shrink Bucket 1.
  • plumbing — framework glue, empty constructors, listener dispatch, thin controllers. Never carries @spec.

Read the report manually before proceeding. The scan is heuristic — wrong Bucket 1 entries produce wrong annotations downstream that are then much harder to undo than to prevent.

Step 4: annotate Bucket 1 with /opsx-annotate

Once you trust the report:

/opsx-annotate openregister

What the skill does, in order:

  1. Creates a ghost change retrofit-{YYYY-MM-DD}-annotate-openregister/ with an empty spec delta and one task per REQ in Bucket 1.
  2. Walks every Bucket 1 file + method and adds @spec openspec/changes/retrofit-{date}-annotate-openregister/tasks.md#task-N to the docblock.
  3. Archives the ghost change before opening the PR (it never needs a separate review cycle).
  4. Updates .git-blame-ignore-revs so the annotation commit doesn't drown git blame.
  5. Opens an annotation-only PR.

A few things to know:

  • Idempotent. Re-running with no code changes produces no new annotations. If a dated ghost change already exists for today, the skill asks whether to reuse it or open a fresh one. Running it again next week creates a new dated ghost change.
  • Annotation-only PR. The diff only adds docblock comments. Reviewers don't need to read it line by line — they need to spot-check the matches against the report.
  • PHPCS rejecting the tag order? Stop. Fix the PHPCS config, never reorder the tags. The ADR-003 + hydra-gate-spdx format is fixed.
  • .git-blame-ignore-revs only works locally if developers enable it once: git config blame.ignoreRevsFile .git-blame-ignore-revs. The skill suggests this but won't run it for you.

Step 5: reverse-spec Bucket 2 entries — one cluster at a time

This is where the real work happens. For every Bucket 2 entry in the report, one run:

# Bucket 2a — extend an existing capability
/opsx-reverse-spec openregister --extend admin-settings

# Bucket 2b — mint a brand-new capability
/opsx-reverse-spec openregister --cluster app-lifecycle

What the skill does, per run:

  1. Reads the cluster's code.
  2. Drafts REQs describing the observed behaviour (capped at 5 REQs per run — if the cluster needs more, split it into smaller clusters and run again).
  3. Creates a ghost change with the spec delta + one task per new REQ.
  4. Invokes /opsx-ff to fill in design.md (so reviewers see how, not just what).
  5. Annotates the cluster's methods inline (does not call /opsx-annotate — that would create a parallel ghost change).
  6. Runs python3 concurrentie-analyse/scripts/sync_spec_content.py openregister to register the spec with Specter.
  7. Archives the ghost change.
  8. Opens one PR per run.

Bias toward --extend. Extending an existing capability is cheaper for the reviewer than minting a new one — same vocabulary, same boundary. Only reach for --cluster when the cluster is genuinely new behaviour territory that no current capability covers.

Each PR is its own review cycle because the REQ language is the review surface. Reviewers focus on: do these REQs describe what the code actually does? Not: is the code correct? (The code already lives; correctness is downstream.)

Documentation-only retrofits

Sometimes a Bucket 2 cluster turns out to need no new REQs at all. Three sub-patterns:

PatternWhenExample
Cross-capability annotation patchCluster's methods map to existing REQs in other capabilities.retrofit-{date}-b2b-crossrefs — 33 tasks pointing across 15 sibling capabilities.
Private-helper inheritanceScanner couldn't follow the call chain into private helpers.retrofit-{date}-schema-hooks — 7 private helpers inherit parent annotate tasks.
Scanner misclassification cleanupScanner placed methods under the wrong capability.retrofit-{date}-tenant-isolation-audit — methods re-routed to their actual capabilities.

In all three cases the ghost change has no specs/ folder, and the proposal must say "no new REQs" / "no new REQs needed" / "behaviors are fully covered" explicitly. App Mode (Step 7) reads that phrase to detect the pattern.

Step 6: address Buckets 3 and 4

These are not part of the retrofit per se, but they fall out of the same report and should not be left lying around.

  • Bucket 3a — REQ orphaned with history match: open separate PRs to fix or remove the broken code. Don't bundle with annotation PRs.
  • Bucket 3b — REQ orphaned with no trace: one PR that marks each REQ status: deferred in spec.md or removes it outright. Either way, document the decision in the PR body.
  • Bucket 4 — ADR conformance: open one follow-up issue "ADR cleanup pass — see openspec/coverage-report.md Bucket 4" and address in a separate cycle.

Step 7: confirm the retrofit is done with App Mode

When you think you're done:

/opsx-verify --app openregister

This is the App Mode of /opsx-verify — the canonical retrofit DoD audit. It walks every retrofit ghost change under openregister/openspec/changes/archive/retrofit-*, scans for dangling @spec paths, audits the cohort frontmatter, and prints a single pass/fail report.

Don't use plain /opsx-verify <change-name> for this. That mode verifies a single (active) change against openspec status and won't see archived retrofit changes at all.

A retrofit is done when App Mode shows ✅ on every row of:

  • Retrofit ghost changes — all archived
  • Tasks completion — every retrofit's tasks all [x]
  • Dangling @spec paths — 0
  • Symlinks under openspec/changes/ — 0
  • Naming convention — every retrofit folder matches retrofit-{YYYY-MM-DD}-{descriptor}
  • Cohort frontmatter — every retrofitted capability carries retrofit: or retrofit_extensions: on its master spec, in block-YAML form with bare REQ-IDs
  • Frontmatter format — block YAML, no inline lists, no full-text values

Plus the workflow items App Mode doesn't check (you do, by hand):

  • Annotation-only PR + per-cluster reverse-spec PRs all merged
  • Bucket 3 issues triaged
  • Bucket 4 follow-up issue opened
  • One final sync_spec_content.py openregister so Specter's cohort columns are populated for every retrofitted capability

Troubleshooting

/opsx-annotate or /opsx-reverse-spec refuses to run — 'dirty working tree'

/opsx-annotate and /opsx-reverse-spec refuse a dirty tree on purpose: an annotation pass touches hundreds of files and a reverse-spec run rewrites spec.md, and you really don't want to mix either with unrelated edits. Stash or commit your other changes first. (/opsx-coverage-scan at Step 1 doesn't have this constraint — it only writes to openspec/coverage-report.{md,json}.)

PHPCS rejects the @spec tag order in the docblock

Do not reorder the tag. The ADR-003 + hydra-gate-spdx format is fixed across all apps. Fix the PHPCS config (usually by allowing the tag in the right slot). A misordered tag here will diverge every other app that gets retrofitted later.

/opsx-reverse-spec wants to draft more than 5 REQs in one run

The 5-per-run cap is deliberate — a reviewer can't seriously read more than ~5 new REQs at once. Split the cluster into smaller clusters and run the skill again per piece.

Specter doesn't show the new retrofit spec after a reverse-spec run

Check that sync_spec_content.py ran without error during the skill — and that the migrate_app_specs_retrofit.py migration was applied beforehand. The skill calls sync synchronously; a silent skip means the migration is missing.

A private helper has no @spec but its public caller does

That's by design. Private helpers inherit their caller's REQ in Pass B of the scan. If the parent is annotated and the helper is not, re-scan and re-annotate — the second pass should pick it up.

App Mode reports a dangling @spec path

Most often a typo in the ghost change name (the path mismatches the folder on disk). Fix the path in the docblock and re-run App Mode. If the ghost change itself was renamed after annotation, that's a hard rule violation — restore the original name.

Test yourself

Five questions to check that this part landed. Stuck? Click Hint. Curious about the answer? Click Answer.

1. What are the three retrofit skills, and in what order do you run them?

Hint

Audit first, then annotations on the easy matches, then drafting new REQs for the hard ones.

Answer

In this exact order:

  1. /opsx-coverage-scan {app} — audits the app and writes openspec/coverage-report.md + .json. Read-only.
  2. /opsx-annotate {app} — adds @spec tags to every Bucket 1 method via a ghost change. Annotation-only PR.
  3. /opsx-reverse-spec {app} --extend <capability> or --cluster <name> — drafts new REQs for one Bucket 2 entry per run, then annotates the cluster's methods. One PR per cluster.

Don't skip the audit and don't run annotate on a stale report — the report is the contract between the three skills.

2. What is spec-coverage, and which six buckets does /opsx-coverage-scan use to measure it?

Hint

It's a per-method classification. Two of the buckets are about code-without-REQ, two are about REQ-without-code, one is about clean matches, one is about ADR violations.

Answer

Spec-coverage is the percentage of an app's public methods that map onto an explicit OpenSpec requirement — traceability, not test coverage. /opsx-coverage-scan measures it by bucketing every method into one of six categories:

  • 1 — Method maps to an existing REQ. High confidence ≥ 0.85, or NEEDS-REVIEW 0.70–0.85.
  • 2a — File belongs to an existing capability but the behaviour is not covered by any current REQ.
  • 2b — File belongs to no current capability.
  • 3a — REQ orphaned, history shows matching keywords in removed lines — probably broken code.
  • 3b — REQ orphaned, no historical trace — never implemented.
  • 4 — ADR conformance findings (missing license header, hardcoded strings, …).

Plus two meta-buckets the report lists but that need no action: annotated (already tagged) and plumbing (framework glue, never tagged).

3. What is a "ghost change", and how does a retrofit-change differ from a normal feature-change?

Hint

Same artefacts on disk, opposite direction of arrow.

Answer

A ghost change has the same shape as a normal OpenSpec change — proposal, spec-delta (sometimes empty), tasks, eventually archived — but exists purely as an anchor for @spec annotations on legacy code. Naming: retrofit-{YYYY-MM-DD}-{descriptor}.

The difference with a feature-change:

  • A feature-change starts from intent ("I want to add full-text search") and ends in code. The spec is written before the code.
  • A retrofit ghost change starts from code that already exists and ends in a documented anchor. The "spec" captures observed behaviour, not original intent — lossy by design.

Same four artefacts on disk; opposite direction of arrow. Once archived, the textual path openspec/changes/archive/<ghost>/tasks.md#task-N keeps resolving forever, so the @spec tags stay valid.

4. The report shows a method in Bucket 2a. Which command do you run, and why is that different from Bucket 2b?

Hint

Both are "code without a REQ", but one has a home capability and the other doesn't.

Answer

For Bucket 2a — extend the existing capability:

/opsx-reverse-spec {app} --extend <capability>

The file already belongs to a known capability. /opsx-reverse-spec --extend drafts the missing REQs inside that capability, keeping the boundary stable.

For Bucket 2b — mint a new capability:

/opsx-reverse-spec {app} --cluster <new-name>

The file belongs to no current capability. /opsx-reverse-spec --cluster drafts a whole new spec.

Bias toward --extend — extending is cheaper than minting (same vocabulary, same boundary). Only use --cluster when the cluster is genuinely new behaviour territory.

5. How do you confirm a retrofit is done, and which command is the wrong one to use for that?

Hint

There are two modes of /opsx-verify — one for a single active change, one for the whole app.

Answer

Run App Mode of verify:

/opsx-verify --app {app}

App Mode is the canonical retrofit DoD audit. It walks every ghost change under {app}/openspec/changes/archive/retrofit-*, scans for dangling @spec paths, audits cohort frontmatter, validates the naming convention, and prints one pass/fail report.

Don't use plain /opsx-verify <change-name> — that mode verifies a single (active) change against openspec status and won't see archived retrofit changes at all.

A retrofit is done when App Mode is ✅ on every row (ghost changes archived, tasks all [x], zero dangling @spec paths, naming convention matched, cohort frontmatter set in block YAML) and the workflow items App Mode doesn't check are also done: the PRs merged, Bucket 3 issues triaged, Bucket 4 follow-up issue opened, and a final sync_spec_content.py run.

Next step

With the retrofit playbook in your hands, two follow-ups make sense: