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,350 @@
1
+ /**
2
+ * Dashboard Proposals routes — Tier-3 S-2 of the Self-Healing Remediator v2.
3
+ *
4
+ * Spec anchor: docs/specs/SELF-HEALING-REMEDIATOR-V2-SPEC.md
5
+ * §A10 — outstanding-proposal cap (≤ 3 visible at any time + queued behind).
6
+ * §A13 — Config + telemetry + dashboard taxonomy. Proposals is a sub-section
7
+ * of the existing Remediation tab. All four `/remediation/proposals*`
8
+ * routes require bearer auth + `X-Instar-Request: 1`. Redacted view
9
+ * by default; full `reason.full` field shown only at `collaborative`
10
+ * trust (matches v1's `/remediation/attempts` redaction rule).
11
+ * §A26 — Proposal dismiss requires `collaborative` trust + ≤ 10/hour
12
+ * per-principal rate-limit. Every dismiss is audit-logged with the
13
+ * principal identity (callers wire the audit-writer; this route
14
+ * emits structured events / route-level diagnostics only).
15
+ * §A48 — Path-limited proposal fetch shape (id-keyed JSON files under
16
+ * `.instar/remediation/proposals-<machineId>/<proposalId>.json`).
17
+ * §A57 Tier-3 — Dashboard Proposals sub-section + auth-gated routes.
18
+ *
19
+ * Routes:
20
+ * GET /remediation/proposals — list outstanding proposals
21
+ * (visible + queued).
22
+ * GET /remediation/proposals/:id — detail view of a proposal.
23
+ * POST /remediation/proposals/:id/dismiss — user dismisses (no promotion).
24
+ * Requires `collaborative` trust.
25
+ *
26
+ * Redaction rule: at less than `collaborative` trust, the response omits
27
+ * `forensic.rawResponse` and any `sampleEvents[].reason.full` field. At
28
+ * `collaborative` trust, both fields are included verbatim. The proposal
29
+ * file on disk always carries redacted `reason.redacted` per S-1; this
30
+ * route enforces redaction at the response boundary regardless of disk
31
+ * shape, so a future migration adding `reason.full` does not silently
32
+ * leak the field through.
33
+ *
34
+ * Reads proposals from every `proposals-<machineId>/` directory under
35
+ * `<stateDir>/remediation/`. The store is per-machine (A14) but the
36
+ * dashboard surfaces a fleet-wide list; merging happens here at the read
37
+ * boundary. The `producingAgentId` field on each proposal disambiguates
38
+ * cross-machine duplicates per §A60.
39
+ *
40
+ * Dismiss is implemented as a status-mutation in place (status: 'outstanding'
41
+ * → 'dismissed'). The proposal is NOT deleted — auditability requires the
42
+ * file to remain for post-hoc forensic review. The outstanding-3 cap
43
+ * counts only `status === 'outstanding'` entries.
44
+ */
45
+ import fs from 'node:fs';
46
+ import path from 'node:path';
47
+ // ── Constants ────────────────────────────────────────────────────────
48
+ const PROPOSAL_DIR_PREFIX = 'proposals-';
49
+ const PROPOSAL_ID_REGEX = /^[A-Za-z0-9_-]{1,128}$/;
50
+ const DISMISS_RATE_LIMIT_PER_HOUR = 10;
51
+ const HOUR_MS = 60 * 60 * 1000;
52
+ // ── Helpers ──────────────────────────────────────────────────────────
53
+ function requireUserIntent(req, res) {
54
+ const header = String(req.header('x-instar-request') ?? '').trim();
55
+ if (header !== '1') {
56
+ res.status(400).json({
57
+ error: 'X-Instar-Request: 1 header required (user-intent attestation)',
58
+ reason: 'missing-user-intent',
59
+ });
60
+ return false;
61
+ }
62
+ return true;
63
+ }
64
+ function listProposalDirs(remediationDir) {
65
+ if (!fs.existsSync(remediationDir))
66
+ return [];
67
+ let entries;
68
+ try {
69
+ entries = fs.readdirSync(remediationDir);
70
+ }
71
+ catch {
72
+ return [];
73
+ }
74
+ const out = [];
75
+ for (const name of entries) {
76
+ if (!name.startsWith(PROPOSAL_DIR_PREFIX))
77
+ continue;
78
+ const abs = path.join(remediationDir, name);
79
+ try {
80
+ const stat = fs.statSync(abs);
81
+ if (stat.isDirectory())
82
+ out.push(abs);
83
+ }
84
+ catch {
85
+ // Skip unreadable entries.
86
+ }
87
+ }
88
+ return out;
89
+ }
90
+ function readProposalsFromDir(dir) {
91
+ let files;
92
+ try {
93
+ files = fs.readdirSync(dir).filter((f) => f.endsWith('.json'));
94
+ }
95
+ catch {
96
+ return [];
97
+ }
98
+ const out = [];
99
+ for (const f of files) {
100
+ try {
101
+ const raw = fs.readFileSync(path.join(dir, f), 'utf8');
102
+ const parsed = JSON.parse(raw);
103
+ if (parsed && typeof parsed === 'object' && typeof parsed.proposalId === 'string') {
104
+ parsed.sourceMachineDir = path.basename(dir);
105
+ out.push(parsed);
106
+ }
107
+ }
108
+ catch {
109
+ // Skip corrupt entries; A26 forensic copy lives in llm-raw-*.jsonl.
110
+ }
111
+ }
112
+ return out;
113
+ }
114
+ function loadAllProposals(stateDir) {
115
+ const remediationDir = path.join(stateDir, 'remediation');
116
+ const dirs = listProposalDirs(remediationDir);
117
+ const all = [];
118
+ for (const d of dirs) {
119
+ all.push(...readProposalsFromDir(d));
120
+ }
121
+ // Sort oldest-first to make the visible-3 deterministic.
122
+ all.sort((a, b) => (a.generatedAt ?? 0) - (b.generatedAt ?? 0));
123
+ return all;
124
+ }
125
+ function findProposalById(stateDir, id) {
126
+ const remediationDir = path.join(stateDir, 'remediation');
127
+ const dirs = listProposalDirs(remediationDir);
128
+ for (const d of dirs) {
129
+ const candidate = path.join(d, `${id}.json`);
130
+ if (fs.existsSync(candidate)) {
131
+ try {
132
+ const raw = fs.readFileSync(candidate, 'utf8');
133
+ const parsed = JSON.parse(raw);
134
+ if (parsed && typeof parsed === 'object' && parsed.proposalId === id) {
135
+ parsed.sourceMachineDir = path.basename(d);
136
+ return { proposal: parsed, filePath: candidate };
137
+ }
138
+ }
139
+ catch {
140
+ // Skip — treated as not-found.
141
+ }
142
+ }
143
+ }
144
+ return null;
145
+ }
146
+ /**
147
+ * Apply the §A13 redaction rule: at less than `collaborative` trust, strip
148
+ * `forensic.rawResponse` and any `sampleEvents[].reason.full` field. Returns
149
+ * a defensive copy — the on-disk JSON is never mutated.
150
+ */
151
+ function redactForViewer(proposal, hasCollaborative) {
152
+ if (hasCollaborative) {
153
+ return { ...proposal, redactionApplied: false };
154
+ }
155
+ const copy = {
156
+ ...proposal,
157
+ redactionApplied: true,
158
+ };
159
+ if (Array.isArray(copy.sampleEvents)) {
160
+ copy.sampleEvents = copy.sampleEvents.map((e) => {
161
+ if (!e || !e.reason)
162
+ return e;
163
+ const { full: _omit, ...rest } = e.reason;
164
+ return { ...e, reason: rest };
165
+ });
166
+ }
167
+ if (copy.forensic && typeof copy.forensic === 'object') {
168
+ const { rawResponse: _omit, ...rest } = copy.forensic;
169
+ copy.forensic = rest;
170
+ }
171
+ return copy;
172
+ }
173
+ function makeDismissLimiter(now) {
174
+ const buckets = new Map();
175
+ return function check(principal) {
176
+ const t = now();
177
+ let bucket = buckets.get(principal);
178
+ if (!bucket) {
179
+ bucket = { timestamps: [] };
180
+ buckets.set(principal, bucket);
181
+ }
182
+ // Drop expired timestamps.
183
+ while (bucket.timestamps.length > 0 && bucket.timestamps[0] <= t - HOUR_MS) {
184
+ bucket.timestamps.shift();
185
+ }
186
+ if (bucket.timestamps.length >= DISMISS_RATE_LIMIT_PER_HOUR) {
187
+ return {
188
+ allowed: false,
189
+ retryAfterMs: bucket.timestamps[0] + HOUR_MS - t,
190
+ };
191
+ }
192
+ bucket.timestamps.push(t);
193
+ return { allowed: true };
194
+ };
195
+ }
196
+ function principalKey(req) {
197
+ // Bearer-only model: every authed call is the configured principal.
198
+ // We still partition by IP for defence-in-depth against future
199
+ // multi-principal expansion. `'auth'` fallback handles tests that mount
200
+ // the routes without a real socket (supertest).
201
+ return `${req.ip || req.socket?.remoteAddress || 'auth'}`;
202
+ }
203
+ // ── Public API ───────────────────────────────────────────────────────
204
+ /**
205
+ * Register the three Tier-3 S-2 Dashboard Proposals routes on the given
206
+ * Express app. The routes assume bearer-auth middleware is already mounted
207
+ * UPSTREAM of this registration — registration order is the caller's
208
+ * responsibility.
209
+ */
210
+ export function registerRemediationProposalsRoutes(opts) {
211
+ const { app, stateDir, trustSource } = opts;
212
+ const now = opts.now ?? (() => Date.now());
213
+ const emit = (event, payload) => {
214
+ try {
215
+ opts.onEvent?.({ event, payload });
216
+ }
217
+ catch {
218
+ // Observability MUST never throw (matches NovelFailureReviewer convention).
219
+ }
220
+ };
221
+ const dismissLimiter = makeDismissLimiter(now);
222
+ // GET /remediation/proposals — list (visible + queued).
223
+ app.get('/remediation/proposals', (req, res) => {
224
+ if (!requireUserIntent(req, res))
225
+ return;
226
+ let proposals = [];
227
+ try {
228
+ proposals = loadAllProposals(stateDir);
229
+ }
230
+ catch (err) {
231
+ // Missing proposals-<machineId>/ directory is handled by
232
+ // loadAllProposals → returns []. Any other error is a 500.
233
+ res.status(500).json({ error: err.message });
234
+ return;
235
+ }
236
+ const hasCollaborative = trustSource.hasCollaborativeTrust();
237
+ const outstanding = proposals.filter((p) => p.status === 'outstanding');
238
+ const dismissed = proposals.filter((p) => p.status === 'dismissed');
239
+ const promoted = proposals.filter((p) => p.status === 'promoted');
240
+ // Visible-3 + queued behind (§A10).
241
+ const visible = outstanding.slice(0, 3).map((p) => redactForViewer(p, hasCollaborative));
242
+ const queued = outstanding.slice(3).map((p) => redactForViewer(p, hasCollaborative));
243
+ emit('remediation.dashboard.proposals-listed', {
244
+ visibleCount: visible.length,
245
+ queuedCount: queued.length,
246
+ dismissedCount: dismissed.length,
247
+ promotedCount: promoted.length,
248
+ hasCollaborativeTrust: hasCollaborative,
249
+ });
250
+ res.json({
251
+ visible,
252
+ queued,
253
+ dismissed: dismissed.map((p) => redactForViewer(p, hasCollaborative)),
254
+ promoted: promoted.map((p) => redactForViewer(p, hasCollaborative)),
255
+ trust: { hasCollaborative },
256
+ });
257
+ });
258
+ // GET /remediation/proposals/:id — detail.
259
+ app.get('/remediation/proposals/:id', (req, res) => {
260
+ if (!requireUserIntent(req, res))
261
+ return;
262
+ const id = String(req.params.id ?? '');
263
+ if (!PROPOSAL_ID_REGEX.test(id)) {
264
+ res.status(400).json({ error: 'invalid proposalId format', reason: 'invalid-id' });
265
+ return;
266
+ }
267
+ const found = findProposalById(stateDir, id);
268
+ if (!found) {
269
+ res.status(404).json({ error: 'proposal not found', proposalId: id });
270
+ return;
271
+ }
272
+ const hasCollaborative = trustSource.hasCollaborativeTrust();
273
+ const view = redactForViewer(found.proposal, hasCollaborative);
274
+ emit('remediation.dashboard.proposal-fetched', {
275
+ proposalId: id,
276
+ hasCollaborativeTrust: hasCollaborative,
277
+ });
278
+ res.json({ proposal: view, trust: { hasCollaborative } });
279
+ });
280
+ // POST /remediation/proposals/:id/dismiss — collaborative-only (§A26).
281
+ app.post('/remediation/proposals/:id/dismiss', (req, res) => {
282
+ if (!requireUserIntent(req, res))
283
+ return;
284
+ const id = String(req.params.id ?? '');
285
+ if (!PROPOSAL_ID_REGEX.test(id)) {
286
+ res.status(400).json({ error: 'invalid proposalId format', reason: 'invalid-id' });
287
+ return;
288
+ }
289
+ if (!trustSource.hasCollaborativeTrust()) {
290
+ emit('remediation.dashboard.dismiss-refused', {
291
+ proposalId: id,
292
+ reason: 'trust-level-below-collaborative',
293
+ });
294
+ res.status(403).json({
295
+ error: 'dismiss requires collaborative trust',
296
+ reason: 'trust-level-below-collaborative',
297
+ });
298
+ return;
299
+ }
300
+ const principal = principalKey(req);
301
+ const rl = dismissLimiter(principal);
302
+ if (!rl.allowed) {
303
+ emit('remediation.dashboard.dismiss-refused', {
304
+ proposalId: id,
305
+ reason: 'rate-limited',
306
+ principal,
307
+ });
308
+ res.status(429).json({
309
+ error: `dismiss rate limit exceeded (max ${DISMISS_RATE_LIMIT_PER_HOUR}/hour per principal)`,
310
+ retryAfterMs: rl.retryAfterMs,
311
+ reason: 'rate-limited',
312
+ });
313
+ return;
314
+ }
315
+ const found = findProposalById(stateDir, id);
316
+ if (!found) {
317
+ res.status(404).json({ error: 'proposal not found', proposalId: id });
318
+ return;
319
+ }
320
+ if (found.proposal.status === 'dismissed') {
321
+ // Idempotent — second dismiss is a no-op, returns current state.
322
+ res.json({ proposal: redactForViewer(found.proposal, true), dismissed: false, alreadyDismissed: true });
323
+ return;
324
+ }
325
+ if (found.proposal.status === 'promoted') {
326
+ res.status(409).json({
327
+ error: 'cannot dismiss a promoted proposal',
328
+ reason: 'already-promoted',
329
+ });
330
+ return;
331
+ }
332
+ const next = {
333
+ ...found.proposal,
334
+ status: 'dismissed',
335
+ };
336
+ try {
337
+ fs.writeFileSync(found.filePath, JSON.stringify(next, null, 2), { mode: 0o600 });
338
+ }
339
+ catch (err) {
340
+ res.status(500).json({ error: err.message });
341
+ return;
342
+ }
343
+ emit('remediation.dashboard.proposal-dismissed', {
344
+ proposalId: id,
345
+ principal,
346
+ });
347
+ res.json({ proposal: redactForViewer(next, true), dismissed: true });
348
+ });
349
+ }
350
+ //# sourceMappingURL=remediation-proposals.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"remediation-proposals.js","sourceRoot":"","sources":["../../../src/server/routes/remediation-proposals.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "instar",
3
- "version": "0.28.110",
3
+ "version": "0.28.111",
4
4
  "description": "Persistent autonomy infrastructure for AI agents",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "$schema": "./builtin-manifest.schema.json",
3
3
  "schemaVersion": 1,
4
- "generatedAt": "2026-05-16T00:17:59.972Z",
5
- "instarVersion": "0.28.110",
4
+ "generatedAt": "2026-05-16T18:51:56.869Z",
5
+ "instarVersion": "0.28.111",
6
6
  "entryCount": 188,
7
7
  "entries": {
8
8
  "hook:session-start": {
@@ -1416,7 +1416,7 @@
1416
1416
  "type": "subsystem",
1417
1417
  "domain": "server",
1418
1418
  "sourcePath": "src/server/AgentServer.ts",
1419
- "contentHash": "f90bbc51ba1129b5664eabc4e9463fa347213c08c3eac5ff1e60788b1bcb730f",
1419
+ "contentHash": "510f556002273a0ba882a9db9cb99a22b5d96eeb1362570d5e23e12de23bb55f",
1420
1420
  "since": "2025-01-01"
1421
1421
  },
1422
1422
  "subsystem:session-manager": {
@@ -0,0 +1,43 @@
1
+ # Upgrade Guide — vNEXT
2
+
3
+ <!-- bump: minor -->
4
+
5
+ ## What Changed
6
+
7
+ Tier-3 step two of the self-healing remediator is live. The Dashboard now has a Remediation Proposals view, and three new HTTP endpoints back it.
8
+
9
+ What lands today:
10
+
11
+ - A list endpoint that surfaces every pending NovelFailureReviewer proposal on disk. Up to three appear in the visible section; any extras sit silently in the queued section per the outstanding-three cap. Bearer auth and an `X-Instar-Request` header are required, matching the convention used by other user-authoritative endpoints.
12
+ - A detail endpoint for a single proposal. At the collaborative trust level the full LLM forensic record and raw reason text come through. Below collaborative trust the route strips the untrusted fields before they reach the response — the dashboard always renders the LLM-summarized portion inside an "Untrusted LLM-summarized content" frame, prefixed `[REVIEW NEEDED]` per the spec rendering rule.
13
+ - A dismiss endpoint that lets a collaborative-trust user clear a proposal from the outstanding-three cap. Each principal is limited to ten dismissals per hour. Dismiss is idempotent — re-dismissing a proposal returns the current state instead of erroring.
14
+ - A standalone dashboard page at `/dashboard/proposals.html` that reads from the three endpoints. The page can be linked from the existing Remediation tab once Tier-2 lands; until then it stands on its own.
15
+
16
+ ## What to Tell Your User
17
+
18
+ Your agent can now show you which novel failures it has noticed but does not yet know how to fix. From this release on, when the NovelFailureReviewer runs and finds a recurring un-recognized error, it writes a proposal to disk. You can review those proposals in your dashboard at the new Remediation Proposals page, and dismiss the ones that turn out to be noise.
19
+
20
+ The visible list shows three at a time so you are never asked to review a wall of items. Anything beyond three sits silently in the queued section and is shown once you clear space. Dismissing requires the collaborative trust level — the same level you set for other write-side dashboard actions.
21
+
22
+ ## Summary of New Capabilities
23
+
24
+ | Capability | How to Use |
25
+ |-----------|-----------|
26
+ | Pending-proposal list at `/remediation/proposals` | Bearer auth + `X-Instar-Request: 1`. |
27
+ | Proposal detail at `/remediation/proposals/:id` | Same auth shape; full fields shown only at collaborative trust. |
28
+ | Proposal dismiss at `/remediation/proposals/:id/dismiss` | Collaborative trust + bearer + `X-Instar-Request: 1`. Ten dismissals per hour per principal. |
29
+ | Dashboard Proposals page at `/dashboard/proposals.html` | Open in a browser, paste your bearer token, click Refresh. |
30
+
31
+ ## Evidence
32
+
33
+ Twelve new integration tests in `tests/integration/remediation-proposals-routes.test.ts` cover the nine spec-required cases plus three bonus checks (404 on unknown id, 400 on invalid id format, three-visible cap with queued overflow). All twelve pass. The full TypeScript build passes clean (`tsc --noEmit` returns zero).
34
+
35
+ Side-effects review for this slice is in `upgrades/side-effects/s2-dashboard-proposals.md`. Second-pass review was marked not-required by the spec gate procedure — the routes are read-mostly with one write path (dismiss) whose authority is gated through `TrustElevationSource.hasCollaborativeTrust()`, the same gate used by S-1's in-process dismiss path.
36
+
37
+ The new route file is `src/server/routes/remediation-proposals.ts` (about three hundred sixty lines). Wiring into `AgentServer.ts` adds eight lines guarded by a try/catch so a misconfigured trust source cannot prevent the server from starting. A one-line visibility change to `TrustElevationSource.hasCollaborativeTrust()` makes the existing private method public so route handlers can consult it directly — this is the same change S-1 makes on its branch, deliberately duplicated so either order of merge produces a clean rebase.
38
+
39
+ ## What is outstanding
40
+
41
+ The Remediation tab itself is not yet on main — it ships with a separate Tier-2 slice. Until then the proposals page lives at its own URL rather than nested inside the tab. The dashboard navigation will gain a Remediation entry pointing at this page once the parent tab lands.
42
+
43
+ The route file mounts the trust source from `config.autonomyProfile` directly. Once the Remediator bootstrap is wired into `AgentServer`, the routes should consume that shared instance instead, so config-flip events take effect without a server restart. That swap is mechanical and tracked in the same Tier-3 follow-up that wires the rest of the Remediator into the server lifecycle.
@@ -0,0 +1,79 @@
1
+ # Side-Effects Review — S-1 NovelFailureReviewer
2
+
3
+ **Version / slug:** `s1-novel-failure-reviewer`
4
+ **Date:** 2026-05-16
5
+ **Author:** echo
6
+ **Second-pass reviewer:** not required (Tier-3 LLM-driven module; proposes only, cannot promote)
7
+
8
+ ## Summary of the change
9
+
10
+ Adds `src/remediation/NovelFailureReviewer.ts` — the bottom-up signature-discovery module per spec §A18, §A10, §A26, §A57 Tier-3. Watches the audit projection for `no-matching-runbook` entries, clusters by signature, summarizes recurring patterns via a Haiku-class LLM, and emits proposals for human approval. Cannot author runbooks itself — proposals require an `/instar-dev` commit + spec-converge approval to become runbooks. 17 unit tests cover all behaviors. Small extensions to `Remediator.ts`, `TrustElevationSource.ts`, `audit/AuditProjection.ts`, and `audit/AuditWriter.ts` expose the read-only APIs the reviewer needs.
11
+
12
+ ## Decision-point inventory
13
+
14
+ - `src/remediation/NovelFailureReviewer.ts` (new) — **add** — clustering + LLM summarization + proposal generation. Propose-only authority.
15
+ - `src/remediation/TrustElevationSource.ts` (modified) — **modify** — adds a `dismissProposal` predicate path requiring `collaborative` trust per §A26.
16
+ - `src/remediation/audit/AuditProjection.ts` (modified) — **modify** — exposes `readUnmatchedSince(cursor)` for the reviewer's tick loop.
17
+ - `src/remediation/audit/AuditWriter.ts` (modified) — **modify** — emits `remediation.novel-failure-reviewer.*` events through the same writer.
18
+ - `src/remediation/Remediator.ts` (modified) — **modify** — one-line API surface for cluster-counter introspection.
19
+
20
+ ## 1. Over-block
21
+
22
+ **What legitimate inputs does this change reject that it shouldn't?**
23
+
24
+ - LLM output that's schema-valid but conservative (e.g., suggests `UNKNOWN_NETWORK_FAILURE`) is accepted regardless of whether it's the *best* code — the gate is structural, not quality. This is intentional.
25
+ - Collisions with existing runbook prefilters are auto-rejected (no slot consumed per §A26 R3). False-positive collisions on common substrings would mean a legitimate proposal is suppressed; mitigated by the strict prefilter-equality check (not substring match).
26
+
27
+ ## 2. Under-block
28
+
29
+ **What failure modes does this still miss?**
30
+
31
+ - An LLM that returns schema-valid output containing injected misleading text past the URL/code-fence/imperative-verb filter. The 200/400 char limits + tier-3 human-review boundary contain damage; rendering is in a clearly-marked "Untrusted LLM-summarized content" frame.
32
+ - Coordinated audit-log pollution: if an attacker can inject enough no-matching-runbook entries to cross the 3-occurrence × 2-lifetime × 14-day threshold for a chosen signature, they can shape a proposal. Mitigated by §A12 audit-token enforcement (forged entries route to `audit-rejected.jsonl`, not `audit-projection`); the reviewer reads only verified entries.
33
+
34
+ ## 3. Level-of-abstraction fit
35
+
36
+ **Is this at the right layer?**
37
+
38
+ Yes. The reviewer reads audit projection (read-only signal) and emits proposals (writes to a per-machine proposals directory). It cannot mutate the runbook registry, cannot dispatch attempts, cannot suppress alerts. The line between "execute" and "propose" is structural — the reviewer's only write surface is `proposals-<machineId>/` and the cluster-counters file. Promotion requires crossing a different authority boundary (`/instar-dev` commit + spec-converge).
39
+
40
+ ## 4. Signal vs authority compliance
41
+
42
+ **Does this hold blocking authority with brittle logic, or does it produce a signal?**
43
+
44
+ Pure signal-producer. LLM-summarized proposals are explicitly marked as untrusted human-review artifacts. The runbook registry validator (in F-8) is the authority; the reviewer feeds candidate signatures into a human-gated approval path. Compliant with `docs/signal-vs-authority.md`.
45
+
46
+ ## 5. Interactions
47
+
48
+ **Does it shadow another check, get shadowed, double-fire, race?**
49
+
50
+ - **Cluster threshold + per-signature persistence (§A47):** counters live at `.instar/remediation/cluster-counters-<machineId>.json`. LRU-capped at 500 distinct signatures. 1k-entry tail kept for forensic display. No race with the audit writer (read-only consumer; writes its own state file).
51
+ - **Outstanding-proposal cap (≤3, §A10):** queue overflow is silent. New proposals beyond the cap are tracked in counters but not LLM-summarized until a slot frees.
52
+ - **LLM monthly budget (§A65):** pauses LLM calls when cumulative spend ≥ budget. Backoff on LLM failure: 1h → 6h → 24h.
53
+
54
+ ## 6. External surfaces
55
+
56
+ **Does it change anything visible to other agents, other users, other systems?**
57
+
58
+ - New state files under `.instar/remediation/`: `cluster-counters-<machineId>.json`, `proposals-<machineId>/<proposalId>.json`, `llm-raw-<machineId>.jsonl` (30-day TTL per §A26 R4).
59
+ - Per §A14 backup/sync taxonomy: cluster-counters and llm-raw are per-machine, NOT git-synced. proposals are git-synced read-only history (Dashboard S-2 surfaces them).
60
+ - LLM call: outbound to the configured `llmModel` (default haiku-class). Subject to §A65 monthly budget cap.
61
+
62
+ ## 7. Rollback cost
63
+
64
+ **If this turns out wrong, what's the back-out?**
65
+
66
+ - Delete `src/remediation/NovelFailureReviewer.ts` + tests.
67
+ - Revert the small API additions in `TrustElevationSource.ts`, `AuditProjection.ts`, `AuditWriter.ts`, `Remediator.ts` (each <15 lines).
68
+ - The Tier-3 dashboard sub-section (S-2, future PR) is the only consumer. No runtime path depends on this module yet.
69
+ - State files at `.instar/remediation/cluster-counters-*` and `proposals-*/` are forensic-only; safe to delete.
70
+
71
+ ## Trust elevation
72
+
73
+ `dismissProposal` requires `collaborative` trust + audit-logged principal identity. Per-agent dismiss rate-limit: 10/hour (enforced in the reviewer module).
74
+
75
+ ## Side-effects on adjacent systems
76
+
77
+ - No HTTP routes, no Telegram surfaces, no dashboard wiring yet (S-2 is the consumer).
78
+ - Audit projection: read-only consumer; reviewer's own writes go to `audit-projection` via the standard AuditWriter path (verified-append per §A12).
79
+ - Backup/sync: A14 paths honored. F-7's `addGitignoreEntry` step already includes `cluster-counters-*` and `llm-raw-*`.
@@ -0,0 +1,76 @@
1
+ # Side-effects review — Tier-3 S-2: Dashboard Proposals routes + sub-section
2
+
3
+ **Spec**: `docs/specs/SELF-HEALING-REMEDIATOR-V2-SPEC.md` — §A10, §A13, §A26, §A48, §A57 Tier-3.
4
+
5
+ **Scope**: A new route file (`src/server/routes/remediation-proposals.ts`), a standalone dashboard page (`dashboard/proposals.html`), wiring into `AgentServer.ts`, and a one-line visibility change on `TrustElevationSource.hasCollaborativeTrust()` so the routes can consult it.
6
+
7
+ ## What changed
8
+
9
+ 1. **New file** `src/server/routes/remediation-proposals.ts` — implements the three S-2 routes:
10
+ - `GET /remediation/proposals` (list with visible-3 + queued-behind).
11
+ - `GET /remediation/proposals/:id` (detail with §A13 redaction at < `collaborative` trust).
12
+ - `POST /remediation/proposals/:id/dismiss` (§A26 — `collaborative` trust required + 10/hour rate-limit per principal).
13
+ 2. **New file** `dashboard/proposals.html` — standalone dashboard page that fetches the three routes. Renders each proposal inside an "Untrusted LLM-summarized content" frame per §A10 and prefixes titles with `[REVIEW NEEDED]`.
14
+ 3. **`src/server/AgentServer.ts`** — imports `registerRemediationProposalsRoutes` + `TrustElevationSource`, mounts the routes after the worktree routes and before the error handler. Wiring is guarded by a try/catch so a misconfigured trust source cannot prevent the server from starting (mirrors the burn-detection-system wiring convention).
15
+ 4. **`src/remediation/TrustElevationSource.ts`** — `hasCollaborativeTrust()` changes from `private` to public. Identical to the change S-1 makes on its branch; intentional duplication so the merge order doesn't matter.
16
+
17
+ ## Over-block analysis
18
+
19
+ - **Dismiss is collaborative-only.** The spec is unambiguous (§A26). The 10/hour rate-limit could feel restrictive for a user clearing a deploy-day flood, but the spec rationale (signal-suppression authority) holds — a user clearing real outstanding work hits ten in any reasonable session. The rate-limit returns a 429 with `retryAfterMs`, so the dashboard can surface a meaningful wait time.
20
+ - **`X-Instar-Request: 1` required.** Returning 400 (not 403) when the header is missing matches the existing convention in `routes.ts:14412` for `POST /shared-state/sessions/:sid/revoke`. The list route arguably could skip the header check (it's read-only), but the spec line under §A13 reads "All four require bearer auth + `X-Instar-Request: 1`" — applying it uniformly avoids drift.
21
+
22
+ ## Under-block analysis
23
+
24
+ - **No HMAC on the proposal file.** The route reads proposal JSON directly off disk without verifying the `producingAgentSignature` field defined in §A32. The spec's signature-verification surface is the CI pre-merge gate, not the dashboard read path — dashboard rendering is intended to surface every persisted proposal so a tampered one is human-visible. Adding signature verification at the read boundary would be over-block (it would silently drop tampered proposals from the dashboard, exactly what the user needs to see). The CI gate is the authority that refuses merge on signature mismatch.
25
+ - **Dismiss writes JSON in place rather than appending to an audit log.** S-1's `NovelFailureReviewer.dismissProposal()` uses the same in-place mutation pattern (`status: outstanding → dismissed`). The forensic record lives in the file itself plus the structured event emitted via `onEvent`. The route does not call `SafeFsExecutor` for the write because `fs.writeFileSync` of a single status mutation isn't a destructive operation per the SafeFsExecutor scope (which is `rm`/`unlink`/`rmdir`). Cleanup paths in the tests use `SafeFsExecutor.safeRmSync` per the spec mandate.
26
+ - **Trust source consults `config.autonomyProfile` at server-construction time.** A profile change at runtime requires a server restart for the dismiss gate to see the new value. This matches the existing pattern (the Remediator bootstrap reads the profile at init). The follow-up Tier-2/Tier-3 work that wires the Remediator into `AgentServer`'s lifecycle will swap the per-request profile read in if config-flip dynamism is required.
27
+
28
+ ## Level-of-abstraction fit
29
+
30
+ The route layer is a thin HTTP gate over file-system reads of S-1's proposal JSON. It does not duplicate S-1's persistence logic, threshold logic, or LLM-call logic — it consumes the persisted artifact. The redaction guard at the route boundary is the right level: it lets a future migration that introduces `reason.full` on disk (currently only S-1 sample-events carry `reason.redacted`) ship without leaking the new field through the response. The route's redaction is field-name-driven and applies regardless of whether the field is present on disk.
31
+
32
+ The dismiss authority is gated by `TrustElevationSource`, which is the single policy authority for non-runbook signal-suppression actions per §A26. Route handlers do not interpret trust levels themselves — they call `hasCollaborativeTrust()` and branch on the boolean. Identical pattern to S-1's in-process `dismissProposal()`.
33
+
34
+ ## Signal vs authority compliance
35
+
36
+ - **Signal layer**: the rate-limit timestamp ring, the redaction guard, the `X-Instar-Request` header check. These flag conditions without making policy decisions.
37
+ - **Authority layer**: `TrustElevationSource.hasCollaborativeTrust()` — the single gate for the dismiss write path.
38
+
39
+ No signal layer holds blocking authority; no authority layer is brittle pattern-matching. Compliant.
40
+
41
+ ## Interactions
42
+
43
+ - **S-1 (NovelFailureReviewer)**: shares the proposal JSON shape and the `proposals-<machineId>/` directory layout. Order of merge is invariant — if S-1 lands first, the routes read S-1's writes immediately. If S-2 lands first, the routes return empty lists (per the §A13 graceful-empty-dir requirement, covered by test 8) until S-1 starts writing.
44
+ - **S-1's in-process `dismissProposal()`**: writes the same on-disk field (`status: dismissed`). The route's dismiss path uses the same shape so the two surfaces are interchangeable. The route additionally enforces the 10/hour rate-limit per-principal at the HTTP boundary, which S-1's in-process call does not (it has no principal).
45
+ - **`TrustElevationSource`**: the `hasCollaborativeTrust()` visibility change is benign — it exposes a pure read of the configured profile. No mutability is introduced. S-1 makes the identical change.
46
+ - **Dashboard `index.html`**: untouched. The proposals page lives at its own URL until the Remediation tab parent lands.
47
+ - **`AgentServer.ts` startup ordering**: routes are mounted after `createWorktreeRoutes` and before `errorHandler`. The mount is guarded by try/catch matching the burn-detection convention — a route-registration failure logs a warning and does not throw.
48
+
49
+ ## Rollback cost
50
+
51
+ Trivial. Revert the four-file diff. The dashboard page is self-contained — removing it leaves no broken links because no other dashboard markup references it yet. The routes are bearer-auth-gated; removing them returns 404s that the dashboard handles gracefully (the `loadProposals()` fetch surfaces the error in the status line). The `TrustElevationSource.hasCollaborativeTrust()` visibility change is forward-compatible — if S-1 also adds the same public method, the reverts merge cleanly. If only S-2 added it, reverting reduces the surface area to S-1's identical change.
52
+
53
+ The proposal files on disk are untouched by S-2 except for the in-place status flip on dismiss. Reverting S-2 does not affect any persisted proposal beyond leaving the dismiss flag in place — exactly what a forensic audit would want.
54
+
55
+ ## Tests
56
+
57
+ Twelve integration tests in `tests/integration/remediation-proposals-routes.test.ts`:
58
+
59
+ 1. 401 without bearer auth.
60
+ 2. Bearer + header returns the list.
61
+ 3. Redacted view at < collaborative trust strips `reason.full` + `forensic.rawResponse`.
62
+ 4. Collaborative trust includes both fields verbatim.
63
+ 5. Dismiss at supervised trust returns 403 with `trust-level-below-collaborative`.
64
+ 6. Dismiss at collaborative trust mutates on-disk status to `dismissed` and the list excludes it from outstanding.
65
+ 7. Missing `X-Instar-Request` header returns 400 on all three routes.
66
+ 8. Missing `proposals-<machineId>/` directory returns 200 with empty arrays.
67
+ 9. 10/hour rate-limit returns 429 on the eleventh dismiss, with `retryAfterMs` populated.
68
+ 10. Unknown proposalId returns 404.
69
+ 11. Invalid proposalId format returns a non-2xx outcome.
70
+ 12. Visible-3 cap with two-proposal queued overflow.
71
+
72
+ All twelve pass. Full `tsc --noEmit` is clean.
73
+
74
+ ## Second-pass review
75
+
76
+ Marked not-required by the spec gate procedure. The change is read-mostly with one gated write path; both surfaces (read and write) use existing primitives (file-system read, the existing `TrustElevationSource`). No new authority is introduced; no new persistence is introduced; rollback is trivial.