Based on `main`. **PR1 of the substrate decomposition** — deliberately
small and boring.
## Mechanical proof (the point of this PR)
Every moved body diffed against its pre-move text:
```
BLOCK KIND DIFF-vs-ORIGINAL
LandedTaskCommit iface EMPTY
commitOwnedByTask helper EMPTY
escapeRegex helper EMPTY
shellQuote helper EMPTY
parseShortstat helper EMPTY
findLandedTaskCommit method EMPTY
findAlreadyMergedTaskCommit method EMPTY
refreshRemoteBaseRef method EMPTY
readCommitTaskOwnership method EMPTY
branchHasNoUniqueDiff method EMPTY
baseHasExplicitTaskOwnership method EMPTY
foreignTipRejection method EMPTY
branchTipForeignOwnership method EMPTY
isCommitReachableFromBranch method EMPTY
findWorktreePathForBranch method EMPTY
repoBranchExists method EMPTY
readShortstatForSha method EMPTY
readLandedFilesForSha method EMPTY
isBranchTipMisboundToTask method EMPTY
RESULT: 19/19 BYTE-IDENTICAL modulo the enumerated deviations; 0 differ.
```
No condition reordered, no signature changed, no rename, no inlining, no
"while I am here" cleanup.
## The premise needed correcting — the cheap lesson this cut was meant
to buy
The brief described **13 pure functions**. They are 13 `private async`
**methods** closing over `this.options`, so nothing here is
byte-identical in the strict sense. Three deviations were structurally
unavoidable:
1. **`private` → `protected`** on the 14 methods and the `options` field
— a subclass cannot call a `private` base member. One token per
declaration.
2. **Two type annotations** rewritten:
`SelfHealingManager["readCommitTaskOwnership"]` → the base class, since
the original would be a circular import.
3. **Four module-level helpers moved along** and re-exported. Forced by
direction — the new module must not import `self-healing.ts`, so
everything the bodies call has to live beside them. `execAsync` and
`shellQuote` are imported back because call sites remain.
## The set is 14, not 13
`foreignTipRejection` had to come too, and it's what **closes** the
cluster — it depends only on `baseHasExplicitTaskOwnership` and
`branchHasNoUniqueDiff`, both already in the set. Without it the
extraction isn't self-contained and `this.options` isn't the only
external dependency.
## Base class, not free functions — deliberately
Converting to free functions would change all 14 signatures: a
behavior-adjacent edit riding inside a file move, which is exactly the
combination that hid the last four safeguard regressions. An abstract
base keeps `this` semantics, so every call site stays
`this.<method>(...)` and every body is unchanged text.
## Two pre-existing collisions, preserved exactly
- The detector module already exports a **free**
`findAlreadyMergedTaskCommit` sharing a name with the protected method;
inside the class body the bare identifier resolves to the import.
- `SelfHealingOptions` is declared in `self-healing.ts`, imported here
**type-only** so it's erased at runtime and creates no module cycle.
## Measured line delta — not an estimate
| | lines |
|---|---:|
| `self-healing.ts` | 13,394 → 12,945 = **−449** |
| new module | **+527** (454 moved verbatim, 73 scaffold) |
| **NET** | **+78** |
Same shape as every consolidation in this program: the target file
shrinks, the total grows slightly. At −449 for the first and safest cut,
the remaining substrate cuts plausibly take `self-healing.ts` under 11k
— but that is **file-size reduction, not code reduction**.
## Verification
464 passed across three engine suites with the single known
**pre-existing** `archiveStaleDoneTasks` failure; tsc clean; lint clean;
merge gate green (299 + 10 + 71).
No changeset: `@fusion/engine` is private.
🤖 Generated with [Claude Code](https://claude.com/claude-code)