Found while deriving the population for the #10619 teardown-shape gate. Filed rather than fixed — the repairs are the #10371 lane's work, and folding them into the gate's PR would have made it unreviewable against its own card.
The gap
#10371 enumerates six Plugin implementations whose teardown is spelled stop() with no destroy(). Re-derived mechanically over packages/** at main = 78ac958 — every class with an implements Plugin clause, every teardown-shaped method or arrow property it declares — the class is eleven, not six.
The five #10371 does not name split into two shapes, and neither is exotic:
Spelled as an arrow PROPERTY, not a method (a method-only reading of the class misses them):
| file | class | alias | what it releases |
|---|
packages/metadata/src/plugin.ts | MetadataPlugin | stop = async (ctx) => … | closes the artifact watcher, manager.dispose(), closes the repository |
packages/runtime/src/app-plugin.ts | AppPlugin | stop = async (ctx) => … | emits app:unregistered to the control plane |
packages/runtime/src/external-validation-plugin.ts | ExternalValidationPlugin | stop = (): void => … | clearInterval over every armed drift-check timer |
Spelled dispose(), not stop() — the seventh-spelling case #10619 predicts, already present:
| file | class | alias | what it releases |
|---|
packages/plugins/plugin-email/src/email-plugin.ts | EmailServicePlugin | async dispose() | two metadata subscriptions, the live SMTP transport, an engine binding |
packages/plugins/plugin-webhooks/src/webhook-outbox-plugin.ts | WebhookOutboxPlugin | async dispose() | stops the auto-enqueuer, unbinds two engine hooks |
Why these are the same defect, not near-misses
ObjectKernel.performShutdown() and LiteKernel.destroy() call plugin.destroy() and nothing else. Verified on the same revision: nothing in the tree calls stop(), dispose(), close() or shutdown() on a plugin. EmailServicePlugin.dispose() has exactly one caller in the whole repo — a test (email-plugin.template-runtime-write.test.ts); WebhookOutboxPlugin.dispose() has none at all.
ExternalValidationPlugin is the one that raises the stakes above "listed for whoever sweeps": it is the only instance besides plugin-reports that owns setInterval timers, and its stop() is reached only from scheduleDriftChecks() re-arming itself. On kernel shutdown those intervals are never cleared — the #9371 mechanism verbatim.
Shape of the repair
The same one PR #10375 applied: move the body into destroy() and keep the old name as a delegating alias, because it is public API of an exported class.
Status in tooling
All eleven are baselined in the #10619 gate's KNOWN_TEARDOWN_UNREACHED list, which is shrink-only and closed to new members. Deleting an entry as each plugin is repaired is the intended burn-down; a stale entry fails the gate, so the ledger cannot drift out of step with the repairs.
Generated by Claude Code
Found while deriving the population for the #10619 teardown-shape gate. Filed rather than fixed — the repairs are the #10371 lane's work, and folding them into the gate's PR would have made it unreviewable against its own card.
The gap
#10371 enumerates six
Pluginimplementations whose teardown is spelledstop()with nodestroy(). Re-derived mechanically overpackages/**atmain= 78ac958 — every class with animplements Pluginclause, every teardown-shaped method or arrow property it declares — the class is eleven, not six.The five #10371 does not name split into two shapes, and neither is exotic:
Spelled as an arrow PROPERTY, not a method (a method-only reading of the class misses them):
packages/metadata/src/plugin.tsMetadataPluginstop = async (ctx) => …manager.dispose(), closes the repositorypackages/runtime/src/app-plugin.tsAppPluginstop = async (ctx) => …app:unregisteredto the control planepackages/runtime/src/external-validation-plugin.tsExternalValidationPluginstop = (): void => …clearIntervalover every armed drift-check timerSpelled
dispose(), notstop()— the seventh-spelling case #10619 predicts, already present:packages/plugins/plugin-email/src/email-plugin.tsEmailServicePluginasync dispose()packages/plugins/plugin-webhooks/src/webhook-outbox-plugin.tsWebhookOutboxPluginasync dispose()Why these are the same defect, not near-misses
ObjectKernel.performShutdown()andLiteKernel.destroy()callplugin.destroy()and nothing else. Verified on the same revision: nothing in the tree callsstop(),dispose(),close()orshutdown()on a plugin.EmailServicePlugin.dispose()has exactly one caller in the whole repo — a test (email-plugin.template-runtime-write.test.ts);WebhookOutboxPlugin.dispose()has none at all.ExternalValidationPluginis the one that raises the stakes above "listed for whoever sweeps": it is the only instance besidesplugin-reportsthat ownssetIntervaltimers, and itsstop()is reached only fromscheduleDriftChecks()re-arming itself. On kernel shutdown those intervals are never cleared — the #9371 mechanism verbatim.Shape of the repair
The same one PR #10375 applied: move the body into
destroy()and keep the old name as a delegating alias, because it is public API of an exported class.Status in tooling
All eleven are baselined in the #10619 gate's
KNOWN_TEARDOWN_UNREACHEDlist, which is shrink-only and closed to new members. Deleting an entry as each plugin is repaired is the intended burn-down; a stale entry fails the gate, so the ledger cannot drift out of step with the repairs.Generated by Claude Code