instar 1.3.518 → 1.3.520

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 (66) hide show
  1. package/dashboard/index.html +40 -10
  2. package/dashboard/mandates.js +115 -34
  3. package/dist/commands/server.d.ts.map +1 -1
  4. package/dist/commands/server.js +26 -2
  5. package/dist/commands/server.js.map +1 -1
  6. package/dist/config/ConfigDefaults.d.ts.map +1 -1
  7. package/dist/config/ConfigDefaults.js +29 -0
  8. package/dist/config/ConfigDefaults.js.map +1 -1
  9. package/dist/core/BackupManager.d.ts.map +1 -1
  10. package/dist/core/BackupManager.js +4 -0
  11. package/dist/core/BackupManager.js.map +1 -1
  12. package/dist/core/PostUpdateMigrator.d.ts.map +1 -1
  13. package/dist/core/PostUpdateMigrator.js +29 -0
  14. package/dist/core/PostUpdateMigrator.js.map +1 -1
  15. package/dist/core/devGatedFeatures.d.ts.map +1 -1
  16. package/dist/core/devGatedFeatures.js +6 -0
  17. package/dist/core/devGatedFeatures.js.map +1 -1
  18. package/dist/scaffold/templates.d.ts.map +1 -1
  19. package/dist/scaffold/templates.js +7 -0
  20. package/dist/scaffold/templates.js.map +1 -1
  21. package/dist/server/AgentServer.d.ts +3 -0
  22. package/dist/server/AgentServer.d.ts.map +1 -1
  23. package/dist/server/AgentServer.js +2 -0
  24. package/dist/server/AgentServer.js.map +1 -1
  25. package/dist/server/routes.d.ts +7 -0
  26. package/dist/server/routes.d.ts.map +1 -1
  27. package/dist/server/routes.js +310 -7
  28. package/dist/server/routes.js.map +1 -1
  29. package/dist/threadline/ConversationStore.d.ts +90 -0
  30. package/dist/threadline/ConversationStore.d.ts.map +1 -1
  31. package/dist/threadline/ConversationStore.js +133 -0
  32. package/dist/threadline/ConversationStore.js.map +1 -1
  33. package/dist/threadline/ThreadLog.d.ts +200 -0
  34. package/dist/threadline/ThreadLog.d.ts.map +1 -0
  35. package/dist/threadline/ThreadLog.js +434 -0
  36. package/dist/threadline/ThreadLog.js.map +1 -0
  37. package/dist/threadline/ThreadlineEndpoints.d.ts +10 -1
  38. package/dist/threadline/ThreadlineEndpoints.d.ts.map +1 -1
  39. package/dist/threadline/ThreadlineEndpoints.js +63 -12
  40. package/dist/threadline/ThreadlineEndpoints.js.map +1 -1
  41. package/dist/threadline/canonicalHistoryRead.d.ts +63 -0
  42. package/dist/threadline/canonicalHistoryRead.d.ts.map +1 -0
  43. package/dist/threadline/canonicalHistoryRead.js +123 -0
  44. package/dist/threadline/canonicalHistoryRead.js.map +1 -0
  45. package/dist/threadline/recordThreadMessage.d.ts +143 -0
  46. package/dist/threadline/recordThreadMessage.d.ts.map +1 -0
  47. package/dist/threadline/recordThreadMessage.js +222 -0
  48. package/dist/threadline/recordThreadMessage.js.map +1 -0
  49. package/dist/threadline/threadDigest.d.ts +90 -0
  50. package/dist/threadline/threadDigest.d.ts.map +1 -0
  51. package/dist/threadline/threadDigest.js +130 -0
  52. package/dist/threadline/threadDigest.js.map +1 -0
  53. package/dist/threadline/threadSymmetry.d.ts +108 -0
  54. package/dist/threadline/threadSymmetry.d.ts.map +1 -0
  55. package/dist/threadline/threadSymmetry.js +225 -0
  56. package/dist/threadline/threadSymmetry.js.map +1 -0
  57. package/package.json +1 -1
  58. package/scripts/instar-dev-precommit.js +46 -0
  59. package/scripts/lib/operator-surface.mjs +54 -0
  60. package/skills/instar-dev/templates/side-effects-artifact.md +15 -0
  61. package/src/data/builtin-manifest.json +65 -65
  62. package/src/scaffold/templates.ts +7 -0
  63. package/upgrades/1.3.519.md +92 -0
  64. package/upgrades/1.3.520.md +50 -0
  65. package/upgrades/side-effects/operator-surface-quality-and-mandates-redesign.md +200 -0
  66. package/upgrades/side-effects/threadline-canonical-history.md +122 -0
@@ -815,18 +815,34 @@
815
815
  .mnd-bad { color: #f85149; border-color: #f8514955; font-weight: 700; }
816
816
  .mnd-deny { color: #d29922; border-color: #d2992255; }
817
817
  .mnd-dead { color: var(--text-dim); }
818
- .mnd-meta { font-size: 13px; color: var(--text-dim); }
819
- .mnd-authorities { margin: 0; padding-left: 18px; font-size: 13px; line-height: 1.7; }
818
+ /* Internals (id, fingerprints, scope slug, action slugs) are SUPPORT
819
+ metadata only de-emphasized, never the headline (Operator-Surface
820
+ Quality item 2). */
821
+ .mnd-meta { font-size: 12px; color: var(--text-dim); }
822
+ /* The card's plain-language headline sentence — the primary content. */
823
+ .mnd-summary { margin: 0; font-size: 15px; line-height: 1.5; color: var(--text-bright); }
820
824
  .mnd-empty { font-size: 16px; color: var(--text-dim); line-height: 1.6; padding: 12px 0; }
821
- .mnd-revoke-row { display: flex; gap: 8px; align-items: center; flex-wrap: wrap; }
825
+ .mnd-revoke-row { display: flex; gap: 8px; align-items: center; flex-wrap: wrap; margin: 8px 0 4px; }
822
826
  .mnd-pin { width: 110px; } .mnd-reason { flex: 1; min-width: 160px; }
823
827
  .mnd-btn { padding: 6px 14px; cursor: pointer; }
824
- .mnd-btn-danger { color: #f85149; }
828
+ .mnd-btn-primary { font-weight: 600; }
829
+ /* Destructive action: quiet by design, never featured (item 3). */
830
+ .mnd-btn-danger { color: #f85149; font-weight: 400; }
825
831
  .mnd-dead-note { font-size: 13px; color: var(--text-dim); font-style: italic; }
826
832
  .mnd-grants-head { font-size: 13px; color: var(--text-dim); }
827
- .mnd-grants { margin: 0; padding-left: 18px; font-size: 13px; line-height: 1.7; }
828
- .mnd-grant-details { font-size: 13px; }
829
- .mnd-grant-summary { cursor: pointer; color: var(--text-dim); }
833
+ .mnd-grants { margin: 0; padding-left: 18px; font-size: 14px; line-height: 1.7; }
834
+ .mnd-grant-id { color: var(--text-dim); font-size: 12px; }
835
+ /* The grant form is the card's PRIMARY action — an open, titled block, never
836
+ a collapsed toggle (Operator-Surface Quality item 1). */
837
+ .mnd-grant-block { border-top: 1px solid var(--border); padding-top: 10px; margin-top: 2px; }
838
+ .mnd-grant-title { font-size: 14px; font-weight: 600; color: var(--text-bright); margin-bottom: 6px; }
839
+ .mnd-grant-sep { font-size: 13px; color: var(--text-dim); }
840
+ /* Revoke is demoted to a quiet, collapsed secondary control (item 3). */
841
+ .mnd-revoke-details { font-size: 13px; margin-top: 4px; }
842
+ .mnd-revoke-summary { cursor: pointer; color: var(--text-dim); font-size: 13px; }
843
+ .mnd-about { font-size: 14px; color: var(--text-dim); margin-top: -8px; }
844
+ .mnd-about summary { cursor: pointer; }
845
+ .mnd-about p { margin: 8px 0 0; line-height: 1.55; }
830
846
  .mnd-grant-row { display: flex; gap: 8px; align-items: center; flex-wrap: wrap; margin: 8px 0 4px; }
831
847
  .mnd-grant-field { min-width: 130px; max-width: 100%; padding: 6px 8px; }
832
848
  .mnd-notice { min-height: 20px; font-size: 14px; color: #3fb950; }
@@ -842,9 +858,19 @@
842
858
  .mnd-notice-ok { border: 1px solid #15803d; background: rgba(21, 128, 61, 0.12); color: #86efac; padding: 12px 14px; border-radius: 8px; font-size: 17px; font-weight: 600; line-height: 1.5; }
843
859
  .mnd-table { width: 100%; border-collapse: collapse; font-size: 13px; }
844
860
  .mnd-table th, .mnd-table td { text-align: left; padding: 6px 8px; border-bottom: 1px solid var(--border); vertical-align: top; }
845
- .mnd-reason-cell { color: var(--text-dim); max-width: 420px; }
846
- .mnd-audit-head { display: flex; gap: 12px; align-items: center; margin-bottom: 8px; }
861
+ .mnd-reason-cell { color: var(--text-dim); }
862
+ .mnd-audit-head { display: flex; gap: 12px; align-items: center; margin-bottom: 8px; flex-wrap: wrap; }
847
863
  .mnd-dim { font-size: 13px; color: var(--text-dim); }
864
+ /* Mobile-first (item 5): at phone width the 5-column audit table stacks into
865
+ labelled rows so the reason — the useful column — is never truncated. */
866
+ @media (max-width: 640px) {
867
+ .mnd-table thead { position: absolute; left: -9999px; }
868
+ .mnd-table tr { display: block; border: 1px solid var(--border); border-radius: 8px; margin-bottom: 10px; padding: 6px 4px; }
869
+ .mnd-table td { display: flex; justify-content: space-between; gap: 12px; border-bottom: none; padding: 4px 8px; }
870
+ .mnd-table td::before { content: attr(data-label); font-weight: 600; color: var(--text-dim); flex: 0 0 auto; }
871
+ .mnd-reason-cell { text-align: right; word-break: break-word; }
872
+ .mnd-grant-field, .mnd-pin, .mnd-reason { width: 100%; min-width: 0; flex: 1 1 100%; }
873
+ }
848
874
 
849
875
  /* ── process-health tab (PROCESS-HEALTH-DASHBOARD-TAB-SPEC §4.1) ──
850
876
  Large, readable, calm. No monospace, no tables, no alarm colors.
@@ -3384,7 +3410,11 @@
3384
3410
  <h2 class="ph-title">Coordination Mandates</h2>
3385
3411
  <div id="mndStamp" class="ph-stamp" aria-live="polite"></div>
3386
3412
  </div>
3387
- <p class="ph-intro">Your bounded "permission slips" for autonomous agent-to-agent work. The slip — never the agent — is the authorizer. Issuing and revoking require <strong>your dashboard PIN</strong>; agent credentials alone are refused by design. With no slip issued, every delegated action is denied.</p>
3413
+ <p class="ph-intro">Permission slips you sign with your PIN to let your agents do bounded work together.</p>
3414
+ <details class="mnd-about">
3415
+ <summary>What is this?</summary>
3416
+ <p>These are your permission slips for autonomous agent-to-agent work — each one authorizes a specific, bounded task. The slip — never the agent — is the authorizer. Issuing and revoking require <strong>your dashboard PIN</strong>; an agent's own credentials are refused by design. With no slip issued, every delegated action is denied.</p>
3417
+ </details>
3388
3418
  <div id="mndNotice" class="mnd-notice" aria-live="polite"></div>
3389
3419
  <section class="ph-section">
3390
3420
  <h3 class="ph-h">Issued mandates</h3>
@@ -61,12 +61,72 @@ function fmtWhen(iso) {
61
61
  } catch { return String(iso); }
62
62
  }
63
63
 
64
- function userLabel(slackUserId, slackUsers) {
64
+ /** The registry name for a Slack id, or the id itself when we don't know them. */
65
+ function userName(slackUserId, slackUsers) {
65
66
  const u = (slackUsers || []).find((x) => x.slackUserId === slackUserId);
66
- return u ? `${u.name} (${slackUserId})` : slackUserId;
67
+ return u ? u.name : slackUserId;
67
68
  }
68
69
 
69
- /** The grants a mandate already carries, in operator language. */
70
+ // Operator-Surface Quality (constitution): the card speaks plain language, never
71
+ // the slugs/JSON the data model stores. These maps turn the stored enum/bounds
72
+ // into the sentence a non-engineer would say. Unknown values fall back to a
73
+ // de-slugified phrase — never a raw token dumped at the operator.
74
+
75
+ // Both the coordination-mandate authority actions AND the RolePolicy floor
76
+ // actions, in plain language. Returns the RAW human string; callers esc() it.
77
+ const ACTION_PHRASES = {
78
+ 'exchange-read-credential': 'exchange a read-only credential',
79
+ 'sign-code-review': 'co-sign a code review',
80
+ 'money-movement': 'move money',
81
+ 'prod-deploy': 'deploy to production',
82
+ 'credential-access': 'use a credential',
83
+ 'destructive-data': 'run a destructive data operation',
84
+ 'external-send': 'send something externally',
85
+ 'grant-authority': 'grant authority to others',
86
+ };
87
+ function humanAction(slug) {
88
+ const s = String(slug ?? '');
89
+ return ACTION_PHRASES[s] || s.replace(/-/g, ' ');
90
+ }
91
+
92
+ /** "you" when the authorizer is the operator-via-PIN; otherwise the raw name. */
93
+ function humanAuthorizer(authorizedBy) {
94
+ const s = String(authorizedBy ?? '');
95
+ return /operator|dashboard pin/i.test(s) ? 'you' : s;
96
+ }
97
+
98
+ /** A short title from the operator's scope name (de-slugified, not the raw slug). */
99
+ function humanScope(scope) {
100
+ const s = String(scope ?? '').trim();
101
+ if (!s) return 'Permission slip';
102
+ const words = s.replace(/[-_]+/g, ' ');
103
+ return words.charAt(0).toUpperCase() + words.slice(1);
104
+ }
105
+
106
+ /** Join phrases the way a person speaks them: "a, b and c". */
107
+ function joinHuman(parts) {
108
+ if (parts.length === 0) return '';
109
+ if (parts.length === 1) return parts[0];
110
+ return parts.slice(0, -1).join(', ') + ' and ' + parts[parts.length - 1];
111
+ }
112
+
113
+ /**
114
+ * One plain sentence describing what the mandate authorizes + when it ends —
115
+ * the card's primary, human-language headline (no slugs, no JSON, no
116
+ * fingerprints). Each interpolated action is escaped at the call site.
117
+ */
118
+ function humanSummary(m) {
119
+ const acts = (m.authorities || []).map((a) => esc(humanAction(a.action)));
120
+ const agents = (m.agents || []).filter(Boolean);
121
+ const who = agents.length >= 2 ? 'two agents' : 'an agent';
122
+ const when = fmtWhen(m.expiresAt);
123
+ if (acts.length === 0) {
124
+ return `A permission slip for ${who} that delegates no actions yet. Expires ${when}.`;
125
+ }
126
+ return `Lets ${who} ${joinHuman(acts)}. Expires ${when}.`;
127
+ }
128
+
129
+ /** The grants a mandate already carries, in plain operator language. */
70
130
  export function renderGrants(m, slackUsers) {
71
131
  const grants = Array.isArray(m.grants) ? m.grants : [];
72
132
  if (grants.length === 0) return '';
@@ -75,16 +135,22 @@ export function renderGrants(m, slackUsers) {
75
135
  const badge = expired
76
136
  ? '<span class="mnd-badge mnd-dead">expired</span>'
77
137
  : '<span class="mnd-badge mnd-ok">active</span>';
78
- return `<li>${badge} <strong>${esc(userLabel(g.grantedTo, slackUsers))}</strong> may <code>${esc(g.floorAction)}</code> until ${fmtWhen(g.expiresAt)} — authorized by ${esc(g.authorizedBy)}</li>`;
138
+ const name = userName(g.grantedTo, slackUsers);
139
+ // The Slack id is support metadata, never the headline — shown muted only
140
+ // when it differs from the name we render (an unknown id IS the name).
141
+ const idDetail = name !== g.grantedTo ? ` <span class="mnd-grant-id">(${esc(g.grantedTo)})</span>` : '';
142
+ return `<li>${badge} <strong>${esc(name)}</strong>${idDetail} can ${esc(humanAction(g.floorAction))} until ${fmtWhen(g.expiresAt)} — authorized by ${esc(humanAuthorizer(g.authorizedBy))}</li>`;
79
143
  }).join('');
80
- return `<div class="mnd-grants-head">User grants this mandate carries:</div><ul class="mnd-grants">${rows}</ul>`;
144
+ return `<div class="mnd-grants-head">Who this slip already lets act:</div><ul class="mnd-grants">${rows}</ul>`;
81
145
  }
82
146
 
83
147
  /**
84
- * The add-grant form for an ACTIVE mandate. Mobile-first by design (the
85
- * 2026-06-12 lesson, instar#1080): the operator PICKS a person and a duration —
86
- * the only thing typed is the PIN. A free-text Slack-id input appears only
87
- * when the user registry has nobody to offer.
148
+ * The add-grant form for an ACTIVE mandate the card's PRIMARY action, always
149
+ * visible (Operator-Surface Quality: lead with the primary action, never behind
150
+ * a toggle). Mobile-first (the 2026-06-12 lesson, instar#1080): the operator
151
+ * PICKS a person and a duration — the only thing typed is the PIN. A free-text
152
+ * Slack-id input appears only when the user registry has nobody to offer. Action
153
+ * labels read in plain language; the slug is the option VALUE the server needs.
88
154
  */
89
155
  export function renderGrantForm(m, slackUsers) {
90
156
  const users = (slackUsers || []).filter((u) => u.slackUserId);
@@ -94,21 +160,23 @@ export function renderGrantForm(m, slackUsers) {
94
160
  }</select>`
95
161
  : `<input type="text" class="mnd-grant-field" data-grant-user="${esc(m.id)}" placeholder="Slack user id (e.g. U0…)" />`;
96
162
  const actionField = `<select class="mnd-grant-field" data-grant-action="${esc(m.id)}">${
97
- FLOOR_ACTIONS.map((a) => `<option value="${a}"${a === 'prod-deploy' ? ' selected' : ''}>${a}</option>`).join('')
163
+ FLOOR_ACTIONS.map((a) => `<option value="${a}"${a === 'prod-deploy' ? ' selected' : ''}>${esc(humanAction(a))}</option>`).join('')
98
164
  }</select>`;
99
165
  const durationField = `<select class="mnd-grant-field" data-grant-duration="${esc(m.id)}">${
100
166
  GRANT_DURATIONS.map((d) => `<option value="${d.minutes}"${d.minutes === 60 ? ' selected' : ''}>${d.label}</option>`).join('')
101
167
  }</select>`;
102
- return `<details class="mnd-grant-details"><summary class="mnd-grant-summary">Grant a user a floor action (PIN required)</summary>
168
+ return `<div class="mnd-grant-block">
169
+ <div class="mnd-grant-title">Grant a person an action</div>
103
170
  <div class="mnd-grant-row">
104
171
  ${granteeField}
105
172
  ${actionField}
173
+ <span class="mnd-grant-sep">for</span>
106
174
  ${durationField}
107
- <input type="password" class="mnd-pin" data-grant-pin="${esc(m.id)}" placeholder="PIN" autocomplete="off" />
108
- <button class="mnd-btn" data-grant="${esc(m.id)}">Grant</button>
175
+ <input type="password" class="mnd-pin" data-grant-pin="${esc(m.id)}" placeholder="Your PIN" autocomplete="off" />
176
+ <button class="mnd-btn mnd-btn-primary" data-grant="${esc(m.id)}">Grant</button>
109
177
  </div>
110
- <span class="mnd-hint">Lifts the picked person to ONE floor action for the picked window. Signed into this mandate by your PIN — revoking the mandate voids it; it can never outlive the mandate.</span>
111
- </details>`;
178
+ <span class="mnd-hint">Lets the person you pick take that one action for the window you choose. Confirmed with your PIN — never stored. A grant can never outlive this slip, and revoking the slip ends it.</span>
179
+ </div>`;
112
180
  }
113
181
 
114
182
  export function renderMandates(list, slackUsers = []) {
@@ -119,30 +187,39 @@ export function renderMandates(list, slackUsers = []) {
119
187
  const expired = Date.parse(m.expiresAt) < Date.now();
120
188
  const state = m.revoked ? 'revoked' : expired ? 'expired' : 'active';
121
189
  const stateCls = state === 'active' ? 'mnd-ok' : 'mnd-dead';
190
+ const stateLabel = state === 'active' ? 'active' : state === 'revoked' ? 'revoked' : 'expired';
122
191
  const authBadge = m.authorshipValid
123
192
  ? '<span class="mnd-badge mnd-ok">authorship verified</span>'
124
193
  : '<span class="mnd-badge mnd-bad">AUTHORSHIP INVALID</span>';
125
- const authorities = (m.authorities || []).map((a) =>
126
- `<li><code>${esc(a.action)}</code> bounds <code>${esc(JSON.stringify(a.bounds))}</code>${a.requiresCondition ? `requires <code>${esc(a.requiresCondition)}</code>` : ''}</li>`,
127
- ).join('');
128
- const revokeUi = state === 'active'
129
- ? `<div class="mnd-revoke-row">
130
- <input type="password" class="mnd-pin" data-revoke-pin="${esc(m.id)}" placeholder="PIN" autocomplete="off" />
131
- <input type="text" class="mnd-reason" data-revoke-reason="${esc(m.id)}" placeholder="reason" />
132
- <button class="mnd-btn mnd-btn-danger" data-revoke="${esc(m.id)}">Revoke</button>
133
- </div>`
134
- : `<div class="mnd-dead-note">${m.revoked ? `revoked ${fmtWhen(m.revoked.at)} — ${esc(m.revoked.reason)}` : `expired ${fmtWhen(m.expiresAt)}`}</div>`;
194
+ // Internals (id, agent fingerprints, scope slug, raw action names, bounds)
195
+ // are SUPPORT metadata, never the headline kept on one muted line so the
196
+ // operator can quote an id when asking for help, but never has to read JSON.
197
+ const actionSlugs = (m.authorities || []).map((a) => esc(a.action)).filter(Boolean).join(', ') || 'none';
198
+ const referenceLine = `<div class="mnd-meta">For support: id <code>${esc(m.id)}</code> · agents <code>${esc(shortFp(m.agents?.[0]))}</code> + <code>${esc(shortFp(m.agents?.[1]))}</code> · scope <code>${esc(m.scope)}</code> · authorizes ${actionSlugs} · issued by ${esc(m.author)}</div>`;
199
+ // The grant form is the PRIMARY action and renders ABOVE revoke. Revoke is
200
+ // destructive, so it is demoted to a quiet collapsed control, never featured.
135
201
  const grantUi = state === 'active' ? renderGrantForm(m, slackUsers) : '';
202
+ const revokeUi = state === 'active'
203
+ ? `<details class="mnd-revoke-details">
204
+ <summary class="mnd-revoke-summary">Revoke this permission slip</summary>
205
+ <div class="mnd-revoke-row">
206
+ <input type="password" class="mnd-pin" data-revoke-pin="${esc(m.id)}" placeholder="Your PIN" autocomplete="off" />
207
+ <input type="text" class="mnd-reason" data-revoke-reason="${esc(m.id)}" placeholder="reason (optional)" />
208
+ <button class="mnd-btn mnd-btn-danger" data-revoke="${esc(m.id)}">Revoke</button>
209
+ </div>
210
+ <span class="mnd-hint">Ends the slip and everything it authorized, right away. This cannot be undone.</span>
211
+ </details>`
212
+ : `<div class="mnd-dead-note">${m.revoked ? `Revoked ${fmtWhen(m.revoked.at)} — ${esc(m.revoked.reason)}` : `Expired ${fmtWhen(m.expiresAt)}`}</div>`;
136
213
  return `<div class="mnd-card">
137
214
  <div class="mnd-card-head">
138
- <span class="mnd-scope">${esc(m.scope)}</span>
139
- <span class="mnd-badge ${stateCls}">${state}</span>
215
+ <span class="mnd-scope">${esc(humanScope(m.scope))}</span>
216
+ <span class="mnd-badge ${stateCls}">${stateLabel}</span>
140
217
  ${authBadge}
141
218
  </div>
142
- <div class="mnd-meta">id <code>${esc(m.id)}</code> · agents <code>${esc(shortFp(m.agents?.[0]))}</code> + <code>${esc(shortFp(m.agents?.[1]))}</code> · by ${esc(m.author)} · expires ${fmtWhen(m.expiresAt)}</div>
143
- <ul class="mnd-authorities">${authorities}</ul>
219
+ <p class="mnd-summary">${humanSummary(m)}</p>
144
220
  ${renderGrants(m, slackUsers)}
145
221
  ${grantUi}
222
+ ${referenceLine}
146
223
  ${revokeUi}
147
224
  </div>`;
148
225
  }).join('');
@@ -157,13 +234,17 @@ export function renderAudit(payload) {
157
234
  if (entries.length === 0) {
158
235
  return `<div class="mnd-audit-head">${chainBadge}</div><div class="mnd-empty">No decisions recorded yet.</div>`;
159
236
  }
237
+ // Mobile-first (Operator-Surface Quality item 5): each cell carries a
238
+ // data-label so the table can stack into labelled rows at phone width (the
239
+ // CSS media query in index.html), instead of cramming five columns and
240
+ // truncating the most useful one — the reason — into an unreadable sliver.
160
241
  const rows = entries.slice(-25).reverse().map((e) =>
161
242
  `<tr>
162
- <td>${fmtWhen(e.ts)}</td>
163
- <td><span class="mnd-badge ${e.decision === 'allow' ? 'mnd-ok' : 'mnd-deny'}">${esc(e.decision)}</span></td>
164
- <td><code>${esc(e.action)}</code></td>
165
- <td><code>${esc(shortFp(e.agentFp))}</code></td>
166
- <td class="mnd-reason-cell">${esc(e.reason)}</td>
243
+ <td data-label="When">${fmtWhen(e.ts)}</td>
244
+ <td data-label="Decision"><span class="mnd-badge ${e.decision === 'allow' ? 'mnd-ok' : 'mnd-deny'}">${esc(e.decision)}</span></td>
245
+ <td data-label="Action"><code>${esc(e.action)}</code></td>
246
+ <td data-label="Agent"><code>${esc(shortFp(e.agentFp))}</code></td>
247
+ <td data-label="Reason" class="mnd-reason-cell">${esc(e.reason)}</td>
167
248
  </tr>`,
168
249
  ).join('');
169
250
  return `<div class="mnd-audit-head">${chainBadge}<span class="mnd-dim">${entries.length} total decisions · newest first</span></div>
@@ -1 +1 @@
1
- {"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../../src/commands/server.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAgCH,OAAO,EAAE,cAAc,EAAE,MAAM,2BAA2B,CAAC;AAS3D,OAAO,EAAE,eAAe,EAAiC,MAAM,iCAAiC,CAAC;AAuBjG,OAAO,EAAE,WAAW,EAAE,MAAM,0BAA0B,CAAC;AAGvD,OAAO,EAAE,YAAY,EAAE,MAAM,+BAA+B,CAAC;AA0G7D,OAAO,EAAE,WAAW,EAAE,MAAM,yBAAyB,CAAC;AAsBtD;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,yBAAyB,CACvC,IAAI,EAAE,MAAM,EACZ,OAAO,EAAE,MAAM,EACf,gBAAgB,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,GAC1C,OAAO,CAUT;AAyID,UAAU,YAAY;IACpB,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb;2DACuD;IACvD,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB;AAm1CD,wBAAgB,mBAAmB,CACjC,QAAQ,EAAE,eAAe,EACzB,cAAc,EAAE,cAAc,EAC9B,YAAY,CAAC,EAAE,YAAY,EAC3B,WAAW,CAAC,EAAE,WAAW,EACzB,WAAW,CAAC,EAAE,WAAW,EACzB,iBAAiB,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,OAAO,CAAC,EAGvE,UAAU,CAAC,EAAE,MAAM,OAAO,8BAA8B,EAAE,WAAW,GAAG,IAAI,EAK5E,qBAAqB,CAAC,EAAE,MAAM,OAAO,gCAAgC,EAAE,kBAAkB,GAAG,IAAI,EAKhG,mBAAmB,CAAC,EAAE,MAAM,MAAM,GAAG,IAAI,GAAG,SAAS,GACpD,IAAI,CA8eN;AA2lBD,wBAAsB,WAAW,CAAC,OAAO,EAAE,YAAY,GAAG,OAAO,CAAC,IAAI,CAAC,CAitZtE;AAED,wBAAsB,UAAU,CAAC,OAAO,EAAE;IAAE,GAAG,CAAC,EAAE,MAAM,CAAA;CAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAsDzE;AAED;;;;;;;;;;;GAWG;AACH,wBAAsB,aAAa,CAAC,OAAO,EAAE;IAAE,GAAG,CAAC,EAAE,MAAM,CAAA;CAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAuD5E"}
1
+ {"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../../src/commands/server.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAgCH,OAAO,EAAE,cAAc,EAAE,MAAM,2BAA2B,CAAC;AAS3D,OAAO,EAAE,eAAe,EAAiC,MAAM,iCAAiC,CAAC;AAuBjG,OAAO,EAAE,WAAW,EAAE,MAAM,0BAA0B,CAAC;AAGvD,OAAO,EAAE,YAAY,EAAE,MAAM,+BAA+B,CAAC;AA4G7D,OAAO,EAAE,WAAW,EAAE,MAAM,yBAAyB,CAAC;AAsBtD;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,yBAAyB,CACvC,IAAI,EAAE,MAAM,EACZ,OAAO,EAAE,MAAM,EACf,gBAAgB,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,GAC1C,OAAO,CAUT;AAyID,UAAU,YAAY;IACpB,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb;2DACuD;IACvD,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB;AAm1CD,wBAAgB,mBAAmB,CACjC,QAAQ,EAAE,eAAe,EACzB,cAAc,EAAE,cAAc,EAC9B,YAAY,CAAC,EAAE,YAAY,EAC3B,WAAW,CAAC,EAAE,WAAW,EACzB,WAAW,CAAC,EAAE,WAAW,EACzB,iBAAiB,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,OAAO,CAAC,EAGvE,UAAU,CAAC,EAAE,MAAM,OAAO,8BAA8B,EAAE,WAAW,GAAG,IAAI,EAK5E,qBAAqB,CAAC,EAAE,MAAM,OAAO,gCAAgC,EAAE,kBAAkB,GAAG,IAAI,EAKhG,mBAAmB,CAAC,EAAE,MAAM,MAAM,GAAG,IAAI,GAAG,SAAS,GACpD,IAAI,CA8eN;AA2lBD,wBAAsB,WAAW,CAAC,OAAO,EAAE,YAAY,GAAG,OAAO,CAAC,IAAI,CAAC,CAuuZtE;AAED,wBAAsB,UAAU,CAAC,OAAO,EAAE;IAAE,GAAG,CAAC,EAAE,MAAM,CAAA;CAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAsDzE;AAED;;;;;;;;;;;GAWG;AACH,wBAAsB,aAAa,CAAC,OAAO,EAAE;IAAE,GAAG,CAAC,EAAE,MAAM,CAAA;CAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAuD5E"}
@@ -144,6 +144,8 @@ import { WarmSessionPool } from '../threadline/WarmSessionPool.js';
144
144
  import { resolveThreadlineMcpEntry } from '../threadline/mcpEntry.js';
145
145
  import { ThreadResumeMap } from '../threadline/ThreadResumeMap.js';
146
146
  import { ConversationStore } from '../threadline/ConversationStore.js';
147
+ import { ThreadLog } from '../threadline/ThreadLog.js';
148
+ import { ThreadMessageRecorder } from '../threadline/recordThreadMessage.js';
147
149
  import { recordInboundAck } from '../threadline/recordInboundAck.js';
148
150
  import { WarrantsReplyGate, evaluateAndRecordInbound } from '../threadline/WarrantsReplyGate.js';
149
151
  import { CollaborationSurfacer } from '../threadline/CollaborationSurfacer.js';
@@ -9504,6 +9506,29 @@ export async function startServer(options) {
9504
9506
  const cj = coherenceJournal;
9505
9507
  conversationStore.setCoherenceJournalSeam((d) => cj.emitThreadlineConversation(d));
9506
9508
  }
9509
+ // ── Robustness Phase 2 (D-A/D-B): the canonical per-thread log + append funnel ──
9510
+ // CORE + UNGATED: the log, funnel, read-source union, and symmetry DETECTION
9511
+ // are additive/observability and ship live (history can only gain). Only the
9512
+ // D-E resolver JOIN is dev-gated + dry-run-first, resolved per-send at the route.
9513
+ const _canonHist = config.threadline?.canonicalHistory ?? {};
9514
+ const threadLog = new ThreadLog(config.stateDir, {
9515
+ maxEntriesPerThread: _canonHist.maxEntriesPerThread,
9516
+ seenSetMaxPerThread: _canonHist.seenSetMaxPerThread,
9517
+ seenSetMaxThreads: _canonHist.seenSetMaxThreads,
9518
+ });
9519
+ const threadMessageRecorder = new ThreadMessageRecorder({
9520
+ threadLog,
9521
+ conversationStore,
9522
+ attention: telegram ?? null,
9523
+ logDir: path.join(config.stateDir, 'logs'),
9524
+ headCacheCoalesceMs: _canonHist.headCacheCoalesceMs,
9525
+ appendFailureAlertThreshold: _canonHist.appendFailureAlertThreshold,
9526
+ inlineMaxBytes: _canonHist.inlineMaxBytes,
9527
+ });
9528
+ // SA5/E1: close-only canonical-log retention — a non-pinned conversation
9529
+ // transitioning INTO resolved/failed deletes its log (via SafeFsExecutor inside
9530
+ // ThreadLog.deleteThread); a COLD archive/LRU eviction never fires this seam.
9531
+ conversationStore.setLogRetentionSeam((threadId) => threadLog.deleteThread(threadId));
9507
9532
  const warrantsReplyGate = new WarrantsReplyGate({ intelligence: sharedIntelligence });
9508
9533
  // CMT-509 §2: surface PARENTLESS Threadline conversations into a single
9509
9534
  // dedicated topic so a peer reaching out cold is visible (not an invisible
@@ -14437,8 +14462,7 @@ export async function startServer(options) {
14437
14462
  carrier: _topicProfileCarrier,
14438
14463
  }
14439
14464
  : null;
14440
- const server = new AgentServer({ config, sessionManager, state, scheduler, telegram, relationships, feedback, feedbackAnomalyDetector, dispatches, updateChecker, autoUpdater, autoDispatcher, quotaTracker, quotaManager, publisher, viewer, tunnel, evolution, watchdog, topicMemory, triageNurse, projectMapper, cartographer: cartographer ?? undefined, coherenceGate: scopeVerifier, contextHierarchy, canonicalState, operationGate, sentinel, adaptiveTrust, memoryMonitor, orphanReaper, coherenceMonitor, commitmentTracker, subscriptionPool, quotaPoller, quotaAwareScheduler: _quotaAwareScheduler ?? undefined, proactiveSwapMonitor: _proactiveSwapMonitor ?? undefined, inUseAccountResolver, enrollmentWizard, semanticMemory, activitySentinel, rateLimitSentinel, releaseReadinessSentinel: releaseReadinessSentinel ?? undefined, greenPrAutoMerger: greenPrAutoMerger ?? undefined, guardLatchStore: guardLatchStore ?? undefined, messageRouter, summarySentinel, spawnManager, systemReviewer, capabilityMapper, selfKnowledgeTree, coverageAuditor, topicResumeMap: _topicResumeMap ?? undefined, topicProfile: _topicProfileCtx ?? undefined, sessionRefresh: _sessionRefresh ?? undefined, autonomyManager, trustElevationTracker, autonomousEvolution, coordinator: coordinator.enabled ? coordinator : undefined, localSigningKeyPem, leaseTransport, onLeasePullRequest: () => leaseCoordinatorRef?.currentLease() ?? null, liveTailReceiver, handoffWireTransport, onHandoffBegin, onHandoffInitiate: handoffInitiate, handoffInProgress: handoffSentinelInProgress, messageLedger, currentInboundByTopic, replyMarkerTransport, onReplyMarker: messageLedger ? (marker) => { const m = marker; messageLedger.applyRemoteReplyMarker(m.dedupeKey, { platform: m.platform, replyIdempotencyKey: m.replyIdempotencyKey, epoch: m.epoch, topic: m.topic ?? null }); } : undefined, whatsapp: whatsappAdapter, slack: slackAdapter, imessage: imessageAdapter, whatsappBusinessBackend, messageBridge, hookEventReceiver, worktreeMonitor, subagentTracker, instructionsVerifier, handshakeManager: threadlineHandshake, threadlineRouter, conversationStore, warrantsReplyGate, collaborationSurfacer, threadResumeMap, topicLinkageHandler: topicLinkageHandler ?? undefined, threadlineRelayClient, threadlineReplyWaiters, listenerManager: listenerManager ?? undefined, a2aDeliveryTracker: a2aDeliveryTracker ?? undefined, responseReviewGate, messagingToneGate, outboundDedupGate, telemetryHeartbeat, pasteManager, featureRegistry, discoveryEvaluator, completionEvaluator, unifiedTrust, liveConfig, sharedStateLedger, ledgerSessionRegistry, worktreeManager, oidcEnrolledRepos: parallelDevConfig?.oidcEnrolledRepos, initiativeTracker, projectRoundRunner, projectDriftChecker, machineHeartbeat, machinePoolRegistry, getInboundQueue: () => _inboundQueue, meshRpcDispatcher, workingSetPullCoordinator, commitmentReplicaStore, preferenceReplicaStore, forwardCommitmentMutate, sessionOwnershipRegistry, topicPinStore: _topicPinStore ?? undefined, streamTicketStore: _streamTicketStore ?? undefined, poolStreamAllowRemoteInput: config.dashboard?.poolStream?.allowRemoteInput ?? false, poolStreamConnector: _poolStreamConnector ?? undefined, secretSync: _secretSyncHandle ?? undefined, meshSelfId: _meshSelfId ?? undefined, resolveRouterUrl: _resolveRouterUrl ?? undefined, resolvePeerUrls: _resolvePeerUrls ?? undefined, guardRegistry, listPoolMachines: _listPoolMachines ?? undefined, poolLink: _poolLink ?? undefined, sessionPoolE2EResultStore, proxyCoordinator, topicIntentStore, topicIntentArcCheck, usherSignalStore, intelligence: sharedIntelligence ?? undefined, telegramBridgeConfig, telegramBridge: telegramBridge ?? undefined, threadlineObservability, briefDeps, workingMemory, taskFlowRegistry, threadlineFlowBridge, sessionReaper, agentWorktreeReaper, mcpProcessReaper, geminiLoopRunner, sleepController, agentActivityState, reapLog, resumeQueue, resumeDrainer, operatorStopRecorder: recordOperatorStop, sleepWakeDetector, unjustifiedStopGate, stopGateDb, stopNotifier });
14441
- // Resolve the late-bound topic-operator getter (increment 2e): routing was
14465
+ const server = new AgentServer({ config, sessionManager, state, scheduler, telegram, relationships, feedback, feedbackAnomalyDetector, dispatches, updateChecker, autoUpdater, autoDispatcher, quotaTracker, quotaManager, publisher, viewer, tunnel, evolution, watchdog, topicMemory, triageNurse, projectMapper, cartographer: cartographer ?? undefined, coherenceGate: scopeVerifier, contextHierarchy, canonicalState, operationGate, sentinel, adaptiveTrust, memoryMonitor, orphanReaper, coherenceMonitor, commitmentTracker, subscriptionPool, quotaPoller, quotaAwareScheduler: _quotaAwareScheduler ?? undefined, proactiveSwapMonitor: _proactiveSwapMonitor ?? undefined, inUseAccountResolver, enrollmentWizard, semanticMemory, activitySentinel, rateLimitSentinel, releaseReadinessSentinel: releaseReadinessSentinel ?? undefined, greenPrAutoMerger: greenPrAutoMerger ?? undefined, guardLatchStore: guardLatchStore ?? undefined, messageRouter, summarySentinel, spawnManager, systemReviewer, capabilityMapper, selfKnowledgeTree, coverageAuditor, topicResumeMap: _topicResumeMap ?? undefined, topicProfile: _topicProfileCtx ?? undefined, sessionRefresh: _sessionRefresh ?? undefined, autonomyManager, trustElevationTracker, autonomousEvolution, coordinator: coordinator.enabled ? coordinator : undefined, localSigningKeyPem, leaseTransport, onLeasePullRequest: () => leaseCoordinatorRef?.currentLease() ?? null, liveTailReceiver, handoffWireTransport, onHandoffBegin, onHandoffInitiate: handoffInitiate, handoffInProgress: handoffSentinelInProgress, messageLedger, currentInboundByTopic, replyMarkerTransport, onReplyMarker: messageLedger ? (marker) => { const m = marker; messageLedger.applyRemoteReplyMarker(m.dedupeKey, { platform: m.platform, replyIdempotencyKey: m.replyIdempotencyKey, epoch: m.epoch, topic: m.topic ?? null }); } : undefined, whatsapp: whatsappAdapter, slack: slackAdapter, imessage: imessageAdapter, whatsappBusinessBackend, messageBridge, hookEventReceiver, worktreeMonitor, subagentTracker, instructionsVerifier, handshakeManager: threadlineHandshake, threadlineRouter, conversationStore, threadLog, threadMessageRecorder, warrantsReplyGate, collaborationSurfacer, threadResumeMap, topicLinkageHandler: topicLinkageHandler ?? undefined, threadlineRelayClient, threadlineReplyWaiters, listenerManager: listenerManager ?? undefined, a2aDeliveryTracker: a2aDeliveryTracker ?? undefined, responseReviewGate, messagingToneGate, outboundDedupGate, telemetryHeartbeat, pasteManager, featureRegistry, discoveryEvaluator, completionEvaluator, unifiedTrust, liveConfig, sharedStateLedger, ledgerSessionRegistry, worktreeManager, oidcEnrolledRepos: parallelDevConfig?.oidcEnrolledRepos, initiativeTracker, projectRoundRunner, projectDriftChecker, machineHeartbeat, machinePoolRegistry, getInboundQueue: () => _inboundQueue, meshRpcDispatcher, workingSetPullCoordinator, commitmentReplicaStore, preferenceReplicaStore, forwardCommitmentMutate, sessionOwnershipRegistry, topicPinStore: _topicPinStore ?? undefined, streamTicketStore: _streamTicketStore ?? undefined, poolStreamAllowRemoteInput: config.dashboard?.poolStream?.allowRemoteInput ?? false, poolStreamConnector: _poolStreamConnector ?? undefined, secretSync: _secretSyncHandle ?? undefined, meshSelfId: _meshSelfId ?? undefined, resolveRouterUrl: _resolveRouterUrl ?? undefined, resolvePeerUrls: _resolvePeerUrls ?? undefined, guardRegistry, listPoolMachines: _listPoolMachines ?? undefined, poolLink: _poolLink ?? undefined, sessionPoolE2EResultStore, proxyCoordinator, topicIntentStore, topicIntentArcCheck, usherSignalStore, intelligence: sharedIntelligence ?? undefined, telegramBridgeConfig, telegramBridge: telegramBridge ?? undefined, threadlineObservability, briefDeps, workingMemory, taskFlowRegistry, threadlineFlowBridge, sessionReaper, agentWorktreeReaper, mcpProcessReaper, geminiLoopRunner, sleepController, agentActivityState, reapLog, resumeQueue, resumeDrainer, operatorStopRecorder: recordOperatorStop, sleepWakeDetector, unjustifiedStopGate, stopGateDb, stopNotifier }); // Resolve the late-bound topic-operator getter (increment 2e): routing was
14442
14466
  // wired before the server existed; from here on inbound binds use the
14443
14467
  // server's own store instance.
14444
14468
  _agentServerRef = server;