Summary
hooksHealth() in LIFEOS/PULSE/modules/hooks.ts declares a return type of { status: string; stats: HookStats } but returns { status: "ok", ...stats }. The spread flattens HookStats into the top level, so the returned object has no stats key at all. The declared type is a lie, and any consumer reading .stats gets undefined.
Verified in v7.28.3 at LifeOS/install/LIFEOS/PULSE/modules/hooks.ts:141-143.
interface HookStats {
requests: number
skillGuard: { total: number; blocked: number; passed: number }
agentGuard: { total: number; warned: number; passed: number }
}
export function hooksHealth(): { status: string; stats: HookStats } {
return { status: "ok", ...stats } // <- flattened, no `stats` key
}
Impact
pulse.ts:493 assigns the result straight into the health payload:
subsystems.hooks = hooksHealth()
So the health endpoint serves { status, requests, skillGuard, agentGuard } instead of the documented { status, stats }. Anything reading subsystems.hooks.stats.requests silently reads undefined — no throw, no log, just a metric that is always empty.
Reproduction
bunx tsc --noEmit --strict # over the hooks module
Minimal repro:
interface HookStats { requests: number }
const stats: HookStats = { requests: 0 }
export function hooksHealth(): { status: string; stats: HookStats } {
return { status: "ok", ...stats }
}
error TS2741: Property 'stats' is missing in type
'{ requests: number; status: string; }' but required in type
'{ status: string; stats: HookStats; }'.
Proposed fix
Nest the stats instead of spreading them, which matches the declared type and the shape the health consumers expect:
export function hooksHealth(): { status: string; stats: HookStats } {
- return { status: "ok", ...stats }
+ return { status: "ok", stats: { ...stats } }
}
(The inner spread keeps the returned snapshot decoupled from the live mutable stats object, so a later stats.requests++ cannot retroactively change a payload already handed out.)
If the flattened shape is the intended wire format instead, the fix is the mirror image — change the signature to HookStats & { status: string } — but then pulse.ts should be checked for readers of .stats.
Summary
hooksHealth()inLIFEOS/PULSE/modules/hooks.tsdeclares a return type of{ status: string; stats: HookStats }but returns{ status: "ok", ...stats }. The spread flattensHookStatsinto the top level, so the returned object has nostatskey at all. The declared type is a lie, and any consumer reading.statsgetsundefined.Verified in
v7.28.3atLifeOS/install/LIFEOS/PULSE/modules/hooks.ts:141-143.Impact
pulse.ts:493assigns the result straight into the health payload:So the health endpoint serves
{ status, requests, skillGuard, agentGuard }instead of the documented{ status, stats }. Anything readingsubsystems.hooks.stats.requestssilently readsundefined— no throw, no log, just a metric that is always empty.Reproduction
bunx tsc --noEmit --strict # over the hooks moduleMinimal repro:
Proposed fix
Nest the stats instead of spreading them, which matches the declared type and the shape the health consumers expect:
export function hooksHealth(): { status: string; stats: HookStats } { - return { status: "ok", ...stats } + return { status: "ok", stats: { ...stats } } }(The inner spread keeps the returned snapshot decoupled from the live mutable
statsobject, so a laterstats.requests++cannot retroactively change a payload already handed out.)If the flattened shape is the intended wire format instead, the fix is the mirror image — change the signature to
HookStats & { status: string }— but thenpulse.tsshould be checked for readers of.stats.