forge-milestone-backcompat-audit
Overview
Milestone-scoped backwards-compatibility audit. Unlike the other milestone audits, scope is genuinely diff-based — the question "did we break anyone" is intrinsically about a delta, not a current state. The baseline is the milestone-start commit; the comparison point is current HEAD.
Scope is restricted to public-facing interfaces only:
- IPC message shapes defined on the crate side and consumed by the webview (
docs/architecture/ipc-contracts.mdis the authoritative catalog) - Config schema — files under
crates/forge_core/src/config*or wherever the config lives - CLI flags — the
clap-annotated struct (or equivalent) in the CLI entry point - Any exposed plugin/extension API, if the project has one yet
Output is one GitHub issue per breaking change plus one consolidated report. Every breaking change must have either (a) a migration note in the changelog / release notes / an ADR, or (b) an explicit decision to document it now — this skill surfaces the ones with neither.
Arguments
| Argument | Form | Meaning |
|---|---|---|
| Milestone (required) | "Phase N: Title" |
The GitHub milestone title to audit |
If the argument is missing, list candidate milestones and ask:
gh api repos/forge-ide/forge/milestones --jq '.[] | {title, state, open_issues, closed_issues}'
Steps
1. Locate interfaces and derive the baseline — invoke Explore
Delegate to an Explore subagent. Brief:
For milestone
"<milestone>":
- Fetch milestone metadata and merged PRs sorted ascending by merge date:
gh pr list --repo forge-ide/forge --search 'milestone:"<milestone>" is:merged sort:created-asc' --json number,mergeCommit,baseRefOid,mergedAt,files,title,body --limit 200.- Derive the baseline SHA: the
baseRefOidof the earliest merged PR. Return it.- Locate the interface surfaces. Forge's:
- IPC: read
docs/architecture/ipc-contracts.mdand follow it to the Rust type definitions that back each contract (typically undercrates/forge_*as#[derive(Serialize, Deserialize)]types). List each contract surface as a (doc section, file path, type name) triple.- Config schema: find the type(s) that represent user config — likely a
Configstruct undercrates/forge_coreor similar. Return file path and type name.- CLI flags: find the
clapParser/Argstypes in the CLI crate. Return file path and type name.- Any exposed plugin/extension API: if the repo has one yet, list the public traits/types; if not, say so.
- For each interface surface, check whether it changed between baseline and HEAD:
git diff <baseline>..HEAD -- <file>on each listed file. Return pass-through if no diff, or the full diff hunks if changed.- Look for PRs in the milestone whose title or body contains "breaking", "migration", or "backcompat" markers — these are self-declared breaking changes. Return their PR numbers and what each claims.
Return: milestone facts, baseline SHA, interface-surface inventory, per-surface diff (or pass-through), self-declared breaking PR list.
2. Derive the change-class model — invoke superpowers:brainstorming
Not every diff is a breaking change. The load-bearing reason this is brainstormed and not checklisted: whether a change is breaking depends on what consumers depend on, and that depends on the interface. For serde-tagged enums, adding a variant can break consumers that match exhaustively; for plain structs with #[serde(default)], adding a field is usually safe. Forge's IPC boundary is a JSON contract, so the rules are specific to JSON-shape compatibility.
Use brainstorming with the user, seeded by Phase 1 findings, to settle the classification rules for this audit:
- Type-level changes:
- Struct field removed → breaking
- Struct field renamed (serialized name changed) → breaking
- Struct field type narrowed (e.g.
String→ specific enum) → breaking - Struct field type widened (e.g. integer → number) → usually safe if consumers are permissive
- New required field → breaking
- New optional field (
#[serde(default)]orOption<T>) → safe - Enum variant removed → breaking
- Enum variant renamed → breaking
- New enum variant → breaking for exhaustive matchers, safe for permissive ones (IPC consumers in JS are typically permissive; Rust consumers typically aren't)
- Tagged-enum tag renamed or discriminator shape changed → breaking
- Config schema: same rules as types, plus: default value change is behavior-breaking even if schema-compatible
- CLI flags: removed flag, renamed flag, semantically-changed flag → breaking; new flag with default → safe
- Consumer asymmetry: the IPC boundary has two consumers — Rust core and TS webview. A change is breaking if either side depends on the old shape. Be explicit about which side is affected.
Output: a concrete classification rule sheet for this milestone, plus a note for each interface surface saying which consumer directions matter.
HARD GATE: Do not begin Step 3 until the classification rules are presented and approved.
3. Classify each diff — invoke superpowers:dispatching-parallel-agents
For each interface surface that changed (from Phase 1), dispatch a subagent. Each one takes a single surface's diff and applies the Step 2 rules.
Brief each identically (substituting the surface):
Classify the changes on this interface surface using the approved rule sheet.
Surface:
<path>—<type name>Consumer directions that matter:<e.g. "Rust core → TS webview", "user config file → crates/forge_core">Rule sheet:<from Step 2, verbatim>Diff:<full diff hunks for this surface>Self-declared breaking PRs for this surface (if any):
<from Phase 1>For each atomic change in the diff (field added/removed, variant added/removed, type changed, etc.), return an object with these fields:
change— one-line description (e.g. "removed fieldToolCallEvent.request_id")classification— safe | breaking | behavior-breakingrule_applied— which rule from the sheet justified the classificationconsumer_impact— which side breaks: Rust-side consumers, TS-side consumers, or both; and a one-line "what breaks" explanationself_declared— yes | no (whether a PR in the milestone already called this change out)migration_note_present— yes | no | unknown (was a migration note written in changelog/release notes/ADR?)suggested_migration— if breaking and no note exists: the note that should be writtenRules:
- When in doubt between safe and breaking, classify breaking — false positives are cheap; missed breaks are expensive
- If the rule sheet doesn't cover a case, say so explicitly — do not invent a classification
Aggregate. Group by classification. The report pivots here: every breaking or behavior-breaking without a migration note is a finding.
4. Triage — invoke superpowers:brainstorming
Present the classified change list. Brainstorm:
- Any
breakingclassifications the user wants to reclassify (they know consumer realities the skill doesn't) - Self-declared breaking changes: were the migration notes adequate, or do they need revision?
- Unannounced breaks: must every one get a new migration note, or is some scope OK to leave unannounced pre-1.0?
- Severity: default is
compat: highfor breaking,compat: mediumfor behavior-breaking,compat: criticalfor a silent break that would shatter live consumers (like existing IPC sessions)
HARD GATE: Do not create any GitHub issues until the triaged list is approved.
5. Create finding issues — sequential
One issue per unannounced or inadequately-announced breaking change. Self-declared breaks with adequate migration notes don't need a new issue — they get recorded in the report.
Find the next F-number, then:
gh issue create \
--repo forge-ide/forge \
--title "[F-NNN] <imperative title>" \
--milestone "<milestone>" \
--label "type: bug,compat: <severity>" \
--body "$(cat <<'EOF'
## Scope
<One paragraph: which interface surface, which consumer direction breaks, why this was not caught in PR review.>
## Change
- **Interface:** `<path>` — `<type name>`
- **Classification:** breaking | behavior-breaking
- **Rule applied:** <from classification rule sheet>
- **Consumer impact:** <Rust | TS | both> — <what breaks>
### Diff
```diff
<relevant hunks>
PR(s) responsible
- #:
Migration note needed
<The migration note that should be written — where (changelog / release notes / ADR) and what it should say.>
Remediation
One of:
- Write the migration note (and optionally revert-and-reland with a proper note)
- Revert the breaking change if it was accidental
- Document the break in the release notes as an intentional decision
Definition of Done
- Migration note written at
- (If applicable) consumer side updated to handle the new shape
- Backcompat audit re-run and this change is
self_declared: yeswithmigration_note_present: yesEOF )"
### 6. Create the consolidated report issue
Title: [F-NNN] Backcompat audit report: Milestone: Labels: type: bug, compat: audit
Body template:
```markdown
## Summary
Milestone: <milestone>
Baseline SHA: <short-sha>
Interface surfaces inspected: <N>
Surfaces changed: <M>
Classified changes: safe <a> / breaking <b> / behavior-breaking <c>
Unannounced breaks (new issues filed): <k>
## Classification rules (from Step 2)
<Verbatim.>
## Interface surface inventory
| Surface | Type | Consumer directions | Changed? |
|---------|------|---------------------|----------|
| `docs/architecture/ipc-contracts.md#session-events` | `SessionEvent` | Rust → TS | yes |
| config schema | `Config` | user → Rust | no |
| CLI | `Args` | user → binary | yes |
## Classified changes
| ID | Interface | Change | Classification | Self-declared | Note present | Issue |
|----|-----------|--------|----------------|---------------|--------------|-------|
| C-01 | `SessionEvent` | removed `request_id` | breaking | no | no | #123 |
| C-02 | `Args` | new `--verbose` flag with default | safe | — | — | — |
## Self-declared breaking changes (no new issue needed)
| PR | Change | Migration note location |
|----|--------|-------------------------|
| #N | ... | CHANGELOG.md §<milestone> |
Raw diff output: `/tmp/forge-backcompat-audit-<milestone-slug>/diffs/`
7. Verify — invoke superpowers:verification-before-completion
gh issue list --repo forge-ide/forge \
--milestone "<milestone>" \
--search 'label:"compat: audit" OR label:"compat: critical" OR label:"compat: high" OR label:"compat: medium" OR label:"compat: low"' \
--json number,title,labels
Confirm every triaged unannounced-break has an issue and the consolidated report exists with the full surface inventory. Do not claim done without this evidence.
Delegation rules
| Work type | Where it runs |
|---|---|
| Interface inventory, baseline derivation, per-surface diffs, self-declared-PR sweep | Explore subagent |
| Classification rule derivation | superpowers:brainstorming with the user |
| Per-surface change classification | parallel subagents via superpowers:dispatching-parallel-agents |
| Triage of classifications and migration-note adequacy | superpowers:brainstorming with the user |
| Issue creation | Main context, strictly sequential |
| Final verification | superpowers:verification-before-completion |
Common mistakes
| Mistake | Correct |
|---|---|
| Treating every diff as breaking | Most diffs are safe; classification requires rules, not alarm |
| Treating every diff as safe | When in doubt, classify breaking — false positives are cheap, missed breaks expensive |
| Forgetting consumer asymmetry at the IPC boundary | Every IPC change must be classified for both Rust and TS consumer directions |
| Collapsing schema-compatible default-value changes to "safe" | Default-value changes are behavior-breaking even when schema-compatible |
| Not checking for self-declared breaking PRs | A PR that already announced its break doesn't need a new issue — just record it in the report |
| Running this on a milestone with no interface changes | Exit early after Phase 1 if no surface actually changed — don't create an empty report |
| Confusing this with quality-review's "API consistency" | That is about internal shape; this is about whether external consumers break |
| Creating issues in parallel | Sequential only — F-number collisions |
Claiming done without gh issue list evidence |
verification-before-completion requires it |
Source: forge-ide/forge — distributed by TomeVault.