Documentation & Modernization
Two complementary workflows for a repository the user already has checked out
locally, bundled as one skill:
- Documentation mode — produce one definitive, cited architecture document
from the code on disk. Ideal for onboarding, system-design maps, or as the
evidence base for a modernization effort.
- Modernization mode — turn that architecture into a phased, safety-laddered
plan to upgrade, migrate, or rewrite a legacy system.
Mode selection
- If the user wants to understand, document, map, research, or onboard onto a
codebase, run Documentation mode.
- If the user wants to modernize, migrate, upgrade, or rewrite a system, run
Modernization mode. Modernization mode is self-sufficient: if no
architecture document exists yet, it runs the Documentation mode workflow
first (in the same pass), then continues straight through to the plan.
When in doubt, produce the architecture document first — it is the audited
evidence base both modes rely on.
Documentation mode
Generate one definitive, cited architecture document for a repository the user
already has checked out locally. The goal is a writeup someone could hand to a
new engineer as their onboarding reference — broad enough to cover the whole
system, deep enough on the hard parts to be useful, and trustworthy because
every claim traces back to a file on disk.
Why local-first
Reading from the local checkout (not the GitHub API or the web) is the deliberate
default. It is faster, free, avoids rate limits, and — most importantly — it
describes the exact code in front of you rather than whatever main happens
to look like remotely. The one tradeoff is that remote-only facts (star counts,
full CI run history, sibling repos) aren't visible. That's fine: state those as
out-of-scope or mark them [UNVERIFIED] rather than guessing.
Local-first is not local-never-remote: a web/API lookup is a deliberate
last-resort fallback, reserved for a fact that genuinely cannot be determined
from disk and that materially matters to the document. When you do reach for it,
flag the result clearly (e.g. [UNVERIFIED] / sourced-remotely) so the reader
knows it didn't come from the checkout, and never let it become the easy path
that displaces reading the code on disk.
Workflow
- Establish identity first. Run
git remote -v, git branch --show-current,
and git log -1 so the document is anchored to a specific remote, branch, and
commit. A reader must be able to tell which snapshot this describes. Remote
URLs can contain embedded credentials (e.g. https://<token>@github.com/...)
— redact any credentials/tokens from the URL before recording it in the
document.
- Detect, don't assume. Read the real manifests (
go.mod, package.json,
Cargo.toml, pyproject.toml, pom.xml, etc.), the Makefile/task runner,
CI config, and any repo-specific agent or contributor docs (AGENTS.md,
CONTRIBUTING, README, docs/). These are the source of truth for the tech
stack and commands — prefer them over your prior knowledge of the framework.
- Map breadth, then drill into depth. First build the whole-repo map (the
three lenses below), then pick the 2-3 hardest subsystems and go deep on them.
- Verify as you go. Open the files you cite. If you reference a line number,
you should have actually read that line. Unsupported claims are worse than
omissions here — the whole value of this document is that it can be trusted.
Output structure
Produce a single Markdown file with the sections below, in this order. Adapt
the headings to the actual project (a CLI tool has no "frontend" lens — fold that
slot into whatever matters for that repo), but keep the three-lens shape and the
verification discipline.
Part 1 — Whole-repo technical deep-dive
- What the repository is (one paragraph, cited to README).
- Tech-stack detection table: layer | technology | evidence (file+line).
- Entry points (backend, frontend, CLI — whatever applies).
- Commands & Verification Inventory — a table of the canonical project
commands (
command | purpose | evidence), verified against the task runner /
manifests / CI config, not guessed. Cover build, run/serve, test (and how to
run a single test), lint, format, and — where they exist — typecheck,
end-to-end/smoke, contract, and any other gate commands, plus the CI
workflow(s) that run them and on what trigger. Also record whether CI is
enforced — i.e. whether any workflow is a required status check /
branch-protection rule that actually blocks merges, versus one that merely
runs — since that distinction is a manual, human-configured setting that
Modernization mode must surface, not assume. Enforcement usually cannot be
determined from the local checkout alone: ask the user, or mark it
[UNVERIFIED] unless confirmed from an authoritative source (any remote
lookup is a flagged last resort, per the local-first rule above). This
inventory is the source of
truth that downstream planning (Modernization mode) cites so its exit
criteria are runnable, not aspirational. Detect these per-ecosystem (npm/yarn/
pnpm, make, just, cargo, go, poetry/tox/nox, gradle/maven,
etc.) — do not assume a stack. Mark any command you could not verify
[UNVERIFIED].
- Directory layout for each major area, with a one-line purpose per directory.
- Deployment & Runtime Surface — a table of every place the language/runtime
and backing-service versions are pinned for running the system (not just
building it): container base images (
Dockerfile/Containerfile,
docker-compose* build contexts), CI runner images / setup-* versions,
engines/.nvmrc/.tool-versions/runtime.txt, serverless/lambda runtimes,
and stateful data-store image tags (DB/cache/broker/search). Cite each with
file+line. This surface is what a later platform/runtime bump must move in
lockstep — flag any drift between build-runtime and run-runtime here so it's
visible before a modernization plan is written.
- EOL / dead-dependency scan — call out frameworks, runtimes, base images,
and libraries that are end-of-life, unmaintained, or removed in a likely target
major (e.g. a framework whose next major renames namespaces or drops a
component family). Mark each
[INFERRED]/[UNVERIFIED] as appropriate. This is
the raw material Modernization mode's feasibility spike and hazard red-team
build on.
- Data/storage layers, APIs, plugins/extensions, background jobs, CI/CD, testing.
Part 2 — Context & ecosystem
- Local checkout identity table (remote, branch, HEAD commit, version, license).
- Repo-specific agent/contributor docs present, and what rules they encode.
- Developer gotchas (test watch-mode defaults, slow builds, codegen-must-commit,
pre-commit hooks) — each cited.
- How this project relates to its broader ecosystem or sibling services, as
visible from disk (build tags, optional linked repos, separately-deployable
components). Don't import remote ecosystem trivia.
Part 3 — Architectural blueprint
- Tech-stack summary (can reference the Part 1 table).
- C4-style diagrams as Mermaid: Level 1 system context, Level 2 containers,
Level 3 a representative request/component lifecycle.
- Layering and dependency rules (what may depend on what, and what enforces it).
- Cross-cutting concerns table: auth, config, logging, metrics/tracing, secrets,
error handling, feature flags — each with its location and evidence.
- Inferred Architectural Decision Records (reconstructed from code + docs).
- Governance & enforcement mechanisms (CI gates, codegen verification,
CODEOWNERS, review gates, compatibility rules).
- "How to add a feature" guide plus common pitfalls.
Subsystem deep-dives
Identify the 2-3 most complex or architecturally significant subsystems — the
parts a new engineer would most struggle with, such as an evaluation/scheduling
engine, a plugin loader pipeline, a state machine, or a rendering/migration
framework. For each, add a dedicated subsection covering its internal structure,
lifecycle or state machine, key types, and data flow, with local file+line
citations and a small Mermaid diagram where it clarifies the flow. This is what
separates a useful onboarding doc from a directory listing — spend real effort
here.
Confidence assessment
A table of the major claim areas rated High / Inferred / Unverified, so a
reader knows exactly which parts to trust outright and which to double-check.
Footnotes — local file citations
A list of the key local files the document relies on, each with a one-line note
on what it establishes.
Conventions that make the document trustworthy
These are the habits that distinguish this skill's output from a generic
overview. They matter because the document's entire value is that a reader can
rely on it without re-deriving everything.
- Cite every non-obvious claim to a local path, with a line number where it
pins something specific (
pkg/server/server.go#L39-L41). Relative paths from
the repo root keep links clickable.
- Mark uncertainty honestly. Use
[INFERRED] for something you reasoned to
but didn't see stated, and [UNVERIFIED] for something you're repeating but
didn't confirm (e.g. a build-timing claim from a doc you didn't re-measure).
Honest gaps are more useful than false confidence.
- Resolve contradictions, don't restate them. If two sources disagree (say a
version literal in code vs. the manifest), go read the code, decide the real
answer, and label it
[Resolved contradiction] with the explanation. Leaving
a reader to puzzle over a conflict is a failure mode.
- Note compatibility and deploy-cadence rules the repo enforces — separate
FE/BE PRs, bidirectional storage compatibility, additive-only protobuf changes
— because these are the rules a newcomer most easily breaks.
- Prefer precise counts over vague ones. "73 service packages", "89 workflow
files" (from a directory listing) reads as verified; "many services" reads as a
guess.
Scope control
Keep the document grounded in the checkout. It's easy to drift outward into the
project's wider ecosystem (related products, README marketing, satellite repos)
— resist that unless it's visible on disk, and clearly label anything that comes
from outside the local tree. The reader asked for this codebase, documented
faithfully.
Modernization mode
Generate a complete, actionable modernization plan for a legacy codebase. This
skill focuses on the forward-looking work — what to modernize, why, in what
order, and how — but it is self-sufficient: it ensures an architecture
document exists first, producing one via Documentation mode when needed.
Assume dead-by-default. People reach for modernization precisely because the
old stack is hard or impossible to upgrade — EOL runtimes, uncompilable native
modules, retired package mirrors, abandoned frameworks. So this skill does not
assume you can resurrect the legacy toolchain and stand up a fully-green CI gate
before touching anything. That "freeze-then-lift" approach is the lucky case,
not the default. Instead the skill runs a time-boxed feasibility spike, then
picks a migration strategy and a safety strategy matched to how alive the
system actually is. On a truly dead app, building a green legacy gate is itself
a modernization project — a circular trap this skill is designed to avoid.
Three ideas drive the whole plan and are introduced below: the Testability
Milestone (when — per component — the system can actually build, run, and pass
a test again), the safety ladder (the highest rung of regression safety
you can actually reach, with a downgrade treated as a blessed outcome, not a
failure), and the CI Milestone (which phase first stands up CI — and the
reminder that enforcing CI as a required check is a manual human step, not
something the agent can do).
Prerequisites
This skill needs an understanding of the system's architecture before it can
plan. Resolve that as follows:
- If an architecture document already exists — produced by Documentation mode
above, or a README / ARCHITECTURE.md the user points to, or enough
prior conversation context — use it and skip to the workflow below.
- If none exists, run the Documentation mode workflow above first to
generate a cited architecture document, then continue straight through
to Phase 1 below in the same pass. Do not stop to ask the user to run it
separately, and do not pause for review between the two documents.
The result is two artifacts: the architecture document (the audited evidence
base) and this modernization plan (the forward-looking action set).
Before planning, confirm a Commands & Verification Inventory exists. Exit
criteria are only worth anything if they are runnable, so the plan must be able
to cite the project's canonical build / run / test / lint / typecheck / e2e /
contract commands and CI gate(s). Documentation mode produces this
inventory in Part 1; if you're working from a README or prior context that lacks
it, detect and record those commands yourself (per-ecosystem — npm/yarn/pnpm,
make, just, cargo, go, poetry/tox/nox, gradle/mvn, etc.) before
writing exit criteria. Never invent a command you haven't verified against the
task runner / manifests / CI config.
Workflow
Phase 1: Assess Current State (from existing docs)
Read the architecture document and extract:
- Tech stack inventory — languages, frameworks, runtimes, dependencies with
versions where visible
- Feature/domain map — the major functional areas of the system
- Pain points already identified — tech debt, concerns, deprecated deps,
architectural violations noted in the existing docs
- Deployment and infrastructure — how it runs today
Do NOT re-read every source file. Trust the architecture doc. Only open specific
files if a modernization question requires verifying a detail (e.g., "is this
dependency actually used or just declared?").
Phase 2: Feasibility Spike, Strategy Fork & Safety Ladder
This is the pivot of the whole plan. Do it before recommending a target
architecture or writing any phases. Its job is to answer, quickly and honestly:
how alive is this system, what migration shape fits, and how much regression
safety can we actually achieve?
2a. Run a time-boxed feasibility spike
Put a hard time box on it (e.g. one day). The spike's question is NOT "can
we make CI green?" — it is "can we get this to run even once to capture its
behavior?" Probe, per component / deployable unit:
- Does it install its dependencies today (ideally from a committed
lockfile without hand-patching)?
- Do native/build steps succeed on a currently supported toolchain?
- Does it boot / start?
- Can the test runner execute at all, and does ≥1 meaningful test pass?
Record what you actually observed — including partial success (common in
monorepos: one service installs and tests green while another can't compile). Do
not sink two sprints resurrecting a corpse to discover it's unresurrectable.
When the time box expires, decide with the evidence you have.
2b. Locate the Testability Milestone (per component)
"Testable" is not a precondition you can satisfy on a dead app — for a dead app
it is an output of modernization, not an input. So every plan must name, up
front, its Testability Milestone: the specific phase at which the system
(or a given component) first satisfies all four conditions at once:
- Runs on a supported (non-EOL) runtime.
- Dependencies install from a lockfile without hand-patching.
- Native / build steps succeed on the current toolchain.
- The test runner executes and ≥1 meaningful test passes in CI.
Assess this per component — as the reference Nylas run showed, a near-modern
cloud service can cross the line immediately while a legacy Electron client
doesn't cross it until several phases later. This milestone is the single most
important marker in the plan. State where it lands for each component, out loud.
2c. Label every phase pre- or post-testability
The Testability Milestone splits the effort into two regimes with different
safety rules:
- Pre-testability ("dark") regime: the component can't be trusted to run.
Safety comes from non-test sources (see the safety ladder below).
Requiring a CI test gate here is a category error — you're demanding green
tests from something that isn't alive yet.
- Post-testability ("lit") regime: it builds and runs on a live, supported
stack. Now real CI, characterization, and e2e are achievable and worth the
investment. The classic green-gate belongs here.
Rule: never require an automated test gate on a component before that component
crosses its own testability line.
2d. Choose the migration strategy — the A/B fork
The spike decides which of two shapes fits (this is a per-component call in a
monorepo):
- (A) Freeze-then-lift. Net the old app as-is, then upgrade under the net.
Requires the old app to be resurrectable at acceptable cost — i.e. it
almost runs. This is the traditional "characterization tests before you
change anything" path. Opt-in, not the default.
- (B) Beachhead-then-expand (walking skeleton). Don't net the corpse. Drive
the thinnest possible end-to-end slice onto the modern stack until it
builds, boots, and tests; then strangle the rest in, writing tests on the new
stack as you go. This is the default for dead apps. In strategy B, the
first phase is "reach the Testability Milestone with the smallest slice," not
"freeze the whole legacy toolchain."
If a component can't be built or run at all, characterization tests on the
old stack are impossible by definition — invert the approach: stop trying to
net the corpse and build the net on the target stack incrementally, using the
old code/output as the reference oracle.
2e. Pick the highest achievable rung on the safety ladder
Regression safety is a ladder, not a binary. Choose the highest rung you can
actually reach per component; a downgrade to a lower rung is a blessed,
first-class outcome — not a failure — provided the residual risk is named.
- L4 — Full automated gate: green lint + unit + characterization + e2e in CI.
- L3 — Partial gate: some suites green + lockfile + CI on the achievable
subset; the rest quarantined with a named list.
- L2 — Characterization / golden-master only: can't run in CI, but capture
I/O snapshots / behavioral diffs (manual is acceptable).
- L1 — Reversibility-based: no runnable tests; safety comes from small
reversible steps, strangler / parallel-run, a smoke checklist, and review.
- L0 — No net possible: treat as a spec-driven rewrite (old code = the spec)
or archive.
Safety is not only tests. There are at least five sources, and on dead apps
2–4 often outweigh 1:
- Tests (characterization / unit / e2e) — strongest, often unachievable on
dead apps.
- Reversibility — tiny commits, branch-per-change, trivial rollback.
- Isolation — feature flags, strangler-fig, run old + new in parallel and
diff outputs.
- The running old system as an oracle — record/replay real I/O to
characterize behavior without a test harness.
- Human review + domain knowledge.
Anchor safety at the behavioral seams, not the unit. The durable,
stack-agnostic contracts of any app are its externally observable behaviors:
HTTP endpoints, DB schema/queries, file/wire formats, CLI output, protocols. Pin
those as golden-master / approval snapshots — they survive the rewrite; unit
tests on dead legacy code get deleted the moment you replace the module. Get an
oracle, ranked by what's available:
- Best — it runs somewhere (prod, a VM, an old container, even read-only),
and you're allowed to use it: record real I/O and replay it against the new
code. One successful capture beats a permanent green legacy CI. (Do not assume
this rung is always available — a faithful third-party reimplementation or a
resurrected legacy binary may be off-limits by policy, licensing, or an
explicit decision to build "with only what we have." If so, skip to the
self-frozen rung.)
- Fallback — it doesn't run at all: the old source is the spec;
supplement with prod logs, DB dumps, docs, tickets, and accept that
characterization tests get written against the new implementation to lock
it once verified correct.
- Self-frozen golden master — no external reference exists or is permitted:
when nothing trustworthy runs and you may not borrow an external oracle, the
modernized component records its own golden master the moment it first runs
(at its Testability Milestone), and every later phase diffs against that frozen
snapshot. This is a real, usable rung — but name its residual risk out loud:
it guarantees the self-consistency of later refactors, not the correctness
of the first boot (the snapshot is only as correct as the run that produced it).
Pair it with human review / spec cross-checks to bless that first run.
2f. Feed an economic/oracle triage into the choice
Weigh regression cost when picking a rung: Is the app in production? Does
it have real users? Is the old system still runnable as a reference? An
abandoned app with zero users has near-zero regression cost — an expensive gate
there is over-engineering. A production system with users justifies a higher
rung and a real oracle.
2g. Locate the CI Milestone (name the phase that stands up CI)
Just as the Testability Milestone names when a component can first run and be
tested, the CI Milestone names when continuous integration is first stood
up — and it must be stated out loud in the roadmap, not buried in a phase
body. A common failure of modernization plans is leaving the reader unable to
tell which phase introduces CI; name it explicitly.
Rule: CI is stood up in the first lit phase — at or immediately after the
component crosses its Testability Milestone — never before. CI cannot be green
on a component that cannot yet build/run, so wiring a CI gate during the dark
regime is the same category error as demanding a test gate there. (Trivial
scaffolding — a lint-only or build-only workflow — may appear earlier, but the
authoritative test gate lands at the CI Milestone.)
CI is two distinct steps, and the second is human-only — call both out:
- Author the CI workflow file (e.g.
.github/workflows/*.yml, .gitlab-ci.yml,
Azure Pipelines) that runs the gate. An agent can do this.
- Make that workflow an enforced gate — a required status check /
branch-protection rule / merge-request approval rule. This is a manual
platform-UI / admin step (GitHub → Settings → Branches; GitLab protected
branches + MR settings; etc.) that an agent cannot perform. Until a human
configures it, CI runs on PRs but does not block merges.
Because step 2 is outside the agent's reach, every plan that stands up CI must
emit it as an explicit user action item (in the phase's exit criteria and in
§9 Open questions / decisions needed from stakeholders), so the reader knows the
gate is not self-enforcing until they turn it on.
Output of Phase 2: for each component — the spike result, the chosen strategy
(A or B), the located Testability Milestone, the pre/post label, and the target
safety-ladder rung with any residual risk named — plus the plan-wide CI
Milestone (which phase stands up CI) and the reminder that enforcing it is a
manual human step. Everything downstream depends on these.
Phase 2.5: Red-team every phase against the hazard catalog (mandatory)
The strategic scaffolding above (testability, safety ladder, CI milestone) is
necessary but not sufficient. Modernization plans also fail on tactical,
ecosystem-predictable hazards — an incomplete quarantine set, a forgotten
namespace codemod, a runtime bump that leaves the base images behind, a stateful
data-store major treated as an image bump, an edge rewrite that drops the
anonymous route class. These are so recurring that they have their own catalog:
references/migration-hazards.md (H1–H8).
Rule: before any phase is implemented, red-team its plan against every hazard
in references/migration-hazards.md. For each hazard, ask "does this phase
trigger it?", run the hazard's detection probe against the real repo, and fold
its plan action into the phase's tasks/exit criteria before writing code. A
hazard caught in planning is a task; caught in review it's rework; caught in prod
it's an incident. Record hazards you checked and cleared, not just the ones
that fired — a silent skip is indistinguishable from a miss.
This is the institutionalized form of an independent-critic pass. Do it yourself
by walking the catalog, and — where an independent second opinion is available
(e.g. a rubber-duck / critique agent) — use it: on the reference PiggyMetrics run
such a pass caught a blocking plan gap in every single phase, and each gap
was an instance of an H1–H8 class. Bake the outcome into each phase's
"Decisions made" and exit criteria so the executing agent inherits the cleared
checklist rather than re-discovering the hazard.
Phase 3: Recommend Target Architecture
Decision framework — always evaluate in this order:
For each component/dependency, work through these levels from least-disruptive to
most-disruptive. Stop at the first level that solves the problem:
Upgrade in place — Can you bump the major version and fix breakages?
(e.g., Node 14→22, Python 2→3, React 16→19, Rails 5→7). This is the default
answer. Only reject it if the upgrade path is officially unsupported, the
breaking changes are so extensive they exceed rewrite cost, or the framework
itself is abandoned/EOL.
Swap the dependency — If the specific library is dead but the pattern is
fine, can you swap to a maintained alternative with the same interface shape?
(e.g., Moment.js → date-fns, Request → got/fetch, CoffeeScript → TypeScript
with decaffeinate). Prefer drop-in or mechanical migration tools when they
exist.
Wrap/adapt (Strangler Fig) — If the component's interface is sound but
internals are unmaintainable, can you put a clean interface in front and
incrementally replace internals behind it? This works when coupling is
manageable and the system can run with old and new coexisting.
Rewrite — Only when: the framework is abandoned with no upgrade path, the
architecture fundamentally cannot support required new capabilities (e.g.,
synchronous-only design that must become real-time), or honest estimation shows
upgrade cost exceeds rewrite cost. Require explicit justification for every
rewrite recommendation — "it's old" is not sufficient.
Remove — Dead code, features with zero usage, deprecated capabilities
superseded by other systems. Verify usage claims before recommending removal.
Bias toward conservatism. The cheapest migration is the one you don't do.
Every level of disruption adds risk, timeline, and cost. A working system on an
older framework is better than a half-finished rewrite on a new one.
Mechanical-migration riders (apply whenever a recommendation is an upgrade,
swap, or removal). These are the parts of a target-architecture decision that
first-draft plans routinely drop; each maps to a hazard class in
references/migration-hazards.md:
- Transitive-quarantine completeness (H1). When removing/cutting over a whole
dependency family, grep the dead artifact across all manifests and list the
full set of dependents that must move or be quarantined — not the subset you
first thought of. State the post-cutover build target explicitly.
- Framework-major codemods (H2). A major bump carries mechanical breaking
changes independent of your logic — namespace renames, removed deprecated APIs,
a changed test engine, changed config keys. Enumerate each as its own task
and prefer the ecosystem's automated recipe/codemod tool (OpenRewrite,
jscodeshift/react-codemod,
2to3/pyupgrade, etc.) over hand-editing on large
trees. Don't forget the test-framework migration — it is the most-forgotten one.
- Runtime↔deployment lockstep (H3). If a recommendation bumps the
language/runtime major, every place the runtime is pinned for running the
app — base images,
docker-compose build contexts, CI runner images,
engines/.nvmrc/.tool-versions, serverless runtimes — must move in the
same phase. Build-runtime and run-runtime must never drift across a phase
boundary, or the app builds green and then fails to run.
Produce a Target Architecture section that includes:
- Recommended tech stack with rationale for each choice (framework, language,
runtime, database, infrastructure)
- Architectural pattern recommendation (monolith → modular monolith →
services, event-driven, serverless, etc.) with justification
- What stays vs. what goes — explicitly categorize each major component using
the decision framework above:
- ✅ Keep as-is (still modern/adequate)
- ⬆️ Upgrade in place (bump version, fix breakages)
- 🔀 Swap dependency (replace dead library with maintained equivalent)
- 🔄 Wrap/adapt (add an interface layer, incrementally replace internals)
- 🔁 Rewrite (fundamentally incompatible — justification required)
- 🗑️ Remove (dead code, deprecated features, superseded)
For each recommendation, write an inline ADR:
#### ADR: [Decision Title]
- **Context:** [Why this decision is needed]
- **Decision:** [What we chose and which decision framework level it falls under]
- **Alternatives considered:** [What else was evaluated and why it lost]
- **Consequences:** [Tradeoffs accepted]
Phase 4: Per-Feature Migration Analysis
For each major feature/domain identified in the architecture doc, produce a
section covering:
- Current implementation — one paragraph summary with file references
- Migration strategy — the A/B choice from Phase 2 for this component
(Freeze-then-lift vs. Beachhead/walking-skeleton), plus the tactic: Strangler
Fig, Big Bang rewrite, Lift & Shift, Incremental Refactor, or Leave In Place
- Testability status — where this component's Testability Milestone
lands, and its safety-ladder rung (L0–L4) with residual risk named
- Dependencies and coupling — what else breaks or must move with it
- Effort estimate — T-shirt size (XS/S/M/L/XL) with justification
- Risk assessment — what can go wrong, data migration concerns, user impact
- Acceptance criteria — how you know the migration succeeded (expressed
against the chosen oracle/seam contracts, not against a not-yet-alive test
suite)
Phase 5: Phased Implementation Plan
Produce an ordered, phased plan. Each phase should be independently deployable
(no half-migrated states that can't run in production).
Phase gating is regime-aware (applies to every phase). State this rule at the
top of the phased plan and honor it in every phase's exit criteria. A phase is
not complete until its Verification & Exit Criteria pass. Those criteria must
be (a) objectively verifiable; (b) actually executed and recorded before
the next phase starts; and (c) gated — do not advance to phase N+1 until
phase N's criteria are demonstrably met. But which criteria are valid depends
on the component's regime (Phase 2c):
- Post-testability ("lit") phases: exit criteria are runnable commands / green
CI — green CI on the phase's branch/PR is the authoritative signal.
- Pre-testability ("dark") phases: the component isn't alive yet, so a green
CI test gate is a category error. Exit criteria come from the achievable
safety-ladder rung instead — captured seam/oracle snapshots, reversibility
scaffolding proven present, a passed smoke checklist, recorded review. Do not
block a dark-regime phase on a test suite that can't run yet.
Report to the user any phase whose pass/fail is unknown rather than assuming it
passed.
The first phase establishes maximum achievable safety — not a fixed "green
legacy gate." Do not hard-code "resurrect everything and make CI green." Shape
the first phase from the Phase 2 outputs, per component:
- Strategy A (Freeze-then-lift), component is resurrectable: the first phase
is the classic Safety Net & Baseline — purely additive, no behavior change:
- Characterization / golden / contract tests at the seams that pin
current observable behavior (protocol traces, API snapshots, output goldens).
- CI + lint + formatter wired up and green (an authoritative gate for later
lit-regime phases).
- Pinned dependencies / committed lockfile and a recorded known-good
baseline (versions, toolchain, how it builds and runs today).
- "Net proven to fail." Deliberately mutate the code (e.g., change one
protocol code or return value), confirm the new tests go red, then
revert. A safety net that never fails is proof of nothing — prove it has
teeth.
- Strategy B (Beachhead/walking-skeleton), component is dead: the first phase
is "reach the Testability Milestone with the smallest slice" — drive the
thinnest end-to-end path onto the modern stack until it builds, boots, and runs
≥1 real test green, capturing seam/oracle contracts from the old system to
validate against. Do not try to net the corpse. Its exit criterion is the
achievable safety-ladder rung with residual risk named — a blessed downgrade
is not a failure.
Only after the first phase's criteria are met (at whatever rung) do the
subsequent infrastructure and feature phases begin.
Structure each phase as:
### Phase N: [Name] (T-shirt size: M)
**Goal:** [One sentence]
**Regime:** [pre-testability ("dark") | post-testability ("lit")] — per component
**Safety rung:** [L0–L4, with residual risk named if below L4]
**Prerequisites:** [Which phases must complete first]
**Duration estimate:** [Relative, not calendar — e.g., "2-4 sprints"]
#### Tasks
| ID | Task | Component | Blocked by |
|----|------|-----------|------------|
| N.1 | ... | ... | — |
| N.2 | ... | ... | N.1 |
#### Risks & Mitigations
- **Risk:** ... → **Mitigation:** ...
#### Decisions made
- [Every sub-decision this phase depends on, resolved and documented here so the
phase can be implemented without further user input. State "dropped" vs
"deferred" explicitly for anything cut.]
#### Verification & Exit Criteria (Definition of Done)
- [ ] [Regime-appropriate criterion. For LIT phases: a runnable command / green
CI check, citing the actual command from the Commands & Verification
Inventory. For DARK phases: the achievable safety-ladder rung's evidence —
captured seam/oracle snapshots, reversibility proven, smoke checklist
passed — NOT a green test suite the component can't yet run.]
- [ ] [Include a parity/characterization check against the chosen oracle whenever
the change is meant to preserve behavior.]
- [ ] [If purely additive, assert it: "no behavior/dependency/logic changed."]
- [ ] [If exiting below L4, state the residual risk and which later phase closes
it (e.g., "client test gate deferred to the client-migration phase").]
Ordering principles:
- Locate the Testability Milestone first, per component — schedule the
smallest slice that reaches it early rather than deferring "testable" to the end
while demanding a gate at the start.
- Never require a component's automated test gate before it crosses its own
testability line.
- Name the CI Milestone in the roadmap. State explicitly which phase stands
up CI (the first lit phase, at/after the Testability Milestone) so the reader
never has to guess when CI arrives — and flag that enforcing it (required
status check / branch protection) is a manual human step, not an agent task.
- First-phase safety before behavior change — establish the highest
achievable rung (not necessarily a full green gate) before modifying behavior.
- Infrastructure and cross-cutting concerns next (auth, config, logging, CI/CD)
- Highest-risk or highest-value features next (prove the pattern early)
- Low-risk, low-coupling features last (easy wins to parallelize)
- Data migrations get their own phase with rollback plans
Phase 6: Execution Governance
Modernization plans fail in execution, not on paper. Bake in the governance that
keeps each phase honest and reversible (generic across any ecosystem):
- Branch per phase. Never commit phase work to the default branch. Open one
PR per phase; let CI on the PR be the authoritative green signal before merge
for lit-regime phases. For dark-regime phases, the PR still carries the
rung's evidence (captured contracts, smoke results, reversibility scaffolding).
- Merge to trunk before the next phase; never stack (H7). Each phase branch
is cut from trunk, and its PR is merged to trunk before the next phase
starts. Never base a phase branch or its PR on a sibling phase branch — that
is how PRs get merged into each other, the trunk silently stalls several phases
behind, and later phases are forced into the exact bind where the no-stacking
rule has to be broken. Before starting a phase, verify the previous phase is
merged to trunk and
git log origin/<trunk>..HEAD is empty at branch creation.
If controlled stacking is genuinely unavoidable, it is allowed only with a
required reconciliation PR that lands the stack onto trunk and an explicit
residual-risk note — never as a silent default.
- Confirm the trunk name; retire legacy defaults. Pin the repo's real trunk
(e.g.
main) and, if a second default-ish branch exists (master), mark it
"history only — do not target" so phase work never lands on the wrong branch.
- The gate matches the regime. Lit phases advance on green CI; dark phases
advance on the achievable safety-ladder rung with residual risk named — not on
a "looks done" judgement, and not on a test suite that can't run yet.
- Stand up CI at the CI Milestone, and hand off enforcement to a human.
Name the phase that introduces CI (the first lit phase). Distinguish the two
steps: authoring the workflow file is agent-doable; making it an enforced
required status check / branch-protection (or MR-approval) rule is a manual
platform-UI step the agent cannot perform. Until a human configures it, CI
runs but does not block merges — so record "enable branch protection /
required checks" as an explicit user action item (in the CI phase's exit
criteria and in §9 stakeholder decisions), never as a completed agent task.
- Interface-preserving & independently deployable. Each phase leaves the
system runnable, so rollback = redeploy the previous version. Prefer
interface-preserving rewrites (wrap internals behind a stable interface) and
prove equivalence with the seam/oracle contracts (golden/characterization
tests where they exist).
- Living plan. Update
MODERNIZATION_PLAN.md status markers as each phase's
exit criteria are met — ✅ complete / ⏭️ descoped / 🗑️ dropped — record the
decisions made during implementation, and **update the safety-ladder rung /
…(truncated)
1---2name: doc-and-modernize3description: >- Two related workflows for a locally-cloned codebase, in one skill. Documentation mode produces a single, comprehensive, verifiable architecture document primarily by reading files on disk (local-first) — use it whenever the user wants to understand, map, document, research, or onboard onto a codebase ("research this repo", "write up the architecture", "do an architecture deep dive", "document how this codebase works", "map the system design", "create an onboarding doc"). Modernization mode generates a phased plan to modernize, migrate, upgrade, or rewrite a legacy system ("modernize this", "plan the migration", "how would we rewrite this", "how do we get off this legacy stack"); if no architecture document exists yet it first runs Documentation mode, then continues straight through to the plan. It assumes the legacy stack may be dead, runs a time-boxed feasibility spike, and picks...4---5# Documentation & Modernization
6
7Two complementary workflows for a repository the user already has checked out
8locally, bundled as one skill:
9
10- **Documentation mode** — produce one definitive, cited architecture document
11 from the code on disk. Ideal for onboarding, system-design maps, or as the
12 evidence base for a modernization effort.
13- **Modernization mode** — turn that architecture into a phased, safety-laddered
14 plan to upgrade, migrate, or rewrite a legacy system.
15
16## Mode selection
17
18- If the user wants to **understand, document, map, research, or onboard onto** a
19 codebase, run **Documentation mode**.
20- If the user wants to **modernize, migrate, upgrade, or rewrite** a system, run
21 **Modernization mode**. Modernization mode is self-sufficient: if no
22 architecture document exists yet, it runs the **Documentation mode** workflow
23 first (in the same pass), then continues straight through to the plan.
24
25When in doubt, produce the architecture document first — it is the audited
26evidence base both modes rely on.
27
28## Documentation mode
29
30Generate one definitive, cited architecture document for a repository the user
31already has checked out locally. The goal is a writeup someone could hand to a
32new engineer as their onboarding reference — broad enough to cover the whole
33system, deep enough on the hard parts to be useful, and trustworthy because
34every claim traces back to a file on disk.
35
36### Why local-first
37
38Reading from the local checkout (not the GitHub API or the web) is the deliberate
39**default**. It is faster, free, avoids rate limits, and — most importantly — it
40describes *the exact code in front of you* rather than whatever `main` happens
41to look like remotely. The one tradeoff is that remote-only facts (star counts,
42full CI run history, sibling repos) aren't visible. That's fine: state those as
43out-of-scope or mark them `[UNVERIFIED]` rather than guessing.
44
45Local-first is not local-*never*-remote: a web/API lookup is a deliberate
46**last-resort fallback**, reserved for a fact that genuinely cannot be determined
47from disk and that materially matters to the document. When you do reach for it,
48flag the result clearly (e.g. `[UNVERIFIED]` / sourced-remotely) so the reader
49knows it didn't come from the checkout, and never let it become the easy path
50that displaces reading the code on disk.
51
52### Workflow
53
541. **Establish identity first.** Run `git remote -v`, `git branch --show-current`,
55 and `git log -1` so the document is anchored to a specific remote, branch, and
56 commit. A reader must be able to tell which snapshot this describes. Remote
57 URLs can contain embedded credentials (e.g. `https://<token>@github.com/...`)
58 — **redact any credentials/tokens** from the URL before recording it in the
59 document.
602. **Detect, don't assume.** Read the real manifests (`go.mod`, `package.json`,
61 `Cargo.toml`, `pyproject.toml`, `pom.xml`, etc.), the `Makefile`/task runner,
62 CI config, and any repo-specific agent or contributor docs (`AGENTS.md`,
63 `CONTRIBUTING`, `README`, `docs/`). These are the source of truth for the tech
64 stack and commands — prefer them over your prior knowledge of the framework.
653. **Map breadth, then drill into depth.** First build the whole-repo map (the
66 three lenses below), then pick the 2-3 hardest subsystems and go deep on them.
674. **Verify as you go.** Open the files you cite. If you reference a line number,
68 you should have actually read that line. Unsupported claims are worse than
69 omissions here — the whole value of this document is that it can be trusted.
70
71### Output structure
72
73Produce a **single Markdown file** with the sections below, in this order. Adapt
74the headings to the actual project (a CLI tool has no "frontend" lens — fold that
75slot into whatever matters for that repo), but keep the three-lens shape and the
76verification discipline.
77
78#### Part 1 — Whole-repo technical deep-dive
79- What the repository is (one paragraph, cited to README).
80- Tech-stack detection table: layer | technology | evidence (file+line).
81- Entry points (backend, frontend, CLI — whatever applies).
82- **Commands & Verification Inventory** — a table of the canonical project
83 commands (`command | purpose | evidence`), verified against the task runner /
84 manifests / CI config, not guessed. Cover build, run/serve, test (and how to
85 run a single test), lint, format, and — where they exist — typecheck,
86 end-to-end/smoke, contract, and any other gate commands, plus the CI
87 workflow(s) that run them and on what trigger. **Also record whether CI is
88 *enforced*** — i.e. whether any workflow is a **required status check /
89 branch-protection rule** that actually blocks merges, versus one that merely
90 runs — since that distinction is a manual, human-configured setting that
91 Modernization mode must surface, not assume. Enforcement usually cannot be
92 determined from the local checkout alone: ask the user, or mark it
93 `[UNVERIFIED]` unless confirmed from an authoritative source (any remote
94 lookup is a flagged last resort, per the local-first rule above). This
95 inventory is the source of
96 truth that downstream planning (Modernization mode) cites so its exit
97 criteria are runnable, not aspirational. Detect these per-ecosystem (npm/yarn/
98 pnpm, `make`, `just`, `cargo`, `go`, `poetry`/`tox`/`nox`, `gradle`/`maven`,
99 etc.) — do not assume a stack. Mark any command you could not verify
100 `[UNVERIFIED]`.
101- Directory layout for each major area, with a one-line purpose per directory.
102- **Deployment & Runtime Surface** — a table of every place the language/runtime
103 and backing-service versions are pinned *for running* the system (not just
104 building it): container base images (`Dockerfile`/`Containerfile`,
105 `docker-compose*` build contexts), CI runner images / `setup-*` versions,
106 `engines`/`.nvmrc`/`.tool-versions`/`runtime.txt`, serverless/lambda runtimes,
107 and stateful data-store image tags (DB/cache/broker/search). Cite each with
108 file+line. This surface is what a later platform/runtime bump must move in
109 lockstep — flag any drift between build-runtime and run-runtime here so it's
110 visible before a modernization plan is written.
111- **EOL / dead-dependency scan** — call out frameworks, runtimes, base images,
112 and libraries that are end-of-life, unmaintained, or removed in a likely target
113 major (e.g. a framework whose next major renames namespaces or drops a
114 component family). Mark each `[INFERRED]`/`[UNVERIFIED]` as appropriate. This is
115 the raw material Modernization mode's feasibility spike and hazard red-team
116 build on.
117- Data/storage layers, APIs, plugins/extensions, background jobs, CI/CD, testing.
118
119#### Part 2 — Context & ecosystem
120- Local checkout identity table (remote, branch, HEAD commit, version, license).
121- Repo-specific agent/contributor docs present, and what rules they encode.
122- Developer gotchas (test watch-mode defaults, slow builds, codegen-must-commit,
123 pre-commit hooks) — each cited.
124- How this project relates to its broader ecosystem or sibling services, *as
125 visible from disk* (build tags, optional linked repos, separately-deployable
126 components). Don't import remote ecosystem trivia.
127
128#### Part 3 — Architectural blueprint
129- Tech-stack summary (can reference the Part 1 table).
130- C4-style diagrams as Mermaid: Level 1 system context, Level 2 containers,
131 Level 3 a representative request/component lifecycle.
132- Layering and dependency rules (what may depend on what, and what enforces it).
133- Cross-cutting concerns table: auth, config, logging, metrics/tracing, secrets,
134 error handling, feature flags — each with its location and evidence.
135- Inferred Architectural Decision Records (reconstructed from code + docs).
136- Governance & enforcement mechanisms (CI gates, codegen verification,
137 CODEOWNERS, review gates, compatibility rules).
138- "How to add a feature" guide plus common pitfalls.
139
140#### Subsystem deep-dives
141Identify the 2-3 most complex or architecturally significant subsystems — the
142parts a new engineer would most struggle with, such as an evaluation/scheduling
143engine, a plugin loader pipeline, a state machine, or a rendering/migration
144framework. For each, add a dedicated subsection covering its internal structure,
145lifecycle or state machine, key types, and data flow, with local file+line
146citations and a small Mermaid diagram where it clarifies the flow. This is what
147separates a useful onboarding doc from a directory listing — spend real effort
148here.
149
150#### Confidence assessment
151A table of the major claim areas rated **High / Inferred / Unverified**, so a
152reader knows exactly which parts to trust outright and which to double-check.
153
154#### Footnotes — local file citations
155A list of the key local files the document relies on, each with a one-line note
156on what it establishes.
157
158### Conventions that make the document trustworthy
159
160These are the habits that distinguish this skill's output from a generic
161overview. They matter because the document's entire value is that a reader can
162rely on it without re-deriving everything.
163
164- **Cite every non-obvious claim** to a local path, with a line number where it
165 pins something specific (`pkg/server/server.go#L39-L41`). Relative paths from
166 the repo root keep links clickable.
167- **Mark uncertainty honestly.** Use `[INFERRED]` for something you reasoned to
168 but didn't see stated, and `[UNVERIFIED]` for something you're repeating but
169 didn't confirm (e.g. a build-timing claim from a doc you didn't re-measure).
170 Honest gaps are more useful than false confidence.
171- **Resolve contradictions, don't restate them.** If two sources disagree (say a
172 version literal in code vs. the manifest), go read the code, decide the real
173 answer, and label it `[Resolved contradiction]` with the explanation. Leaving
174 a reader to puzzle over a conflict is a failure mode.
175- **Note compatibility and deploy-cadence rules** the repo enforces — separate
176 FE/BE PRs, bidirectional storage compatibility, additive-only protobuf changes
177 — because these are the rules a newcomer most easily breaks.
178- **Prefer precise counts over vague ones.** "73 service packages", "89 workflow
179 files" (from a directory listing) reads as verified; "many services" reads as a
180 guess.
181
182### Scope control
183
184Keep the document grounded in the checkout. It's easy to drift outward into the
185project's wider ecosystem (related products, README marketing, satellite repos)
186— resist that unless it's visible on disk, and clearly label anything that comes
187from outside the local tree. The reader asked for *this codebase*, documented
188faithfully.
189
190## Modernization mode
191
192Generate a complete, actionable modernization plan for a legacy codebase. This
193skill focuses on the forward-looking work — what to modernize, why, in what
194order, and how — but it is **self-sufficient**: it ensures an architecture
195document exists first, producing one via Documentation mode when needed.
196
197**Assume dead-by-default.** People reach for modernization precisely because the
198old stack is hard or impossible to upgrade — EOL runtimes, uncompilable native
199modules, retired package mirrors, abandoned frameworks. So this skill does **not**
200assume you can resurrect the legacy toolchain and stand up a fully-green CI gate
201before touching anything. That "freeze-then-lift" approach is the *lucky* case,
202not the default. Instead the skill runs a **time-boxed feasibility spike**, then
203picks a migration strategy and a **safety strategy** matched to how alive the
204system actually is. On a truly dead app, building a green legacy gate *is itself
205a modernization project* — a circular trap this skill is designed to avoid.
206
207Three ideas drive the whole plan and are introduced below: the **Testability
208Milestone** (when — per component — the system can actually build, run, and pass
209a test again), the **safety ladder** (the highest rung of regression safety
210you can actually reach, with a downgrade treated as a blessed outcome, not a
211failure), and the **CI Milestone** (which phase first stands up CI — and the
212reminder that *enforcing* CI as a required check is a manual human step, not
213something the agent can do).
214
215### Prerequisites
216
217This skill needs an understanding of the system's architecture before it can
218plan. Resolve that as follows:
219
2201. If an architecture document already exists — produced by **Documentation mode**
221 above, or a README / ARCHITECTURE.md the user points to, or enough
222 prior conversation context — use it and skip to the workflow below.
2232. **If none exists, run the Documentation mode workflow above first** to
224 generate a cited architecture document, then continue **straight through**
225 to Phase 1 below in the same pass. Do not stop to ask the user to run it
226 separately, and do not pause for review between the two documents.
227
228The result is two artifacts: the architecture document (the audited evidence
229base) and this modernization plan (the forward-looking action set).
230
231**Before planning, confirm a Commands & Verification Inventory exists.** Exit
232criteria are only worth anything if they are *runnable*, so the plan must be able
233to cite the project's canonical build / run / test / lint / typecheck / e2e /
234contract commands and CI gate(s). Documentation mode produces this
235inventory in Part 1; if you're working from a README or prior context that lacks
236it, detect and record those commands yourself (per-ecosystem — npm/yarn/pnpm,
237`make`, `just`, `cargo`, `go`, `poetry`/`tox`/`nox`, `gradle`/`mvn`, etc.) before
238writing exit criteria. Never invent a command you haven't verified against the
239task runner / manifests / CI config.
240
241### Workflow
242
243#### Phase 1: Assess Current State (from existing docs)
244
245Read the architecture document and extract:
246
2471. **Tech stack inventory** — languages, frameworks, runtimes, dependencies with
248 versions where visible
2492. **Feature/domain map** — the major functional areas of the system
2503. **Pain points already identified** — tech debt, concerns, deprecated deps,
251 architectural violations noted in the existing docs
2524. **Deployment and infrastructure** — how it runs today
253
254Do NOT re-read every source file. Trust the architecture doc. Only open specific
255files if a modernization question requires verifying a detail (e.g., "is this
256dependency actually used or just declared?").
257
258#### Phase 2: Feasibility Spike, Strategy Fork & Safety Ladder
259
260This is the pivot of the whole plan. **Do it before recommending a target
261architecture or writing any phases.** Its job is to answer, quickly and honestly:
262*how alive is this system, what migration shape fits, and how much regression
263safety can we actually achieve?*
264
265##### 2a. Run a time-boxed feasibility spike
266
267Put a **hard time box** on it (e.g. one day). The spike's question is **NOT "can
268we make CI green?"** — it is **"can we get this to run even *once* to capture its
269behavior?"** Probe, per component / deployable unit:
270
271- Does it **install** its dependencies today (ideally **from a committed
272 lockfile without hand-patching**)?
273- Do **native/build steps** succeed on a **currently supported** toolchain?
274- Does it **boot / start**?
275- Can the **test runner execute at all**, and does **≥1 meaningful test pass**?
276
277Record what you actually observed — including partial success (common in
278monorepos: one service installs and tests green while another can't compile). Do
279**not** sink two sprints resurrecting a corpse to discover it's unresurrectable.
280When the time box expires, decide with the evidence you have.
281
282##### 2b. Locate the Testability Milestone (per component)
283
284**"Testable" is not a precondition you can satisfy on a dead app — for a dead app
285it is an *output* of modernization, not an input.** So every plan must name, up
286front, its **Testability Milestone**: the specific phase at which the system
287(or a given component) first satisfies **all four** conditions at once:
288
2891. Runs on a **supported** (non-EOL) runtime.
2902. Dependencies install **from a lockfile without hand-patching**.
2913. Native / build steps **succeed on the current toolchain**.
2924. The **test runner executes and ≥1 meaningful test passes in CI**.
293
294Assess this **per component** — as the reference Nylas run showed, a near-modern
295cloud service can cross the line immediately while a legacy Electron client
296doesn't cross it until several phases later. **This milestone is the single most
297important marker in the plan.** State where it lands for each component, out loud.
298
299##### 2c. Label every phase pre- or post-testability
300
301The Testability Milestone splits the effort into two regimes with **different
302safety rules**:
303
304- **Pre-testability ("dark") regime:** the component can't be trusted to run.
305 Safety comes from **non-test** sources (see the safety ladder below).
306 **Requiring a CI test gate here is a category error** — you're demanding green
307 tests from something that isn't alive yet.
308- **Post-testability ("lit") regime:** it builds and runs on a live, supported
309 stack. *Now* real CI, characterization, and e2e are achievable and worth the
310 investment. The classic green-gate belongs **here**.
311
312**Rule: never require an automated test gate on a component before that component
313crosses its own testability line.**
314
315##### 2d. Choose the migration strategy — the A/B fork
316
317The spike decides which of two shapes fits (this is a per-component call in a
318monorepo):
319
320- **(A) Freeze-then-lift.** Net the old app *as-is*, then upgrade under the net.
321 Requires the old app to be **resurrectable at acceptable cost** — i.e. it
322 *almost* runs. This is the traditional "characterization tests before you
323 change anything" path. **Opt-in, not the default.**
324- **(B) Beachhead-then-expand (walking skeleton).** Don't net the corpse. Drive
325 the **thinnest possible end-to-end slice onto the modern stack** until it
326 builds, boots, and tests; then strangle the rest in, writing tests **on the new
327 stack** as you go. **This is the default for dead apps.** In strategy B, the
328 first phase is *"reach the Testability Milestone with the smallest slice,"* not
329 *"freeze the whole legacy toolchain."*
330
331If a component **can't be built or run at all**, characterization tests on the
332*old* stack are impossible by definition — invert the approach: **stop trying to
333net the corpse and build the net on the *target* stack incrementally**, using the
334old code/output as the reference oracle.
335
336##### 2e. Pick the highest achievable rung on the safety ladder
337
338Regression safety is a ladder, not a binary. Choose the **highest rung you can
339actually reach** per component; **a downgrade to a lower rung is a blessed,
340first-class outcome — not a failure — provided the residual risk is named.**
341
342- **L4 — Full automated gate:** green lint + unit + characterization + e2e in CI.
343- **L3 — Partial gate:** some suites green + lockfile + CI on the achievable
344 subset; the rest **quarantined** with a named list.
345- **L2 — Characterization / golden-master only:** can't run in CI, but capture
346 I/O snapshots / behavioral diffs (manual is acceptable).
347- **L1 — Reversibility-based:** no runnable tests; safety comes from small
348 reversible steps, strangler / parallel-run, a smoke checklist, and review.
349- **L0 — No net possible:** treat as a spec-driven rewrite (old code = the spec)
350 or archive.
351
352**Safety is not only tests.** There are at least five sources, and on dead apps
3532–4 often outweigh 1:
354
3551. **Tests** (characterization / unit / e2e) — strongest, often unachievable on
356 dead apps.
3572. **Reversibility** — tiny commits, branch-per-change, trivial rollback.
3583. **Isolation** — feature flags, strangler-fig, run old + new in parallel and
359 **diff outputs**.
3604. **The running old system as an oracle** — record/replay real I/O to
361 characterize behavior without a test harness.
3625. **Human review + domain knowledge.**
363
364**Anchor safety at the behavioral *seams*, not the unit.** The durable,
365stack-agnostic contracts of any app are its externally observable behaviors:
366HTTP endpoints, DB schema/queries, file/wire formats, CLI output, protocols. Pin
367*those* as golden-master / approval snapshots — **they survive the rewrite; unit
368tests on dead legacy code get deleted the moment you replace the module.** Get an
369**oracle**, ranked by what's available:
370
371- **Best — it runs *somewhere*** (prod, a VM, an old container, even read-only),
372 **and you're allowed to use it**: record real I/O and replay it against the new
373 code. One successful capture beats a permanent green legacy CI. *(Do not assume
374 this rung is always available — a faithful third-party reimplementation or a
375 resurrected legacy binary may be off-limits by policy, licensing, or an
376 explicit decision to build "with only what we have." If so, skip to the
377 self-frozen rung.)*
378- **Fallback — it doesn't run at all:** the old **source is the spec**;
379 supplement with prod logs, DB dumps, docs, tickets, and accept that
380 characterization tests get written **against the new implementation** to lock
381 it once verified correct.
382- **Self-frozen golden master — no external reference exists or is permitted:**
383 when nothing trustworthy runs *and* you may not borrow an external oracle, the
384 **modernized component records its *own* golden master the moment it first runs**
385 (at its Testability Milestone), and every later phase diffs against that frozen
386 snapshot. This is a real, usable rung — but **name its residual risk out loud**:
387 it guarantees the *self-consistency of later refactors*, **not** the correctness
388 of the first boot (the snapshot is only as correct as the run that produced it).
389 Pair it with human review / spec cross-checks to bless that first run.
390
391##### 2f. Feed an economic/oracle triage into the choice
392
393Weigh **regression cost** when picking a rung: Is the app **in production**? Does
394it have **real users**? Is the **old system still runnable** as a reference? An
395abandoned app with zero users has near-zero regression cost — an expensive gate
396there is **over-engineering**. A production system with users justifies a higher
397rung and a real oracle.
398
399##### 2g. Locate the CI Milestone (name the phase that stands up CI)
400
401Just as the Testability Milestone names *when a component can first run and be
402tested*, the **CI Milestone** names *when continuous integration is first stood
403up* — and it must be **stated out loud in the roadmap, not buried in a phase
404body.** A common failure of modernization plans is leaving the reader unable to
405tell which phase introduces CI; name it explicitly.
406
407**Rule: CI is stood up in the *first lit phase* — at or immediately after the
408component crosses its Testability Milestone — never before.** CI cannot be green
409on a component that cannot yet build/run, so wiring a CI *gate* during the dark
410regime is the same category error as demanding a test gate there. (Trivial
411scaffolding — a lint-only or build-only workflow — may appear earlier, but the
412authoritative test gate lands at the CI Milestone.)
413
414**CI is two distinct steps, and the second is human-only** — call both out:
4151. **Author the CI workflow file** (e.g. `.github/workflows/*.yml`, `.gitlab-ci.yml`,
416 Azure Pipelines) that runs the gate. **An agent can do this.**
4172. **Make that workflow an *enforced* gate** — a **required status check /
418 branch-protection rule / merge-request approval rule**. This is a manual
419 platform-UI / admin step (GitHub → Settings → Branches; GitLab protected
420 branches + MR settings; etc.) that **an agent cannot perform.** Until a human
421 configures it, CI *runs* on PRs but does **not** *block* merges.
422
423Because step 2 is outside the agent's reach, **every plan that stands up CI must
424emit it as an explicit user action item** (in the phase's exit criteria and in
425§9 Open questions / decisions needed from stakeholders), so the reader knows the
426gate is not self-enforcing until they turn it on.
427
428**Output of Phase 2:** for each component — the spike result, the chosen strategy
429(A or B), the located Testability Milestone, the pre/post label, and the target
430safety-ladder rung with any residual risk named — **plus the plan-wide CI
431Milestone** (which phase stands up CI) and the reminder that enforcing it is a
432manual human step. Everything downstream depends on these.
433
434#### Phase 2.5: Red-team every phase against the hazard catalog (mandatory)
435
436The strategic scaffolding above (testability, safety ladder, CI milestone) is
437necessary but **not sufficient**. Modernization plans also fail on *tactical,
438ecosystem-predictable* hazards — an incomplete quarantine set, a forgotten
439namespace codemod, a runtime bump that leaves the base images behind, a stateful
440data-store major treated as an image bump, an edge rewrite that drops the
441anonymous route class. These are so recurring that they have their own catalog:
442**`references/migration-hazards.md`** (H1–H8).
443
444**Rule: before any phase is implemented, red-team its plan against every hazard
445in `references/migration-hazards.md`.** For each hazard, ask "does this phase
446trigger it?", run the hazard's detection probe against the real repo, and fold
447its plan action into the phase's tasks/exit criteria **before** writing code. A
448hazard caught in planning is a task; caught in review it's rework; caught in prod
449it's an incident. Record hazards you checked and **cleared**, not just the ones
450that fired — a silent skip is indistinguishable from a miss.
451
452This is the institutionalized form of an independent-critic pass. Do it yourself
453by walking the catalog, and — where an independent second opinion is available
454(e.g. a rubber-duck / critique agent) — use it: on the reference PiggyMetrics run
455such a pass caught a **blocking plan gap in every single phase**, and each gap
456was an instance of an H1–H8 class. Bake the outcome into each phase's
457"Decisions made" and exit criteria so the executing agent inherits the cleared
458checklist rather than re-discovering the hazard.
459
460#### Phase 3: Recommend Target Architecture
461
462**Decision framework — always evaluate in this order:**
463
464For each component/dependency, work through these levels from least-disruptive to
465most-disruptive. Stop at the first level that solves the problem:
466
4671. **Upgrade in place** — Can you bump the major version and fix breakages?
468 (e.g., Node 14→22, Python 2→3, React 16→19, Rails 5→7). This is the default
469 answer. Only reject it if the upgrade path is officially unsupported, the
470 breaking changes are so extensive they exceed rewrite cost, or the framework
471 itself is abandoned/EOL.
472
4732. **Swap the dependency** — If the specific library is dead but the pattern is
474 fine, can you swap to a maintained alternative with the same interface shape?
475 (e.g., Moment.js → date-fns, Request → got/fetch, CoffeeScript → TypeScript
476 with decaffeinate). Prefer drop-in or mechanical migration tools when they
477 exist.
478
4793. **Wrap/adapt (Strangler Fig)** — If the component's interface is sound but
480 internals are unmaintainable, can you put a clean interface in front and
481 incrementally replace internals behind it? This works when coupling is
482 manageable and the system can run with old and new coexisting.
483
4844. **Rewrite** — Only when: the framework is abandoned with no upgrade path, the
485 architecture fundamentally cannot support required new capabilities (e.g.,
486 synchronous-only design that must become real-time), or honest estimation shows
487 upgrade cost exceeds rewrite cost. Require explicit justification for every
488 rewrite recommendation — "it's old" is not sufficient.
489
4905. **Remove** — Dead code, features with zero usage, deprecated capabilities
491 superseded by other systems. Verify usage claims before recommending removal.
492
493**Bias toward conservatism.** The cheapest migration is the one you don't do.
494Every level of disruption adds risk, timeline, and cost. A working system on an
495older framework is better than a half-finished rewrite on a new one.
496
497**Mechanical-migration riders (apply whenever a recommendation is an upgrade,
498swap, or removal).** These are the parts of a target-architecture decision that
499first-draft plans routinely drop; each maps to a hazard class in
500`references/migration-hazards.md`:
501
502- **Transitive-quarantine completeness (H1).** When removing/cutting over a whole
503 dependency family, grep the dead artifact across **all** manifests and list the
504 *full* set of dependents that must move or be quarantined — not the subset you
505 first thought of. State the post-cutover build target explicitly.
506- **Framework-major codemods (H2).** A **major** bump carries mechanical breaking
507 changes independent of your logic — namespace renames, removed deprecated APIs,
508 a changed **test engine**, changed config keys. Enumerate each as its own task
509 and prefer the ecosystem's automated **recipe/codemod tool** (OpenRewrite,
510 jscodeshift/react-codemod, `2to3`/`pyupgrade`, etc.) over hand-editing on large
511 trees. Don't forget the test-framework migration — it is the most-forgotten one.
512- **Runtime↔deployment lockstep (H3).** If a recommendation bumps the
513 language/runtime **major**, every place the runtime is pinned for *running* the
514 app — base images, `docker-compose` build contexts, CI runner images,
515 `engines`/`.nvmrc`/`.tool-versions`, serverless runtimes — must move in the
516 **same phase**. Build-runtime and run-runtime must never drift across a phase
517 boundary, or the app builds green and then fails to run.
518
519Produce a **Target Architecture** section that includes:
520
521- **Recommended tech stack** with rationale for each choice (framework, language,
522 runtime, database, infrastructure)
523- **Architectural pattern** recommendation (monolith → modular monolith →
524 services, event-driven, serverless, etc.) with justification
525- **What stays vs. what goes** — explicitly categorize each major component using
526 the decision framework above:
527 - ✅ Keep as-is (still modern/adequate)
528 - ⬆️ Upgrade in place (bump version, fix breakages)
529 - 🔀 Swap dependency (replace dead library with maintained equivalent)
530 - 🔄 Wrap/adapt (add an interface layer, incrementally replace internals)
531 - 🔁 Rewrite (fundamentally incompatible — justification required)
532 - 🗑️ Remove (dead code, deprecated features, superseded)
533
534For each recommendation, write an inline ADR:
535
536```markdown
537#### ADR: [Decision Title]
538- **Context:** [Why this decision is needed]
539- **Decision:** [What we chose and which decision framework level it falls under]
540- **Alternatives considered:** [What else was evaluated and why it lost]
541- **Consequences:** [Tradeoffs accepted]
542```
543
544#### Phase 4: Per-Feature Migration Analysis
545
546For each major feature/domain identified in the architecture doc, produce a
547section covering:
548
5491. **Current implementation** — one paragraph summary with file references
5502. **Migration strategy** — the A/B choice from Phase 2 for this component
551 (Freeze-then-lift vs. Beachhead/walking-skeleton), plus the tactic: Strangler
552 Fig, Big Bang rewrite, Lift & Shift, Incremental Refactor, or Leave In Place
5533. **Testability status** — where this component's **Testability Milestone**
554 lands, and its **safety-ladder rung** (L0–L4) with residual risk named
5554. **Dependencies and coupling** — what else breaks or must move with it
5565. **Effort estimate** — T-shirt size (XS/S/M/L/XL) with justification
5576. **Risk assessment** — what can go wrong, data migration concerns, user impact
5587. **Acceptance criteria** — how you know the migration succeeded (expressed
559 against the chosen oracle/seam contracts, not against a not-yet-alive test
560 suite)
561
562#### Phase 5: Phased Implementation Plan
563
564Produce an ordered, phased plan. Each phase should be independently deployable
565(no half-migrated states that can't run in production).
566
567**Phase gating is regime-aware (applies to every phase).** State this rule at the
568top of the phased plan and honor it in every phase's exit criteria. A phase is
569**not complete** until its Verification & Exit Criteria pass. Those criteria must
570be (a) **objectively verifiable**; (b) **actually executed and recorded** before
571the next phase starts; and (c) **gated** — do not advance to phase N+1 until
572phase N's criteria are demonstrably met. **But which criteria are valid depends
573on the component's regime (Phase 2c):**
574
575- **Post-testability ("lit") phases:** exit criteria are runnable commands / green
576 CI — **green CI on the phase's branch/PR is the authoritative signal.**
577- **Pre-testability ("dark") phases:** the component isn't alive yet, so a green
578 CI test gate is a **category error**. Exit criteria come from the achievable
579 safety-ladder rung instead — captured seam/oracle snapshots, reversibility
580 scaffolding proven present, a passed smoke checklist, recorded review. Do **not**
581 block a dark-regime phase on a test suite that can't run yet.
582
583Report to the user any phase whose pass/fail is unknown rather than assuming it
584passed.
585
586**The first phase establishes maximum *achievable* safety — not a fixed "green
587legacy gate."** Do not hard-code "resurrect everything and make CI green." Shape
588the first phase from the Phase 2 outputs, per component:
589
590- **Strategy A (Freeze-then-lift), component is resurrectable:** the first phase
591 *is* the classic Safety Net & Baseline — purely additive, no behavior change:
592 - **Characterization / golden / contract tests** at the **seams** that pin
593 current observable behavior (protocol traces, API snapshots, output goldens).
594 - **CI + lint + formatter** wired up and green (an authoritative gate for later
595 lit-regime phases).
596 - **Pinned dependencies / committed lockfile** and a recorded known-good
597 baseline (versions, toolchain, how it builds and runs today).
598 - **"Net proven to fail."** Deliberately mutate the code (e.g., change one
599 protocol code or return value), confirm the new tests go **red**, then
600 revert. A safety net that never fails is proof of nothing — prove it has
601 teeth.
602- **Strategy B (Beachhead/walking-skeleton), component is dead:** the first phase
603 is **"reach the Testability Milestone with the smallest slice"** — drive the
604 thinnest end-to-end path onto the modern stack until it builds, boots, and runs
605 ≥1 real test green, capturing seam/oracle contracts from the old system to
606 validate against. **Do not try to net the corpse.** Its exit criterion is the
607 achievable safety-ladder rung with residual risk named — a **blessed downgrade
608 is not a failure.**
609
610Only after the first phase's criteria are met (at whatever rung) do the
611subsequent infrastructure and feature phases begin.
612
613Structure each phase as:
614
615```markdown
616### Phase N: [Name] (T-shirt size: M)
617
618**Goal:** [One sentence]
619**Regime:** [pre-testability ("dark") | post-testability ("lit")] — per component
620**Safety rung:** [L0–L4, with residual risk named if below L4]
621**Prerequisites:** [Which phases must complete first]
622**Duration estimate:** [Relative, not calendar — e.g., "2-4 sprints"]
623
624#### Tasks
625| ID | Task | Component | Blocked by |
626|----|------|-----------|------------|
627| N.1 | ... | ... | — |
628| N.2 | ... | ... | N.1 |
629
630#### Risks & Mitigations
631- **Risk:** ... → **Mitigation:** ...
632
633#### Decisions made
634- [Every sub-decision this phase depends on, resolved and documented here so the
635 phase can be implemented without further user input. State "dropped" vs
636 "deferred" explicitly for anything cut.]
637
638#### Verification & Exit Criteria (Definition of Done)
639- [ ] [Regime-appropriate criterion. For LIT phases: a runnable command / green
640 CI check, citing the actual command from the Commands & Verification
641 Inventory. For DARK phases: the achievable safety-ladder rung's evidence —
642 captured seam/oracle snapshots, reversibility proven, smoke checklist
643 passed — NOT a green test suite the component can't yet run.]
644- [ ] [Include a parity/characterization check against the chosen oracle whenever
645 the change is meant to preserve behavior.]
646- [ ] [If purely additive, assert it: "no behavior/dependency/logic changed."]
647- [ ] [If exiting below L4, state the residual risk and which later phase closes
648 it (e.g., "client test gate deferred to the client-migration phase").]
649```
650
651**Ordering principles:**
652- **Locate the Testability Milestone first**, per component — schedule the
653 smallest slice that reaches it early rather than deferring "testable" to the end
654 while demanding a gate at the start.
655- **Never require a component's automated test gate before it crosses its own
656 testability line.**
657- **Name the CI Milestone in the roadmap.** State explicitly which phase stands
658 up CI (the first *lit* phase, at/after the Testability Milestone) so the reader
659 never has to guess when CI arrives — and flag that *enforcing* it (required
660 status check / branch protection) is a manual human step, not an agent task.
661- **First-phase safety before behavior change** — establish the highest
662 achievable rung (not necessarily a full green gate) before modifying behavior.
663- Infrastructure and cross-cutting concerns next (auth, config, logging, CI/CD)
664- Highest-risk or highest-value features next (prove the pattern early)
665- Low-risk, low-coupling features last (easy wins to parallelize)
666- Data migrations get their own phase with rollback plans
667
668#### Phase 6: Execution Governance
669
670Modernization plans fail in execution, not on paper. Bake in the governance that
671keeps each phase honest and reversible (generic across any ecosystem):
672
673- **Branch per phase.** Never commit phase work to the default branch. Open one
674 PR per phase; let CI on the PR be the authoritative green signal before merge
675 **for lit-regime phases**. For dark-regime phases, the PR still carries the
676 rung's evidence (captured contracts, smoke results, reversibility scaffolding).
677- **Merge to trunk before the next phase; never stack (H7).** Each phase branch
678 is cut **from trunk**, and its PR is **merged to trunk before the next phase
679 starts**. Never base a phase branch or its PR on a *sibling* phase branch — that
680 is how PRs get merged into each other, the trunk silently stalls several phases
681 behind, and later phases are forced into the exact bind where the no-stacking
682 rule has to be broken. Before starting a phase, verify the previous phase is
683 merged to trunk and `git log origin/<trunk>..HEAD` is empty at branch creation.
684 **If controlled stacking is genuinely unavoidable, it is allowed only with a
685 required reconciliation PR** that lands the stack onto trunk **and an explicit
686 residual-risk note** — never as a silent default.
687- **Confirm the trunk name; retire legacy defaults.** Pin the repo's real trunk
688 (e.g. `main`) and, if a second default-ish branch exists (`master`), mark it
689 "history only — do not target" so phase work never lands on the wrong branch.
690- **The gate matches the regime.** Lit phases advance on green CI; dark phases
691 advance on the achievable safety-ladder rung with residual risk named — not on
692 a "looks done" judgement, and not on a test suite that can't run yet.
693- **Stand up CI at the CI Milestone, and hand off enforcement to a human.**
694 Name the phase that introduces CI (the first lit phase). Distinguish the two
695 steps: authoring the workflow file is agent-doable; making it an **enforced
696 required status check / branch-protection (or MR-approval) rule is a manual
697 platform-UI step the agent cannot perform.** Until a human configures it, CI
698 *runs* but does not *block* merges — so record "enable branch protection /
699 required checks" as an explicit user action item (in the CI phase's exit
700 criteria and in §9 stakeholder decisions), never as a completed agent task.
701- **Interface-preserving & independently deployable.** Each phase leaves the
702 system runnable, so rollback = redeploy the previous version. Prefer
703 interface-preserving rewrites (wrap internals behind a stable interface) and
704 prove equivalence with the seam/oracle contracts (golden/characterization
705 tests where they exist).
706- **Living plan.** Update `MODERNIZATION_PLAN.md` status markers as each phase's
707 exit criteria are met — ✅ complete / ⏭️ descoped / 🗑️ dropped — record the
708 decisions made during implementation, and **update the safety-ladder rung /
709
710
711…(truncated)