instar 1.3.29 → 1.3.31
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.
- package/.claude/skills/autonomous/hooks/autonomous-stop-hook.sh +32 -0
- package/dashboard/index.html +112 -0
- package/dashboard/process-health.js +504 -0
- package/dist/core/PostUpdateMigrator.d.ts.map +1 -1
- package/dist/core/PostUpdateMigrator.js +22 -3
- package/dist/core/PostUpdateMigrator.js.map +1 -1
- package/dist/monitoring/FailureLedger.d.ts +11 -3
- package/dist/monitoring/FailureLedger.d.ts.map +1 -1
- package/dist/monitoring/FailureLedger.js +22 -5
- package/dist/monitoring/FailureLedger.js.map +1 -1
- package/dist/scaffold/templates.d.ts.map +1 -1
- package/dist/scaffold/templates.js +6 -0
- package/dist/scaffold/templates.js.map +1 -1
- package/dist/server/routes.d.ts.map +1 -1
- package/dist/server/routes.js +48 -4
- package/dist/server/routes.js.map +1 -1
- package/package.json +2 -1
- package/scripts/analyze-release.js +21 -7
- package/src/data/builtin-manifest.json +62 -62
- package/src/scaffold/templates.ts +6 -0
- package/upgrades/1.3.30.md +35 -0
- package/upgrades/1.3.31.md +29 -0
- package/upgrades/side-effects/notify-on-stop-layer-a.md +31 -0
- package/upgrades/side-effects/process-health-dashboard-tab.md +59 -0
- package/upgrades/side-effects/release-readiness-visibility-pr1.md +26 -0
|
@@ -0,0 +1,504 @@
|
|
|
1
|
+
// Process Health tab — a calm, human-readable read surface for the Failure-Learning Loop.
|
|
2
|
+
// Spec: docs/specs/PROCESS-HEALTH-DASHBOARD-TAB-SPEC.md (v4, converged).
|
|
3
|
+
//
|
|
4
|
+
// Browser-native ESM (no build step; served at /dashboard/process-health.js and
|
|
5
|
+
// loaded by index.html via <script type="module">). The pure functions are
|
|
6
|
+
// exported so the 3-tier jsdom tests exercise the SHIPPED code, not a copy; the
|
|
7
|
+
// controller is also attached to window.ProcessHealth so the (classic) index.html
|
|
8
|
+
// script can drive start/stop on tab activation. This is one self-contained
|
|
9
|
+
// module for the new tab — NOT a modularization of the SPA (that is §10-deferred).
|
|
10
|
+
//
|
|
11
|
+
// Load-bearing safety contract (§4.6): every dynamic value flows through
|
|
12
|
+
// sanitizeForDisplay before the DOM; all DOM writes are textContent only; all
|
|
13
|
+
// presentation chrome is renderer-OWNED static literals (CHROME below) so no
|
|
14
|
+
// attacker codepoint can ever sit where chrome is read.
|
|
15
|
+
|
|
16
|
+
// ── Renderer-owned static chrome (§4.6 r8 belt-and-suspenders, adversarial NEW-1) ──
|
|
17
|
+
// These glyphs are the tab's presentation alphabet. They live HERE as static
|
|
18
|
+
// literals and are NEVER sourced from data — and sanitizeForDisplay strips this
|
|
19
|
+
// exact class from every dynamic value — so a record field can never impersonate
|
|
20
|
+
// a marker/separator/caret.
|
|
21
|
+
export const CHROME = Object.freeze({
|
|
22
|
+
hereMarker: '← you’re here', // "← you're here"
|
|
23
|
+
sep: ' · ', // " · " record-sentence separator
|
|
24
|
+
caret: '▾', // "▾" drawer caret
|
|
25
|
+
stageDone: '●', // "●" completed stage
|
|
26
|
+
stageHere: '●', // "●" current stage (paired with hereMarker)
|
|
27
|
+
stageFuture: '○', // "○" future stage
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
const CAPS = { summary: 240, recommendation: 320, label: 64, detail: 2048 };
|
|
31
|
+
|
|
32
|
+
// Structural presentation-glyph class (NOT an enumerated denylist). Strips, after
|
|
33
|
+
// the NFKC fold, every codepoint in the symbol classes the chrome draws from, so
|
|
34
|
+
// confusable variants (✔ vs ✓, ⇒ vs →, ●︎ with a variation selector) cannot slip
|
|
35
|
+
// past: \p{So} (other-symbol) + arrows + geometric + box-drawing + dingbats +
|
|
36
|
+
// variation-selectors + the bullet/middot set.
|
|
37
|
+
// \p{So} (other-symbol: ★ ● ✓ ✔ ℹ 🔒 …) + arrows U+2190–21FF + geometric
|
|
38
|
+
// U+25A0–25FF + box-drawing U+2500–257F + dingbats U+2700–27BF + variation
|
|
39
|
+
// selectors U+FE00–FE0F + bullet/middot set (U+2022, U+00B7, U+2027, U+2043).
|
|
40
|
+
// Built via RegExp(...) with explicit code points — no invisible source chars.
|
|
41
|
+
const CHROME_GLYPH_RE = new RegExp(
|
|
42
|
+
'[\\p{So}\\u2190-\\u21FF\\u25A0-\\u25FF\\u2500-\\u257F\\u2700-\\u27BF\\uFE00-\\uFE0F\\u2022\\u00B7\\u2027\\u2043]',
|
|
43
|
+
'gu',
|
|
44
|
+
);
|
|
45
|
+
// Invisible-character classes (also explicit code points, no literal controls):
|
|
46
|
+
// C0/C1 controls except \t (U+0009) and \n (U+000A); Unicode bidi-control marks.
|
|
47
|
+
const CONTROL_RE = new RegExp('[\\u0000-\\u0008\\u000B\\u000C\\u000E-\\u001F\\u007F-\\u009F]', 'g');
|
|
48
|
+
const BIDI_RE = new RegExp('[\\u202A-\\u202E\\u2066-\\u2069]', 'g');
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* §4.6 sanitize contract. Rules: 1 null-coerce; 2 NFKC-fold (collapses full-width/
|
|
52
|
+
* variant confusables BEFORE stripping); 3 strip C0/C1 controls (keep \n,\t);
|
|
53
|
+
* 4 strip bidi-control; 5 collapse newlines + cap whitespace runs; 8 structural
|
|
54
|
+
* chrome-glyph strip; 6 grapheme-safe length cap. (Strip precedes the cap so the
|
|
55
|
+
* cap reflects final visible length — closes the cosmetic under-cap the reviewer
|
|
56
|
+
* flagged; safety is unaffected by the 6/8 order.)
|
|
57
|
+
*/
|
|
58
|
+
export function sanitizeForDisplay(value, fieldKind = 'summary') {
|
|
59
|
+
let s = value == null ? '' : String(value); // 1
|
|
60
|
+
s = s.normalize('NFKC'); // 2
|
|
61
|
+
s = s.replace(CONTROL_RE, ''); // 3 (keep \n=0A, \t=09)
|
|
62
|
+
s = s.replace(BIDI_RE, ''); // 4 bidi-control
|
|
63
|
+
s = s.replace(/\n{2,}/g, '\n').replace(/[ \t]{5,}/g, ' '); // 5
|
|
64
|
+
s = s.replace(CHROME_GLYPH_RE, ''); // 8 structural chrome-glyph strip
|
|
65
|
+
s = capGraphemes(s, CAPS[fieldKind] ?? CAPS.summary); // 6 grapheme-safe cap
|
|
66
|
+
return s;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function capGraphemes(s, max) {
|
|
70
|
+
if (s.length <= max) return s; // UTF-16 length ≥ grapheme count → safe fast path
|
|
71
|
+
if (typeof Intl !== 'undefined' && Intl.Segmenter) {
|
|
72
|
+
const arr = Array.from(new Intl.Segmenter(undefined, { granularity: 'grapheme' }).segment(s), (x) => x.segment);
|
|
73
|
+
if (arr.length <= max) return s;
|
|
74
|
+
return arr.slice(0, max - 1).join('') + '…';
|
|
75
|
+
}
|
|
76
|
+
// Fallback: never cut between surrogate halves.
|
|
77
|
+
let cut = max - 1;
|
|
78
|
+
const c = s.charCodeAt(cut - 1);
|
|
79
|
+
if (c >= 0xd800 && c <= 0xdbff) cut -= 1;
|
|
80
|
+
return s.slice(0, cut) + '…';
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Heuristic mixed-script flag (§4.6 r7): Latin letters co-occurring with confusable
|
|
85
|
+
* Cyrillic/Greek. The caller renders such a row inertly (no special glyph) — we
|
|
86
|
+
* never add chrome; this only signals.
|
|
87
|
+
*/
|
|
88
|
+
export function isMixedScript(value) {
|
|
89
|
+
const s = value == null ? '' : String(value);
|
|
90
|
+
const latin = /[A-Za-z]/.test(s);
|
|
91
|
+
const confusable = /[Ѐ-ӿͰ-Ͽ]/.test(s);
|
|
92
|
+
return latin && confusable;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* §4.6 safeUrl. v1 renders NO dynamic URL-bearing attributes (defense-in-depth):
|
|
97
|
+
* allow relative (`/`,`#`) and same-origin http(s) only; reject javascript:/data:/
|
|
98
|
+
* vbscript:/file: and all off-origin hosts → empty string.
|
|
99
|
+
*/
|
|
100
|
+
export function safeUrl(value) {
|
|
101
|
+
const s = (value == null ? '' : String(value)).trim();
|
|
102
|
+
if (s === '') return '';
|
|
103
|
+
if (s.startsWith('/') || s.startsWith('#')) return s;
|
|
104
|
+
let u;
|
|
105
|
+
try {
|
|
106
|
+
const base = typeof location !== 'undefined' && location.href ? location.href : 'http://localhost/';
|
|
107
|
+
u = new URL(s, base);
|
|
108
|
+
} catch {
|
|
109
|
+
return '';
|
|
110
|
+
}
|
|
111
|
+
if (u.protocol !== 'http:' && u.protocol !== 'https:') return '';
|
|
112
|
+
const origin = typeof location !== 'undefined' && location.origin ? location.origin : 'http://localhost';
|
|
113
|
+
if (u.origin !== origin) return '';
|
|
114
|
+
return u.href;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// ── Friendly wording (§4.2c) ───────────────────────────────────────────────
|
|
118
|
+
const CATEGORY_WORDS = {
|
|
119
|
+
concurrency: 'A concurrency issue',
|
|
120
|
+
'config-parse': 'A config problem',
|
|
121
|
+
'type-error': 'A type error',
|
|
122
|
+
'logic-error': 'A logic bug',
|
|
123
|
+
'test-failure': 'A failing test',
|
|
124
|
+
'build-failure': 'A build break',
|
|
125
|
+
'integration-gap': 'An integration gap',
|
|
126
|
+
regression: 'A regression',
|
|
127
|
+
'wiring-gap': 'A wiring gap',
|
|
128
|
+
unknown: 'An issue',
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
export function friendlyCategory(category) {
|
|
132
|
+
const key = typeof category === 'string' ? category : 'unknown';
|
|
133
|
+
return CATEGORY_WORDS[key] || 'An issue';
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// initiativeId → human label. NEVER expose the raw initiativeId (§4.2c). Seeded
|
|
137
|
+
// with the known instar initiatives; unmapped IDs fall back to a generic phrase.
|
|
138
|
+
export const ATTRIBUTION_LABELS = {
|
|
139
|
+
'failure-learning-loop': 'the failure-learning loop',
|
|
140
|
+
'process-health-dashboard-tab': 'the process-health tab',
|
|
141
|
+
'ledger-spine': 'the ledger spine',
|
|
142
|
+
'threadline-keystone': 'the agent-network keystone',
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
export function attributionLabel(initiativeId, labelMap = ATTRIBUTION_LABELS) {
|
|
146
|
+
if (typeof initiativeId === 'string' && labelMap[initiativeId]) return labelMap[initiativeId];
|
|
147
|
+
return 'a tracked feature';
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
export function relativeTime(iso, now = Date.now()) {
|
|
151
|
+
const t = typeof iso === 'string' ? Date.parse(iso) : NaN;
|
|
152
|
+
if (Number.isNaN(t)) return 'recently';
|
|
153
|
+
const sec = Math.max(0, Math.floor((now - t) / 1000));
|
|
154
|
+
if (sec < 90) return 'just now';
|
|
155
|
+
const min = Math.floor(sec / 60);
|
|
156
|
+
if (min < 60) return `about ${min} minute${min === 1 ? '' : 's'} ago`;
|
|
157
|
+
const hr = Math.floor(min / 60);
|
|
158
|
+
if (hr < 24) return `about ${hr} hour${hr === 1 ? '' : 's'} ago`;
|
|
159
|
+
const day = Math.floor(hr / 24);
|
|
160
|
+
if (day < 14) return `about ${spellSmall(day)} day${day === 1 ? '' : 's'} ago`;
|
|
161
|
+
const wk = Math.floor(day / 7);
|
|
162
|
+
if (wk < 8) return `about ${spellSmall(wk)} week${wk === 1 ? '' : 's'} ago`;
|
|
163
|
+
const mo = Math.floor(day / 30);
|
|
164
|
+
return `about ${spellSmall(mo)} month${mo === 1 ? '' : 's'} ago`;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const SMALL = ['zero', 'one', 'two', 'three', 'four', 'five', 'six', 'seven', 'eight', 'nine', 'ten'];
|
|
168
|
+
function spellSmall(n) {
|
|
169
|
+
return n >= 0 && n <= 10 ? SMALL[n] : String(n);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// ── DOM helpers (textContent ONLY — never innerHTML; §4.6) ──────────────────
|
|
173
|
+
function el(doc, tag, cls, text) {
|
|
174
|
+
const node = doc.createElement(tag);
|
|
175
|
+
if (cls) node.setAttribute('class', cls); // class is a static literal
|
|
176
|
+
if (text != null) node.textContent = text; // dynamic text → textContent ONLY
|
|
177
|
+
return node;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// ── Renderers (pure data→DOM; one section per function) ─────────────────────
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Headline (§4.2a, informational — NOT a verdict) + staleness escalation (§4.1).
|
|
184
|
+
* staleness keyed on the headline endpoint's OWN freshness (adversarial NEW-3).
|
|
185
|
+
*/
|
|
186
|
+
export function renderHeadline(doc, target, { failures, stale, staleAgeMin }) {
|
|
187
|
+
if (!target) return;
|
|
188
|
+
target.replaceChildren();
|
|
189
|
+
if (stale) {
|
|
190
|
+
target.appendChild(
|
|
191
|
+
el(doc, 'div', 'ph-headline', `Connection paused — showing the last view${staleAgeMin != null ? ` from ${staleAgeMin}m ago` : ''}`),
|
|
192
|
+
);
|
|
193
|
+
target.appendChild(el(doc, 'div', 'ph-subline', "Couldn't refresh just now — showing the last good view. Will retry."));
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
const list = Array.isArray(failures) ? failures : [];
|
|
197
|
+
const n = list.length;
|
|
198
|
+
const open = list.filter((f) => f && f.status !== 'resolved' && f.status !== 'closed').length;
|
|
199
|
+
target.appendChild(el(doc, 'div', 'ph-headline', `Watching — ${n} issue${n === 1 ? '' : 's'} recorded so far`));
|
|
200
|
+
const subline = n === 0 ? 'Nothing recorded yet — capture-only mode' : open === 0 ? 'all linked to a known cause · capture-only mode' : `${open} still being traced · capture-only mode`;
|
|
201
|
+
target.appendChild(el(doc, 'div', 'ph-subline', subline));
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Disabled (503) state (§4.5). Pinned copy + operator hint as PLAIN PROSE — no
|
|
206
|
+
* `<code>`, no monospace, no config-key string (reconciles §2/§4.5/§6.3).
|
|
207
|
+
*/
|
|
208
|
+
export function renderDisabled(doc, els) {
|
|
209
|
+
if (els.headline) {
|
|
210
|
+
els.headline.replaceChildren();
|
|
211
|
+
els.headline.appendChild(el(doc, 'div', 'ph-headline', 'Process Health isn’t turned on for this agent yet.'));
|
|
212
|
+
els.headline.appendChild(el(doc, 'div', 'ph-subline', 'Once it is, this page will show what it’s noticing.'));
|
|
213
|
+
const det = doc.createElement('details');
|
|
214
|
+
det.setAttribute('class', 'ph-operator');
|
|
215
|
+
const sum = doc.createElement('summary');
|
|
216
|
+
sum.setAttribute('class', 'ph-operator-summary');
|
|
217
|
+
sum.textContent = 'for operators';
|
|
218
|
+
det.appendChild(sum);
|
|
219
|
+
det.appendChild(el(doc, 'p', 'ph-operator-hint', 'An operator can enable it by turning the failure-learning monitor on in the agent settings.'));
|
|
220
|
+
els.headline.appendChild(det);
|
|
221
|
+
}
|
|
222
|
+
for (const k of ['patterns', 'captured', 'maturation', 'detail']) {
|
|
223
|
+
if (els[k]) els[k].replaceChildren();
|
|
224
|
+
}
|
|
225
|
+
if (els.stamp) els.stamp.textContent = '';
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/** Patterns cards (§4.2b — awareness-only, NO action authority). */
|
|
229
|
+
export function renderPatterns(doc, target, insights) {
|
|
230
|
+
if (!target) return;
|
|
231
|
+
target.replaceChildren();
|
|
232
|
+
const list = Array.isArray(insights) ? insights : [];
|
|
233
|
+
if (list.length === 0) {
|
|
234
|
+
target.appendChild(
|
|
235
|
+
el(doc, 'p', 'ph-empty', 'Nothing flagged yet. The monitor needs to see a wider variety of issues before it surfaces a pattern — that’s expected this early.'),
|
|
236
|
+
);
|
|
237
|
+
return;
|
|
238
|
+
}
|
|
239
|
+
for (const ins of list) {
|
|
240
|
+
const card = el(doc, 'div', 'ph-card');
|
|
241
|
+
card.appendChild(el(doc, 'div', 'ph-card-summary', sanitizeForDisplay(ins && ins.summary, 'summary')));
|
|
242
|
+
// Renderer-owned framing line (static literal — NOT a data-restateable prefix).
|
|
243
|
+
card.appendChild(el(doc, 'div', 'ph-card-frame', 'A pattern the monitor noticed — verify before acting.'));
|
|
244
|
+
if (ins && ins.recommendation) {
|
|
245
|
+
card.appendChild(el(doc, 'div', 'ph-card-rec', sanitizeForDisplay(ins.recommendation, 'recommendation')));
|
|
246
|
+
}
|
|
247
|
+
const ds = ins && Number.isFinite(ins.distinctSessions) ? ins.distinctSessions : 0;
|
|
248
|
+
card.appendChild(el(doc, 'div', 'ph-card-evidence', `Seen across ${ds} change${ds === 1 ? '' : 's'}.`));
|
|
249
|
+
target.appendChild(card);
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/** What's been captured (§4.2c — operator-safe subset only, sentences not rows). */
|
|
254
|
+
export function renderCaptured(doc, target, failures, labelMap = ATTRIBUTION_LABELS, now = Date.now()) {
|
|
255
|
+
if (!target) return;
|
|
256
|
+
target.replaceChildren();
|
|
257
|
+
const list = Array.isArray(failures) ? failures.slice(0, 10) : [];
|
|
258
|
+
if (list.length === 0) {
|
|
259
|
+
target.appendChild(el(doc, 'p', 'ph-empty', 'No issues recorded yet — that just means nothing has come through since this was turned on.'));
|
|
260
|
+
return;
|
|
261
|
+
}
|
|
262
|
+
for (const f of list) {
|
|
263
|
+
if (!f) continue;
|
|
264
|
+
const cat = friendlyCategory(f.category);
|
|
265
|
+
const summary = sanitizeForDisplay(f.summary, 'summary');
|
|
266
|
+
const label = attributionLabel(f.initiativeId, labelMap); // NEVER raw initiativeId
|
|
267
|
+
const when = relativeTime(f.detectedAt, now);
|
|
268
|
+
// ONE plain-English sentence. Separator + wording are renderer-owned literals.
|
|
269
|
+
const text = `${cat}: ${summary}${CHROME.sep}attributed to ${label}${CHROME.sep}${when}.`;
|
|
270
|
+
const row = el(doc, 'p', 'ph-record', text);
|
|
271
|
+
if (isMixedScript(f.summary)) row.setAttribute('class', 'ph-record ph-record-inert');
|
|
272
|
+
target.appendChild(row);
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/** Maturation track (§4.2d). 4th stage is permanently-future (no per-agent flag). */
|
|
277
|
+
const STAGES = [
|
|
278
|
+
{ key: 'dark', label: 'Dark' },
|
|
279
|
+
{ key: 'capture-only', label: 'Capture-only' },
|
|
280
|
+
{ key: 'insight-push', label: 'Insight push' },
|
|
281
|
+
{ key: 'default-on', label: 'Default for all agents' },
|
|
282
|
+
];
|
|
283
|
+
|
|
284
|
+
export function renderMaturation(doc, target, rollout) {
|
|
285
|
+
if (!target) return;
|
|
286
|
+
target.replaceChildren();
|
|
287
|
+
const stage = rollout && typeof rollout.stage === 'string' ? rollout.stage : 'dark';
|
|
288
|
+
const currentIdx = STAGES.findIndex((s) => s.key === stage);
|
|
289
|
+
for (let i = 0; i < STAGES.length; i++) {
|
|
290
|
+
const s = STAGES[i];
|
|
291
|
+
const isHere = i === currentIdx;
|
|
292
|
+
const done = currentIdx >= 0 && i < currentIdx;
|
|
293
|
+
const row = el(doc, 'div', isHere ? 'ph-stage ph-stage-here' : 'ph-stage');
|
|
294
|
+
// glyph + label + marker are ALL renderer-owned static literals
|
|
295
|
+
row.appendChild(el(doc, 'span', 'ph-stage-glyph', done || isHere ? CHROME.stageDone : CHROME.stageFuture));
|
|
296
|
+
row.appendChild(el(doc, 'span', 'ph-stage-label', s.label));
|
|
297
|
+
if (isHere) row.appendChild(el(doc, 'span', 'ph-stage-marker', CHROME.hereMarker));
|
|
298
|
+
else if (done) row.appendChild(el(doc, 'span', 'ph-stage-note', 'done'));
|
|
299
|
+
target.appendChild(row);
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
/** Detail drawer body (§4.2e — labeled list items, no tables/JSON/monospace). */
|
|
304
|
+
export function renderDetail(doc, target, analysis) {
|
|
305
|
+
if (!target) return;
|
|
306
|
+
target.replaceChildren();
|
|
307
|
+
if (!analysis) {
|
|
308
|
+
target.appendChild(el(doc, 'p', 'ph-empty', 'No detail yet.'));
|
|
309
|
+
return;
|
|
310
|
+
}
|
|
311
|
+
const add = (label, val) => target.appendChild(el(doc, 'div', 'ph-detail-row', `${label}: ${val}`));
|
|
312
|
+
add('Total recorded', String(analysis.total ?? 0));
|
|
313
|
+
add('Linked to a known cause', String(analysis.attributed ?? 0));
|
|
314
|
+
add('Not yet linked to a feature', String(analysis.noFeatureLink ?? 0));
|
|
315
|
+
const byCat = analysis.byCategory && typeof analysis.byCategory === 'object' ? analysis.byCategory : {};
|
|
316
|
+
for (const [k, v] of Object.entries(byCat)) {
|
|
317
|
+
add(`Category — ${sanitizeForDisplay(k, 'label')}`, String(v));
|
|
318
|
+
}
|
|
319
|
+
const unknown = analysis.unknownToolchainByAuthor && typeof analysis.unknownToolchainByAuthor === 'object' ? analysis.unknownToolchainByAuthor : {};
|
|
320
|
+
const unknownTotal = Object.values(unknown).reduce((a, b) => a + (Number(b) || 0), 0);
|
|
321
|
+
add('Records with an unknown toolchain', String(unknownTotal));
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
// ── Polling controller (§4.3 — visibility-gated, abort-safe, coordinated, diff-aware) ──
|
|
325
|
+
|
|
326
|
+
const ENDPOINTS = ['analysis', 'insights', 'failures'];
|
|
327
|
+
const URLS = {
|
|
328
|
+
analysis: '/failures/analysis',
|
|
329
|
+
insights: '/failures/insights?limit=50',
|
|
330
|
+
failures: '/failures?limit=10',
|
|
331
|
+
};
|
|
332
|
+
|
|
333
|
+
/**
|
|
334
|
+
* Factory so tests can inject fetch + clock + DOM. opts:
|
|
335
|
+
* - doc: Document; els: { headline, patterns, captured, maturation, detail, stamp }
|
|
336
|
+
* - fetchImpl: (url, {headers, signal}) => Promise<Response-like>
|
|
337
|
+
* - now: () => ms; cadenceMs (default 60_000); staleMs (default 180_000 = 3×cadence)
|
|
338
|
+
* - schedule/cancel: timer hooks (default setTimeout/clearTimeout)
|
|
339
|
+
*/
|
|
340
|
+
export function createController(opts) {
|
|
341
|
+
const {
|
|
342
|
+
doc,
|
|
343
|
+
els = {},
|
|
344
|
+
fetchImpl,
|
|
345
|
+
now = () => Date.now(),
|
|
346
|
+
cadenceMs = 60_000,
|
|
347
|
+
staleMs = 180_000,
|
|
348
|
+
schedule = (fn, ms) => setTimeout(fn, ms),
|
|
349
|
+
cancel = (id) => clearTimeout(id),
|
|
350
|
+
labelMap = ATTRIBUTION_LABELS,
|
|
351
|
+
} = opts;
|
|
352
|
+
|
|
353
|
+
const state = {
|
|
354
|
+
timerId: null,
|
|
355
|
+
inFlight: null,
|
|
356
|
+
active: false,
|
|
357
|
+
snapshot: { analysis: null, insights: null, failures: null },
|
|
358
|
+
etags: { analysis: null, insights: null, failures: null },
|
|
359
|
+
last200At: { analysis: 0, insights: 0, failures: 0 },
|
|
360
|
+
consecutiveFailedTicks: 0,
|
|
361
|
+
consecutive304Ticks: 0,
|
|
362
|
+
renderedSig: { headline: null, patterns: null, captured: null, maturation: null, detail: null, disabled: false },
|
|
363
|
+
nextDelayMs: cadenceMs,
|
|
364
|
+
};
|
|
365
|
+
|
|
366
|
+
async function fetchOne(key, controller) {
|
|
367
|
+
const headers = {};
|
|
368
|
+
if (state.etags[key]) headers['If-None-Match'] = state.etags[key];
|
|
369
|
+
const resp = await fetchImpl(URLS[key], { headers, signal: controller.signal });
|
|
370
|
+
if (resp.status === 304) return { key, status: 304 };
|
|
371
|
+
if (resp.status === 503) return { key, status: 503 }; // feature OFF — distinct from a transient failure
|
|
372
|
+
if (!resp.ok) throw new Error(`${key} ${resp.status}`);
|
|
373
|
+
const etag = resp.headers && typeof resp.headers.get === 'function' ? resp.headers.get('ETag') : null;
|
|
374
|
+
const body = await resp.json();
|
|
375
|
+
return { key, status: 200, etag, body };
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
async function tick() {
|
|
379
|
+
if (!state.active) return;
|
|
380
|
+
if (state.inFlight) {
|
|
381
|
+
try { state.inFlight.abort(); } catch { /* ignore */ }
|
|
382
|
+
}
|
|
383
|
+
const controller = typeof AbortController !== 'undefined' ? new AbortController() : { signal: undefined, abort() {} };
|
|
384
|
+
state.inFlight = controller;
|
|
385
|
+
let results;
|
|
386
|
+
try {
|
|
387
|
+
results = await Promise.all(ENDPOINTS.map((k) => fetchOne(k, controller)));
|
|
388
|
+
} catch (err) {
|
|
389
|
+
// Any endpoint hard-failed → drop the whole tick, keep prior render.
|
|
390
|
+
if (controller.signal && controller.signal.aborted) return; // superseded; let the new tick own state
|
|
391
|
+
state.inFlight = null;
|
|
392
|
+
state.consecutiveFailedTicks += 1;
|
|
393
|
+
render();
|
|
394
|
+
reschedule();
|
|
395
|
+
return;
|
|
396
|
+
}
|
|
397
|
+
if (controller.signal && controller.signal.aborted) return;
|
|
398
|
+
state.inFlight = null;
|
|
399
|
+
// Feature OFF (503) → pinned disabled copy, not the "connection paused" path.
|
|
400
|
+
if (results.some((r) => r.status === 503)) {
|
|
401
|
+
state.consecutiveFailedTicks = 0;
|
|
402
|
+
state.consecutive304Ticks = 0;
|
|
403
|
+
if (state.renderedSig.disabled !== true) {
|
|
404
|
+
renderDisabled(doc, els);
|
|
405
|
+
state.renderedSig = { headline: null, patterns: null, captured: null, maturation: null, detail: null, disabled: true };
|
|
406
|
+
}
|
|
407
|
+
reschedule();
|
|
408
|
+
return;
|
|
409
|
+
}
|
|
410
|
+
state.renderedSig.disabled = false;
|
|
411
|
+
let any200 = false;
|
|
412
|
+
let all304 = true;
|
|
413
|
+
for (const r of results) {
|
|
414
|
+
if (r.status === 200) {
|
|
415
|
+
any200 = true;
|
|
416
|
+
all304 = false;
|
|
417
|
+
state.snapshot[r.key] = r.body;
|
|
418
|
+
if (r.etag) state.etags[r.key] = r.etag;
|
|
419
|
+
state.last200At[r.key] = now();
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
if (any200) {
|
|
423
|
+
state.consecutiveFailedTicks = 0;
|
|
424
|
+
state.consecutive304Ticks = 0;
|
|
425
|
+
} else if (all304) {
|
|
426
|
+
state.consecutive304Ticks += 1;
|
|
427
|
+
}
|
|
428
|
+
render();
|
|
429
|
+
reschedule();
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
function reschedule() {
|
|
433
|
+
if (!state.active) return;
|
|
434
|
+
state.nextDelayMs = state.consecutive304Ticks >= 5 ? 300_000 : cadenceMs;
|
|
435
|
+
if (state.timerId != null) cancel(state.timerId);
|
|
436
|
+
state.timerId = schedule(() => { void tick(); }, state.nextDelayMs);
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
// Headline staleness keyed on the headline endpoint's OWN last-200 (NEW-3):
|
|
440
|
+
// either 2 hard-fail ticks, OR /failures hasn't produced a fresh 200 within the
|
|
441
|
+
// ceiling — even if siblings keep returning 200/304.
|
|
442
|
+
function headlineStale() {
|
|
443
|
+
if (state.consecutiveFailedTicks >= 2) return true;
|
|
444
|
+
const last = state.last200At.failures;
|
|
445
|
+
if (last === 0) return false; // never loaded yet → not "stale", just initial
|
|
446
|
+
return now() - last > staleMs;
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
function render() {
|
|
450
|
+
const stale = headlineStale();
|
|
451
|
+
const staleAgeMin = state.last200At.failures ? Math.max(0, Math.round((now() - state.last200At.failures) / 60_000)) : null;
|
|
452
|
+
const failuresBody = state.snapshot.failures;
|
|
453
|
+
const failures = failuresBody && Array.isArray(failuresBody.failures) ? failuresBody.failures : [];
|
|
454
|
+
const insightsBody = state.snapshot.insights;
|
|
455
|
+
const insights = insightsBody && Array.isArray(insightsBody.insights) ? insightsBody.insights : [];
|
|
456
|
+
const analysis = state.snapshot.analysis;
|
|
457
|
+
const rollout = analysis && analysis.rollout ? analysis.rollout : null;
|
|
458
|
+
|
|
459
|
+
// Diff-aware: skip a section's DOM writes when its inputs are unchanged
|
|
460
|
+
// (guarantees 0 mutations on identical ticks; no flicker). Sections rebuild
|
|
461
|
+
// via replaceChildren (NOT innerHTML) only when their signature changes.
|
|
462
|
+
sectionRender('headline', sig(stale, staleAgeMin, failures.length, countOpen(failures)), () =>
|
|
463
|
+
renderHeadline(doc, els.headline, { failures, stale, staleAgeMin }),
|
|
464
|
+
);
|
|
465
|
+
sectionRender('patterns', sig(insights), () => renderPatterns(doc, els.patterns, insights));
|
|
466
|
+
sectionRender('captured', sig(failures), () => renderCaptured(doc, els.captured, failures, labelMap, now()));
|
|
467
|
+
sectionRender('maturation', sig(rollout), () => renderMaturation(doc, els.maturation, rollout));
|
|
468
|
+
sectionRender('detail', sig(analysis), () => renderDetail(doc, els.detail, analysis));
|
|
469
|
+
|
|
470
|
+
if (els.stamp) {
|
|
471
|
+
const ageS = state.last200At.failures ? Math.max(0, Math.round((now() - state.last200At.failures) / 1000)) : null;
|
|
472
|
+
const stampText = ageS == null ? '' : ageS < 60 ? `updated ${ageS}s ago` : `updated ${Math.round(ageS / 60)}m ago`;
|
|
473
|
+
if (els.stamp.textContent !== stampText) els.stamp.textContent = stampText;
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
function sectionRender(key, signature, fn) {
|
|
478
|
+
if (state.renderedSig[key] === signature) return; // unchanged → 0 mutations
|
|
479
|
+
state.renderedSig[key] = signature;
|
|
480
|
+
fn();
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
function start() {
|
|
484
|
+
if (state.active) return;
|
|
485
|
+
state.active = true;
|
|
486
|
+
void tick();
|
|
487
|
+
}
|
|
488
|
+
function stop() {
|
|
489
|
+
state.active = false;
|
|
490
|
+
if (state.timerId != null) { cancel(state.timerId); state.timerId = null; }
|
|
491
|
+
if (state.inFlight) { try { state.inFlight.abort(); } catch { /* ignore */ } state.inFlight = null; }
|
|
492
|
+
}
|
|
493
|
+
function onVisible() { if (!state.active) { start(); } }
|
|
494
|
+
function onHidden() { stop(); }
|
|
495
|
+
|
|
496
|
+
return { start, stop, onVisible, onHidden, tick, render, _state: state };
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
function countOpen(failures) {
|
|
500
|
+
return failures.filter((f) => f && f.status !== 'resolved' && f.status !== 'closed').length;
|
|
501
|
+
}
|
|
502
|
+
function sig(...parts) {
|
|
503
|
+
try { return JSON.stringify(parts); } catch { return String(Math.random()); }
|
|
504
|
+
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"PostUpdateMigrator.d.ts","sourceRoot":"","sources":["../../src/core/PostUpdateMigrator.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAoCH,OAAO,EAEL,KAAK,YAAY,EACjB,KAAK,qBAAqB,EAC3B,MAAM,yBAAyB,CAAC;AAIjC,MAAM,WAAW,eAAe;IAC9B,wBAAwB;IACxB,QAAQ,EAAE,MAAM,EAAE,CAAC;IACnB,kCAAkC;IAClC,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,2CAA2C;IAC3C,MAAM,EAAE,MAAM,EAAE,CAAC;CAClB;AAED,MAAM,WAAW,cAAc;IAC7B,UAAU,EAAE,MAAM,CAAC;IACnB,QAAQ,EAAE,MAAM,CAAC;IACjB,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,OAAO,CAAC;IACrB,WAAW,EAAE,MAAM,CAAC;CACrB;AAED,qBAAa,kBAAkB;IAC7B,OAAO,CAAC,MAAM,CAAiB;IAC/B;;;;;;OAMG;IACH,OAAO,CAAC,UAAU,CAAiC;gBAEvC,MAAM,EAAE,cAAc;IAIlC;;;;;;;;;;;;;;OAcG;IACH,OAAO,CAAC,oBAAoB;IAoB5B;;;;;;;;;;;OAWG;IACH,YAAY,CAAC,IAAI,EAAE,YAAY,GAAG,IAAI;IAItC;;;;;;OAMG;IACG,eAAe,CACnB,WAAW,EAAE,MAAM,EACnB,SAAS,EAAE,MAAM,GAChB,OAAO,CAAC,qBAAqB,CAAC;IAIjC,OAAO,CAAC,aAAa;IAOrB,OAAO,CAAC,kBAAkB;IAO1B;;;;OAIG;IACH,OAAO,CAAC,YAAY;IASpB;;;OAGG;IACH,OAAO,IAAI,eAAe;IAwC1B;;;;;;;;;;;;;OAaG;IACH,OAAO,CAAC,0BAA0B;IAsElC,OAAO,CAAC,0BAA0B;IAmDlC;;;;;;;;;;OAUG;IACH,OAAO,CAAC,kCAAkC;IAwH1C,OAAO,CAAC,uBAAuB;IAwE/B,OAAO,CAAC,4CAA4C;IA+CpD,OAAO,CAAC,yBAAyB;IA6FjC;;;;;;;;;;OAUG;IACG,YAAY,IAAI,OAAO,CAAC,eAAe,CAAC;YA4BhC,uBAAuB;IAkGrC,OAAO,CAAC,0BAA0B;IAkGlC,OAAO,CAAC,0BAA0B;IAkElC,OAAO,CAAC,oBAAoB;IA4G5B,OAAO,CAAC,8BAA8B;IA2EtC;;;;OAIG;IACH,OAAO,CAAC,gBAAgB;IAaxB;;;;;;;;;;;;OAYG;IACH,OAAO,CAAC,0BAA0B;IA8BlC;;;;;;;;;;;OAWG;IACH,OAAO,CAAC,4BAA4B;IAwBpC;;;;;;;;;;;;;;;;OAgBG;IACH,OAAO,CAAC,mCAAmC;
|
|
1
|
+
{"version":3,"file":"PostUpdateMigrator.d.ts","sourceRoot":"","sources":["../../src/core/PostUpdateMigrator.ts"],"names":[],"mappings":"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"}
|
|
@@ -1263,9 +1263,11 @@ export class PostUpdateMigrator {
|
|
|
1263
1263
|
}
|
|
1264
1264
|
};
|
|
1265
1265
|
// Marker = the latest capability signature (bumped each time the bundled hook/
|
|
1266
|
-
// setup gains a feature, so prior installs upgrade): now
|
|
1267
|
-
//
|
|
1268
|
-
|
|
1266
|
+
// setup gains a feature, so prior installs upgrade): now notify-on-stop —
|
|
1267
|
+
// terminal exits (completion/duration/emergency) send a Telegram explaining
|
|
1268
|
+
// why the run stopped (2026-05-27 silent-stalls postmortem, Task 2). The
|
|
1269
|
+
// `notify_terminal_stop` function name is absent from all prior installs.
|
|
1270
|
+
upgrade('.claude/skills/autonomous/hooks/autonomous-stop-hook.sh', 'notify_terminal_stop', 'Autonomous Mode Stop Hook', 'skills/autonomous/hooks/autonomous-stop-hook.sh (topic-keyed + multi-session + completion evaluator + native /goal + notify-on-stop)');
|
|
1269
1271
|
upgrade('.claude/skills/autonomous/scripts/setup-autonomous.sh', 'native-goal/set', 'autonomous-state.local.md', 'skills/autonomous/scripts/setup-autonomous.sh (per-topic + completion-condition + native /goal)');
|
|
1270
1272
|
}
|
|
1271
1273
|
/**
|
|
@@ -3132,6 +3134,22 @@ Create worktrees for collaborator repos with \`instar worktree create <branch>\`
|
|
|
3132
3134
|
patched = true;
|
|
3133
3135
|
result.upgraded.push('CLAUDE.md: added "what are we working on" Registry-First row (initiative discoverability)');
|
|
3134
3136
|
}
|
|
3137
|
+
// Process Health (Dashboard Tab) — Agent Awareness backfill for the
|
|
3138
|
+
// Failure-Learning Loop's read surface. Fresh inits get this via
|
|
3139
|
+
// generateClaudeMd; existing agents get it here on update. The copy mirrors
|
|
3140
|
+
// the template exactly. Idempotent via content-sniff on the section title.
|
|
3141
|
+
if (!content.includes('Process Health (Dashboard Tab)')) {
|
|
3142
|
+
const section = `
|
|
3143
|
+
**Process Health (Dashboard Tab)** — A calm, human-readable window into the Failure-Learning Loop. The loop's findings are otherwise invisible (API-only); this tab shows, in plain English and large type, what's being watched, any patterns surfaced, and where the rollout sits.
|
|
3144
|
+
- **Where**: the "Process Health" tab in the dashboard. Refreshes itself quietly; nothing to run.
|
|
3145
|
+
- **What it shows**: an informational headline ("Watching — N issues recorded"), surfaced patterns (awareness-only — never auto-acted-on), recent captures as plain sentences, and the maturation track. A collapsed "Detail" drawer holds the aggregate counts.
|
|
3146
|
+
- **Proactive trigger**: when the user asks "is the loop noticing anything? / how's the rollout going? / what's it found?" → point them to the Process Health tab (give the dashboard URL + PIN). Do NOT paraphrase \`/failures*\` curl output at them — the tab IS the answer surface. Only read \`/failures/analysis\` yourself when you need the raw numbers for your own reasoning.
|
|
3147
|
+
- **Disabled note**: when \`monitoring.failureLearning.enabled\` is false the tab shows a friendly "not turned on yet" message, not an error.
|
|
3148
|
+
`;
|
|
3149
|
+
content += '\n' + section;
|
|
3150
|
+
patched = true;
|
|
3151
|
+
result.upgraded.push('CLAUDE.md: added Process Health dashboard tab awareness section');
|
|
3152
|
+
}
|
|
3135
3153
|
if (patched) {
|
|
3136
3154
|
try {
|
|
3137
3155
|
fs.writeFileSync(claudeMdPath, content);
|
|
@@ -3197,6 +3215,7 @@ Create worktrees for collaborator repos with \`instar worktree create <branch>\`
|
|
|
3197
3215
|
'## Threadline Network (Agent-to-Agent Communication)',
|
|
3198
3216
|
'## Worktree Convention',
|
|
3199
3217
|
'**Multi-Session Autonomy**',
|
|
3218
|
+
'**Process Health (Dashboard Tab)**',
|
|
3200
3219
|
];
|
|
3201
3220
|
for (const shadowName of ['AGENTS.md', 'GEMINI.md']) {
|
|
3202
3221
|
const shadowPath = path.join(this.config.projectDir, shadowName);
|