opencode-swarm 7.159.2 → 7.160.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (91) hide show
  1. package/dist/background/delegation-health.d.ts +1 -1
  2. package/dist/cli/{coder-settlement-hny3vh3e.js → coder-settlement-zn552sp9.js} +11 -10
  3. package/dist/cli/{config-doctor-00ex0s3k.js → config-doctor-ddtdz2g0.js} +3 -3
  4. package/dist/cli/{core-03tedwzg.js → core-embasn2j.js} +2 -2
  5. package/dist/cli/{curation-policy-z0a5akng.js → curation-policy-agr6ga1s.js} +6 -6
  6. package/dist/cli/{curator-n81nzdw8.js → curator-dgp6dxxn.js} +32 -31
  7. package/dist/cli/curator-llm-factory-7cwvmc46.js +67 -0
  8. package/dist/cli/{evidence-summary-service-nz7y1w2a.js → evidence-summary-service-sbnyann4.js} +14 -13
  9. package/dist/cli/{gate-evidence-dxafmzrn.js → gate-evidence-5v67a1dz.js} +7 -7
  10. package/dist/cli/guardrail-explain-2gwarnca.js +68 -0
  11. package/dist/cli/{guardrail-log-t8c7acxg.js → guardrail-log-zbnbff1y.js} +9 -6
  12. package/dist/cli/{guardrail-reset-0ft2gf03.js → guardrail-reset-3cnz54hx.js} +32 -31
  13. package/dist/cli/{hive-promoter-brq9cx7c.js → hive-promoter-7cr7t2kg.js} +32 -31
  14. package/dist/cli/{index-1xcve0gc.js → index-1f4n43yz.js} +2 -2
  15. package/dist/cli/{index-b5s1n149.js → index-3055dgzp.js} +7 -7
  16. package/dist/cli/{index-bmst1sje.js → index-3yh737hc.js} +6 -6
  17. package/dist/cli/{index-0zerbq1v.js → index-4mm22fzj.js} +23 -14
  18. package/dist/cli/{index-qypnqmcj.js → index-5m7qjj29.js} +24 -7
  19. package/dist/cli/{index-rq461prn.js → index-7qv869f0.js} +6 -6
  20. package/dist/cli/{index-ky2r092m.js → index-85qjc7b2.js} +2 -2
  21. package/dist/cli/{index-y55hqgcj.js → index-8bjv01t8.js} +4 -4
  22. package/dist/cli/{index-dgt8z19x.js → index-962jgzdq.js} +1 -1
  23. package/dist/cli/{index-vq4d1h7t.js → index-9fspg1xw.js} +1 -1
  24. package/dist/cli/{index-7tjamatf.js → index-9k3zc2t8.js} +7 -7
  25. package/dist/cli/{index-p2m1skjz.js → index-9q6w7kke.js} +1 -1
  26. package/dist/cli/{index-av3gnekk.js → index-b5adxpy6.js} +1 -1
  27. package/dist/cli/{index-w1rhyqw2.js → index-cn233hsy.js} +1 -1
  28. package/dist/cli/{index-mcan50mx.js → index-d9s5hb0m.js} +4 -4
  29. package/dist/cli/{index-prknc0xn.js → index-dke4wzkn.js} +108 -44
  30. package/dist/cli/index-e75bzfgd.js +785 -0
  31. package/dist/cli/{index-8gh97gkz.js → index-fzkx24q5.js} +2 -2
  32. package/dist/cli/{index-m0gfzxw1.js → index-gt8cxfbp.js} +2 -2
  33. package/dist/cli/{index-yt75r1er.js → index-h3re5c78.js} +1 -1
  34. package/dist/cli/{index-k7kvrwbj.js → index-jmths6jj.js} +34 -33
  35. package/dist/cli/{index-8ahjx7vt.js → index-kkfgygvn.js} +4 -4
  36. package/dist/cli/{index-q8yma345.js → index-p39xem8b.js} +5 -5
  37. package/dist/cli/{index-e1jcdpdg.js → index-py6bqwnb.js} +2 -2
  38. package/dist/cli/{index-6sheh3yp.js → index-q6gjwy7m.js} +19 -10
  39. package/dist/cli/{index-200myvdx.js → index-rhc8jve7.js} +2 -2
  40. package/dist/cli/{index-9ev0bbqh.js → index-rnyt2z5r.js} +346 -98
  41. package/dist/cli/{index-1e9rzhmr.js → index-rq7k03rk.js} +4 -4
  42. package/dist/cli/{index-egc8d995.js → index-s7w428r3.js} +1 -1
  43. package/dist/cli/{index-caqjwnak.js → index-sy5aeegg.js} +11 -2
  44. package/dist/cli/{index-b2dh37cf.js → index-va1edd2n.js} +5 -5
  45. package/dist/cli/{index-eh1gh043.js → index-w699ekk9.js} +4 -4
  46. package/dist/cli/{index-nmp47dmm.js → index-wzx6atbv.js} +13 -4
  47. package/dist/cli/{index-jd7mz4yg.js → index-xs15netk.js} +1 -1
  48. package/dist/cli/{index-wzc6vzq6.js → index-yrar1ask.js} +3 -3
  49. package/dist/cli/{index-v7wm4mw8.js → index-zjvf7edj.js} +3 -3
  50. package/dist/cli/index.js +32 -31
  51. package/dist/cli/{knowledge-escalator-rxagfxnk.js → knowledge-escalator-zksm9833.js} +13 -12
  52. package/dist/cli/{knowledge-events-ymwdv799.js → knowledge-events-ayta750w.js} +11 -10
  53. package/dist/cli/{knowledge-link-n1825n09.js → knowledge-link-7c7fjnzc.js} +5 -5
  54. package/dist/cli/{knowledge-store-nxm4xvvq.js → knowledge-store-q5axge8b.js} +6 -6
  55. package/dist/cli/{knowledge-validator-gtmq35k8.js → knowledge-validator-3rrbva2p.js} +8 -8
  56. package/dist/cli/{model-preflight-w85ecyxh.js → model-preflight-x9szw1v4.js} +1 -1
  57. package/dist/cli/{pending-delegations-zdgazhdf.js → pending-delegations-kc7tzxg9.js} +5 -3
  58. package/dist/cli/{pr-review-reentry-authorization-jgzb0pma.js → pr-review-reentry-authorization-vag5gh50.js} +32 -31
  59. package/dist/cli/{pr-subscriptions-pxv24d5k.js → pr-subscriptions-wbdw9a82.js} +5 -4
  60. package/dist/cli/{runner-fzgkhx6a.js → runner-p9wg6z34.js} +6 -6
  61. package/dist/cli/{scan-cursor-fasdj7kj.js → scan-cursor-4fjwtt1k.js} +7 -7
  62. package/dist/cli/{schema-3x28z3rp.js → schema-d0nbzj5k.js} +2 -2
  63. package/dist/cli/{scope-persistence-p8dnxfhx.js → scope-persistence-bckcbc8h.js} +10 -9
  64. package/dist/cli/{skill-generator-6tjyffgd.js → skill-generator-kebhevet.js} +15 -14
  65. package/dist/cli/{telemetry-3rh94wv3.js → telemetry-gpr9s3sm.js} +3 -1
  66. package/dist/cli/{worktree-collision-ownership-3bbn1n2r.js → worktree-collision-ownership-h8qswnrp.js} +6 -5
  67. package/dist/cli/{worktree-isolation-1470tnxc.js → worktree-isolation-q0kbmadz.js} +32 -31
  68. package/dist/config/context-window.d.ts +11 -4
  69. package/dist/config/evidence-schema.d.ts +6 -6
  70. package/dist/context-map/telemetry.d.ts +1 -1
  71. package/dist/health/learning-health.d.ts +356 -0
  72. package/dist/hooks/context-budget.d.ts +10 -1
  73. package/dist/hooks/curator.d.ts +2 -0
  74. package/dist/hooks/final-context-accounting.d.ts +2 -0
  75. package/dist/hooks/model-limits.d.ts +56 -25
  76. package/dist/hooks/skill-usage-pending.d.ts +1 -1
  77. package/dist/index.js +588 -587
  78. package/dist/observability/catalog.d.ts +2 -2
  79. package/dist/observability/ids.d.ts +17 -0
  80. package/dist/prm/trajectory-store.d.ts +1 -1
  81. package/dist/services/diagnose-service.d.ts +7 -0
  82. package/dist/services/status-service.d.ts +18 -0
  83. package/dist/telemetry.d.ts +52 -1
  84. package/dist/tools/context-status.d.ts +15 -1
  85. package/dist/tools/repo-graph/ontology.d.ts +6 -0
  86. package/dist/tools/repo-graph/pack-query.d.ts +36 -0
  87. package/dist/tools/repo-graph/types.d.ts +198 -1
  88. package/dist/tools/repo-graph.d.ts +5 -3
  89. package/package.json +1 -1
  90. package/dist/cli/curator-llm-factory-24bfn7h1.js +0 -66
  91. package/dist/cli/guardrail-explain-b3k9xrx9.js +0 -67
@@ -0,0 +1,356 @@
1
+ /**
2
+ * Learning/operations health — bounded-window alarm registry (issue #2044).
3
+ *
4
+ * Lives in `src/health/`, NOT `src/observability/`: this module performs
5
+ * bounded artifact I/O (`.swarm/learning-health.json`) and imports telemetry,
6
+ * both of which the observability directory's zero-I/O contract forbids
7
+ * (enforced by `tests/unit/observability/no-io.test.ts`).
8
+ *
9
+ * This module owns the eight alarm families the observability series assigned
10
+ * to PR 16:
11
+ *
12
+ * 1. `headroom_dead_streak` — repeated zero/negative effective headroom
13
+ * 2. `model_limit_fallback` — context-limit resolution fell back off the
14
+ * host/live or user-override rungs
15
+ * 3. `retrieval_outcome_liveness` — retrieval → terminal receipt → application
16
+ * outcome chain stalled
17
+ * 4. `role_participation` — eligible workflow roles not participating
18
+ * 5. `promoted_fixture_share` — promoted-tier evidence dominated by
19
+ * non-field (fixture/synthetic-class) sources
20
+ * 6. `archive_activity_mismatch` — close archive empty/invalid while recorded
21
+ * activity predicted content
22
+ * 7. `recovery_ledger_pressure` — background delegation recovery ledger near
23
+ * its 4 MiB bound
24
+ * 8. `compaction_drop_coverage` — store compaction dropping/corrupting
25
+ * records across the six audited stores
26
+ *
27
+ * ## Contracts (issue #2044 items 5-10)
28
+ *
29
+ * - **Bounded windows with cooldown + hysteresis.** Every alarm raises only on
30
+ * its per-family condition inside a bounded window, re-emits at most once per
31
+ * cooldown interval (`sustained`), and recovers explicitly. Duplicate facts
32
+ * (same kind + timestamp) and late/out-of-order facts never storm alerts: the
33
+ * window is a monotonic append; a late fact counts but never rewinds
34
+ * `windowStartMs`.
35
+ * - **Per-canonical-project/session state with explicit eviction.** Session
36
+ * scopes are keyed `<projectRef>/<sessionRef>` when the producer knows its
37
+ * project directory and `u/<sessionRef>` when it does not (the chat-transform
38
+ * hooks receive no directory), so identical session ids under distinct
39
+ * projects never collide. Scope maps are FIFO-evicted at
40
+ * {@link MAX_HEALTH_SCOPES}; fact rings at {@link MAX_FACTS_PER_SCOPE}.
41
+ * - **No raw content.** Payloads and the persisted artifact carry counts,
42
+ * closed-vocabulary enums, booleans, millisecond timestamps, model/provider
43
+ * identity, and 16-hex salted session refs from
44
+ * `pseudonymousSessionRef` — never raw session ids, paths, queries, prompts,
45
+ * or responses (item 10).
46
+ * - **Persistence is transitions + compact per-scope counters ONLY** (item 9):
47
+ * what incident reconstruction needs. This module intentionally contains NO
48
+ * invocation-owned transient-retry or `nonTransientCircuit` state, so there is
49
+ * nothing of that class to persist or rehydrate — restarts resume health
50
+ * accumulation from the persisted counters, which is health state, not
51
+ * invocation state.
52
+ * - **Typed health-source registration, no fake sink** (item 8):
53
+ * {@link HEALTH_SOURCES} cites every real producer and reader file:line.
54
+ * There is deliberately NO `sink` source and NO "sink loss" value — issue
55
+ * #2047 / PR 19 must register its own real producer and reader.
56
+ *
57
+ * Every alarm is wired to production readers: the `/swarm status` Learning
58
+ * Health section and the `/swarm diagnose` learning-health check (both via
59
+ * {@link readLearningHealth}); `context_status` additionally exposes model-limit
60
+ * provenance directly.
61
+ */
62
+ import type { ContextWindowSource } from '../config/context-window';
63
+ /** The eight alarm families owned by this module. */
64
+ export type LearningHealthAlarmId = 'headroom_dead_streak' | 'model_limit_fallback' | 'retrieval_outcome_liveness' | 'role_participation' | 'promoted_fixture_share' | 'archive_activity_mismatch' | 'recovery_ledger_pressure' | 'compaction_drop_coverage';
65
+ export type LearningHealthTransition = 'raised' | 'sustained' | 'recovered';
66
+ export type LearningHealthSeverity = 'warning' | 'critical';
67
+ export type LearningHealthScopeClass = 'session' | 'identity' | 'project';
68
+ /** Identifier of a producer subsystem feeding alarms (typed registration). */
69
+ export type HealthSourceId = 'context_budget' | 'model_limits' | 'knowledge_receipts' | 'curator_compliance' | 'promotion_evidence' | 'close_archive' | 'delegation_ledger' | 'store_health_events';
70
+ export interface HealthSourceRegistration {
71
+ /** file:line of the live producer call site. */
72
+ readonly producer: string;
73
+ /** file:line of every live reader of this source's data. */
74
+ readonly readers: readonly string[];
75
+ /** Alarms this source feeds. */
76
+ readonly alarms: readonly LearningHealthAlarmId[];
77
+ }
78
+ export declare const HEALTH_SOURCES: Readonly<Record<HealthSourceId, HealthSourceRegistration>>;
79
+ /** Per-family thresholds. Documented constants, deliberately not user config. */
80
+ export declare const LEARNING_HEALTH_ALARM_CONFIG: Readonly<{
81
+ headroom_dead_streak: Readonly<{
82
+ windowMs: 600000;
83
+ raiseFacts: 3;
84
+ cooldownMs: 1800000;
85
+ severity: LearningHealthSeverity;
86
+ }>;
87
+ model_limit_fallback: Readonly<{
88
+ windowMs: 1800000;
89
+ cooldownMs: 3600000;
90
+ severity: LearningHealthSeverity;
91
+ }>;
92
+ retrieval_outcome_liveness: Readonly<{
93
+ windowMs: 1800000;
94
+ cooldownMs: 900000;
95
+ severity: LearningHealthSeverity;
96
+ gap2StalenessMs: 600000;
97
+ }>;
98
+ role_participation: Readonly<{
99
+ windowMs: 86400000;
100
+ raiseFacts: 2;
101
+ cooldownMs: 43200000;
102
+ severity: LearningHealthSeverity;
103
+ }>;
104
+ promoted_fixture_share: Readonly<{
105
+ windowMs: 604800000;
106
+ raiseShare: 0.5;
107
+ clearShare: 0.3;
108
+ minEvidence: 4;
109
+ cooldownMs: 86400000;
110
+ severity: LearningHealthSeverity;
111
+ }>;
112
+ archive_activity_mismatch: Readonly<{
113
+ windowMs: 0;
114
+ cooldownMs: 86400000;
115
+ severity: LearningHealthSeverity;
116
+ }>;
117
+ recovery_ledger_pressure: Readonly<{
118
+ windowMs: 900000;
119
+ raisePct: 0.8;
120
+ clearPct: 0.7;
121
+ cooldownMs: 1800000;
122
+ severity: LearningHealthSeverity;
123
+ }>;
124
+ compaction_drop_coverage: Readonly<{
125
+ windowMs: 3600000;
126
+ corruptRaise: 1;
127
+ droppedRaise: 100;
128
+ cooldownMs: 3600000;
129
+ severity: LearningHealthSeverity;
130
+ }>;
131
+ }>;
132
+ /**
133
+ * Receipt sources that count as FIELD evidence for the fixture-share alarm:
134
+ * real workflow actors in a real run. `delegate` is field-but-non-independent —
135
+ * the independence dimension belongs to the promotion gate
136
+ * (src/hooks/knowledge-types.ts:410-418) and is deliberately NOT duplicated
137
+ * here.
138
+ */
139
+ export declare const FIELD_RECEIPT_SOURCES: ReadonlySet<string>;
140
+ /**
141
+ * Receipt sources that count as NON-FIELD (fixture/synthetic-class) evidence:
142
+ * authorized overrides, manual/migration imports, administrative gate
143
+ * releases, and `unknown` (fail-closed — a forged or missing label never counts
144
+ * as field).
145
+ */
146
+ export declare const NON_FIELD_RECEIPT_SOURCES: ReadonlySet<string>;
147
+ interface PersistedTransition {
148
+ alarm: LearningHealthAlarmId;
149
+ transition: LearningHealthTransition;
150
+ severity: LearningHealthSeverity;
151
+ atMs: number;
152
+ coverageFacts: number;
153
+ }
154
+ export declare const _internals: {
155
+ now: () => number;
156
+ emitTelemetry: (payload: Record<string, unknown>) => void;
157
+ writeArtifact: (directory: string, contents: string) => Promise<void>;
158
+ readArtifact: (directory: string) => Promise<string | null>;
159
+ };
160
+ /** Alarm payload detail — counts/enums/refs only, never raw content. */
161
+ export interface AlarmEmitDetail {
162
+ sessionRef?: string;
163
+ model?: string;
164
+ provider?: string;
165
+ limitSource?: string;
166
+ denominatorFallback?: boolean;
167
+ pressurePct?: number;
168
+ band?: string;
169
+ sharePct?: number;
170
+ fieldCount?: number;
171
+ nonFieldCount?: number;
172
+ store?: string;
173
+ dropped?: number;
174
+ corrupt?: number;
175
+ retained?: number;
176
+ accepted?: number;
177
+ gapType?: 'membership_to_terminal' | 'terminal_to_application';
178
+ role?: string;
179
+ phase?: number;
180
+ reason?: string;
181
+ }
182
+ /** Serialize current health state to `.swarm/learning-health.json` (fire-and-forget). */
183
+ export declare function persistLearningHealth(directory: string): Promise<void>;
184
+ /**
185
+ * Observe one context-budget evaluation. `dead` = usagePercent ≥ 1.0
186
+ * (zero/negative effective headroom). The alarm raises after
187
+ * {@link LEARNING_HEALTH_ALARM_CONFIG.headroom_dead_streak.raiseFacts} distinct
188
+ * dead observations in the window and recovers on a healthy observation below
189
+ * the warn threshold. Attribution, not suppression: the payload always carries
190
+ * `limit_source` and `denominator_fallback` so an operator can tell real
191
+ * exhaustion from a stale static-table denominator — the "fallback hides
192
+ * dead-headroom loops" defect this alarm exists to surface.
193
+ */
194
+ export declare function observeContextHeadroom(input: {
195
+ sessionID: string;
196
+ /** Project directory when the producer knows it (threaded from the plugin
197
+ * wiring at handler construction); the chat-transform input itself carries
198
+ * none. Scopes key by `<projectRef>/<sessionRef>` when present. */
199
+ directory?: string;
200
+ usagePercent: number;
201
+ limit: number;
202
+ limitSource: string;
203
+ warnThreshold: number;
204
+ }): void;
205
+ /**
206
+ * Observe one model-limit resolution. Fallback-active when the resolution came
207
+ * from the static tables or the flat default (not host/live, not a user
208
+ * override); recovery when the same identity later resolves from host/override.
209
+ */
210
+ export declare function observeModelLimitResolution(input: {
211
+ modelID?: string;
212
+ providerID?: string;
213
+ /** Project directory when the producer knows it (threaded through
214
+ * resolveModelLimit); identity scopes key project-prefixed when present. */
215
+ directory?: string;
216
+ resolution: ContextWindowSource;
217
+ /** Which override key class matched (compound/model/default) — the alias
218
+ * provenance for user-authored limits (issue #2044 item 2). */
219
+ aliasKeyClass?: 'compound' | 'model' | 'default';
220
+ /** True when a user-authored override failed usability validation and was
221
+ * skipped (never coerced) — surfaced durably, not just as a warning. */
222
+ invalidOverride?: boolean;
223
+ }): void;
224
+ /**
225
+ * Observe one receipt-ledger transition. Called from the ledger's post-lock
226
+ * observation drain where the project directory and the observation record are
227
+ * both in hand. Gap-1: membership committed without a terminal within the
228
+ * window. Gap-2: a terminal with outcome 'applied' without an authoritative
229
+ * application closure — either an `architect_marker`-sourced application
230
+ * outcome or any gate release (the release valve is one-way, so a release MUST
231
+ * count as closure). Gap-2 opens only past the gate's staleness horizon.
232
+ */
233
+ export declare function observeReceiptTransition(input: {
234
+ directory: string;
235
+ kind: string;
236
+ traceId: string;
237
+ receiptOutcome?: string;
238
+ receiptSource?: string;
239
+ /**
240
+ * OPTIONAL membership commit time for gap-2 staleness anchoring. Intentionally
241
+ * not populated by the current ledger drain (PRR-008): anchoring gap-2
242
+ * eligibility to the TERMINAL time (the fallback when absent) is the
243
+ * conservative bound — it can only delay eligibility, never fire the alarm
244
+ * before the application gate's own staleness escalation has had its
245
+ * window. Kept as an explicit input so a future ledger observation can
246
+ * tighten the anchor without an API change.
247
+ */
248
+ membershipCommittedAtMs?: number;
249
+ atMs?: number;
250
+ }): void;
251
+ /**
252
+ * Observe one curator phase-compliance pass. `gaps` carries the compliance
253
+ * observation types counted as participation gaps; `agentsUsed` is the set of
254
+ * roles that DID participate. The structural-zero guard keeps a single review
255
+ * window with no eligible opportunity from ever raising.
256
+ */
257
+ export declare function observeCuratorCompliance(input: {
258
+ directory: string;
259
+ phase?: number;
260
+ gapTypes: readonly string[];
261
+ agentsUsed: readonly string[];
262
+ }): void;
263
+ /**
264
+ * Observe one promotion-evidence record. Classification is closed-vocabulary:
265
+ * field sources are real workflow actors; everything else (overrides, manual
266
+ * and migration imports, administrative releases, unknown) is non-field and
267
+ * `unknown` NEVER counts as field (fail-closed against forged labels).
268
+ */
269
+ export declare function observePromotionEvidence(input: {
270
+ directory: string;
271
+ receiptSource: string | undefined;
272
+ }): void;
273
+ /**
274
+ * Observe one close-archive result. Raises when the archive is empty/invalid
275
+ * while recorded activity predicted content; recovers on the next valid,
276
+ * non-empty archive. Late-arriving activity never retro-raises — the payload
277
+ * records what was known at decision time.
278
+ */
279
+ export declare function observeCloseArchive(input: {
280
+ directory: string;
281
+ archiveValid: boolean;
282
+ archiveEmpty: boolean;
283
+ activityPredictsContent: boolean;
284
+ }): void;
285
+ /**
286
+ * Observe one delegation-ledger health collection. Raises at pressure ≥ 0.8
287
+ * (near the 4 MiB recovery bound) or on the compact-overdue / fail-closed
288
+ * bands; clears below 0.7 (hysteresis). `uncertain` surfaces recovery
289
+ * observation failures as closed reason codes.
290
+ */
291
+ export declare function observeDelegationLedgerPressure(input: {
292
+ directory: string;
293
+ pressurePct: number;
294
+ band: string;
295
+ uncertain?: boolean;
296
+ }): void;
297
+ /**
298
+ * Direct, directory-bearing store-health observation (final-critic finding 3):
299
+ * the six audited stores call this at their health-event emit sites, so family
300
+ * 8 is fed from its real producers from the FIRST event — not only after an
301
+ * operator opens a status surface.
302
+ */
303
+ export declare function observeStoreHealth(input: {
304
+ directory: string;
305
+ kind: string;
306
+ payload: Record<string, unknown>;
307
+ }): void;
308
+ declare function observeStoreHealthEvent(event: string, data: Record<string, unknown>, directory?: string): void;
309
+ /**
310
+ * Idempotent no-op registration point (PR #2446 review): family 8 is fed
311
+ * DIRECTLY by all six audited stores via {@link observeStoreHealth}, so no
312
+ * telemetry listener is attached. The historical listener double-fed every
313
+ * store event (direct + listener observed the same emission into two scope
314
+ * namespaces); it was removed. This function remains as the documented
315
+ * registration seam so future feeds have one call site to wire — it performs
316
+ * no work today and never touches the plugin init path.
317
+ */
318
+ export declare function ensureLearningHealth(): void;
319
+ /**
320
+ * Test teardown: clear all in-memory health state. Never used in production
321
+ * code paths.
322
+ */
323
+ export declare function resetLearningHealthForTest(): void;
324
+ export interface ActiveLearningHealthAlarm {
325
+ alarm: LearningHealthAlarmId;
326
+ severity: LearningHealthSeverity;
327
+ scopeClass: LearningHealthScopeClass;
328
+ scopeRef: string;
329
+ ageMs: number;
330
+ coverageFacts: number;
331
+ transitionCount: number;
332
+ }
333
+ export interface LearningHealthSnapshot {
334
+ activeAlarms: readonly ActiveLearningHealthAlarm[];
335
+ totalTransitions: number;
336
+ updatedAtMs: number;
337
+ }
338
+ /**
339
+ * Read learning health for a project: rehydrate any persisted artifact,
340
+ * lazily evaluate window-based alarms (liveness gaps age into raises without a
341
+ * timer), and persist the refreshed state. Fail-open: any error yields an
342
+ * empty snapshot.
343
+ */
344
+ export declare function readLearningHealth(directory: string): Promise<LearningHealthSnapshot>;
345
+ /** All transitions in the bounded ring (newest last). Test/diagnostic accessor. */
346
+ export declare function getLearningHealthTransitions(): readonly PersistedTransition[];
347
+ /**
348
+ * Tier-0 test exports (writing-tests skill): pure-or-near-pure internals that
349
+ * tests exercise directly. `observeStoreHealthEvent` is the shared core the
350
+ * public `observeStoreHealth` wraps; tests use it to drive family-8 logic
351
+ * without constructing a directory scope.
352
+ */
353
+ export declare const _test_exports: {
354
+ observeStoreHealthEvent: typeof observeStoreHealthEvent;
355
+ };
356
+ export {};
@@ -6,6 +6,7 @@
6
6
  * to provide proactive context management guidance to the architect agent.
7
7
  */
8
8
  import type { PluginConfig } from '../config';
9
+ import { observeContextHeadroom } from '../health/learning-health';
9
10
  import { telemetry } from '../telemetry';
10
11
  interface MessageInfo {
11
12
  role: string;
@@ -34,13 +35,21 @@ interface MessageWithParts {
34
35
  }
35
36
  export declare const _internals: {
36
37
  telemetryContextPruned: typeof telemetry.contextPruned;
38
+ observeContextHeadroom: typeof observeContextHeadroom;
37
39
  };
38
40
  /**
39
41
  * Creates the experimental.chat.messages.transform hook for context budget tracking.
40
42
  * Injects warnings when context usage exceeds configured thresholds.
41
43
  * Only operates on messages for the architect agent.
42
44
  */
43
- export declare function createContextBudgetHandler(config: PluginConfig, resolveAgentModel?: (agentName: string) => string | undefined): (_input: Record<string, never>, _output: {
45
+ export declare function createContextBudgetHandler(config: PluginConfig, resolveAgentModel?: (agentName: string) => string | undefined,
46
+ /**
47
+ * Project directory (threaded from the plugin wiring at handler
48
+ * construction — the chat-transform hook input itself carries none). Used
49
+ * ONLY to scope + persist the #2044 headroom health observation; every
50
+ * budget behavior is identical without it.
51
+ */
52
+ directory?: string): (_input: Record<string, never>, _output: {
44
53
  messages?: MessageWithParts[];
45
54
  }) => Promise<void>;
46
55
  export {};
@@ -24,6 +24,7 @@
24
24
  * This dual dispatch means agent lists are incomplete — they capture factory-dispatched
25
25
  * curators but omit hook-dispatched ones. This is by design for hook-internal operations.
26
26
  */
27
+ import { observeCuratorCompliance } from '../health/learning-health';
27
28
  import { checkRecommendations, recordEmittedRecommendations } from '../services/recommendation-ledger.js';
28
29
  import { listSkills, parseDraftFrontmatter, retireOrMarkStale, retireSkill } from '../services/skill-generator.js';
29
30
  import { getSkillVersion, reviseSkill } from '../services/skill-reviser.js';
@@ -51,6 +52,7 @@ export declare const _internals: {
51
52
  checkPhaseCompliance: typeof checkPhaseCompliance;
52
53
  normalizeAgentName: typeof normalizeAgentName;
53
54
  autoRetireSkills: typeof autoRetireSkills;
55
+ observeCuratorCompliance: typeof observeCuratorCompliance;
54
56
  /**
55
57
  * Retained deliberately (issue #2038 §7). The three curator decision sites
56
58
  * now read through `readSkillUsageEntriesWithCoverage` because a coverage
@@ -43,6 +43,8 @@ interface FinalAccountingOptions {
43
43
  /** Same seam createContextBudgetHandler uses: resolves the
44
44
  * configured target model for an agent name. */
45
45
  resolveAgentModelFn?: (agentName: string) => string | undefined;
46
+ /** Project directory (#2044): scopes the model-limit health observation. */
47
+ directory?: string;
46
48
  }
47
49
  /**
48
50
  * One-shot-per-band warning suppression (#2107 §3), backed by the
@@ -25,6 +25,8 @@
25
25
  * live derivation replaced. They exist so a first-turn `messages.transform`
26
26
  * consumer degrades to a plausible number instead of the flat 128 000 floor.
27
27
  */
28
+ import { type ContextWindowSource } from '../config/context-window';
29
+ import { observeModelLimitResolution } from '../health/learning-health';
28
30
  /**
29
31
  * Native model context limits (in tokens) when used on their native platform.
30
32
  *
@@ -85,21 +87,46 @@ export declare function extractModelInfo(messages: MessageWithParts[]): {
85
87
  * @returns the sessionID, or `undefined` when no message carries one.
86
88
  */
87
89
  export declare function extractSessionId(messages: MessageWithParts[] | undefined): string | undefined;
90
+ /**
91
+ * Coarse provenance class for a resolved model limit (issue #2044 item 1).
92
+ * Maps the fine-grained {@link ContextWindowSource} rung onto the five classes
93
+ * the issue names: `host` (the live `model.limit.context`), `override` (any
94
+ * user-authored `model_limits` entry), `provider_cap` / `native` (the two
95
+ * known-stale static tables), and `fallback` (the flat 128k default).
96
+ */
97
+ export type ModelLimitSource = 'host' | 'override' | 'provider_cap' | 'native' | 'fallback';
98
+ /** Provenance-bearing result of {@link resolveModelLimit} (issue #2044). */
99
+ export interface ModelLimitResolution {
100
+ /** The resolved context limit in tokens. */
101
+ limit: number;
102
+ /** Coarse provenance class — the issue's enum. */
103
+ source: ModelLimitSource;
104
+ /** Which fine-grained resolution rung produced the limit. */
105
+ resolution: ContextWindowSource;
106
+ }
107
+ export declare function classifyModelLimitSource(resolution: ContextWindowSource): ModelLimitSource;
88
108
  /**
89
109
  * Static-table lookup used as `fallbackLookup` for
90
- * {@link resolveContextWindow}. Preserves the historical precedence of
91
- * the two tables relative to each other (provider cap first, then a
92
- * longest-prefix native match), and is reachable only when no live
93
- * `model.limit.context` was available.
110
+ * {@link resolveContextWindow}. Preserves the historical precedence of the two
111
+ * tables relative to each other (provider cap first, then a longest-prefix
112
+ * native match), and says WHICH table matched so the resolution source can
113
+ * distinguish `static_provider_cap` from `static_native` (issue #2044).
114
+ * Reachable only when no live `model.limit.context` was available.
94
115
  */
95
- export declare function lookupStaticModelLimit(modelID?: string, providerID?: string): number | undefined;
116
+ export declare function lookupStaticModelLimit(modelID?: string, providerID?: string): {
117
+ tokens: number;
118
+ table: 'provider_cap' | 'native';
119
+ } | undefined;
96
120
  /**
97
- * Resolves the context limit for a given model/provider combination.
121
+ * Resolves the context limit for a given model/provider combination WITH
122
+ * provenance (issue #2044).
98
123
  *
99
124
  * Thin adapter over the single derivation in `src/config/context-window.ts` —
100
125
  * this function exists so the `experimental.chat.messages.transform` consumers
101
- * (`context-budget.ts`, `knowledge-injector.ts`, `tools/context-status.ts`)
102
- * keep their existing `(modelID, providerID, overrides)` call shape. All
126
+ * (`context-budget.ts`, `knowledge-injector.ts`, `tools/context-status.ts`,
127
+ * `final-context-accounting.ts`) keep their existing
128
+ * `(modelID, providerID, overrides)` call shape while receiving the typed
129
+ * `{ limit, source }` provenance the observability series requires. All
103
130
  * ordering logic lives in the shared resolver; see its module header.
104
131
  *
105
132
  * Resolution order (first usable value wins):
@@ -111,44 +138,48 @@ export declare function lookupStaticModelLimit(modelID?: string, providerID?: st
111
138
  * match (both known-stale; see the module header)
112
139
  * 6. `DEFAULT_MODEL_CONTEXT_TOKENS` (128000)
113
140
  *
114
- * Two ordering changes vs. the pre-#1619 implementation, both deliberate:
115
- * - `configOverrides.default` moved from below the static tables to above
116
- * them. An explicitly authored user value losing to a hardcoded table was a
117
- * defect; it is now consistent with the compound and model-only keys, which
118
- * already outranked the tables.
119
- * - The live `model.limit.context` was inserted above both tables, which is
120
- * the entire point of this change.
121
- *
122
- * Any value that is not a finite number ≥ 1000 is skipped rather than used, so
123
- * a malformed override or a catalog entry with `limit.context: 0` can never
124
- * produce a `NaN` / `Infinity` percentage.
141
+ * Any value that is not a finite number ≥ 1000 (untrusted rungs) / ≥ 1 (user
142
+ * rungs) is SKIPPED, never coerced; skipped user overrides are surfaced via a
143
+ * bounded `invalid_override_skipped` observation instead of silently
144
+ * disappearing (issue #2044 item 2).
125
145
  *
126
146
  * @param modelID - The model identifier (e.g., "claude-sonnet-4-6", "gpt-5")
127
147
  * @param providerID - The provider identifier (e.g., "github-copilot", "anthropic")
128
148
  * @param configOverrides - User configuration overrides (`context_budget.model_limits`)
129
149
  * @param liveContextLimit - The live `model.limit.context` recorded for this
130
150
  * session by the `system.transform` hook, when one has been seen
131
- * @returns The resolved context limit in tokens
151
+ * @returns The resolved limit with coarse source class and fine-grained rung
132
152
  *
133
153
  * @example
134
154
  * // Live value wins over the stale static tables
135
155
  * resolveModelLimit("claude-sonnet-4-6", "github-copilot", {}, 200000)
136
- * // Returns: 200000
156
+ * // Returns: { limit: 200000, source: 'host', resolution: 'live_model_limit' }
137
157
  *
138
158
  * @example
139
159
  * // Explicit user override beats the live value
140
160
  * resolveModelLimit("gpt-5", "github-copilot", { "github-copilot/gpt-5": 60000 }, 400000)
141
- * // Returns: 60000
161
+ * // Returns: { limit: 60000, source: 'override', resolution: 'user_provider_model' }
142
162
  *
143
163
  * @example
144
164
  * // No live value: falls back to the static table (prefix match)
145
165
  * resolveModelLimit("claude-sonnet-4-6-20260301", "anthropic", {})
146
- * // Returns: 200000
166
+ * // Returns: { limit: 200000, source: 'native', resolution: 'static_native' }
147
167
  *
148
168
  * @example
149
169
  * // Malformed live value is ignored, not divided by
150
170
  * resolveModelLimit(undefined, undefined, {}, 0)
151
- * // Returns: 128000
171
+ * // Returns: { limit: 128000, source: 'fallback', resolution: 'static_default' }
152
172
  */
153
- export declare function resolveModelLimit(modelID?: string, providerID?: string, configOverrides?: Record<string, number>, liveContextLimit?: unknown): number;
173
+ export declare function resolveModelLimit(modelID?: string, providerID?: string, configOverrides?: Record<string, number>, liveContextLimit?: unknown,
174
+ /** Project directory (threaded from the consumers) — scopes the #2044
175
+ * fallback-health observation to the owning project. */
176
+ directory?: string): ModelLimitResolution;
177
+ /**
178
+ * Bounded observation seam (invariant-7 DI): routes the model-limit fallback
179
+ * fact into the learning-health registry. Tests replace this to avoid touching
180
+ * global health state; production uses the real observer.
181
+ */
182
+ export declare const _internals: {
183
+ observeResolution: typeof observeModelLimitResolution;
184
+ };
154
185
  export {};
@@ -389,4 +389,4 @@ export declare function buildSkillUsageHealthPayload(doc: SkillUsagePendingDocum
389
389
  export declare function emitSkillUsageHealth(doc: SkillUsagePendingDocument, trigger: SkillUsageHealthPayload['trigger'], gauges: {
390
390
  bytes: number;
391
391
  limitBytes: number;
392
- }): void;
392
+ }, directory?: string): void;