instar 1.3.44 → 1.3.45

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.
@@ -795,24 +795,74 @@
795
795
  .ph-headline { font-size: 26px; font-weight: 600; line-height: 1.5; color: var(--text-bright); }
796
796
  .ph-subline { font-size: 18px; color: var(--text-dim); margin-top: 8px; line-height: 1.55; }
797
797
  .ph-section { display: flex; flex-direction: column; gap: 10px; }
798
- .ph-h { font-size: 20px; font-weight: 600; color: var(--text-bright); line-height: 1.5; }
798
+ .ph-h { font-size: 20px; font-weight: 600; color: var(--text-bright); line-height: 1.5; margin: 0; }
799
+ .ph-h-sub { font-size: 15px; color: var(--text-dim); line-height: 1.55; margin: 4px 0 4px 0; max-width: 720px; }
799
800
  .ph-empty { font-size: 18px; color: var(--text-dim); line-height: 1.6; }
800
- .ph-card {
801
- padding: 14px 16px; border: 1px solid var(--border); border-radius: 10px;
802
- display: flex; flex-direction: column; gap: 6px;
801
+ /* Page intro — one-sentence grounding right under the title so terms like
802
+ "it" / "the system" later in the page have a clear referent. */
803
+ .ph-intro { font-size: 17px; color: var(--text); line-height: 1.55; margin: 0; max-width: 720px; }
804
+ /* Status color tokens — calm, never alarming. The dot is a scan-aid:
805
+ glance the collapsed list, see which problems are still being worked
806
+ on (amber), cause known (blue), or worked out (green). */
807
+ .ph-status-dot {
808
+ display: inline-block; width: 10px; height: 10px; border-radius: 50%;
809
+ flex-shrink: 0; margin-top: 8px; background: var(--text-dim);
810
+ }
811
+ .ph-status-dot.status-open { background: #f59e0b; } /* amber: still looking */
812
+ .ph-status-dot.status-attributed{ background: #60a5fa; } /* soft blue: cause known */
813
+ .ph-status-dot.status-verified { background: var(--ph-accent); }
814
+ .ph-status-dot.status-resolved { background: var(--ph-accent); }
815
+ .ph-status-dot.status-closed { background: #6b7280; } /* muted gray */
816
+ /* Labeled rows inside the expanded body — scaffolding, not paragraphs. */
817
+ .ph-label { font-weight: 600; color: var(--text); margin-right: 4px; }
818
+ .ph-value { color: var(--text-dim); }
819
+ /* Expandable items (captured problems + pattern cards): summary always
820
+ visible, body revealed on tap. Plain ELI16 inside both. */
821
+ .ph-item {
822
+ border: 1px solid var(--border); border-radius: 10px; padding: 0;
823
+ background: transparent;
803
824
  }
825
+ .ph-item + .ph-item { margin-top: 10px; }
826
+ .ph-item-summary {
827
+ padding: 14px 44px 14px 16px; font-size: 18px; color: var(--text); line-height: 1.6;
828
+ cursor: pointer; list-style: none; position: relative;
829
+ display: flex; flex-direction: row; align-items: flex-start; gap: 12px;
830
+ }
831
+ .ph-item-text { flex: 1; display: flex; flex-direction: column; gap: 6px; }
832
+ .ph-item-summary::-webkit-details-marker { display: none; }
833
+ /* Subtle chevron on the right edge — signals "tap to expand" without shouting. */
834
+ .ph-item-summary::after {
835
+ content: '▾'; position: absolute; right: 16px; top: 16px;
836
+ color: var(--text-dim); font-size: 16px; opacity: 0.8;
837
+ }
838
+ .ph-item[open] > .ph-item-summary::after { content: '▴'; opacity: 1; }
839
+ .ph-item[open] > .ph-item-summary { color: var(--text-bright); border-bottom: 1px solid var(--border); }
840
+ .ph-item-body { padding: 14px 16px; display: flex; flex-direction: column; gap: 8px; }
841
+ .ph-item-row { font-size: 16px; color: var(--text-dim); line-height: 1.55; }
842
+ .ph-item-row.ph-card-rec { color: var(--text); }
843
+ .ph-item-inert { opacity: 0.7; }
844
+ /* Pattern-card-specific lines inside the summary stack. */
804
845
  .ph-card-summary { font-size: 18px; color: var(--text-bright); line-height: 1.5; }
805
846
  .ph-card-frame { font-size: 15px; color: var(--text-dim); font-style: italic; }
806
- .ph-card-rec { font-size: 17px; color: var(--text); line-height: 1.55; }
807
- .ph-card-evidence { font-size: 15px; color: var(--text-dim); }
808
- .ph-record { font-size: 18px; color: var(--text); line-height: 1.6; padding: 5px 0; }
809
- .ph-record-inert { color: var(--text-dim); }
810
- .ph-stage { display: flex; align-items: baseline; gap: 10px; font-size: 18px; padding: 3px 0; color: var(--text-dim); line-height: 1.5; }
811
- .ph-stage-here { color: var(--text-bright); }
847
+ /* Maturation track: each stage is also an expandable row. */
848
+ .ph-stage {
849
+ padding: 4px 0; line-height: 1.5; color: var(--text-dim);
850
+ }
851
+ .ph-stage-summary {
852
+ display: flex; align-items: baseline; gap: 10px; font-size: 18px;
853
+ cursor: pointer; list-style: none; padding: 4px 0; position: relative;
854
+ }
855
+ .ph-stage-summary::-webkit-details-marker { display: none; }
856
+ .ph-stage-summary::after {
857
+ content: '▾'; margin-left: auto; color: var(--text-dim); font-size: 12px; opacity: 0.4;
858
+ }
859
+ .ph-stage[open] > .ph-stage-summary::after { content: '▴'; opacity: 0.8; }
860
+ .ph-stage-here > .ph-stage-summary { color: var(--text-bright); }
812
861
  .ph-stage-glyph { color: var(--text-dim); }
813
862
  .ph-stage-here .ph-stage-glyph { color: var(--ph-accent); }
814
863
  .ph-stage-marker { color: var(--ph-accent); font-size: 15px; }
815
864
  .ph-stage-note { color: var(--text-dim); font-size: 15px; }
865
+ .ph-stage-body { font-size: 16px; color: var(--text-dim); line-height: 1.55; padding: 6px 0 6px 26px; }
816
866
  .ph-detail { margin-top: 4px; }
817
867
  .ph-detail-summary { font-size: 17px; color: var(--text-dim); cursor: pointer; }
818
868
  .ph-detail-body { display: flex; flex-direction: column; gap: 6px; margin-top: 12px; }
@@ -3063,21 +3113,25 @@
3063
3113
  <h2 class="ph-title">Process Health</h2>
3064
3114
  <div id="phStamp" class="ph-stamp" aria-live="polite"></div>
3065
3115
  </div>
3116
+ <p class="ph-intro">A quiet look at what the failure-watching system has spotted in this agent’s recent work — problems it’s caught, any patterns that keep coming up, and how far the system is switched on for this agent.</p>
3066
3117
  <div id="phHeadline" class="ph-headline-wrap" aria-live="polite"></div>
3067
3118
  <section class="ph-section">
3068
- <h3 class="ph-h">Patterns to know about</h3>
3119
+ <h3 class="ph-h">Recurring patterns</h3>
3120
+ <p class="ph-h-sub">Kinds of problem that have come up more than once across different changes.</p>
3069
3121
  <div id="phPatterns"></div>
3070
3122
  </section>
3071
3123
  <section class="ph-section">
3072
- <h3 class="ph-h">What’s been captured</h3>
3124
+ <h3 class="ph-h">Problems it’s noticed lately</h3>
3125
+ <p class="ph-h-sub">Individual things the system has caught. When the same kind keeps repeating, it shows up as a pattern above.</p>
3073
3126
  <div id="phCaptured"></div>
3074
3127
  </section>
3075
3128
  <section class="ph-section">
3076
- <h3 class="ph-h">Maturation</h3>
3129
+ <h3 class="ph-h">How far the system is switched on</h3>
3130
+ <p class="ph-h-sub">Where this agent sits on the rollout — from "off" to "on for everyone by default".</p>
3077
3131
  <div id="phMaturation"></div>
3078
3132
  </section>
3079
3133
  <details class="ph-detail">
3080
- <summary class="ph-detail-summary">Detail</summary>
3134
+ <summary class="ph-detail-summary">More detail</summary>
3081
3135
  <div id="phDetail" class="ph-detail-body"></div>
3082
3136
  </details>
3083
3137
  </div>
@@ -116,35 +116,51 @@ export function safeUrl(value) {
116
116
 
117
117
  // ── Friendly wording (§4.2c) ───────────────────────────────────────────────
118
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',
119
+ concurrency: 'A timing problem (two things ran at once)',
120
+ 'config-parse': 'A settings problem',
121
+ 'type-error': 'A data mix-up (wrong kind of value)',
122
+ 'logic-error': 'A logic mistake',
123
+ 'test-failure': 'A test that failed',
124
+ 'build-failure': 'The build broke',
125
+ 'integration-gap': "Two parts that didn't connect",
126
+ regression: 'Something that used to work broke',
127
+ 'wiring-gap': "Something that wasn't hooked up",
128
+ unknown: 'A problem',
129
129
  };
130
130
 
131
131
  export function friendlyCategory(category) {
132
132
  const key = typeof category === 'string' ? category : 'unknown';
133
- return CATEGORY_WORDS[key] || 'An issue';
133
+ return CATEGORY_WORDS[key] || 'A problem';
134
134
  }
135
135
 
136
+ // Short plural nouns for the Detail drawer's per-type counts (plain English, no
137
+ // raw category codes like "config-parse"). Unknown keys fall back to the
138
+ // sanitized raw key (defense-in-depth — these are server-controlled enum values).
139
+ const TYPE_WORDS = {
140
+ concurrency: 'Timing problems',
141
+ 'config-parse': 'Settings problems',
142
+ 'type-error': 'Data mix-ups',
143
+ 'logic-error': 'Logic mistakes',
144
+ 'test-failure': 'Failing tests',
145
+ 'build-failure': 'Build breaks',
146
+ 'integration-gap': 'Parts that didn’t connect',
147
+ regression: 'Things that broke again',
148
+ 'wiring-gap': 'Things not hooked up',
149
+ unknown: 'Other',
150
+ };
151
+
136
152
  // initiativeId → human label. NEVER expose the raw initiativeId (§4.2c). Seeded
137
153
  // with the known instar initiatives; unmapped IDs fall back to a generic phrase.
138
154
  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',
155
+ 'failure-learning-loop': 'the failure-watching feature',
156
+ 'process-health-dashboard-tab': 'this Process Health page',
157
+ 'ledger-spine': 'the record-keeping feature',
158
+ 'threadline-keystone': 'the agent-to-agent messaging feature',
143
159
  };
144
160
 
145
161
  export function attributionLabel(initiativeId, labelMap = ATTRIBUTION_LABELS) {
146
162
  if (typeof initiativeId === 'string' && labelMap[initiativeId]) return labelMap[initiativeId];
147
- return 'a tracked feature';
163
+ return 'a feature we were working on';
148
164
  }
149
165
 
150
166
  export function relativeTime(iso, now = Date.now()) {
@@ -188,16 +204,21 @@ export function renderHeadline(doc, target, { failures, stale, staleAgeMin }) {
188
204
  target.replaceChildren();
189
205
  if (stale) {
190
206
  target.appendChild(
191
- el(doc, 'div', 'ph-headline', `Connection paused — showing the last view${staleAgeMin != null ? ` from ${staleAgeMin}m ago` : ''}`),
207
+ el(doc, 'div', 'ph-headline', `Can't refresh right now — showing the last update${staleAgeMin != null ? ` from ${staleAgeMin} min ago` : ''}`),
192
208
  );
193
- target.appendChild(el(doc, 'div', 'ph-subline', "Couldn't refresh just now — showing the last good view. Will retry."));
209
+ target.appendChild(el(doc, 'div', 'ph-subline', "We'll keep trying — nothing is lost."));
194
210
  return;
195
211
  }
196
212
  const list = Array.isArray(failures) ? failures : [];
197
213
  const n = list.length;
198
214
  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`;
215
+ const headline = n === 0 ? 'Keeping an eye out — all quiet so far' : `Keeping an eye out — ${n} thing${n === 1 ? '' : 's'} noticed so far`;
216
+ target.appendChild(el(doc, 'div', 'ph-headline', headline));
217
+ const subline = n === 0
218
+ ? "Nothing has come up yet. We're just quietly watching — no alerts."
219
+ : open === 0
220
+ ? "We know what caused each one. Just quietly watching — no alerts yet."
221
+ : `Still looking into ${open} of them. Just quietly watching — no alerts yet.`;
201
222
  target.appendChild(el(doc, 'div', 'ph-subline', subline));
202
223
  }
203
224
 
@@ -214,9 +235,9 @@ export function renderDisabled(doc, els) {
214
235
  det.setAttribute('class', 'ph-operator');
215
236
  const sum = doc.createElement('summary');
216
237
  sum.setAttribute('class', 'ph-operator-summary');
217
- sum.textContent = 'for operators';
238
+ sum.textContent = 'For whoever set this up';
218
239
  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.'));
240
+ det.appendChild(el(doc, 'p', 'ph-operator-hint', 'You can switch it on in this agent’s settings.'));
220
241
  els.headline.appendChild(det);
221
242
  }
222
243
  for (const k of ['patterns', 'captured', 'maturation', 'detail']) {
@@ -225,38 +246,139 @@ export function renderDisabled(doc, els) {
225
246
  if (els.stamp) els.stamp.textContent = '';
226
247
  }
227
248
 
228
- /** Patterns cards (§4.2b — awareness-only, NO action authority). */
249
+ /** Insight lifecycle wording — informational only, no action-implying language. */
250
+ const INSIGHT_STATUS_WORDS = {
251
+ discovered: 'Just noticed',
252
+ 'acted-on': 'Being looked into',
253
+ verified: 'Confirmed no longer happening',
254
+ reopened: 'Came back after the previous attempt',
255
+ };
256
+ function friendlyInsightStatus(status) {
257
+ return INSIGHT_STATUS_WORDS[status] || 'Not set';
258
+ }
259
+
260
+ /**
261
+ * Patterns cards (§4.2b — awareness-only, NO action authority). Each card is
262
+ * an expandable <details> element: the headline summary is visible by default;
263
+ * the recommendation, evidence breakdown, and lifecycle are revealed on tap.
264
+ * The renderer-owned "verify before acting" framing stays on the summary so
265
+ * the no-action-authority signal is never hidden behind a tap.
266
+ */
229
267
  export function renderPatterns(doc, target, insights) {
230
268
  if (!target) return;
231
269
  target.replaceChildren();
232
270
  const list = Array.isArray(insights) ? insights : [];
233
271
  if (list.length === 0) {
234
272
  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.'),
273
+ el(doc, 'p', 'ph-empty', 'Nothing to flag yet. We need to see a few more different problems before a real pattern shows up — totally normal this early.'),
236
274
  );
237
275
  return;
238
276
  }
239
277
  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
- }
278
+ const card = doc.createElement('details');
279
+ card.setAttribute('class', 'ph-item ph-card');
280
+ const sum = doc.createElement('summary');
281
+ sum.setAttribute('class', 'ph-item-summary');
282
+ sum.appendChild(statusDot(doc, ins && ins.status));
283
+ // Just the pattern headline — the section title + subtitle above already
284
+ // establish "this is a kind of problem that has repeated", so a per-card
285
+ // restatement would be the third echo of the same idea. The "informational
286
+ // only, not a prompt to act" signal is structural now (the rendered card
287
+ // has nothing actionable on it), not chrome.
288
+ const text = el(doc, 'div', 'ph-item-text');
289
+ text.appendChild(el(doc, 'div', 'ph-card-summary', sanitizeForDisplay(ins && ins.summary, 'summary')));
290
+ sum.appendChild(text);
291
+ card.appendChild(sum);
292
+ // Body: same fixed label vocabulary as captured items, in the same order.
293
+ // Status · Times seen · First noticed. (Where/Cause don't apply to a class.)
294
+ // The recommendation field is intentionally omitted: this tab informs,
295
+ // doesn't direct — actionable insight is the deferred §10 surface.
296
+ const body = el(doc, 'div', 'ph-item-body');
297
+ body.appendChild(labeledRow(doc, 'Status', friendlyInsightStatus(ins && ins.status)));
247
298
  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'}.`));
299
+ const dc = ins && Number.isFinite(ins.distinctCauseCommits) ? ins.distinctCauseCommits : 0;
300
+ const timesSeen = dc > 0
301
+ ? `Across ${ds} different change${ds === 1 ? '' : 's'} (from ${dc} separate fix${dc === 1 ? '' : 'es'})`
302
+ : `Across ${ds} different change${ds === 1 ? '' : 's'}`;
303
+ body.appendChild(labeledRow(doc, 'Times seen', timesSeen));
304
+ if (ins && ins.discoveredAt) {
305
+ body.appendChild(labeledRow(doc, 'First noticed', relativeTime(ins.discoveredAt)));
306
+ }
307
+ card.appendChild(body);
249
308
  target.appendChild(card);
250
309
  }
251
310
  }
252
311
 
253
- /** What's been captured (§4.2c — operator-safe subset only, sentences not rows). */
312
+ /**
313
+ * Friendly status / cause-confidence wording for the expandable body (§4.2c).
314
+ * Plain ELI16, no Instar-internal jargon. Unknown values fall back to a generic
315
+ * phrase — the body never shows raw status/attribution codes.
316
+ */
317
+ // Terser status values — these pair with a bold "Status:" label in the body
318
+ // (so the value carries the state, the label carries the noun).
319
+ const STATUS_WORDS = {
320
+ open: 'Still looking into this',
321
+ attributed: 'We know what caused it',
322
+ verified: 'Confirmed worked out',
323
+ resolved: 'Worked out',
324
+ closed: 'Closed',
325
+ };
326
+ function friendlyStatus(status) {
327
+ return STATUS_WORDS[status] || 'Not set';
328
+ }
329
+
330
+ const CAUSE_CONFIDENCE_WORDS = {
331
+ automatic: 'Auto-spotted from the fix commit — we’re confident',
332
+ 'one-tap': 'Tagged by hand by whoever found it',
333
+ inferred: 'Best guess from the surrounding context',
334
+ };
335
+ function friendlyCauseConfidence(attribution) {
336
+ return CAUSE_CONFIDENCE_WORDS[attribution] || 'Not recorded';
337
+ }
338
+
339
+ /**
340
+ * Map an upstream status (failure or insight) to a status-dot CSS class.
341
+ * The dot is a scan-aid only — colors are calm (amber/blue/green/gray),
342
+ * never alarm reds.
343
+ */
344
+ export function statusDotClass(status) {
345
+ switch (status) {
346
+ case 'open': case 'reopened': case 'discovered': return 'status-open';
347
+ case 'attributed': case 'acted-on': return 'status-attributed';
348
+ case 'verified': case 'resolved': return 'status-verified';
349
+ case 'closed': return 'status-closed';
350
+ default: return 'status-closed';
351
+ }
352
+ }
353
+
354
+ /**
355
+ * Build a labeled-row inside the expanded body: `<bold-label>:<value>`. Keeps
356
+ * the structure scannable without paragraph-walls. Both halves are textContent
357
+ * (the safety contract is unaffected).
358
+ */
359
+ function labeledRow(doc, label, value) {
360
+ const row = el(doc, 'div', 'ph-item-row');
361
+ row.appendChild(el(doc, 'span', 'ph-label', `${label}:`));
362
+ row.appendChild(doc.createTextNode(' '));
363
+ row.appendChild(el(doc, 'span', 'ph-value', value));
364
+ return row;
365
+ }
366
+
367
+ /** Status-dot span (always at the left edge of an item summary; color-coded by status). */
368
+ function statusDot(doc, status) {
369
+ const dot = doc.createElement('span');
370
+ dot.setAttribute('class', `ph-status-dot ${statusDotClass(status)}`);
371
+ dot.setAttribute('aria-hidden', 'true');
372
+ return dot;
373
+ }
374
+
375
+ /** What's been captured (§4.2c — operator-safe subset only, expandable for depth on demand). */
254
376
  export function renderCaptured(doc, target, failures, labelMap = ATTRIBUTION_LABELS, now = Date.now()) {
255
377
  if (!target) return;
256
378
  target.replaceChildren();
257
379
  const list = Array.isArray(failures) ? failures.slice(0, 10) : [];
258
380
  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.'));
381
+ target.appendChild(el(doc, 'p', 'ph-empty', 'Nothing yet — that just means nothing has come up since this was switched on.'));
260
382
  return;
261
383
  }
262
384
  for (const f of list) {
@@ -265,22 +387,45 @@ export function renderCaptured(doc, target, failures, labelMap = ATTRIBUTION_LAB
265
387
  const summary = sanitizeForDisplay(f.summary, 'summary');
266
388
  const label = attributionLabel(f.initiativeId, labelMap); // NEVER raw initiativeId
267
389
  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);
390
+ // The collapsed summary line — one plain-English sentence, as before.
391
+ const summaryText = `${cat}: ${summary}${CHROME.sep}came up while building ${label}${CHROME.sep}${when}.`;
392
+ const item = doc.createElement('details');
393
+ item.setAttribute('class', isMixedScript(f.summary) ? 'ph-item ph-item-inert' : 'ph-item');
394
+ const sum = doc.createElement('summary');
395
+ sum.setAttribute('class', 'ph-item-summary');
396
+ sum.appendChild(statusDot(doc, f.status));
397
+ sum.appendChild(el(doc, 'div', 'ph-item-text', summaryText));
398
+ item.appendChild(sum);
399
+ // Expanded body: a fixed-shape fact sheet. Same labels in the same order
400
+ // are used by patterns (§4.2b) where applicable — predictable scanning.
401
+ // Order: Status · Where · Times seen · First noticed · Cause.
402
+ const body = el(doc, 'div', 'ph-item-body');
403
+ body.appendChild(labeledRow(doc, 'Status', friendlyStatus(f.status)));
404
+ if (f.detail && typeof f.detail.redacted === 'string' && f.detail.redacted.trim()) {
405
+ body.appendChild(labeledRow(doc, 'Where', sanitizeForDisplay(f.detail.redacted, 'detail')));
406
+ }
407
+ const occ = Number.isFinite(f.occurrenceCount) ? f.occurrenceCount : 1;
408
+ body.appendChild(labeledRow(doc, 'Times seen', occ > 1 ? `${occ} times so far` : 'Just once'));
409
+ body.appendChild(labeledRow(doc, 'First noticed', relativeTime(f.detectedAt, now)));
410
+ body.appendChild(labeledRow(doc, 'Cause', friendlyCauseConfidence(f.attribution)));
411
+ item.appendChild(body);
412
+ target.appendChild(item);
273
413
  }
274
414
  }
275
415
 
276
416
  /** Maturation track (§4.2d). 4th stage is permanently-future (no per-agent flag). */
277
417
  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' },
418
+ { key: 'dark', label: 'Not switched on yet', description: 'Not switched on for this agent yet — nothing is being recorded.' },
419
+ { key: 'capture-only', label: 'Quietly watching', description: 'Quietly recording what comes up. No alerts go anywhere — we’re just building up evidence to learn from.' },
420
+ { key: 'insight-push', label: 'Watching + flagging patterns to you', description: 'Same quiet recording, plus a heads-up when the same kind of problem keeps showing up. Only fires on real patterns, never on one-offs.' },
421
+ { key: 'default-on', label: 'On for everyone by default', description: 'Switched on for every agent by default — the fully-rolled-out end state.' },
282
422
  ];
283
423
 
424
+ /**
425
+ * Maturation track (§4.2d). Each stage is an expandable <details> row so the
426
+ * label stays glanceable but the meaning is one tap away. The 4th stage is
427
+ * permanently-future (no per-agent flag).
428
+ */
284
429
  export function renderMaturation(doc, target, rollout) {
285
430
  if (!target) return;
286
431
  target.replaceChildren();
@@ -290,12 +435,17 @@ export function renderMaturation(doc, target, rollout) {
290
435
  const s = STAGES[i];
291
436
  const isHere = i === currentIdx;
292
437
  const done = currentIdx >= 0 && i < currentIdx;
293
- const row = el(doc, 'div', isHere ? 'ph-stage ph-stage-here' : 'ph-stage');
438
+ const row = doc.createElement('details');
439
+ row.setAttribute('class', isHere ? 'ph-stage ph-stage-here' : 'ph-stage');
440
+ const sum = doc.createElement('summary');
441
+ sum.setAttribute('class', 'ph-stage-summary');
294
442
  // 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'));
443
+ sum.appendChild(el(doc, 'span', 'ph-stage-glyph', done || isHere ? CHROME.stageDone : CHROME.stageFuture));
444
+ sum.appendChild(el(doc, 'span', 'ph-stage-label', s.label));
445
+ if (isHere) sum.appendChild(el(doc, 'span', 'ph-stage-marker', CHROME.hereMarker));
446
+ else if (done) sum.appendChild(el(doc, 'span', 'ph-stage-note', 'done'));
447
+ row.appendChild(sum);
448
+ row.appendChild(el(doc, 'div', 'ph-stage-body', s.description));
299
449
  target.appendChild(row);
300
450
  }
301
451
  }
@@ -309,16 +459,17 @@ export function renderDetail(doc, target, analysis) {
309
459
  return;
310
460
  }
311
461
  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));
462
+ add('Total noticed', String(analysis.total ?? 0));
463
+ add('Ones we know the cause of', String(analysis.attributed ?? 0));
464
+ add('Not yet tied to a feature', String(analysis.noFeatureLink ?? 0));
315
465
  const byCat = analysis.byCategory && typeof analysis.byCategory === 'object' ? analysis.byCategory : {};
316
466
  for (const [k, v] of Object.entries(byCat)) {
317
- add(`Category — ${sanitizeForDisplay(k, 'label')}`, String(v));
467
+ const typeLabel = TYPE_WORDS[k] || sanitizeForDisplay(k, 'label');
468
+ add(typeLabel, String(v));
318
469
  }
319
470
  const unknown = analysis.unknownToolchainByAuthor && typeof analysis.unknownToolchainByAuthor === 'object' ? analysis.unknownToolchainByAuthor : {};
320
471
  const unknownTotal = Object.values(unknown).reduce((a, b) => a + (Number(b) || 0), 0);
321
- add('Records with an unknown toolchain', String(unknownTotal));
472
+ add("Ones where we're not sure which tools were involved", String(unknownTotal));
322
473
  }
323
474
 
324
475
  // ── Polling controller (§4.3 — visibility-gated, abort-safe, coordinated, diff-aware) ──
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "instar",
3
- "version": "1.3.44",
3
+ "version": "1.3.45",
4
4
  "description": "Coherence infrastructure for self-evolving AI agents — on the Claude Code or Codex subscription you already have.",
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-28T01:59:27.143Z",
5
- "instarVersion": "1.3.44",
4
+ "generatedAt": "2026-05-28T02:17:30.185Z",
5
+ "instarVersion": "1.3.45",
6
6
  "entryCount": 193,
7
7
  "entries": {
8
8
  "hook:session-start": {
@@ -0,0 +1,28 @@
1
+ # Upgrade Guide — vNEXT
2
+
3
+ <!-- bump: patch -->
4
+
5
+ ## What Changed
6
+
7
+ UX refinement of the Process Health dashboard tab shipped in v1.3.27. Eight rounds of user-driven copy + structure iteration landed on the design that became the **Dashboard Standard** — the bar all future dashboard features now start from. Pure dashboard-only change (`dashboard/process-health.js` + `dashboard/index.html` + tests); no `src/` / `scripts/` / `.husky/` / `skills/` changes.
8
+
9
+ What the page looks like now: a one-sentence intro grounds the subject so "it" later in the page has a referent; each section title carries a one-line subtitle that distinguishes it from the others (with explicit cross-references — "When the same kind keeps repeating, it shows up as a pattern above"); each captured problem and pattern card is a `<details>` element with a status color dot on the left (amber/blue/green/gray — never alarm reds) and a chevron on the right; the expanded body is a labeled fact-sheet using a fixed vocabulary (Status / Where / Times seen / First noticed / Cause) used identically across both lists. All action-implying language ("Suggested next step", "verify before acting", "Fix worked") is gone — the page informs, it doesn't direct. Internal codenames (`capture-only`, `failure-learning-loop`, `config-parse`) never reach the DOM; they pass through renderer-owned plain-English maps.
10
+
11
+ The reference implementation now backs a durable agent memory (`feedback_dashboard_copy_eli16.md`) that codifies the seven rules + eight default substitutions + four status-color tokens so future dashboard work starts from this bar.
12
+
13
+ ## What to Tell Your User
14
+
15
+ - The Process Health tab on your dashboard reads a lot more clearly now: a calm intro tells you what the page is showing in one sentence, every problem and pattern collapses to one plain line with a small color dot (amber = still being looked into, blue = cause known, green = worked out, gray = closed), and tapping any row opens up the same set of labeled details — Status, Where, Times seen, First noticed, Cause — every time. The bar this set for "what a calm, informative dashboard reads like" is now the standard for everything I build next.
16
+
17
+ ## Summary of New Capabilities
18
+
19
+ | Capability | How to Use |
20
+ |-----------|-----------|
21
+ | Process Health tab — ELI16 + structure pass | Open the Process Health tab in your dashboard (no action needed; it’s just clearer now) |
22
+ | Dashboard Standard (durable, applies to future dashboard work) | Reference: `feedback_dashboard_copy_eli16.md` (in agent memory) — 7 rules + 8 substitutions + 4 status-color tokens |
23
+
24
+ ## Evidence
25
+
26
+ - 46 tests green: 30 unit + 12 integration + 4 e2e — includes structural assertions for the unified label order, status-dot CSS classes, and that the dropped framing line is genuinely absent.
27
+ - Safety contract unchanged: XSS negative fixture still fires zero live elements + no canary; `detail.full` redaction is verified end-to-end.
28
+ - Show-don't-tell iteration: eight rounds, each rendered to a real browser and screenshotted for user thumbs-up before committing — no merge-and-iterate churn. The harness pattern is captured in the standard.
@@ -0,0 +1,49 @@
1
+ # Side-Effects Review — Process Health tab ELI16 + structure pass
2
+
3
+ **Version / slug:** `process-health-tab-eli16-pass`
4
+ **Date:** 2026-05-27
5
+ **Author:** echo
6
+ **Spec:** `docs/specs/PROCESS-HEALTH-DASHBOARD-TAB-SPEC.md` (v4, the original feature spec; this PR is a UX-refinement follow-up that does NOT change the spec's behavioral surface)
7
+
8
+ ## Summary of the change
9
+
10
+ Eight rounds of user-driven copy + UX iteration on the Process Health dashboard tab (originally shipped in v1.3.27 via PR #435). Pure dashboard-only refinement — no `src/` / `scripts/` / `.husky/` / `skills/` changes — so this PR is outside the instar-dev gate's in-scope set. The reference implementation now backs a durable agent memory (`feedback_dashboard_copy_eli16.md`) that codifies the **Dashboard Standard** for future dashboard features.
11
+
12
+ **Files changed (UI):**
13
+ - `dashboard/process-health.js` (renderers): rewritten `CATEGORY_WORDS`, `ATTRIBUTION_LABELS`, `STAGES`, `INSIGHT_STATUS_WORDS`; new `TYPE_WORDS`, `STATUS_WORDS`, `CAUSE_CONFIDENCE_WORDS`, `statusDotClass`, `statusDot`, `labeledRow`. Restructured `renderCaptured` and `renderPatterns` around the fixed label vocabulary + `<details>` expandability + status color dots. `renderMaturation` reshaped per-stage as expandable `<details>` with plain-English descriptions. Dropped the redundant per-card framing line in v7.
14
+ - `dashboard/index.html`: added page-intro paragraph + per-section subtitles that distinguish each section and cross-reference adjacent ones; new CSS for `.ph-intro`, `.ph-h-sub`, `.ph-item`, `.ph-item-summary`, `.ph-item-text`, `.ph-item-body`, `.ph-item-row`, `.ph-label`, `.ph-value`, `.ph-status-dot` (+ 5 status-color variants), `.ph-stage-summary`, `.ph-stage-body`; chevron disclosure indicator on every `<details>` (opacity 0.8).
15
+
16
+ **Files changed (tests):**
17
+ - `tests/unit/process-health-render.test.ts`: assertions rewritten for new label vocabulary + new status copy + new framing absence + structural assertions on `<details>` + status-dot CSS classes (3 new structural tests added).
18
+ - `tests/integration/process-health-tab.test.ts`: assertions updated for new copy; new framing-absence check.
19
+ - `tests/e2e/process-health-tab-lifecycle.test.ts`: assertions updated for new copy.
20
+
21
+ **Files changed (release):**
22
+ - `upgrades/NEXT.md` (NEW for this PR, `bump: patch`).
23
+
24
+ ## Decision-point inventory
25
+
26
+ - **Pure UX refinement vs. functional change?** Pure UX. No new endpoints, no new data fields, no new auth surfaces, no behavioral changes to the polling controller, no changes to the route layer, no changes to migration parity. Only the rendered text + DOM structure changes.
27
+ - **Drop the recommendation field entirely vs. keep + relabel?** Dropped. Justin explicitly directed "avoid language that implies user needs to act unless ABSOLUTELY essential". The recommendation field is upstream prose that is action-implying by content; relabeling chrome around it doesn't change that. Actionable insight delivery is the deferred §10 surface anyway; v1 of the tab is the calm informational surface.
28
+ - **Drop the per-card framing line vs. keep + rephrase?** Dropped in v7. Once "verify before acting" was rephrased to "Same kind of problem has come up more than once" in v5, it became the third echo of the section title + section subtitle. The no-action-authority signal that the framing was originally carrying is now structural (the rendered card has nothing actionable on it), not chrome.
29
+ - **Add status color dots vs. status text only?** Dots. Lets the user scan the collapsed list and read state at a glance without expanding each item. Colors are calm (amber/blue/green/gray) — never alarm reds, per the existing visual rules.
30
+
31
+ ## Side-effects analysis
32
+
33
+ - **Behavioral:** None. The polling controller, route layer, and migration paths are unchanged. Only the rendered DOM differs.
34
+ - **Architectural:** None. Same file structure; the renderer module is the same self-contained ESM as before. No new build step, no new runtime dependency.
35
+ - **Security:** The safety contract (`sanitizeForDisplay`, textContent-only DOM writes, element ban, safeUrl) is unchanged. New label spans (`.ph-label`, `.ph-value`) use textContent only. The XSS negative fixture continues to fire zero live elements + no canary. The `detail.full` redaction is unchanged.
36
+ - **Migration parity:** Not applicable — no agent-installed-file changes (`templates.ts` / `PostUpdateMigrator` / shadow markers untouched).
37
+ - **Performance:** Marginal increase in DOM nodes per item (each captured item now has 4–5 `<div>`s of body content instead of 0–4 flat children). Diff-aware rendering still works (signature mechanism is structure-agnostic). No new fetches.
38
+ - **Reversibility:** Fully reversible — revert the commit and the previous render returns.
39
+
40
+ ## Standard captured (the durable artifact this PR exists to back)
41
+
42
+ The 8-round iteration converged on a design that's now codified as the **Dashboard Standard** in `feedback_dashboard_copy_eli16.md` (agent memory): 7 rules (ELI16 copy + codename maps + grounding intro + self-contained section titles with subtitles + expandable items + fixed label vocabulary + no action-implying language), 8 default substitutions, 4 calm status-color tokens, and the show-don't-tell iteration pattern. The Process Health tab is now the reference implementation.
43
+
44
+ ## Evidence pointers
45
+
46
+ - All 46 tests green: 30 unit + 12 integration + 4 e2e — includes structural assertions for the unified label order, status-dot CSS classes, and that the dropped framing line is genuinely absent.
47
+ - Safety verified end-to-end: XSS negative fixture still fires zero live elements + no canary; `detail.full` redaction unchanged.
48
+ - Typecheck: `tsc --noEmit` clean.
49
+ - Live-rendered both views (collapsed + expanded) to Chrome headless across 8 iterations and sent screenshots to the user for thumbs-up before each commit. Justin approved v8 with "this is good enough, let's go with it; save this as the standard for future dashboard features".