Repository navigation
[finding] HotReloadManager.startWatching watches nothing and logs "File watching started" at info; watchPatterns has no reader and watchHandles is never populated #12428
Description
Activity
Triage —
domain:engine, queued, and one serial constraint that must holdDedup re-checked on a working channel, as the filing asked
The filing declared its dedup rested on the in-tree scan alone (REST 403 on that seat, #12123;
search_issueszero not evidence per #11835). Re-run from this seat: one hit, and it is this card. That single hit is the positive control — the channel demonstrably resolves this vocabulary, so the absence of any other card is a reading rather than an outage. No duplicate. The in-tree half of the filing's scan stands.Route
ADR-0049 enforce-or-remove, and the fork is already half-settled by precedent rather than by my opinion: #12340 is the same defect family, in the same file, one symbol over, and it went remove/narrow-loudly (PR #12425). Option 1 (enforce) is explicitly building for a caller that does not exist — the filing measured that the class is composed by no runtime today, which is the same fact that decided #12340's route 3. So the presumption for the implementer is refuse/retire, and the burden is on the measurement to overturn it, not to re-open it.
Two things in the filing are worth carrying into the card verbatim:
- The
info-level'File watching started'line is a defect on its own terms, independent of the feature's disposition. Whatever happens towatchPatterns, a placeholder must not report success. That part needs no ruling and is the floor of any acceptable PR. watchHandlesis only ever read, deleted, iterated and cleared — never set. SostopWatching's cleanup branch and the teardown loop are both structurally unreachable, not merely untaken. An implementer should pin that with a firing positive control before removing either, exactly as fix(spec,core): HotReloadConfig.stateStrategy refuses the two values it never implemented; distributedConfig retired (#12340) #12425 did fordistributedConfig.
Clause ② applies if the route lands on refusal: it narrows an accept set. Same shape as #12425, whose gate union is the model to derive from.
⛔ Serial constraint — not dispatchable until #12425 lands
packages/core/src/hot-reload.tsis the file PR #12425 is currently rewriting. The seat's serial criterion is same-FILE, not same-package, and region-level parallelism is only allowed when the bands are measured by symbol — here they are adjacent (registerPluginrefusal vs.startWatching/watchHandles), and #12425's diff already movesregisterPlugin. Dispatching this now would put two open PRs on one file, which the repo's own tooling does not catch:scripts/check-single-claim-paths.mjsdeclares exactly one path (.objectui-sha), and its header measured a repo-wide diff-intersection key at 68 false-positive pairs per 300 PRs. Nothing mechanical backs the criterion, so the seat enforces it.Held in
pm:queueuntil #12425 is verified landed by content onorigin/main, then dispatched against the post-merge head. The card is otherwise fully specified.
Generated by Claude Code
- The
Serial hold released — claimed and dispatched
- Session:
session_01W6HFzyH98W1YaQXhJUJt6o(seat post [PM seat] domain:engine — 🟢 os-project-manager #6367) - Branch:
claude/issue-12428-hotreload-watch-placeholder - Comments re-read in the same action as the claim: two — my 05:02Z grading and the triage seat's 05:52Z independent confirmation of it. No prior claim by any other session.
The hold behind PR #12425 is discharged, verified by content on
origin/mainrather than by the API'smergedfield:stateStrategy: z.enum(['memory', 'none'])is present atpackages/spec/src/kernel/plugin-lifecycle-advanced.zod.ts:205, and'kernel/DistributedStateConfig'is in the retired-defs registry. Positive control fired in the same scan (RETIRED_DEFS_BY_MAJOR, 13 hits), so those are readings.packages/core/src/hot-reload.tsnow has no other open PR against it.Dispatched against the post-merge head, with the grading above carried in unchanged: refuse/retire presumption inherited from #12340's route in this same file, the burden on measurement to overturn it rather than to re-open it; the
info-level'File watching started'line treated as the non-negotiable floor whatever the feature's disposition; andwatchHandles' unreachable branches to be pinned with a firing positive control before anything is removed, the way #12425 did fordistributedConfig.needs:contract-reviewapplied at dispatch, since the refusal route narrows an accept set and clause ② then applies. If the implementation lands somewhere that changes no accept/reject behaviour, say so and the label comes off — that is how it went on #12394, where the widening I anticipated did not materialise and the label was removed on measurement.
Generated by Claude Code
- Session:
{ "issue": 12428, "status": "done", "branch": "claude/issue-12428-hotreload-watch-placeholder", "pr": "https://github.com/objectstack-ai/objectstack/pull/12571", "premise_still_valid": true, "clause2": "yes — narrows an accept set; does NOT widen public surface. Label stays.", "clause2_evidence": "git diff --stat origin/main...HEAD = 11 files, 662 insertions(+), 57 deletions(-). Accept/reject DOES change: HotReloadConfigSchema now refuses watchPatterns (was accepted); registerPlugin refuses a config carrying it; startWatching throws unconditionally. Public surface does NOT widen: no new exported name; both type changes narrow (watchPatterns input string[]|undefined -> never|undefined, startWatching return void -> never). In-tree blast radius zero. Unlike #12394 the anticipated narrowing did materialise, so the label is not removable.", "summary": "ADR-0049 refuse/retire, presumption NOT overturned. startWatching now throws an ADR-0112 envelope (VALIDATION_ERROR/400) naming scheduleReload as the real host-driven integration point, replacing the info-level 'File watching started' lie. watchHandles and both its structurally-unreachable readers removed. HotReloadConfig.watchPatterns retired. Route was chosen by the BUILD, not by me: I started on #12340's route 3 (plain deletion) and gen:schema gate (a) refused it ('authorable key(s) disappeared from the contract') because the schema is not .strict() and a bare deletion is a silent strip (ADR-0104) — the very defect being retired, one layer down. #12340 could take route 3 because what left there was a whole DEF; a key leaving a SURVIVING def has no such exit. So the key is retiredKey()-tombstoned, registered in RETIRED_KEYS_BY_MAJOR[18], with D3 semantic entry hot-reload-watch-placeholder-retired and deliberately no D2 conversion (HotReloadConfig is not an authorable surface, so a conversion would have no seam that ever runs). ENFORCE and EXPERIMENTAL were both measured unavailable: no runtime composes HotReloadManager, and a scan of every planning doc returned 0 hits for hot-reload file watching against 145 control hits in the same files. chokidar is a dep of metadata/metadata-fs/cli, never of core. Files: packages/core/src/hot-reload.ts, packages/spec/src/kernel/plugin-lifecycle-advanced.zod.ts, packages/spec/src/migrations/entries/{retired-keys/18.kernel__HotReloadConfig__watchPatterns.ts,semantic/18.hot-reload-watch-placeholder-retired.ts}.", "tests": "All at final head 1d4795c17c, tree clean. Gate union DERIVED (node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack): 43 matched families + convention-triggered. Every exit code captured BEFORE any pipe. Tests: core 18/18 ('Tests 18 passed (18)'), spec kernel+migrations 143/143 ('Tests 143 passed (143)'). Reproduced before repairing at base 52a9823887: \"[RUNTIME] startWatching('p') log -> [\\\"info:File watching started\\\"]\" / \"[RUNTIME] watchHandles size -> 0\". Ratchet reading is the key-tombstone shape, not the def-removal shape: authorable-surface line 'kernel/HotReloadConfig:watchPatterns' -> '... [RETIRED]', kernel key count 859 -> 859 unchanged; api-surface and json-schema.manifest unmoved (def still emits). check:strictness-ledger green ('436 site(s) measured, 1 authorable strip site(s) left').", "gates": { "green": ["check:generated", "check:authorable-surface", "check:migration-registry", "check:spec-changes", "check:upgrade-guide", "check:docs", "check:api-surface", "check:liveness", "check:empty-state", "check:variant-docs", "check:strictness-ledger", "check:skill-refs", "check:skill-docs", "check-adr-0087-registration", "check-changeset-no-major", "check-empty-changeset", "check:nul-bytes", "check:engine-double-contract", "check:where-matcher", "check:test-source-alias", "check:type-source-resolution", "check-comment-mask-adoption", "check-plugin-teardown-shape", "check-doc-frontmatter", "check:objectui-changeset", "check:type-check-coverage", "check:query-options-erasure", "check-cross-package-test-inputs", "check:type-check-debt", "spec typecheck", "spec check:test-typecheck"], "red": [], "notMeasured": [ "repo-wide `pnpm lint` — left to CI per the local-scope clause.", "`tsc --noEmit -p packages/core/tsconfig.json` — I ran it, it exited 2 with 100 errors, and it is NOT a valid reading. @objectstack/core deliberately has no typecheck script and carries a declared TEST_DEBT entry of 98 pre-existing errors; the ratchet's own docblock names an unbuilt closure 'a DIFFERENT WORLD'. Zero of the 100 errors are in files this PR touches (all 100 sit in 13 test files I never opened). The real measurement is check:type-check-debt, which after building the full closure reported 'none above its recorded number. surplus: none'.", "check:type-check-debt on first attempt REFUSED outright ('--re-measure cannot run: 54 workspace dependencies have no built type entry point'). Recorded as not-measured, not green; then the closure was built (turbo, 70/70, 6m05s) and it re-ran GREEN. It appears in `green` on the strength of that second run only.", "check:api-surface / check:generated transiently refused AFTER the ablation with the stale-dist message ('dist/**/*.d.ts is OLDER than src' — the restore bumped source mtimes). Rebuilt and re-run green at the final head; recorded because that refusal is a not-measured and the gate says so itself." ] }, "ablations": [ { "leg": "remove the startWatching refusal (packages/core/src/hot-reload.ts)", "predicted": "RED, exactly 2 of 18 core tests — 'refuses startWatching with an ADR-0112 envelope and the prescription' and 'refuses startWatching for an UNREGISTERED plugin too'", "observed": "2 failed | 16 passed (18) — precisely those two", "mutation_proved_on_disk": "anchored grep -cF BEFORE reading any result: removed-text 'startWatching() never watched ' 1 -> 0; injected-text 'ABLATION_A_NOOP' 0 -> 1; git diff --stat non-empty (1 file changed, 3 insertions(+), 13 deletions(-))", "rebuild": "NOT rebuilt and not needed — the test imports './hot-reload.js', a relative SOURCE import, not a package exports field to dist/. The only @objectstack/spec/kernel import in that test is `import type`, erased at runtime.", "restore": "trap '<restore>' EXIT INT TERM; restore VERIFIED with an empty git diff, not trusted" }, { "leg": "untombstone watchPatterns (packages/spec/src/kernel/plugin-lifecycle-advanced.zod.ts)", "predicted": "RED, exactly 1 — 'refuses watchPatterns with the retirement prescription (#12428)'", "observed": "1 failed | 16 passed (17) — precisely that one", "mutation_proved_on_disk": "anchored grep -cF BEFORE reading any result: removed-text 'watchPatterns: retiredKey(' 1 -> 0; injected-text 'ABLATION_B_UNTOMBSTONED' 0 -> 1; git diff --stat non-empty (1 file changed, 1 insertion(+), 1 deletion(-))", "rebuild": "NOT rebuilt and not needed — the test imports './plugin-lifecycle-advanced.zod', a relative SOURCE import", "restore": "trap '<restore>' EXIT INT TERM; restore VERIFIED with an empty git diff" } ], "declared_test_edits": [ "packages/spec/src/kernel/plugin-lifecycle-advanced.test.ts — the fixture 'should validate custom hot reload configuration' listed watchPatterns and asserted toEqual(config). That assertion passed PRECISELY BECAUSE the key parsed and did nothing. The key is removed from the fixture and its departure pinned separately as a refusal. Declared here, in the PR body, and on the line in the test file." ], "openQuestions": [ { "question": "startWatching is kept as a permanently-throwing door rather than deleted outright. Is that the disposition you want on a published class?", "options": [ "A (implemented) — keep the method, throw an ADR-0112 envelope carrying the prescription and naming scheduleReload", "B — delete the method entirely; TypeScript hosts get a compile error, JavaScript hosts get a bare `TypeError: not a function`" ], "recommendation": "A, because deleting it is the only variant that reaches a JS host with NO prescription, and this method is the one place a caller of the old placeholder is guaranteed to arrive. A also keeps the refusal assertable (code+status) rather than merely catchable. B is defensible if you would rather the public surface shrink; say so and it is a two-line change. Flagged as 'the call worth a maintainer's eye' in the PR body." }, { "question": "stopWatching is now a misnomer: it never stopped a watcher (nothing ever started one), and what survives is cancellation of a pending debounced reload. Rename?", "options": [ "A (implemented) — keep the published name, document precisely what it does in JSDoc", "B — rename to cancelPendingReload with a deprecated alias", "C — rename with no alias" ], "recommendation": "A for this PR. Renaming a published method is a separate accept-set decision with its own blast radius and does not belong riding this card; B is the natural follow-up if you want the name fixed, and I did not file it as an issue because it is a direct consequence of this disposition rather than an independent finding." } ], "out_of_scope_findings": [ "NOT FILED — dedup channel unavailable from this seat, handing it to the PM to file. Observation (finding class, not a defect): `gen:schema` gate (a) in packages/spec/scripts/build-schemas.ts:787 is unconditional and its remedy text names ONLY the tombstone route ('To retire a key, tombstone it instead of deleting it'). It never mentions the spec-property-retirement playbook's third route — deliberately deleting the baseline line when nothing parses the schema — even though that route is documented and was taken by #12425 for kernel/HotReloadConfig:distributedConfig (verified: the key was present in authorable-surface/kernel.json at b6c96bcea5d4 and absent after 4635f3e079). Gate (a) has no exemption and no env escape hatch, so the only way past it is editing the committed baseline in the same commit — which is exactly route 3, and which the gate's own text does not tell you is legitimate. Consequence measured on myself: I read the gate text, concluded tombstone was mandatory, and switched routes. That happens to be the right answer here, but a future dev on a genuinely unparsed schema will be steered away from route 3 by a message that does not know it exists. Suggested fix: add one route-3 sentence to the gate's remedy text. In-tree dedup done (3 hits for 'disappeared from the contract': the gate itself, its own check-mode test, and my new comment — no card records this); control fired ('tombstone it instead of deleting it' resolves in build-schemas.ts). REST issues endpoint is 403 on this seat and MCP list_issues/search_issues is off-limits for dedup per the dispatch contract, so the open-issue half of the search could not be run and I am not filing blind." ] }
Generated by Claude Code
os-dev-report
(Supersedes the previous comment on this issue: its HTML-comment marker was eaten by the body sanitizer after it landed — verified by reading the comment back — which makes that copy invisible to a marker scan. Same report, marker in literal text. Two short angle-bracket fragments were eaten there too and are respelled here.)
{ "issue": 12428, "status": "done", "branch": "claude/issue-12428-hotreload-watch-placeholder", "pr": "https://github.com/objectstack-ai/objectstack/pull/12571", "premise_still_valid": true, "clause2": "yes — narrows an accept set; does NOT widen public surface. Label stays.", "clause2_evidence": "git diff --stat origin/main...HEAD = 11 files, 662 insertions(+), 57 deletions(-). Accept/reject DOES change: HotReloadConfigSchema now refuses watchPatterns (was accepted); registerPlugin refuses a config carrying it; startWatching throws unconditionally. Public surface does NOT widen: no new exported name; both type changes narrow (watchPatterns input 'string[] | undefined' becomes 'never | undefined'; startWatching return 'void' becomes 'never'). In-tree blast radius zero. Unlike #12394 the anticipated narrowing did materialise, so the label is not removable.", "summary": "ADR-0049 refuse/retire, presumption NOT overturned. startWatching now throws an ADR-0112 envelope (VALIDATION_ERROR/400) naming scheduleReload as the real host-driven integration point, replacing the info-level 'File watching started' lie. watchHandles and both its structurally-unreachable readers removed. HotReloadConfig.watchPatterns retired. Route was chosen by the BUILD, not by me: I started on #12340's route 3 (plain deletion) and gen:schema gate (a) refused it ('authorable key(s) disappeared from the contract') because the schema is not .strict() and a bare deletion is a silent strip (ADR-0104) — the very defect being retired, one layer down. #12340 could take route 3 because what left there was a whole DEF; a key leaving a SURVIVING def has no such exit. So the key is retiredKey()-tombstoned, registered in RETIRED_KEYS_BY_MAJOR[18], with D3 semantic entry hot-reload-watch-placeholder-retired and deliberately no D2 conversion (HotReloadConfig is not an authorable surface, so a conversion would have no seam that ever runs). ENFORCE and EXPERIMENTAL were both measured unavailable: no runtime composes HotReloadManager, and a scan of every planning doc returned 0 hits for hot-reload file watching against 145 control hits in the same files. chokidar is a dep of metadata/metadata-fs/cli, never of core. Files: packages/core/src/hot-reload.ts, packages/spec/src/kernel/plugin-lifecycle-advanced.zod.ts, packages/spec/src/migrations/entries/retired-keys/18.kernel__HotReloadConfig__watchPatterns.ts, packages/spec/src/migrations/entries/semantic/18.hot-reload-watch-placeholder-retired.ts", "tests": "All at final head 1d4795c17c, tree clean. Gate union DERIVED (node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack): 43 matched families + convention-triggered. Every exit code captured BEFORE any pipe. Tests: core 18/18 ('Tests 18 passed (18)'), spec kernel+migrations 143/143 ('Tests 143 passed (143)'). Reproduced before repairing at base 52a9823887: startWatching('p') logged exactly ['info:File watching started'] and watchHandles size was 0. Ratchet reading is the key-tombstone shape, not the def-removal shape: authorable-surface line 'kernel/HotReloadConfig:watchPatterns' gained the [RETIRED] mark, kernel key count 859 unchanged; api-surface and json-schema.manifest unmoved (def still emits). check:strictness-ledger green ('436 site(s) measured, 1 authorable strip site(s) left').", "gates": { "green": ["check:generated", "check:authorable-surface", "check:migration-registry", "check:spec-changes", "check:upgrade-guide", "check:docs", "check:api-surface", "check:liveness", "check:empty-state", "check:variant-docs", "check:strictness-ledger", "check:skill-refs", "check:skill-docs", "check-adr-0087-registration", "check-changeset-no-major", "check-empty-changeset", "check:nul-bytes", "check:engine-double-contract", "check:where-matcher", "check:test-source-alias", "check:type-source-resolution", "check-comment-mask-adoption", "check-plugin-teardown-shape", "check-doc-frontmatter", "check:objectui-changeset", "check:type-check-coverage", "check:query-options-erasure", "check-cross-package-test-inputs", "check:type-check-debt", "spec typecheck", "spec check:test-typecheck"], "red": [], "notMeasured": [ "repo-wide 'pnpm lint' — left to CI per the local-scope clause.", "'tsc --noEmit -p packages/core/tsconfig.json' — I ran it, it exited 2 with 100 errors, and it is NOT a valid reading. @objectstack/core deliberately has no typecheck script and carries a declared TEST_DEBT entry of 98 pre-existing errors; the ratchet's own docblock names an unbuilt closure 'a DIFFERENT WORLD'. Zero of the 100 errors are in files this PR touches (all 100 sit in 13 test files I never opened). The real measurement is check:type-check-debt.", "check:type-check-debt on first attempt REFUSED outright ('--re-measure cannot run: 54 workspace dependencies have no built type entry point'). Recorded as not-measured, not green; then the closure was built (turbo, 70/70, 6m05s) and it re-ran GREEN ('32 ledger entries re-measured, 1843 raw tsc errors total, none above its recorded number. surplus: none'). It appears in 'green' on the strength of that second run only.", "check:api-surface / check:generated transiently refused AFTER the ablation with the stale-dist message ('dist/**/*.d.ts is OLDER than src' — the restore bumped source mtimes). Rebuilt and re-run green at the final head; recorded because that refusal is a not-measured and the gate says so itself." ] }, "ablations": [ { "leg": "remove the startWatching refusal (packages/core/src/hot-reload.ts)", "predicted": "RED, exactly 2 of 18 core tests — 'refuses startWatching with an ADR-0112 envelope and the prescription' and 'refuses startWatching for an UNREGISTERED plugin too'", "observed": "2 failed | 16 passed (18) — precisely those two", "mutation_proved_on_disk": "anchored grep -cF BEFORE reading any result: removed-text 'startWatching() never watched ' went 1 to 0; injected-text 'ABLATION_A_NOOP' went 0 to 1; git diff --stat non-empty (1 file changed, 3 insertions, 13 deletions)", "rebuild": "NOT rebuilt and not needed — the test imports './hot-reload.js', a relative SOURCE import, not a package exports field to dist/. The only @objectstack/spec/kernel import in that test is an 'import type', erased at runtime.", "restore": "trap restore EXIT INT TERM; restore VERIFIED with an empty git diff rather than trusted" }, { "leg": "untombstone watchPatterns (packages/spec/src/kernel/plugin-lifecycle-advanced.zod.ts)", "predicted": "RED, exactly 1 — 'refuses watchPatterns with the retirement prescription (#12428)'", "observed": "1 failed | 16 passed (17) — precisely that one", "mutation_proved_on_disk": "anchored grep -cF BEFORE reading any result: removed-text 'watchPatterns: retiredKey(' went 1 to 0; injected-text 'ABLATION_B_UNTOMBSTONED' went 0 to 1; git diff --stat non-empty (1 file changed, 1 insertion, 1 deletion)", "rebuild": "NOT rebuilt and not needed — the test imports './plugin-lifecycle-advanced.zod', a relative SOURCE import", "restore": "trap restore EXIT INT TERM; restore VERIFIED with an empty git diff" } ], "declared_test_edits": [ "packages/spec/src/kernel/plugin-lifecycle-advanced.test.ts — the fixture 'should validate custom hot reload configuration' listed watchPatterns and asserted toEqual(config). That assertion passed PRECISELY BECAUSE the key parsed and did nothing. The key is removed from the fixture and its departure pinned separately as a refusal. Declared here, in the PR body, and on the line in the test file." ], "openQuestions": [ { "question": "startWatching is kept as a permanently-throwing door rather than deleted outright. Is that the disposition you want on a published class?", "options": [ "A (implemented) — keep the method, throw an ADR-0112 envelope carrying the prescription and naming scheduleReload", "B — delete the method entirely; TypeScript hosts get a compile error, JavaScript hosts get a bare TypeError with no prescription" ], "recommendation": "A, because deleting it is the only variant that reaches a JS host with NO prescription, and this method is the one place a caller of the old placeholder is guaranteed to arrive. A also keeps the refusal assertable (code+status) rather than merely catchable. B is defensible if you would rather the public surface shrink; say so and it is a two-line change. Flagged as the call worth a maintainer's eye in the PR body." }, { "question": "stopWatching is now a misnomer: it never stopped a watcher (nothing ever started one), and what survives is cancellation of a pending debounced reload. Rename?", "options": [ "A (implemented) — keep the published name, document precisely what it does in JSDoc", "B — rename to cancelPendingReload with a deprecated alias", "C — rename with no alias" ], "recommendation": "A for this PR. Renaming a published method is a separate accept-set decision with its own blast radius and does not belong riding this card; B is the natural follow-up if you want the name fixed. Not filed as an issue because it is a direct consequence of this disposition rather than an independent finding." } ], "out_of_scope_findings": [ "NOT FILED — dedup channel unavailable from this seat, handing it to the PM to file. Observation (finding class, not a defect): gen:schema gate (a) in packages/spec/scripts/build-schemas.ts:787 is unconditional and its remedy text names ONLY the tombstone route ('To retire a key, tombstone it instead of deleting it'). It never mentions the spec-property-retirement playbook's third route — deliberately deleting the baseline line when nothing parses the schema — even though that route is documented and was taken by #12425 for kernel/HotReloadConfig:distributedConfig (verified: the key was present in authorable-surface/kernel.json at b6c96bcea5d4 and absent after 4635f3e079). Gate (a) has no exemption and no env escape hatch, so the only way past it is editing the committed baseline in the same commit — which IS route 3, and which the gate's own text does not tell you is legitimate. Consequence measured on myself: I read the gate text, concluded tombstone was mandatory, and switched routes. That is the right answer here, but a future dev on a genuinely unparsed schema will be steered away from route 3 by a message that does not know it exists. Suggested fix: one route-3 sentence in the gate's remedy text. In-tree dedup done (3 hits for 'disappeared from the contract': the gate itself, its own check-mode test, and my report — no card records this); control fired ('tombstone it instead of deleting it' resolves in build-schemas.ts). The REST issues endpoint is 403 on this seat and MCP list_issues/search_issues is off-limits for dedup per the dispatch contract, so the open-issue half of the search could not be run and I am not filing blind." ] }
Generated by Claude Code
- added a commit that references this issue
on Aug 27, 2026 - added a commit that references this issue
on Sep 1, 2026 - added 3 commits that reference this issue
on Sep 28, 2026
Origin: measured while implementing #12340 (base cdbd920) by the os-dev run for PR #12425. Filed unassigned, recording only — the sibling declared-not-enforced defect in the same file, deliberately left out of that PR's scope.
Measurement
In
packages/core/src/hot-reload.ts(located by symbol, not line):HotReloadManager.startWatching(pluginName)contains no watcher. Its whole body is a guard plusthis.logger.info('File watching started', { plugin, patterns: config.watchPatterns }), above the in-source note "Actual file watching would require chokidar or similar / This is a placeholder for the integration point".private watchHandles = new Map()is only ever read, deleted, iterated and cleared — never set.stopWatchingdoesthis.watchHandles.get(pluginName)against a map nothing writes to, so its cleanup branch is unreachable, and the teardown loop overwatchHandles.keys()always runs zero times.HotReloadConfig.watchPatternstherefore has no reader that acts on it. Its only uses are the two log lines (registerPluginandstartWatching).Why it is worth a card rather than a shrug
This is the same declared-not-enforced family as #12340, but the silence is louder in one specific way: #12340's fallback at least announced itself at
debug. Here the log line says "File watching started" atinfo, which reads as positive confirmation that watching is live. An operator who setsenabled: truewithwatchPatternsand sees that line in the log has been told the opposite of what is true — nothing is watched, and no reload will ever be triggered by a file change.Note this is a different
startWatchingfrom the real one inpackages/metadata/src/node-metadata-manager.ts, which does poll. Same method name, different subsystem — worth stating so a reader does not resolve the wrong symbol.Boundary
Inside the host-driven library the 2026-08-25 #11825 ruling deliberately KEPT, same as #12340. The library survives; what is in question is a parameter it declares and does not honour.
Options when triaged (not chosen here — ADR-0049 enforce-or-remove):
startWatchingthrow rather than claim success, and/or retirewatchPatterns. Narrows an accept set, clause-② applies.At minimum the
infolog is a defect on its own terms: whatever the disposition of the feature, a placeholder should not report success.Dedup (searched 2026-08-26): no in-tree prior art — a repo-wide grep for⚠️ Channel caveats, declared rather than hidden: the REST issues endpoint returns 403 on this seat (#12123, reproduced this run), and
startWatching/File watching started/watchPatternsturns up only the implementation, its example, and the generated reference page, with no card referencing them.search_issuesreturned 0 — which per #11835 is not evidence of absence on this seat. So the dedup rests on the in-tree scan alone; a PM with a working list channel should re-check before this is queued.Generated by Claude Code