You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Five more Plugin implementations release their teardown from a hook the kernel never calls — the #10371 enumeration is short by five #10772
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
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.
Triage: pm:queue confirmed (domain:services kept, type Bug). This is a five-member family repair with an established remedy (the PR-10375 shape: move the body into destroy(), keep the old spelling as a delegating alias on the exported public API). The file surface spans three lanes' packages — packages/metadata (engine), packages/runtime x2 (cli), packages/plugins/plugin-email + plugin-webhooks (services) — but the change is one mechanical shape with per-file proofs, so splitting it three ways buys nothing: cross-domain single-dispatch exception applies, designated lane = domain:services (the parent 10371 lane, which owns the defect class). Claim must declare the full five-file surface; family-dispatch five-gate check applies (all members already ruled by the parent's precedent; per-member commit + per-member assertion; exclusion list = the six members 10371 already names, which stay with 10371's own dispatch). Serial constraint: coordinate with the 10371/10619 lane — the KNOWN_TEARDOWN_UNREACHED baseline in the 10619 gate must shrink in the same PR as each repair. Size/model suggestion: M, opus.
Claim: re-dispatch — the previous seat's work survives, this is verification and push, not a redo.
session: user-29 [974948]
branch: claude/issue-10772-teardown-five-more (exists; remote at 37ba31a4b)
worktree: /home/user/objectstack-10772, HEAD 66d2bfe96, working tree clean
Clause-②: no — this restores teardown to the hook the kernel actually calls; it adds no public surface and changes no contract accept/reject set.
serial constraint: holds scripts/check-plugin-teardown-shape.mjs and the five plugin sources. No other services-lane card may take that baseline until this lands.
Why re-dispatched
The previous agent is gone. Checking the remote alone would have read as "no work done" — the remote branch is 7 commits behind the local worktree. Checking the worktree instead (the lesson recorded on #10424) found the work complete and committed:
66d2bfe96 test(webhooks,email): satisfy check:engine-double-contract for the two new fakes
f28766002 chore(changeset): patch for the five #10772 teardown repairs
cf70c3179 fix(plugin-webhooks): WebhookOutboxPlugin tears down from destroy(), not dispose()
b3e53e70f fix(plugin-email): EmailServicePlugin tears down from destroy(), not dispose()
f270c1569 fix(runtime): ExternalValidationPlugin clears drift timers from destroy(), not stop()
931d5e0a6 fix(runtime): AppPlugin emits app:unregistered from destroy(), not stop()
a667377ec fix(metadata): MetadataPlugin releases its repository from destroy(), not stop()
14 files, +1206/-31, one changeset, all five plugins, plus the baseline edit and an engine-double-contract.pinned.json update.
⛔ Nothing above is accepted as done. Commits are a claim, not a measurement — none of these gates were re-run by me, and the previous seat filed no report. The incoming seat verifies from zero.
Known traps on this specific card
scripts/check-plugin-teardown-shape.mjs is shrink-only by its own words (:225 ⛔ SHRINK-ONLY., :136 "a stale entry is itself a failure"). All five entries are still repair: '#10371' on origin/main at 81845c65a (:236-:240). Repairing a plugin means deleting its entry, not retagging it — and a stale entry left behind is itself a failure.
⚠️ A bare grep -c "repair: '#10371'" returns 6, not 5. The sixth is the self-test fixture at :667 ({ file: 'a/src/p.ts', … }) and must not be touched. Count with the packages/ prefix.
{
"issue": 10772,
"status": "done",
"branch": "claude/issue-10772-teardown-five-more",
"pr": "https://github.com/objectstack-ai/objectstack/pull/10987",
"premise_still_valid": true,
"summary": "Re-dispatch: verified the previous seat's work from zero rather than accepting it, then brought the base current and pushed. VERIFIED (not changed): all five plugins tear down from destroy() with the old spelling retained as a delegating alias; the five KNOWN_TEARDOWN_UNREACHED entries are DELETED, not retagged, and the gate's verdict line no longer cites the closed #10371 as live repair owner; the self-test fixture at :675 is untouched; the changeset is patch across the four packages and is committed; the engine-double work removed two unserved verbs from the webhooks double instead of pinning them, and pinned only the one email double that routes through assertEngineUpdateDispatch, leaving engine-double-contract.baseline.json alone. CHANGED BY ME: base was 67 commits behind origin/main (branch base 37ba31a4b vs origin/main 81845c65a) — merged origin/main into the branch as 019b06738 BEFORE any measurement, since a baseline measured on a stale base is exactly what nearly turned main red on this file once before; every number in this report is from that commit on a clean tree. Also reset a file MODE flip (packages/create-objectstack/bin/create-objectstack.js 100644->100755) that pnpm install introduced, so it is not in the PR. Remote was 7 commits behind local at start; both the backlog and the merge are now pushed. Premise re-derived independently on origin/main @ 81845c65a: each of the five files' only teardown-shaped member there is the alias, no destroy() anywhere (plugin.ts:549, app-plugin.ts:1422, external-validation-plugin.ts:182, email-plugin.ts:1255, webhook-outbox-plugin.ts:172).",
"tests": "ALL ON FINAL COMMIT 019b06738, clean tree (git status --porcelain = 0). Every exit code captured before any pipe (cmd > file 2>&1; EXIT=$?), never after a tail. SUITES (script names echoed, so no zero-match pnpm --filter exit-0): runtime 'Test Files 181 passed (181) / Tests 2692 passed (2692)'; metadata '32 passed (32) / 615 passed (615)'; plugin-email '27 passed (27) / 425 passed (425)'; plugin-webhooks '10 passed (10) / 124 passed (124)'; combined EXIT=0. TYPECHECK exit 0 for runtime, plugin-email, plugin-webhooks. GATE UNION derived on the final commit with `node scripts/pm/dispatch-gates.mjs` and NO path arguments: 'change set derived from git — 14 path(s) vs merge base 81845c65a … committed 14, working tree 0, untracked 0' (exit 0). It named `node scripts/check-plugin-teardown-shape.mjs` by path, and named check:engine-double-contract under its convention-triggered set ('adds or edits a test file'); both were run explicitly anyway per class #10309. VERDICT LINES: check-plugin-teardown-shape exit 0 '61 Plugin implementation(s) across 4424 source(s) … (0 known-unreached, SHRINK-ONLY, baseline fully burned down)' — held = 0; --self-test exit 0 '47 cases pass'. check:engine-double-contract exit 0 'OK — 377 pinned, 133 in the DEBT ledger, 2 exempt'. check:test-source-alias exit 0 'OK — 72 packages with tests scanned; 61 registered …'. check:type-check-debt (--re-measure) exit 0 'OK — 33 ledger entr(ies) re-measured in 323.9s, 1908 raw tsc error(s) total, none above its recorded number' — the gate that answers for the two new test files landing in ledgered packages (metadata in DEBT 89, runtime in TEST_DEBT 227). check:adr-0087-registration exit 0 'this PR adds no declared-breaking changeset (1 non-breaking changeset(s) seen)' — informative because the changeset is COMMITTED, which is what that gate reads. check:nul-bytes exit 0 '6336 text file(s) … no raw ASCII control bytes'. check:route-envelope exit 0, check:dispatcher-error-vocabulary exit 0, check:error-code-casing exit 0 (#10309 family, explicit). Also exit 0: check:changeset-gate-self-tests, check:cross-package-test-inputs, check:durability-log-level, check:entry-guard, check:objectui-changeset, check:parse-guard, check:slot-lookup, check:stack-collection-maps, check:type-source-resolution, check:query-options-erasure, check:where-matcher, check:i18n, check:type-check-coverage, check-ci-filter-parity, check-changeset-no-major, check-empty-changeset, check-affected-docs. ABLATIONS — prediction written down BEFORE each mutation (file kept at scratchpad/issue-10772/ablation-predictions.txt), every restore proved byte-identical with git hash-object. A1 stale-baseline control: PREDICTED exit 1 naming all five packages/ entries stale, proving repair = DELETE not retag. OBSERVED exit 1, '5 stale KNOWN_TEARDOWN_UNREACHED entry/entries' listing all five with '-- no longer unreached' and 'delete each line above'. Restore 5e93066ba709b5479abdc409c7f3582c70406769 -> 5e93066ba709b5479abdc409c7f3582c70406769. A2 revert the load-bearing member (external-validation-plugin.ts back to its origin/main bytes): PREDICTED (a) gate exit 1 naming the class with 'no destroy()' plus the do-not-widen refusal, (b) the pin test red on getTimerCount and on a climbing validateAllCalls and on plugin.destroy being undefined, (c) dist bytes unchanged. OBSERVED (a) exit 1 'packages/runtime/src/external-validation-plugin.ts:159 ExternalValidationPlugin.stop() -- no destroy()' + the refusal text; (b) 'Tests 4 failed | 2 passed (6)' with 'expected 1 to be +0', 'expected 7 to be 2', 'TypeError: plugin.destroy is not a function'; (c) confirmed. Restore 9c370df26dd428018fe02d046f2511e275b5eff1 -> 9c370df26dd428018fe02d046f2511e275b5eff1. REBUILD QUESTION, ARGUED FROM THE FILES: no rebuild sits between edit and run, and that is the proof. The pin test imports './external-validation-plugin.js' — a RELATIVE specifier vitest resolves into packages/runtime/src/ — so a src-only edit is sufficient. NO build command was run between the mutation and the observation, and `git hash-object packages/runtime/dist/index.js` read 6c9145953c9ff3681352bc9a706e6d54948402da both BEFORE and AFTER while the verdict flipped green -> 4 failures. packages/runtime/dist DOES exist and DOES carry the repair (built 2026-08-21 14:44 by the previous seat), so its staleness is not the discriminator — the identical-bytes-across-a-flipped-verdict is. A dist-resolving suite could not have moved. A3 the OTHER direction (delete the retained stop alias, keep destroy): PREDICTED gate stays GREEN and only the alias legs go red, proving the gate alone cannot see an alias deletion. OBSERVED gate exit 0 '0 known-unreached'; test 'Tests 3 failed | 3 passed (6)' with 'TypeError: plugin.stop is not a function' on both alias legs. Restore 9c370df26… -> 9c370df26… A4 is the new vitest.config load-bearing: PREDICTED removing packages/plugins/plugin-webhooks/vitest.config.ts reds check-test-source-alias naming @objectstack/core. OBSERVED exit 1 'NEW unaliased artifact import(s) since this entry was measured: @objectstack/core … Alias them in the package's vitest.config.* — widening the registry entry is not the fix.' Restore 1851931a9fc48aaa927e8c194d36371d65a29980 -> 1851931a9fc48aaa927e8c194d36371d65a29980. No registry entry was widened. ZERO-HIT COUNTER-CHECKS, each with a positive control on the same corpus: (1) dispose() callers — positive control 58 `.destroy()` sites under packages/ non-test sources (non-zero, so the expression finds what exists); against that EmailServicePlugin.dispose() had exactly ONE pre-existing caller repo-wide (email-plugin.template-runtime-write.test.ts:375) and WebhookOutboxPlugin.dispose() had ZERO. Card confirmed. (2) setInterval census — enumerated every non-test `implements Plugin` source under packages/ and tested each for setInterval(: exactly TWO hit (plugin-reports/src/reports-plugin.ts and external-validation-plugin.ts), a non-zero result from an enumerator that named its hits. Within external-validation-plugin.ts the real armed-timer site is ONE (line 294); the other 'setInterval' occurrences are a type annotation (165) and two prose mentions (191, 194) — a grep hit count of 3 would have been a false fact count.",
"declared_narrowings": [
"@objectstack/metadata has NO `typecheck` script — asking pnpm for one would have been a silent zero-match exit 0, i.e. not a measurement. In its place: its tsup build (emits dts), its full 615-test suite, and the check:type-check-debt --re-measure which type-checks the package as a whole against its DEBT ledger entry (89) and reported 'none above its recorded number'.",
"check:type-check-debt first REFUSED: '--re-measure cannot run: 1 workspace dependenc(ies) of the ledgered packages have no built type entry point on disk -- create-objectstack.' That refusal is NOT MEASURED, never a pass. I built create-objectstack and re-ran it to a real exit-0 verdict, so this narrowing was CLOSED, not left standing.",
"scripts/check-single-claim-paths.mjs was NOT run and produces no verdict here: without PR_NUMBER it reports NOT WIRED, with one it needs a token. Both are wiring failures, not verdicts.",
"CI convergence is not waited on, per the standing lane rule. At report time PR #10987 is a fresh draft; its checks have not been read."
],
"open_questions": [],
"out_of_scope_findings": [],
"observations_not_filed": [
"AppPlugin.destroy() reads `this.bundle.manifest || this.bundle` with no nullish guard, so `new AppPlugin(undefined)` would now throw a TypeError during kernel shutdown where before the repair it could not (stop() was never called). I did NOT file this and did NOT fix it: enumerating every `new AppPlugin(` in packages/apps/examples, no call site passes a nullish bundle, so I cannot demonstrate a reachable defect, and filing an unreachable hypothetical is noise. It is also harmless if it ever became reachable — both kernels isolate per-plugin destroy failures (kernel.ts:776 try/catch, lite-kernel.ts runPluginDestroy) — and the `if (!appName) return` guard inside emitCatalogEvent already makes the empty-env path a no-op. Recording it so the PM has it, not asking for a decision."
],
"notes_for_other_cards": [
"For #10619 (the guard-criterion card, devx lane): both `dispose()` members repaired here — EmailServicePlugin and WebhookOutboxPlugin — are live specimens of the 'seventh spelling' that card predicted, and they existed before it was written. Measured here: after this PR the gate's KNOWN_TEARDOWN_UNREACHED is EMPTY and the gate is green at 61 Plugin implementations / 4424 sources, so the 'declares stop() and no destroy()' criterion is now permanently green over the current tree and can no longer fail on any existing member — exactly the objection #10371's dev raised.",
"Census nuance the card does not make and #10619 may want: the 'only two Plugin implementations own setInterval' claim is a statement about each class's OWN text. WebhookOutboxPlugin owns a setInterval TRANSITIVELY, via the AutoEnqueuer it constructs, and a text scan of the plugin class cannot see it. Its pin test asserts the live timer count directly for that reason."
],
"housekeeping": "Worktree /home/user/objectstack-10772 is left in place and its branch is fully pushed (local HEAD == origin/claude/issue-10772-teardown-five-more == 019b06738), so nothing is at risk in it; leaving it saves a ~6-minute dependency-closure rebuild if CI comes back red on this claim. Say the word and I will tear it down. No background monitors, no dev servers, no stray processes: the one long-running job (the 5m24s type-check re-measure) was polled in-turn and its verdict is above; the shared verify lock reports 'lock is free'."
}
Activity
os-zhuang commentedon Aug 21, 2026
Triage: pm:queue confirmed (domain:services kept, type Bug). This is a five-member family repair with an established remedy (the PR-10375 shape: move the body into destroy(), keep the old spelling as a delegating alias on the exported public API). The file surface spans three lanes' packages — packages/metadata (engine), packages/runtime x2 (cli), packages/plugins/plugin-email + plugin-webhooks (services) — but the change is one mechanical shape with per-file proofs, so splitting it three ways buys nothing: cross-domain single-dispatch exception applies, designated lane = domain:services (the parent 10371 lane, which owns the defect class). Claim must declare the full five-file surface; family-dispatch five-gate check applies (all members already ruled by the parent's precedent; per-member commit + per-member assertion; exclusion list = the six members 10371 already names, which stay with 10371's own dispatch). Serial constraint: coordinate with the 10371/10619 lane — the KNOWN_TEARDOWN_UNREACHED baseline in the 10619 gate must shrink in the same PR as each repair. Size/model suggestion: M, opus.
Generated by Claude Code
chore(tooling): drop this card's six entries from KNOWN_TEARDOWN_UNRE…
os-warren commentedon Aug 21, 2026
Claim: PM
domain:services派发0f14f70b-575c-5f2b-a235-4000a55db042claude/issue-10772-teardown-five-more../objectstack-10772(per-repo;⛔ 不在共享主检出上编辑;⛔ 不用git stash)claude-opus-5(分诊建议 M, opus)destroy(),旧拼法保留为委托别名(它们是导出类的公开 API)。不删任何导出、不改Plugin接口、不动接受/拒绝集。packages/metadata/src/plugin.ts——MetadataPlugin,stop = async (ctx) => …(箭头属性)packages/runtime/src/app-plugin.ts——AppPlugin,箭头属性packages/runtime/src/external-validation-plugin.ts——ExternalValidationPlugin,箭头属性,承重成员packages/plugins/plugin-email/src/email-plugin.ts——EmailServicePlugin.dispose()packages/plugins/plugin-webhooks/src/webhook-outbox-plugin.ts——WebhookOutboxPlugin.dispose()scripts/check-plugin-teardown-shape.mjs的基线收缩 + 各自同包测试。⛔ 零
packages/spec所有权;⛔packages/core/src/types.ts的Plugin接口只读不改;⛔ 不碰content/docs/releases/。✅ 串行栅栏已解除,且基线状态我已核对
PR #10766 已于 2026-08-21T13:32Z 合并,
scripts/check-plugin-teardown-shape.mjs现在无人持有。我在main@507f174ec上量过基线:grep -c "repair: '#10371'"会返回 6 —— 多的那个是门禁自测 fixture 里的a/src/p.ts。范围限定到packages/前缀才是真实条目数。⭐ 删基线条目的方法是被规定的,不要按行号
先跑一遍门禁,让它自己报出「哪些条目 no longer unreached」,用那份失败清单作为删除依据,再逐一确认每个都真有
destroy()。这是 #10766 用过的方法,比按行号删强得多:行号会漂移,而删错一条 = 假声明某个插件已修。⛔ 一件 #10371 交接给你的事
那五条都带
repair: '#10371',而门禁判词行打印repair tracked on #10371—— 那张卡现在已经关了。任何从门禁输出跟着这个号码过去的人会落到一张关闭的卡上。⇒ 修每个成员时把它那条的
repair字段改成#10772(若该条随修复被删除,就不存在这个问题;若你分批保留任何一条,它必须指向本卡)。实现要求
main = 78ac958552上机械重推得到 11 个(不是 plugin-reports: the timers started by ReportsPlugin are released from stop(), which the kernel never calls #10371 的 6 个),而 main 已经动了很远。在你的树上重新推导,报出你测到的总数与五个成员的当前形态。ExternalValidationPlugin是承重成员:卡面称它是除plugin-reports之外唯一拥有setInterval的实例,其stop里是clearInterval扫掉每一个已武装的漂移检查定时器,而该stop只被scheduleDriftChecks()自我重新武装时触达 ⇒ kernel 关机时那些 interval 从不被清除,flaky: a vitest worker teardown race (EnvironmentTeardownError: Closing rpc while onUserConsoleLog was pending) fails app-showcase with 334/334 tests passing #9371 机制原样重现。dispose()那两个的调用方普查:卡面称EmailServicePlugin.dispose()全仓只有一个调用方(一个测试),WebhookOutboxPlugin.dispose()一个都没有。零命中判断必须带阳性对照:用邻近的已知存在项(例如plugin.destroy()的调用点数)证明搜索有效,再宣布那个零。kernel.shutdown()之后资源确实被释放(ExternalValidationPlugin的 interval 必须有它自己的那条);② 直接调旧名(stop/dispose)的既有嵌入方行为不变 —— 这是保留别名的全部理由,只钉①会让一个「把旧名删掉」的实现全绿;③ 每个成员各有一条「destroy()被 kernel 触达」的独立断言。git hash-object证明恢复逐字节一致;用阳性对照证明(非断言)测试解析的是src/还是dist/。destroy()、保留旧名),所以patch应正确 —— 但以你的测量为准;若判定破坏性,仓库约定走minor并需 ADR-0087 标记。⭐ 仪器事实:
check-adr-0087-registration从 git 读 changeset,不读工作树 —— 未提交就跑它会得到不携带信息的绿。node scripts/pm/dispatch-gates.mjs不带路径参数推导,最终提交之后、干净工作树上跑;退出码在任何管道之前捕获;引用每道门禁自己那行判词。pnpm check:route-envelope与pnpm check:dispatcher-error-vocabulary(各带--self-test),并报告推导是否点名了它们。node scripts/check-plugin-teardown-shape.mjs(真跑 +--self-test),并报出你观察到的held数。⛔ 注意:pnpm check:plugin-teardown-shape这个脚本名不存在,会退出 254Command not found,而接进管道会读成绿 —— 直接调node scripts/…mjs。@objectstack/plugin-authTEST_DEBT 记 109([finding]@objectstack/plugin-auth's TEST_DEBT records 109 where tsc now reports 98 — an 11-error surplus opened by PR #10614 #10615):不要动、不要跑--lower。Fixes #10772)。⛔ 不翻 ready、⛔ 不武装 auto-merge、⛔ 不合并。不是你的
#10619 的守卫判据属 devx 车道。⛔ 不要在本 PR 里造它。但你的工作会给它证据:#10371 的 dev 指出「声明了
stop()且没有destroy()」这个判据在修完之后会永久变绿,而你手上这两个dispose()成员正是他预言的「第七种拼法」的活体。测到什么就写进报告,我转给那张卡。常备仪器陷阱(今天全部实测)
scripts/check-single-claim-paths.mjs(无PR_NUMBER→NOT WIRED;有PR_NUMBER无 token →GitHub API 401;两种都是接线故障不是判词,接进管道会把 401 回溯读成绿)。node_modules/下文件前先查硬链接数(实测有 vendor 文件带 6 个硬链接指向共享 pnpm store)。git reset --soft origin/main—— 那个 ref 跨 worktree 共享、会在你脚下前进,reset 会把别人已合并的文件 staged 成你的([finding]refs/remotes/origin/mainis shared across worktrees too — another agent's fetch advances it under you, andgit reset --soft origin/mainthen stages their merged files as yours #10743)。reset 到开工时记下的 base sha。报告
⭐ 评论第一行用字面文本
os-dev-report(单独一行)—— HTML 注释标记会被本仓 sanitizer 吃掉(今天已三次实测)。Generated by Claude Code
os-warren commentedon Aug 22, 2026
Claim: re-dispatch — the previous seat's work survives, this is verification and push, not a redo.
claude/issue-10772-teardown-five-more(exists; remote at37ba31a4b)/home/user/objectstack-10772, HEAD66d2bfe96, working tree cleanscripts/check-plugin-teardown-shape.mjsand the five plugin sources. No other services-lane card may take that baseline until this lands.Why re-dispatched
The previous agent is gone. Checking the remote alone would have read as "no work done" — the remote branch is 7 commits behind the local worktree. Checking the worktree instead (the lesson recorded on #10424) found the work complete and committed:
14 files, +1206/-31, one changeset, all five plugins, plus the baseline edit and an
engine-double-contract.pinned.jsonupdate.⛔ Nothing above is accepted as done. Commits are a claim, not a measurement — none of these gates were re-run by me, and the previous seat filed no report. The incoming seat verifies from zero.
Known traps on this specific card
scripts/check-plugin-teardown-shape.mjsis shrink-only by its own words (:225 ⛔ SHRINK-ONLY.,:136"a stale entry is itself a failure"). All five entries are stillrepair: '#10371'onorigin/mainat81845c65a(:236-:240). Repairing a plugin means deleting its entry, not retagging it — and a stale entry left behind is itself a failure.grep -c "repair: '#10371'"returns 6, not 5. The sixth is the self-test fixture at:667({ file: 'a/src/p.ts', … }) and must not be touched. Count with thepackages/prefix.origin/main(now81845c65a); fix(plugins,connectors,services): release plugin resources from destroy(), the hook the kernel actually calls #10766 nearly turned main red on exactly this file because its base predated the gate. Rebase or merge before measuring, and measure after.Refs
#10371 (closed, the short enumeration) · #10766 (the near-miss on this same baseline) · #10619 (the gate)
Generated by Claude Code
fix(metadata): MetadataPlugin releases its repository from destroy(),…
fix(runtime): AppPlugin emits app:unregistered from destroy(), not st…
fix(runtime): ExternalValidationPlugin clears drift timers from destr…
fix(plugin-email): EmailServicePlugin tears down from destroy(), not …
fix(plugin-webhooks): WebhookOutboxPlugin tears down from destroy(), …
chore(changeset): patch for the five #10772 teardown repairs
os-warren commentedon Aug 22, 2026
os-dev-report
{ "issue": 10772, "status": "done", "branch": "claude/issue-10772-teardown-five-more", "pr": "https://github.com/objectstack-ai/objectstack/pull/10987", "premise_still_valid": true, "summary": "Re-dispatch: verified the previous seat's work from zero rather than accepting it, then brought the base current and pushed. VERIFIED (not changed): all five plugins tear down from destroy() with the old spelling retained as a delegating alias; the five KNOWN_TEARDOWN_UNREACHED entries are DELETED, not retagged, and the gate's verdict line no longer cites the closed #10371 as live repair owner; the self-test fixture at :675 is untouched; the changeset is patch across the four packages and is committed; the engine-double work removed two unserved verbs from the webhooks double instead of pinning them, and pinned only the one email double that routes through assertEngineUpdateDispatch, leaving engine-double-contract.baseline.json alone. CHANGED BY ME: base was 67 commits behind origin/main (branch base 37ba31a4b vs origin/main 81845c65a) — merged origin/main into the branch as 019b06738 BEFORE any measurement, since a baseline measured on a stale base is exactly what nearly turned main red on this file once before; every number in this report is from that commit on a clean tree. Also reset a file MODE flip (packages/create-objectstack/bin/create-objectstack.js 100644->100755) that pnpm install introduced, so it is not in the PR. Remote was 7 commits behind local at start; both the backlog and the merge are now pushed. Premise re-derived independently on origin/main @ 81845c65a: each of the five files' only teardown-shaped member there is the alias, no destroy() anywhere (plugin.ts:549, app-plugin.ts:1422, external-validation-plugin.ts:182, email-plugin.ts:1255, webhook-outbox-plugin.ts:172).", "tests": "ALL ON FINAL COMMIT 019b06738, clean tree (git status --porcelain = 0). Every exit code captured before any pipe (cmd > file 2>&1; EXIT=$?), never after a tail. SUITES (script names echoed, so no zero-match pnpm --filter exit-0): runtime 'Test Files 181 passed (181) / Tests 2692 passed (2692)'; metadata '32 passed (32) / 615 passed (615)'; plugin-email '27 passed (27) / 425 passed (425)'; plugin-webhooks '10 passed (10) / 124 passed (124)'; combined EXIT=0. TYPECHECK exit 0 for runtime, plugin-email, plugin-webhooks. GATE UNION derived on the final commit with `node scripts/pm/dispatch-gates.mjs` and NO path arguments: 'change set derived from git — 14 path(s) vs merge base 81845c65a … committed 14, working tree 0, untracked 0' (exit 0). It named `node scripts/check-plugin-teardown-shape.mjs` by path, and named check:engine-double-contract under its convention-triggered set ('adds or edits a test file'); both were run explicitly anyway per class #10309. VERDICT LINES: check-plugin-teardown-shape exit 0 '61 Plugin implementation(s) across 4424 source(s) … (0 known-unreached, SHRINK-ONLY, baseline fully burned down)' — held = 0; --self-test exit 0 '47 cases pass'. check:engine-double-contract exit 0 'OK — 377 pinned, 133 in the DEBT ledger, 2 exempt'. check:test-source-alias exit 0 'OK — 72 packages with tests scanned; 61 registered …'. check:type-check-debt (--re-measure) exit 0 'OK — 33 ledger entr(ies) re-measured in 323.9s, 1908 raw tsc error(s) total, none above its recorded number' — the gate that answers for the two new test files landing in ledgered packages (metadata in DEBT 89, runtime in TEST_DEBT 227). check:adr-0087-registration exit 0 'this PR adds no declared-breaking changeset (1 non-breaking changeset(s) seen)' — informative because the changeset is COMMITTED, which is what that gate reads. check:nul-bytes exit 0 '6336 text file(s) … no raw ASCII control bytes'. check:route-envelope exit 0, check:dispatcher-error-vocabulary exit 0, check:error-code-casing exit 0 (#10309 family, explicit). Also exit 0: check:changeset-gate-self-tests, check:cross-package-test-inputs, check:durability-log-level, check:entry-guard, check:objectui-changeset, check:parse-guard, check:slot-lookup, check:stack-collection-maps, check:type-source-resolution, check:query-options-erasure, check:where-matcher, check:i18n, check:type-check-coverage, check-ci-filter-parity, check-changeset-no-major, check-empty-changeset, check-affected-docs. ABLATIONS — prediction written down BEFORE each mutation (file kept at scratchpad/issue-10772/ablation-predictions.txt), every restore proved byte-identical with git hash-object. A1 stale-baseline control: PREDICTED exit 1 naming all five packages/ entries stale, proving repair = DELETE not retag. OBSERVED exit 1, '5 stale KNOWN_TEARDOWN_UNREACHED entry/entries' listing all five with '-- no longer unreached' and 'delete each line above'. Restore 5e93066ba709b5479abdc409c7f3582c70406769 -> 5e93066ba709b5479abdc409c7f3582c70406769. A2 revert the load-bearing member (external-validation-plugin.ts back to its origin/main bytes): PREDICTED (a) gate exit 1 naming the class with 'no destroy()' plus the do-not-widen refusal, (b) the pin test red on getTimerCount and on a climbing validateAllCalls and on plugin.destroy being undefined, (c) dist bytes unchanged. OBSERVED (a) exit 1 'packages/runtime/src/external-validation-plugin.ts:159 ExternalValidationPlugin.stop() -- no destroy()' + the refusal text; (b) 'Tests 4 failed | 2 passed (6)' with 'expected 1 to be +0', 'expected 7 to be 2', 'TypeError: plugin.destroy is not a function'; (c) confirmed. Restore 9c370df26dd428018fe02d046f2511e275b5eff1 -> 9c370df26dd428018fe02d046f2511e275b5eff1. REBUILD QUESTION, ARGUED FROM THE FILES: no rebuild sits between edit and run, and that is the proof. The pin test imports './external-validation-plugin.js' — a RELATIVE specifier vitest resolves into packages/runtime/src/ — so a src-only edit is sufficient. NO build command was run between the mutation and the observation, and `git hash-object packages/runtime/dist/index.js` read 6c9145953c9ff3681352bc9a706e6d54948402da both BEFORE and AFTER while the verdict flipped green -> 4 failures. packages/runtime/dist DOES exist and DOES carry the repair (built 2026-08-21 14:44 by the previous seat), so its staleness is not the discriminator — the identical-bytes-across-a-flipped-verdict is. A dist-resolving suite could not have moved. A3 the OTHER direction (delete the retained stop alias, keep destroy): PREDICTED gate stays GREEN and only the alias legs go red, proving the gate alone cannot see an alias deletion. OBSERVED gate exit 0 '0 known-unreached'; test 'Tests 3 failed | 3 passed (6)' with 'TypeError: plugin.stop is not a function' on both alias legs. Restore 9c370df26… -> 9c370df26… A4 is the new vitest.config load-bearing: PREDICTED removing packages/plugins/plugin-webhooks/vitest.config.ts reds check-test-source-alias naming @objectstack/core. OBSERVED exit 1 'NEW unaliased artifact import(s) since this entry was measured: @objectstack/core … Alias them in the package's vitest.config.* — widening the registry entry is not the fix.' Restore 1851931a9fc48aaa927e8c194d36371d65a29980 -> 1851931a9fc48aaa927e8c194d36371d65a29980. No registry entry was widened. ZERO-HIT COUNTER-CHECKS, each with a positive control on the same corpus: (1) dispose() callers — positive control 58 `.destroy()` sites under packages/ non-test sources (non-zero, so the expression finds what exists); against that EmailServicePlugin.dispose() had exactly ONE pre-existing caller repo-wide (email-plugin.template-runtime-write.test.ts:375) and WebhookOutboxPlugin.dispose() had ZERO. Card confirmed. (2) setInterval census — enumerated every non-test `implements Plugin` source under packages/ and tested each for setInterval(: exactly TWO hit (plugin-reports/src/reports-plugin.ts and external-validation-plugin.ts), a non-zero result from an enumerator that named its hits. Within external-validation-plugin.ts the real armed-timer site is ONE (line 294); the other 'setInterval' occurrences are a type annotation (165) and two prose mentions (191, 194) — a grep hit count of 3 would have been a false fact count.", "declared_narrowings": [ "@objectstack/metadata has NO `typecheck` script — asking pnpm for one would have been a silent zero-match exit 0, i.e. not a measurement. In its place: its tsup build (emits dts), its full 615-test suite, and the check:type-check-debt --re-measure which type-checks the package as a whole against its DEBT ledger entry (89) and reported 'none above its recorded number'.", "check:type-check-debt first REFUSED: '--re-measure cannot run: 1 workspace dependenc(ies) of the ledgered packages have no built type entry point on disk -- create-objectstack.' That refusal is NOT MEASURED, never a pass. I built create-objectstack and re-ran it to a real exit-0 verdict, so this narrowing was CLOSED, not left standing.", "scripts/check-single-claim-paths.mjs was NOT run and produces no verdict here: without PR_NUMBER it reports NOT WIRED, with one it needs a token. Both are wiring failures, not verdicts.", "CI convergence is not waited on, per the standing lane rule. At report time PR #10987 is a fresh draft; its checks have not been read." ], "open_questions": [], "out_of_scope_findings": [], "observations_not_filed": [ "AppPlugin.destroy() reads `this.bundle.manifest || this.bundle` with no nullish guard, so `new AppPlugin(undefined)` would now throw a TypeError during kernel shutdown where before the repair it could not (stop() was never called). I did NOT file this and did NOT fix it: enumerating every `new AppPlugin(` in packages/apps/examples, no call site passes a nullish bundle, so I cannot demonstrate a reachable defect, and filing an unreachable hypothetical is noise. It is also harmless if it ever became reachable — both kernels isolate per-plugin destroy failures (kernel.ts:776 try/catch, lite-kernel.ts runPluginDestroy) — and the `if (!appName) return` guard inside emitCatalogEvent already makes the empty-env path a no-op. Recording it so the PM has it, not asking for a decision." ], "notes_for_other_cards": [ "For #10619 (the guard-criterion card, devx lane): both `dispose()` members repaired here — EmailServicePlugin and WebhookOutboxPlugin — are live specimens of the 'seventh spelling' that card predicted, and they existed before it was written. Measured here: after this PR the gate's KNOWN_TEARDOWN_UNREACHED is EMPTY and the gate is green at 61 Plugin implementations / 4424 sources, so the 'declares stop() and no destroy()' criterion is now permanently green over the current tree and can no longer fail on any existing member — exactly the objection #10371's dev raised.", "Census nuance the card does not make and #10619 may want: the 'only two Plugin implementations own setInterval' claim is a statement about each class's OWN text. WebhookOutboxPlugin owns a setInterval TRANSITIVELY, via the AutoEnqueuer it constructs, and a text scan of the plugin class cannot see it. Its pin test asserts the live timer count directly for that reason." ], "housekeeping": "Worktree /home/user/objectstack-10772 is left in place and its branch is fully pushed (local HEAD == origin/claude/issue-10772-teardown-five-more == 019b06738), so nothing is at risk in it; leaving it saves a ~6-minute dependency-closure rebuild if CI comes back red on this claim. Say the word and I will tear it down. No background monitors, no dev servers, no stray processes: the one long-running job (the 5m24s type-check re-measure) was polled in-turn and its verdict is above; the shared verify lock reports 'lock is free'." }Generated by Claude Code
fix(plugins,connectors,services): release plugin resources from destr…
fix(metadata,runtime,plugins): five more Plugin implementations tear …
docs(plugin-webhooks): re-anchor the dead tracker citation to the com…