instar 0.28.110 → 0.28.111

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 (32) hide show
  1. package/dashboard/proposals.html +418 -0
  2. package/dist/remediation/NovelFailureReviewer.d.ts +212 -0
  3. package/dist/remediation/NovelFailureReviewer.d.ts.map +1 -0
  4. package/dist/remediation/NovelFailureReviewer.js +605 -0
  5. package/dist/remediation/NovelFailureReviewer.js.map +1 -0
  6. package/dist/remediation/Remediator.d.ts.map +1 -1
  7. package/dist/remediation/Remediator.js +1 -0
  8. package/dist/remediation/Remediator.js.map +1 -1
  9. package/dist/remediation/TrustElevationSource.d.ts +8 -1
  10. package/dist/remediation/TrustElevationSource.d.ts.map +1 -1
  11. package/dist/remediation/TrustElevationSource.js +8 -1
  12. package/dist/remediation/TrustElevationSource.js.map +1 -1
  13. package/dist/remediation/audit/AuditProjection.d.ts +7 -0
  14. package/dist/remediation/audit/AuditProjection.d.ts.map +1 -1
  15. package/dist/remediation/audit/AuditProjection.js +16 -0
  16. package/dist/remediation/audit/AuditProjection.js.map +1 -1
  17. package/dist/remediation/audit/AuditWriter.d.ts +8 -0
  18. package/dist/remediation/audit/AuditWriter.d.ts.map +1 -1
  19. package/dist/remediation/audit/AuditWriter.js +5 -1
  20. package/dist/remediation/audit/AuditWriter.js.map +1 -1
  21. package/dist/server/AgentServer.d.ts.map +1 -1
  22. package/dist/server/AgentServer.js +24 -0
  23. package/dist/server/AgentServer.js.map +1 -1
  24. package/dist/server/routes/remediation-proposals.d.ts +114 -0
  25. package/dist/server/routes/remediation-proposals.d.ts.map +1 -0
  26. package/dist/server/routes/remediation-proposals.js +350 -0
  27. package/dist/server/routes/remediation-proposals.js.map +1 -0
  28. package/package.json +1 -1
  29. package/src/data/builtin-manifest.json +3 -3
  30. package/upgrades/0.28.111.md +43 -0
  31. package/upgrades/side-effects/s1-novel-failure-reviewer.md +79 -0
  32. package/upgrades/side-effects/s2-dashboard-proposals.md +76 -0
@@ -0,0 +1,418 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8" />
5
+ <meta name="viewport" content="width=device-width,initial-scale=1" />
6
+ <title>Remediation Proposals — Instar Dashboard</title>
7
+ <link rel="icon" href="/dashboard/favicon.png" />
8
+ <style>
9
+ :root {
10
+ --bg: #0c0c10;
11
+ --bg-card: #15161c;
12
+ --bg-hover: #1c1d24;
13
+ --fg: #e4e4eb;
14
+ --fg-dim: #888894;
15
+ --accent: #6aa9ff;
16
+ --border: #2a2b34;
17
+ --red: #e57373;
18
+ --amber: #e0a060;
19
+ --green: #80c080;
20
+ --untrusted: #2a2018;
21
+ --untrusted-border: #6a4828;
22
+ }
23
+ * { box-sizing: border-box; }
24
+ body {
25
+ margin: 0;
26
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
27
+ background: var(--bg);
28
+ color: var(--fg);
29
+ font-size: 14px;
30
+ line-height: 1.5;
31
+ }
32
+ .page {
33
+ max-width: 1100px;
34
+ margin: 0 auto;
35
+ padding: 24px 20px 60px;
36
+ }
37
+ header.top {
38
+ display: flex;
39
+ align-items: baseline;
40
+ gap: 16px;
41
+ margin-bottom: 24px;
42
+ }
43
+ h1 { font-size: 20px; font-weight: 600; margin: 0; }
44
+ .sub { color: var(--fg-dim); font-size: 13px; }
45
+ .auth-bar {
46
+ display: flex;
47
+ gap: 8px;
48
+ align-items: center;
49
+ margin-bottom: 16px;
50
+ flex-wrap: wrap;
51
+ }
52
+ .auth-bar input {
53
+ flex: 1;
54
+ min-width: 200px;
55
+ padding: 6px 10px;
56
+ background: var(--bg-card);
57
+ border: 1px solid var(--border);
58
+ border-radius: 4px;
59
+ color: var(--fg);
60
+ font-family: ui-monospace, monospace;
61
+ font-size: 12px;
62
+ }
63
+ .btn {
64
+ padding: 6px 14px;
65
+ background: var(--bg-card);
66
+ border: 1px solid var(--border);
67
+ border-radius: 4px;
68
+ color: var(--fg);
69
+ cursor: pointer;
70
+ font-size: 13px;
71
+ }
72
+ .btn:hover { background: var(--bg-hover); border-color: var(--accent); }
73
+ .btn-primary { border-color: var(--accent); color: var(--accent); }
74
+ .btn-danger { border-color: var(--red); color: var(--red); }
75
+ .btn:disabled { opacity: 0.5; cursor: not-allowed; }
76
+ .status-line {
77
+ color: var(--fg-dim);
78
+ font-size: 12px;
79
+ margin-bottom: 12px;
80
+ min-height: 16px;
81
+ }
82
+ .status-line.err { color: var(--red); }
83
+ .status-line.ok { color: var(--green); }
84
+ section.bucket {
85
+ margin-top: 28px;
86
+ }
87
+ section.bucket h2 {
88
+ font-size: 14px;
89
+ font-weight: 600;
90
+ color: var(--fg-dim);
91
+ margin: 0 0 10px 0;
92
+ text-transform: uppercase;
93
+ letter-spacing: 0.06em;
94
+ }
95
+ section.bucket .count {
96
+ color: var(--fg-dim);
97
+ font-weight: 400;
98
+ margin-left: 6px;
99
+ }
100
+ .empty {
101
+ color: var(--fg-dim);
102
+ font-style: italic;
103
+ padding: 16px;
104
+ background: var(--bg-card);
105
+ border: 1px dashed var(--border);
106
+ border-radius: 6px;
107
+ text-align: center;
108
+ font-size: 13px;
109
+ }
110
+ .card {
111
+ background: var(--bg-card);
112
+ border: 1px solid var(--border);
113
+ border-radius: 6px;
114
+ padding: 14px 16px;
115
+ margin-bottom: 10px;
116
+ }
117
+ .card.queued { opacity: 0.78; }
118
+ .card-head {
119
+ display: flex;
120
+ justify-content: space-between;
121
+ align-items: baseline;
122
+ gap: 12px;
123
+ margin-bottom: 8px;
124
+ }
125
+ .card-title {
126
+ font-weight: 600;
127
+ font-size: 14px;
128
+ }
129
+ .card-meta {
130
+ color: var(--fg-dim);
131
+ font-size: 11px;
132
+ font-family: ui-monospace, monospace;
133
+ }
134
+ .card-body {
135
+ font-size: 13px;
136
+ color: var(--fg);
137
+ }
138
+ .card-actions {
139
+ display: flex;
140
+ gap: 8px;
141
+ margin-top: 12px;
142
+ }
143
+ .untrusted-frame {
144
+ background: var(--untrusted);
145
+ border: 1px solid var(--untrusted-border);
146
+ border-radius: 4px;
147
+ padding: 10px 12px;
148
+ margin-top: 8px;
149
+ font-size: 12px;
150
+ }
151
+ .untrusted-frame .untrusted-label {
152
+ color: var(--amber);
153
+ font-size: 10px;
154
+ text-transform: uppercase;
155
+ letter-spacing: 0.08em;
156
+ margin-bottom: 6px;
157
+ font-weight: 600;
158
+ }
159
+ .untrusted-frame .untrusted-content {
160
+ white-space: pre-wrap;
161
+ word-break: break-word;
162
+ }
163
+ .kv {
164
+ display: grid;
165
+ grid-template-columns: 130px 1fr;
166
+ gap: 4px 10px;
167
+ font-size: 12px;
168
+ margin-top: 8px;
169
+ }
170
+ .kv .k { color: var(--fg-dim); }
171
+ .kv .v {
172
+ font-family: ui-monospace, monospace;
173
+ word-break: break-all;
174
+ }
175
+ .redaction-note {
176
+ color: var(--amber);
177
+ font-size: 11px;
178
+ margin-top: 6px;
179
+ }
180
+ </style>
181
+ </head>
182
+ <body>
183
+ <div class="page">
184
+ <header class="top">
185
+ <h1>Remediation Proposals</h1>
186
+ <span class="sub">
187
+ Pending <code>NovelFailureReviewer</code> outputs — review-needed,
188
+ un-promoted. Untrusted LLM-summarized content rendered in a clearly
189
+ marked frame per spec §A10.
190
+ </span>
191
+ </header>
192
+
193
+ <div class="auth-bar">
194
+ <input
195
+ id="authToken"
196
+ type="password"
197
+ placeholder="Bearer token (from .instar/config.json#authToken)"
198
+ autocomplete="off"
199
+ />
200
+ <button class="btn btn-primary" id="refreshBtn" type="button">Refresh</button>
201
+ </div>
202
+
203
+ <div class="status-line" id="status"></div>
204
+
205
+ <section class="bucket" id="visibleSection">
206
+ <h2>Visible <span class="count" id="visibleCount">(0)</span></h2>
207
+ <div id="visibleList"></div>
208
+ </section>
209
+
210
+ <section class="bucket" id="queuedSection">
211
+ <h2>Queued (silent, behind the visible 3) <span class="count" id="queuedCount">(0)</span></h2>
212
+ <div id="queuedList"></div>
213
+ </section>
214
+
215
+ <section class="bucket" id="dismissedSection">
216
+ <h2>Dismissed <span class="count" id="dismissedCount">(0)</span></h2>
217
+ <div id="dismissedList"></div>
218
+ </section>
219
+ </div>
220
+
221
+ <script>
222
+ // ── State ────────────────────────────────────────────────────────
223
+ const TOKEN_STORAGE_KEY = 'instar.remediation-proposals.authToken';
224
+
225
+ function loadToken() {
226
+ try {
227
+ return localStorage.getItem(TOKEN_STORAGE_KEY) || '';
228
+ } catch { return ''; }
229
+ }
230
+ function saveToken(t) {
231
+ try { localStorage.setItem(TOKEN_STORAGE_KEY, t); } catch { /* noop */ }
232
+ }
233
+
234
+ const authInput = document.getElementById('authToken');
235
+ const statusEl = document.getElementById('status');
236
+ const refreshBtn = document.getElementById('refreshBtn');
237
+ authInput.value = loadToken();
238
+ authInput.addEventListener('change', () => saveToken(authInput.value));
239
+
240
+ function setStatus(msg, kind) {
241
+ statusEl.textContent = msg;
242
+ statusEl.className = 'status-line' + (kind ? ' ' + kind : '');
243
+ }
244
+
245
+ function authHeaders() {
246
+ const t = authInput.value.trim();
247
+ const headers = { 'X-Instar-Request': '1' };
248
+ if (t) headers['Authorization'] = 'Bearer ' + t;
249
+ return headers;
250
+ }
251
+
252
+ function escapeHtml(s) {
253
+ if (s == null) return '';
254
+ return String(s)
255
+ .replace(/&/g, '&amp;')
256
+ .replace(/</g, '&lt;')
257
+ .replace(/>/g, '&gt;')
258
+ .replace(/"/g, '&quot;')
259
+ .replace(/'/g, '&#39;');
260
+ }
261
+
262
+ function fmtDate(ts) {
263
+ if (typeof ts !== 'number') return 'unknown time';
264
+ try {
265
+ const d = new Date(ts);
266
+ return d.toISOString().replace('T', ' ').slice(0, 19);
267
+ } catch { return 'invalid time'; }
268
+ }
269
+
270
+ function renderCard(p, opts) {
271
+ const queued = !!(opts && opts.queued);
272
+ const dismissed = !!(opts && opts.dismissed);
273
+ const id = escapeHtml(p.proposalId || '(missing id)');
274
+ const summary = escapeHtml(p.llmSummary || '(no summary)');
275
+ const hypothesis = escapeHtml(p.hypothesis || '');
276
+ const errCode = escapeHtml(p.suggestedErrorCode || '');
277
+ const agent = escapeHtml(p.producingAgentId || '');
278
+ const gen = fmtDate(p.generatedAt);
279
+ const occ = p.occurrencesObserved != null ? p.occurrencesObserved : '?';
280
+ const lifetimes = p.processLifetimes != null ? p.processLifetimes : '?';
281
+ const redacted = p.redactionApplied === true;
282
+ const showDismiss = !queued && !dismissed && p.status === 'outstanding';
283
+ return [
284
+ '<div class="card ' + (queued ? 'queued' : '') + '" data-id="' + id + '">',
285
+ ' <div class="card-head">',
286
+ ' <div class="card-title">[REVIEW NEEDED] ' + (errCode ? escapeHtml(errCode) : '(no error code)') + '</div>',
287
+ ' <div class="card-meta">' + id + '</div>',
288
+ ' </div>',
289
+ ' <div class="card-body">',
290
+ ' <div class="untrusted-frame">',
291
+ ' <div class="untrusted-label">Untrusted LLM-summarized content</div>',
292
+ ' <div class="untrusted-content">' + summary + (hypothesis ? '\n\nHypothesis: ' + hypothesis : '') + '</div>',
293
+ ' </div>',
294
+ ' <div class="kv">',
295
+ ' <div class="k">Cluster sig</div><div class="v">' + escapeHtml(p.clusterSignature || '') + '</div>',
296
+ ' <div class="k">Occurrences</div><div class="v">' + occ + ' across ' + lifetimes + ' process lifetimes</div>',
297
+ ' <div class="k">Producing agent</div><div class="v">' + agent + '</div>',
298
+ ' <div class="k">Generated</div><div class="v">' + gen + '</div>',
299
+ ' <div class="k">Status</div><div class="v">' + escapeHtml(p.status || 'unknown') + '</div>',
300
+ ' </div>',
301
+ redacted ? '<div class="redaction-note">reason.full + forensic.rawResponse hidden — collaborative trust required to view.</div>' : '',
302
+ ' </div>',
303
+ ' <div class="card-actions">',
304
+ ' <button class="btn" type="button" data-action="detail" data-id="' + id + '">View detail</button>',
305
+ showDismiss ? ' <button class="btn btn-danger" type="button" data-action="dismiss" data-id="' + id + '">Dismiss</button>' : '',
306
+ ' </div>',
307
+ '</div>',
308
+ ].join('\n');
309
+ }
310
+
311
+ function renderList(elementId, arr, opts) {
312
+ const el = document.getElementById(elementId);
313
+ if (!arr || arr.length === 0) {
314
+ el.innerHTML = '<div class="empty">None.</div>';
315
+ return;
316
+ }
317
+ el.innerHTML = arr.map((p) => renderCard(p, opts)).join('');
318
+ }
319
+
320
+ async function loadProposals() {
321
+ setStatus('Loading...', '');
322
+ try {
323
+ const res = await fetch('/remediation/proposals', { headers: authHeaders() });
324
+ if (res.status === 401) {
325
+ setStatus('Auth required — enter your bearer token above.', 'err');
326
+ return;
327
+ }
328
+ if (!res.ok) {
329
+ const body = await res.text();
330
+ setStatus('Error ' + res.status + ': ' + body, 'err');
331
+ return;
332
+ }
333
+ const data = await res.json();
334
+ document.getElementById('visibleCount').textContent = '(' + data.visible.length + ')';
335
+ document.getElementById('queuedCount').textContent = '(' + data.queued.length + ')';
336
+ document.getElementById('dismissedCount').textContent = '(' + (data.dismissed || []).length + ')';
337
+ renderList('visibleList', data.visible, { queued: false });
338
+ renderList('queuedList', data.queued, { queued: true });
339
+ renderList('dismissedList', data.dismissed || [], { dismissed: true });
340
+ const collaborative = data.trust && data.trust.hasCollaborative;
341
+ setStatus(
342
+ 'Loaded ' + data.visible.length + ' visible / ' + data.queued.length + ' queued. Trust: ' +
343
+ (collaborative ? 'collaborative (full view)' : 'below collaborative (redacted view)'),
344
+ 'ok',
345
+ );
346
+ } catch (err) {
347
+ setStatus('Network error: ' + (err && err.message ? err.message : err), 'err');
348
+ }
349
+ }
350
+
351
+ async function dismissProposal(id) {
352
+ if (!id) return;
353
+ if (!confirm('Dismiss proposal ' + id + '? This removes it from the outstanding-3 cap.')) return;
354
+ try {
355
+ const res = await fetch('/remediation/proposals/' + encodeURIComponent(id) + '/dismiss', {
356
+ method: 'POST',
357
+ headers: authHeaders(),
358
+ });
359
+ if (res.status === 403) {
360
+ setStatus('Dismiss refused: collaborative trust required.', 'err');
361
+ return;
362
+ }
363
+ if (res.status === 429) {
364
+ const body = await res.json().catch(() => ({}));
365
+ setStatus('Dismiss rate-limited. ' + (body.error || ''), 'err');
366
+ return;
367
+ }
368
+ if (!res.ok) {
369
+ const body = await res.text();
370
+ setStatus('Dismiss failed ' + res.status + ': ' + body, 'err');
371
+ return;
372
+ }
373
+ setStatus('Dismissed.', 'ok');
374
+ loadProposals();
375
+ } catch (err) {
376
+ setStatus('Network error during dismiss: ' + err, 'err');
377
+ }
378
+ }
379
+
380
+ async function showDetail(id) {
381
+ try {
382
+ const res = await fetch('/remediation/proposals/' + encodeURIComponent(id), {
383
+ headers: authHeaders(),
384
+ });
385
+ if (!res.ok) {
386
+ const body = await res.text();
387
+ setStatus('Detail fetch failed ' + res.status + ': ' + body, 'err');
388
+ return;
389
+ }
390
+ const data = await res.json();
391
+ const w = window.open('', '_blank', 'width=720,height=600');
392
+ if (!w) return;
393
+ const json = JSON.stringify(data.proposal, null, 2);
394
+ w.document.write(
395
+ '<!doctype html><html><head><title>Proposal ' + escapeHtml(id) +
396
+ '</title><style>body{background:#0c0c10;color:#e4e4eb;font-family:ui-monospace,monospace;padding:16px;font-size:12px;white-space:pre-wrap}</style></head>' +
397
+ '<body>' + escapeHtml(json) + '</body></html>',
398
+ );
399
+ w.document.close();
400
+ } catch (err) {
401
+ setStatus('Network error: ' + err, 'err');
402
+ }
403
+ }
404
+
405
+ refreshBtn.addEventListener('click', loadProposals);
406
+ document.addEventListener('click', (ev) => {
407
+ const t = ev.target;
408
+ if (!(t instanceof HTMLElement)) return;
409
+ const action = t.dataset.action;
410
+ const id = t.dataset.id;
411
+ if (action === 'dismiss') dismissProposal(id);
412
+ else if (action === 'detail') showDetail(id);
413
+ });
414
+
415
+ if (authInput.value) loadProposals();
416
+ </script>
417
+ </body>
418
+ </html>
@@ -0,0 +1,212 @@
1
+ /**
2
+ * NovelFailureReviewer — bottom-up signature discovery for `no-matching-runbook`
3
+ * degradation events.
4
+ *
5
+ * Tier-3 S-1 of the Self-Healing Remediator v2 rollout.
6
+ *
7
+ * NOT a `CoherenceReviewer` subclass and NOT under `src/core/reviewers/`
8
+ * (§A50). NOT the same as `src/monitoring/SystemReviewer.ts` (the probe
9
+ * runner — see §A18 for the rename history). This module lives at
10
+ * `src/remediation/NovelFailureReviewer.ts` and is structurally distinct
11
+ * from both.
12
+ *
13
+ * Spec anchor: docs/specs/SELF-HEALING-REMEDIATOR-V2-SPEC.md
14
+ * §A10 caps / batching / structured rendering / schema constraints
15
+ * §A18 module rename (SystemReviewer → NovelFailureReviewer)
16
+ * §A26 additional guardrails (dismiss-trust, no-slot-for-collisions,
17
+ * model allowlist, raw-response redaction)
18
+ * §A32 signed `producingAgentId` on every proposal
19
+ * §A47 per-signature counter persistence at
20
+ * `.instar/remediation/cluster-counters-<machineId>.json`
21
+ * §A50 naming clarity (this comment)
22
+ * §A57 Tier-3 placement
23
+ * §A60 deterministic proposalId = sha256(clusterSignature || windowStartMs || fleetScope)
24
+ * §A65 LLM monthly budget circuit-breaker + per-call cost cap
25
+ *
26
+ * Pipeline per `runTick()`:
27
+ * 1. Read `auditProjection.unmatched()` — `no-matching-runbook` entries.
28
+ * 2. Cluster by signature: `(subsystem, errorCode-token-classed,
29
+ * reason-prefix-hashed)` (§A10).
30
+ * 3. Persist per-signature counters with HMAC (§A47).
31
+ * 4. For clusters crossing thresholds (≥3 occurrences × ≥2 process
32
+ * lifetimes within `windowDays`):
33
+ * - If outstanding-proposal cap reached → silently queue (no LLM
34
+ * call, no slot consumed).
35
+ * - Else: call `llmCaller()` with a fixed safety frame; validate
36
+ * schema (§A10), strip injection artifacts, check collision
37
+ * against active runbook errorCodes (§A26 — collision rejection
38
+ * does NOT consume an outstanding slot), persist the signed
39
+ * proposal, and emit observability events.
40
+ *
41
+ * Out of scope for S-1 (handled by S-2 Dashboard / S-3 promotion-gate):
42
+ * - Batched Telegram notification rendering — S-1 emits structured
43
+ * events; S-2 owns UI.
44
+ * - Primary-aggregator lease + fencing tokens — A47 / A60 cross-machine
45
+ * coordination ships alongside S-2 / S-3.
46
+ */
47
+ import { AuditProjection } from './audit/AuditProjection.js';
48
+ import type { AuditEntry } from './audit/AuditWriter.js';
49
+ import type { RemediationKeyVault } from './RemediationKeyVault.js';
50
+ import type { TrustElevationSource } from './TrustElevationSource.js';
51
+ export interface NovelFailureReviewerConfig {
52
+ /** Minimum occurrences to qualify a cluster for proposal (§A10/§A17). */
53
+ clusterThresholdOccurrences: number;
54
+ /** Minimum distinct process lifetimes spanned (§A10/§A17). */
55
+ clusterThresholdLifetimes: number;
56
+ /** Sliding window in days for occurrence / lifetime counting. */
57
+ windowDays: number;
58
+ /** Max outstanding proposals per agent at any time (§A10). */
59
+ outstandingProposalCap: number;
60
+ /** Cluster LRU bound (§A10). */
61
+ clusterLruCap: number;
62
+ /** LLM model id (allowlisted in production via §A26). */
63
+ llmModel: string;
64
+ /** Monthly cumulative LLM spend ceiling in USD (§A65). */
65
+ llmMonthlyBudgetUsd: number;
66
+ /** Per-call estimated cost ceiling in USD (§A65). */
67
+ llmPerCallCostCapUsd?: number;
68
+ /** Estimated USD cost per LLM call. Default fits Haiku-class pricing. */
69
+ llmEstimatedCostPerCallUsd?: number;
70
+ }
71
+ export declare const DEFAULT_NOVEL_FAILURE_REVIEWER_CONFIG: NovelFailureReviewerConfig;
72
+ /** §A10 — small, fixed allowlist enforced at config-load (per §A26). */
73
+ export declare const LLM_MODEL_ALLOWLIST: ReadonlySet<string>;
74
+ export interface SampleEvent {
75
+ subsystem: string;
76
+ errorCode: string;
77
+ reason: {
78
+ redacted: string;
79
+ };
80
+ timestamp: number;
81
+ }
82
+ export interface NovelFailureProposal {
83
+ /** §A60: sha256(clusterSignature || windowStartMs || fleetScope). */
84
+ proposalId: string;
85
+ clusterSignature: string;
86
+ occurrencesObserved: number;
87
+ processLifetimes: number;
88
+ /** ≤ 3 redacted entries — §A26 raw-response redaction handled by writer. */
89
+ sampleEvents: SampleEvent[];
90
+ llmSummary: string;
91
+ suggestedErrorCode: string;
92
+ hypothesis: string;
93
+ /** §A32 — signed agent identity stamped on every proposal. */
94
+ producingAgentId: string;
95
+ producingAgentSignature: string;
96
+ generatedAt: number;
97
+ status: 'outstanding' | 'dismissed' | 'promoted';
98
+ /** §A10 forensic fields (prompt-hash, raw-response, model, generated-at). */
99
+ forensic: {
100
+ promptHash: string;
101
+ llmModel: string;
102
+ /** Redacted raw response. Pre-redaction copy lives in `llm-raw-*.jsonl`. */
103
+ rawResponse: string;
104
+ };
105
+ }
106
+ export type LlmCaller = (prompt: string) => Promise<string>;
107
+ export interface ObservabilityEvent {
108
+ event: string;
109
+ payload?: Record<string, unknown>;
110
+ }
111
+ export interface NovelFailureReviewerOpts {
112
+ stateDir: string;
113
+ auditProjection: AuditProjection;
114
+ llmCaller: LlmCaller;
115
+ keyVault: RemediationKeyVault;
116
+ /** Agent identity stamped on every proposal (§A32). */
117
+ agentId: string;
118
+ /** Machine id — disambiguates persistence files (§A47). */
119
+ machineId: string;
120
+ /** Fleet scope identifier for §A60 proposalId derivation. */
121
+ fleetScope?: string;
122
+ /** §A26 dismiss gate — when present, dismiss requires `collaborative`. */
123
+ trustSource?: TrustElevationSource;
124
+ /** Active runbook errorCodes for §A10 / §A26 collision rejection. */
125
+ getActiveRunbookErrorCodes?: () => ReadonlySet<string>;
126
+ /**
127
+ * Receives observability events synchronously. Production wires to the
128
+ * server's metrics sink; tests collect events for assertion.
129
+ */
130
+ onEvent?: (e: ObservabilityEvent) => void;
131
+ /** Optional `Date.now`-equivalent for deterministic tests. */
132
+ now?: () => number;
133
+ /** Process lifetime token — distinct restarts must use distinct values. */
134
+ processLifetimeToken?: string;
135
+ config?: Partial<NovelFailureReviewerConfig>;
136
+ }
137
+ export declare class NovelFailureReviewer {
138
+ private readonly opts;
139
+ private readonly cfg;
140
+ private readonly proposalDir;
141
+ private readonly countersPath;
142
+ private readonly spendPath;
143
+ private readonly rawLlmPath;
144
+ private readonly hmacKey;
145
+ private readonly proposalSigningKey;
146
+ /** In-memory mirror of the counters file. */
147
+ private counters;
148
+ /** LRU touch order — oldest first; mirrors `counters` membership. */
149
+ private lru;
150
+ constructor(opts: NovelFailureReviewerOpts);
151
+ /**
152
+ * Run one hourly tick. Reads unmatched audit entries, updates per-signature
153
+ * counters, and emits proposals for clusters crossing thresholds.
154
+ */
155
+ runTick(): Promise<{
156
+ clusters: number;
157
+ proposals: number;
158
+ }>;
159
+ /** Returns currently persisted proposals, sorted oldest-first. */
160
+ listProposals(): Promise<NovelFailureProposal[]>;
161
+ /**
162
+ * Mark a proposal dismissed. Requires `collaborative` trust via the
163
+ * injected TrustElevationSource (§A26).
164
+ */
165
+ dismissProposal(proposalId: string, principal: {
166
+ userId: string;
167
+ }): Promise<void>;
168
+ private emit;
169
+ /** Update an in-memory cluster counter; touches LRU. */
170
+ private touch;
171
+ private evictExpired;
172
+ private loadCounters;
173
+ private persistCounters;
174
+ private signCounters;
175
+ private computeProposalId;
176
+ private buildPrompt;
177
+ private buildProposal;
178
+ private persistProposal;
179
+ private canSpendLlm;
180
+ private recordLlmSpend;
181
+ private readMonthlySpend;
182
+ private appendRawLlmLog;
183
+ private gcRawLlmLog;
184
+ }
185
+ /**
186
+ * §A10 token-classed signature: paths → `<path>`, hex → `<hex>`,
187
+ * numbers → `<num>`. Collapses cardinality.
188
+ */
189
+ export declare function tokenClassify(s: string): string;
190
+ export declare function computeClusterSignature(e: AuditEntry): string;
191
+ interface ParsedLlmOutput {
192
+ summary: string;
193
+ suggestedErrorCode: string;
194
+ hypothesis: string;
195
+ }
196
+ /**
197
+ * Parse + sanitize LLM JSON output. Returns null when:
198
+ * - JSON parse fails
199
+ * - fields missing / wrong types
200
+ * - schema constraints violated AFTER sanitization
201
+ *
202
+ * Per §A10: URLs, code fences, and imperative-verb markers are stripped
203
+ * BEFORE schema enforcement. Length / regex constraints then enforced.
204
+ */
205
+ export declare function parseAndSanitizeLlmOutput(raw: string): ParsedLlmOutput | null;
206
+ /**
207
+ * §A10 — strip URLs, code fences, and imperative-verb markers from
208
+ * LLM-emitted free-text before persistence.
209
+ */
210
+ export declare function redactInjectionArtifacts(s: string): string;
211
+ export {};
212
+ //# sourceMappingURL=NovelFailureReviewer.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"NovelFailureReviewer.d.ts","sourceRoot":"","sources":["../../src/remediation/NovelFailureReviewer.ts"],"names":[],"mappings":"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"}