Spec-Driven Refactoring
Large refactors (schema migrations, concept renames, structural reorganizations) have more
surface area for stale assumptions than normal features. This workflow adds two review gates
to catch inconsistencies before they compound.
When to use
- Multi-file structural changes (5+ files across schemas, scripts, tests, docs)
- Schema migrations (field renames, type changes, file relocations)
- Concept renames that ripple through the entire codebase
- Any refactor where "I thought X was at line Y" mistakes would be expensive
Procedure
1. Analyze
Read all affected files. Identify every contract the refactor touches:
- Schemas (JSON Schema, TypeScript interfaces, etc.)
- Scripts (build, scaffold, validate, install)
- Tests (which test files reference the old structure)
- Docs (architecture docs, authoring guides, README)
2. Write spec
Produce a markdown specification covering:
- Problem statement — what's wrong and why
- Design decisions — numbered, explicit choices with rationale
- Proposed structure — new/modified/removed files in a table
- Schemas — full JSON Schema definitions for new contracts
- Implementation order — phased, with validation gates between phases
- Risks — what could go wrong
Save to docs/specs/ (or project equivalent).
3. Spec review
Delegate a review sub-agent. Feed it the spec + all key context files. Ask it to check:
- Logical gaps or missing pieces
- Naming consistency (especially across old→new renames)
- Migration path correctness
- Schema completeness and self-consistency
- Implementation order dependencies (does Phase 2 need Phase 1's output?)
Categorize findings: Critical / Medium / Low. Return GO/NO-GO with conditions.
4. Fix spec findings
Address all Critical and Medium findings. Low findings can be deferred to implementation
but should be tracked in the spec's review tracker.
5. Pre-implementation consistency check
Delegate a SECOND sub-agent to verify the spec's claims against the actual codebase:
- Do claimed line numbers match?
- Do file paths exist?
- Do schema fields match the spec's descriptions?
- Do test files reference the paths the spec claims?
- Are there files the spec forgot to mention?
This catches stale assumptions the spec author made during analysis.
6. Fix consistency findings
Update the spec and any affected files (e.g., aligning agent enum ordering).
7. Implement
Follow the spec's implementation order. Between each phase:
- Run the project's validation/test commands
- Verify the phase's changes don't break the next phase's prerequisites
8. Post-implementation consistency + quality review
First: establish filesystem ground truth. Before reading any files:
git branch --show-current — confirm you're on the refactoring branch, not main
git status --short — confirm working tree matches expectations
git diff main..<branch> --stat — get the actual file list
Use terminal for these, NOT execute_code/read_file — those tools may operate in a
sandboxed environment with pre-seeded content that doesn't match the real filesystem. A
review built on sandbox data is worthless. See pitfalls below.
Then verify the completed implementation:
- All new schemas validate
- Script pipeline aligns (discovery ↔ index ↔ validation)
- Docs match the actual code
- Every new contract has tests
- No dead code or stale references to removed structures
- Existing tests still pass
Fix findings before merging.
9. Verify scaffold works end-to-end
Run the project's scaffold/refresh tool against a real target (or the project itself)
to verify the refactored scaffolding actually works:
- den-refresh / welcome-ai-badger / equivalent
- Verify symlinks, config registration, and manifest generation
- Check that the scaffolded output validates against the updated schemas
This catches integration issues that unit tests miss (e.g., a schema change that
passes validation in isolation but breaks scaffold.py's manifest generation).
10. Issue management
Check the project's issue tracker for issues addressed or invalidated by the refactor:
- Update open issues with status comments (what was fixed, what remains)
- Close issues that are fully resolved
- Note partial progress on multi-step issues
11. File move reference sweep
When moving files (not just renaming fields), search ALL references:
- Test files (
load_script("old/path"))
- Doc files (installation instructions, path examples)
- Cross-referencing files (extension.md pointing at hook scripts)
- CI config (workflow files referencing paths)
Use grep -r "old/path" . to catch what IDE cleanup misses.
Explicit checks the quality review sub-agent should run:
- Orphaned test files — every test file must have a corresponding implementation file. If
tests/test_foo.py exists but scripts/foo.py doesn't, either remove the test or implement the script.
- Schema enum ↔ code constant alignment — manifest entry feature enum, validate KIND_TO_SCHEMA keys, and index.schema.json feature keys must all match
badger_lib.FEATURES. Mismatches are silent until a scaffold run hits them.
- Docstring accuracy — validate.py's docstring lists
--kind options; if they don't match KIND_TO_SCHEMA, the CLI help is wrong.
- Dead code from removed features — after removing a feature, search for deprecated functions, branches, and references.
grep -r "old_feature" scripts/ tests/ catches what IDE cleanup misses.
- Scaffold version propagation — after re-scaffold, does the config's frameworkVersion update? If not, the next drift detection will re-scaffold unnecessarily.
Gotchas
execute_code/read_file sandbox may show pre-seeded content, not real files. When
reviewing a refactoring on a feature branch, the sandbox environment may be populated with
mock data for the target state rather than reflecting the actual filesystem. You can build
an entire multi-step analysis — schema validation, test results, file existence checks — on
phantom data. Always use terminal for ground-truth filesystem checks (git status,
ls, cat, python3 -m pytest). If execute_code returns file contents that terminal says
don't exist, trust terminal. Caught when: review of refactoring branch produced 20+ tool
calls of fabricated analysis before discovering the real repo was on main.
jsonschema $ref to sibling files fails without a custom resolver. The Python
jsonschema library's Draft202012Validator cannot resolve relative $ref paths like
"$ref": "agents.schema.json#/$defs/agentName" without a custom RefResolver. Either
inline the enum in every schema or provide a resolver that maps URIs to local files.
Discovered via: test failure Unresolvable: agents.schema.json#/$defs/agentName.
Schema renames must happen atomically with code that writes the old field. Renaming
pluginScope → skillScope in a schema breaks any script that still writes pluginScope.
Do the rename + script update in the same phase, or the intermediate state fails validation.
Agent enum ordering drifts silently. When the same enum appears in multiple schemas,
different files end up with different orderings (e.g., junie, hermes vs hermes, junie).
This doesn't affect validation but creates confusion about the canonical order. Align once
and add "keep in sync" comments.
Phase ordering matters: remove old data before running validation. If you update a
schema to remove a feature key (e.g., plugins) but don't remove the data directory that
produces those entries (e.g., features/*/plugins/), the next index_build.py run will
produce entries the updated schema rejects. Remove old data in the same phase as the schema
change.
Two extension.json files can exist for different scopes. A refactor that removes
agent-level extension.json must not accidentally remove stack-level extension.json.
Verify every file path individually.
Manifest schema transitions need both old and new fields. When renaming a field
in manifest.schema.json (e.g. pluginScope → skillScope), the manifest is
written by scaffold.py. If you rename the schema field without updating scaffold.py
in the same commit, validation fails. The safe pattern: (1) add the new field to
manifest.schema.json alongside the old one, (2) update scaffold.py to write both
during transition, (3) commit both together, (4) remove the old field in a follow-up
once all scaffolded projects have migrated.
User may add scope mid-implementation. During a long refactor, the user may
request additional features (breaking version support, new docs, safety wrapping).
Incorporate these into the current PR if they're closely related; otherwise note
them as follow-up PRs. Don't block the current work waiting for perfect scope.
"Ensure X always Y" is a non-negotiable invariant. When the user says
"ensure that hooks will always catch an exception and log it" or similar safety
directives, treat it as a hard requirement — implement it across ALL entry points,
test it, and don't defer it. The pattern: wrap every public hook/adjustment function
in try/except Exception with logger.debug(..., exc_info=True) so a broken hook
never crashes the host process. Apply to hooks, adjustments, and plugin scripts.
Scaffolded config.frameworkVersion must be updated after re-scaffold. When a
scaffolding/refresh tool re-scaffolds a project, it updates manifest.json with the
current framework version but often preserves the original config.json's
frameworkVersion. This causes the next drift detection to re-scaffold again
unnecessarily. Fix: update config.frameworkVersion to the current version after
a successful re-scaffold. Caught when: den-refresh re-scaffolded but config still
showed the original 0.2.0 scaffold version.
Version + changelog on EVERY commit that touches production code. User explicitly
requires: bump VERSION (semver), add docs/changelog/{version}-{slug}.md, and enforce
as an invariant. No exceptions — even small fixes get a patch bump. The invariant file
at features/common/invariants/version-changelog-required.md codifies this.
Hermes skills must be namespaced per project, NOT in shared external_dirs. Hermes
does NOT auto-discover .hermes/skills/ in the CWD. It only scans ~/.hermes/skills/
(global) and skills.external_dirs in ~/.hermes/config.yaml. Using external_dirs
for multiple projects causes skill name conflicts (e.g., every project has a task skill)
with last-match-wins semantics. The correct approach: symlink each project's skills into
~/.hermes/skills/<project-name>/ as a namespace directory. Each project gets isolated
skills that don't conflict with other projects. Do NOT register in global external_dirs.
Caught when: the maintainer-home-page's task skill was shadowed by the reference repo's older
task skill because the latter was last in external_dirs.
Test isolation for config file writes. When testing code that writes to
~/.hermes/config.yaml, mock Path.home() to return a temp dir AND create the
.hermes/ subdirectory structure in the temp dir. The method resolves
Path.home() / ".hermes" / "config.yaml", not Path.home() / "config.yaml".
Caught when: test paths leaked into the real config because the temp dir structure
was wrong.
Breaking version semver boundary is exclusive on the lower bound.
is_breaking_transition(from, to, root) checks from_v < breaking <= to_v. A
project already at the breaking version (e.g., 0.7.0 → 0.8.0 with 0.7.0 as
breaking) does NOT trigger — because 0.7.0 < 0.7.0 is false. This is correct:
the project was scaffolded at or after the breaking version, so no migration is
needed. Only projects crossing the boundary (e.g., 0.6.0 → 0.8.0) need the full
re-scaffold with backup.
Python mixin-based module split
For splitting a single large Python file into domain-specific modules while preserving the
public API — read references/python-mixin-module-split.md when splitting a Python module. Covers the mixin inheritance pattern,
dynamic script loading (sys.path), circular import handling, and multi-copy sync.
Feature design spec patterns
When writing a feature design specification (not a refactoring spec), see
references/feature-design-spec-patterns.md for section structure, requirement ID conventions,
domain type documentation, testing strategy, and acceptance criteria patterns observed in
DDD/.NET projects.
Multi-PR sequential merge pattern
For large refactors (8+ phases), split into independently-mergeable PRs. See
read references/multi-sequential-pr-pattern.md for the full workflow when the change needs multiple sequential PRs.
References
references/plugin-to-skills-migration.md — worked case: migrating a plugin layout to per-stack skills; read when migrating a plugin layout.
1---2name: spec-driven-refactoring3description: Use when the user says 'refactor', 'migrate', or 'rename across the codebase', or a change touches 5+ files across schemas, scripts, tests, and docs: write a spec, run two review gates (pre-implementation consistency + post-implementation quality), then implement against it. Covers schema migrations, concept renames, structural reorganizations.4license: MIT5---67# Spec-Driven Refactoring89Large refactors (schema migrations, concept renames, structural reorganizations) have more10surface area for stale assumptions than normal features. This workflow adds two review gates11to catch inconsistencies before they compound.1213## When to use1415- Multi-file structural changes (5+ files across schemas, scripts, tests, docs)16- Schema migrations (field renames, type changes, file relocations)17- Concept renames that ripple through the entire codebase18- Any refactor where "I thought X was at line Y" mistakes would be expensive1920## Procedure2122### 1. Analyze2324Read all affected files. Identify every contract the refactor touches:25- Schemas (JSON Schema, TypeScript interfaces, etc.)26- Scripts (build, scaffold, validate, install)27- Tests (which test files reference the old structure)28- Docs (architecture docs, authoring guides, README)2930### 2. Write spec3132Produce a markdown specification covering:33- **Problem statement** — what's wrong and why34- **Design decisions** — numbered, explicit choices with rationale35- **Proposed structure** — new/modified/removed files in a table36- **Schemas** — full JSON Schema definitions for new contracts37- **Implementation order** — phased, with validation gates between phases38- **Risks** — what could go wrong3940Save to `docs/specs/` (or project equivalent).4142### 3. Spec review4344Delegate a review sub-agent. Feed it the spec + all key context files. Ask it to check:45- Logical gaps or missing pieces46- Naming consistency (especially across old→new renames)47- Migration path correctness48- Schema completeness and self-consistency49- Implementation order dependencies (does Phase 2 need Phase 1's output?)5051Categorize findings: Critical / Medium / Low. Return GO/NO-GO with conditions.5253### 4. Fix spec findings5455Address all Critical and Medium findings. Low findings can be deferred to implementation56but should be tracked in the spec's review tracker.5758### 5. Pre-implementation consistency check5960Delegate a SECOND sub-agent to verify the spec's claims against the actual codebase:61- Do claimed line numbers match?62- Do file paths exist?63- Do schema fields match the spec's descriptions?64- Do test files reference the paths the spec claims?65- Are there files the spec forgot to mention?6667This catches stale assumptions the spec author made during analysis.6869### 6. Fix consistency findings7071Update the spec and any affected files (e.g., aligning agent enum ordering).7273### 7. Implement7475Follow the spec's implementation order. Between each phase:76- Run the project's validation/test commands77- Verify the phase's changes don't break the next phase's prerequisites7879### 8. Post-implementation consistency + quality review8081**First: establish filesystem ground truth.** Before reading any files:821. `git branch --show-current` — confirm you're on the refactoring branch, not `main`832. `git status --short` — confirm working tree matches expectations843. `git diff main..<branch> --stat` — get the actual file list8586Use `terminal` for these, NOT `execute_code`/`read_file` — those tools may operate in a87sandboxed environment with pre-seeded content that doesn't match the real filesystem. A88review built on sandbox data is worthless. See pitfalls below.8990Then verify the completed implementation:91- All new schemas validate92- Script pipeline aligns (discovery ↔ index ↔ validation)93- Docs match the actual code94- Every new contract has tests95- No dead code or stale references to removed structures96- Existing tests still pass9798Fix findings before merging.99100### 9. Verify scaffold works end-to-end101102Run the project's scaffold/refresh tool against a real target (or the project itself)103to verify the refactored scaffolding actually works:104- den-refresh / welcome-ai-badger / equivalent105- Verify symlinks, config registration, and manifest generation106- Check that the scaffolded output validates against the updated schemas107108This catches integration issues that unit tests miss (e.g., a schema change that109passes validation in isolation but breaks scaffold.py's manifest generation).110111### 10. Issue management112113Check the project's issue tracker for issues addressed or invalidated by the refactor:114- Update open issues with status comments (what was fixed, what remains)115- Close issues that are fully resolved116- Note partial progress on multi-step issues117118### 11. File move reference sweep119120When moving files (not just renaming fields), search ALL references:121- Test files (`load_script("old/path")`)122- Doc files (installation instructions, path examples)123- Cross-referencing files (extension.md pointing at hook scripts)124- CI config (workflow files referencing paths)125126Use `grep -r "old/path" .` to catch what IDE cleanup misses.127128Explicit checks the quality review sub-agent should run:129- **Orphaned test files** — every test file must have a corresponding implementation file. If `tests/test_foo.py` exists but `scripts/foo.py` doesn't, either remove the test or implement the script.130- **Schema enum ↔ code constant alignment** — manifest entry feature enum, validate KIND_TO_SCHEMA keys, and index.schema.json feature keys must all match `badger_lib.FEATURES`. Mismatches are silent until a scaffold run hits them.131- **Docstring accuracy** — validate.py's docstring lists `--kind` options; if they don't match `KIND_TO_SCHEMA`, the CLI help is wrong.132- **Dead code from removed features** — after removing a feature, search for deprecated functions, branches, and references. `grep -r "old_feature" scripts/ tests/` catches what IDE cleanup misses.133- **Scaffold version propagation** — after re-scaffold, does the config's frameworkVersion update? If not, the next drift detection will re-scaffold unnecessarily.134135## Gotchas136- **`execute_code`/`read_file` sandbox may show pre-seeded content, not real files.** When137 reviewing a refactoring on a feature branch, the sandbox environment may be populated with138 mock data for the *target* state rather than reflecting the actual filesystem. You can build139 an entire multi-step analysis — schema validation, test results, file existence checks — on140 phantom data. **Always use `terminal` for ground-truth filesystem checks** (git status,141 ls, cat, python3 -m pytest). If `execute_code` returns file contents that `terminal` says142 don't exist, trust `terminal`. Caught when: review of refactoring branch produced 20+ tool143 calls of fabricated analysis before discovering the real repo was on `main`.144145- **jsonschema `$ref` to sibling files fails without a custom resolver.** The Python146 `jsonschema` library's `Draft202012Validator` cannot resolve relative `$ref` paths like147 `"$ref": "agents.schema.json#/$defs/agentName"` without a custom `RefResolver`. Either148 inline the enum in every schema or provide a resolver that maps URIs to local files.149 Discovered via: test failure `Unresolvable: agents.schema.json#/$defs/agentName`.150151- **Schema renames must happen atomically with code that writes the old field.** Renaming152 `pluginScope` → `skillScope` in a schema breaks any script that still writes `pluginScope`.153 Do the rename + script update in the same phase, or the intermediate state fails validation.154155- **Agent enum ordering drifts silently.** When the same enum appears in multiple schemas,156 different files end up with different orderings (e.g., `junie, hermes` vs `hermes, junie`).157 This doesn't affect validation but creates confusion about the canonical order. Align once158 and add "keep in sync" comments.159160- **Phase ordering matters: remove old data before running validation.** If you update a161 schema to remove a feature key (e.g., `plugins`) but don't remove the data directory that162 produces those entries (e.g., `features/*/plugins/`), the next `index_build.py` run will163 produce entries the updated schema rejects. Remove old data in the same phase as the schema164 change.165166- **Two extension.json files can exist for different scopes.** A refactor that removes167 agent-level extension.json must not accidentally remove stack-level extension.json.168 Verify every file path individually.169170- **Manifest schema transitions need both old and new fields.** When renaming a field171 in `manifest.schema.json` (e.g. `pluginScope` → `skillScope`), the manifest is172 written by `scaffold.py`. If you rename the schema field without updating scaffold.py173 in the same commit, validation fails. The safe pattern: (1) add the new field to174 manifest.schema.json alongside the old one, (2) update scaffold.py to write both175 during transition, (3) commit both together, (4) remove the old field in a follow-up176 once all scaffolded projects have migrated.177178- **User may add scope mid-implementation.** During a long refactor, the user may179 request additional features (breaking version support, new docs, safety wrapping).180 Incorporate these into the current PR if they're closely related; otherwise note181 them as follow-up PRs. Don't block the current work waiting for perfect scope.182183- **"Ensure X always Y" is a non-negotiable invariant.** When the user says184 "ensure that hooks will always catch an exception and log it" or similar safety185 directives, treat it as a hard requirement — implement it across ALL entry points,186 test it, and don't defer it. The pattern: wrap every public hook/adjustment function187 in `try/except Exception` with `logger.debug(..., exc_info=True)` so a broken hook188 never crashes the host process. Apply to hooks, adjustments, and plugin scripts.189190- **Scaffolded config.frameworkVersion must be updated after re-scaffold.** When a191 scaffolding/refresh tool re-scaffolds a project, it updates `manifest.json` with the192 current framework version but often preserves the original `config.json`'s193 `frameworkVersion`. This causes the next drift detection to re-scaffold again194 unnecessarily. Fix: update `config.frameworkVersion` to the current version after195 a successful re-scaffold. Caught when: den-refresh re-scaffolded but config still196 showed the original 0.2.0 scaffold version.197198- **Version + changelog on EVERY commit that touches production code.** User explicitly199 requires: bump VERSION (semver), add `docs/changelog/{version}-{slug}.md`, and enforce200 as an invariant. No exceptions — even small fixes get a patch bump. The invariant file201 at `features/common/invariants/version-changelog-required.md` codifies this.202203- **Hermes skills must be namespaced per project, NOT in shared external_dirs.** Hermes204 does NOT auto-discover `.hermes/skills/` in the CWD. It only scans `~/.hermes/skills/`205 (global) and `skills.external_dirs` in `~/.hermes/config.yaml`. Using `external_dirs`206 for multiple projects causes skill name conflicts (e.g., every project has a `task` skill)207 with last-match-wins semantics. The correct approach: symlink each project's skills into208 `~/.hermes/skills/<project-name>/` as a namespace directory. Each project gets isolated209 skills that don't conflict with other projects. Do NOT register in global `external_dirs`.210 Caught when: the maintainer-home-page's task skill was shadowed by the reference repo's older211 task skill because the latter was last in external_dirs.212213- **Test isolation for config file writes.** When testing code that writes to214 `~/.hermes/config.yaml`, mock `Path.home()` to return a temp dir AND create the215 `.hermes/` subdirectory structure in the temp dir. The method resolves216 `Path.home() / ".hermes" / "config.yaml"`, not `Path.home() / "config.yaml"`.217 Caught when: test paths leaked into the real config because the temp dir structure218 was wrong.219220- **Breaking version semver boundary is exclusive on the lower bound.**221 `is_breaking_transition(from, to, root)` checks `from_v < breaking <= to_v`. A222 project already at the breaking version (e.g., 0.7.0 → 0.8.0 with 0.7.0 as223 breaking) does NOT trigger — because `0.7.0 < 0.7.0` is false. This is correct:224 the project was scaffolded at or after the breaking version, so no migration is225 needed. Only projects crossing the boundary (e.g., 0.6.0 → 0.8.0) need the full226 re-scaffold with backup.227228## Python mixin-based module split229230For splitting a single large Python file into domain-specific modules while preserving the231public API — read `references/python-mixin-module-split.md` when splitting a Python module. Covers the mixin inheritance pattern,232dynamic script loading (`sys.path`), circular import handling, and multi-copy sync.233234## Feature design spec patterns235236When writing a **feature design specification** (not a refactoring spec), see237`references/feature-design-spec-patterns.md` for section structure, requirement ID conventions,238domain type documentation, testing strategy, and acceptance criteria patterns observed in239DDD/.NET projects.240241## Multi-PR sequential merge pattern242243For large refactors (8+ phases), split into independently-mergeable PRs. See244read `references/multi-sequential-pr-pattern.md` for the full workflow when the change needs multiple sequential PRs.245246## References247248- `references/plugin-to-skills-migration.md` — worked case: migrating a plugin layout to per-stack skills; read when migrating a plugin layout.