instar 1.3.519 → 1.3.521

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.
@@ -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;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"}
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;AA8G7D,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,CA6yZtE;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"}
@@ -85,6 +85,8 @@ import { GitSyncManager } from '../core/GitSync.js';
85
85
  import { RegistrySyncDebouncer } from '../core/RegistrySyncDebouncer.js';
86
86
  import { wireRegistrySync } from '../core/wireRegistrySync.js';
87
87
  import { assertSeamlessnessInvariants } from '../core/seamlessnessConfig.js';
88
+ import { assertStateSyncInvariants } from '../core/stateSyncConfig.js';
89
+ import { ReplicatedKindRegistry, checkPoolFlagCoherence } from '../core/ReplicatedRecordEnvelope.js';
88
90
  import { FencedLease } from '../core/FencedLease.js';
89
91
  import { GitLeaseStore } from '../core/GitLeaseStore.js';
90
92
  import { LocalLeaseStore } from '../core/LocalLeaseStore.js';
@@ -3271,6 +3273,38 @@ export async function startServer(options) {
3271
3273
  // that breaks the RPO bound) is REJECTED here with a clear message rather
3272
3274
  // than degrading silently. Default/absent config resolves to valid values.
3273
3275
  const seamlessness = assertSeamlessnessInvariants(config.multiMachine);
3276
+ // Replicated-store foundation (multi-machine-replicated-store-foundation §10.2)
3277
+ // — resolve + validate the FOUNDATION-LEVEL stateSync knobs at startup. An
3278
+ // out-of-range value (e.g. a maxDriftMs below the 60s floor that would start
3279
+ // quarantining ordinary NTP jitter, or a non-positive journal budget) is
3280
+ // REJECTED here with a clear message rather than silently coerced (§10.2:
3281
+ // "a bad config is REJECTED, not silently degraded"). Default/absent config
3282
+ // resolves to valid values. The per-store on-switches are dark by default and
3283
+ // not range-validated; with none on, the foundation is inert.
3284
+ const stateSync = assertStateSyncInvariants(config.multiMachine);
3285
+ void stateSync; // foundation knobs are consumed by the store PRs (WS2.1+)
3286
+ // The replicated-kind registry (Component 2). Ships EMPTY in this step — the
3287
+ // first concrete store (WS2.1) registers its kind onto it. Constructed here so
3288
+ // the substrate is wired and the per-store stateSyncReceive advert can be
3289
+ // self-reported from the set of registered+enabled stores (machinery presence,
3290
+ // never a hardcoded true). A machine advertises a store's receive capability
3291
+ // ONLY when that store's kind is registered here AND emission is enabled in
3292
+ // config — i.e. it can both validate AND apply that kind. With an empty
3293
+ // registry the advert is `{}` (non-participant for every store): the correct
3294
+ // single-machine / no-store-yet behavior (a strict no-op).
3295
+ const replicatedKindRegistry = new ReplicatedKindRegistry();
3296
+ const selfStateSyncReceive = () => {
3297
+ const out = {};
3298
+ const stores = config.multiMachine?.stateSync;
3299
+ for (const store of replicatedKindRegistry.stores()) {
3300
+ // Advertise the receive capability iff the machinery exists (kind
3301
+ // registered) AND the store is enabled here — so a peer never forwards a
3302
+ // kind we'd silently drop (the named skew-failure mode).
3303
+ if (stores?.[store]?.enabled === true)
3304
+ out[store] = true;
3305
+ }
3306
+ return out;
3307
+ };
3274
3308
  // Read local signing key for machine route authentication
3275
3309
  let localSigningKeyPem = '';
3276
3310
  if (coordinator.enabled && coordinator.identity) {
@@ -12150,7 +12184,7 @@ export async function startServer(options) {
12150
12184
  // WS1.1 capability advertisement (spec invariant 5): a bounded
12151
12185
  // fixed-size summary, never an inventory. Reported live each
12152
12186
  // heartbeat so a queue going dark withdraws the capability.
12153
- seamlessnessFlags: { ws11DeliverReceive: !!_inboundQueue, ws12DrainReceive: !!_drainRunner, ws44PoolLinks: !!_poolLink },
12187
+ seamlessnessFlags: { ws11DeliverReceive: !!_inboundQueue, ws12DrainReceive: !!_drainRunner, ws44PoolLinks: !!_poolLink, stateSyncReceive: selfStateSyncReceive() },
12154
12188
  // Durable Inbound Message Queue §5.1: depth + oldest + tenure +
12155
12189
  // bounded top-K — the survivor's loss-SUSPECTED item, capped
12156
12190
  // re-placement arm, and supersede-dedupe key all read these.
@@ -12189,6 +12223,46 @@ export async function startServer(options) {
12189
12223
  const poolTimer = setInterval(refreshPool, 30_000);
12190
12224
  if (typeof poolTimer.unref === 'function')
12191
12225
  poolTimer.unref();
12226
+ // Boot-time pool-flag-coherence check (multi-machine-replicated-store-
12227
+ // foundation §4). For each LOCALLY-ENABLED replicated store, surface
12228
+ // ONCE (coalesced — never per-peer-per-tick) any peer that does NOT
12229
+ // advertise the matching stateSyncReceive capability: that peer would
12230
+ // SILENTLY DROP our kind (the journal applier drops unknown kinds), the
12231
+ // NAMED data-loss skew mode. Correct for N peers — checkPoolFlagCoherence
12232
+ // iterates ALL advertising peers. With an EMPTY registry (the Step-2
12233
+ // substrate-only state) this is a strict no-op (no registered stores →
12234
+ // empty verdict → nothing surfaced). The first concrete store (WS2.1)
12235
+ // registers its kind and this check starts doing real work automatically.
12236
+ let stateSyncCoherenceSurfaced = false;
12237
+ const checkStateSyncCoherence = () => {
12238
+ if (stateSyncCoherenceSurfaced)
12239
+ return;
12240
+ if (replicatedKindRegistry.size === 0)
12241
+ return; // no concrete store yet
12242
+ const peers = machinePoolRegistry
12243
+ .getCapacities()
12244
+ .filter((c) => c.machineId !== poolSelfId)
12245
+ .map((c) => ({
12246
+ machineId: c.machineId,
12247
+ online: c.online,
12248
+ stateSyncReceive: c.seamlessnessFlags?.stateSyncReceive,
12249
+ }));
12250
+ const stores = config.multiMachine?.stateSync;
12251
+ const verdict = checkPoolFlagCoherence(replicatedKindRegistry, stores, peers);
12252
+ if (verdict.mixedStores.length > 0) {
12253
+ // Surface ONCE (coalesced): one log line listing every mixed store.
12254
+ // A richer surface (one Attention item) is the store PR's to add;
12255
+ // the substrate guarantees the single, deduped detection here.
12256
+ stateSyncCoherenceSurfaced = true;
12257
+ console.log(pc.yellow(` [stateSync] pool-flag-coherence — mixed flag state across ${verdict.mixedStores.length} store(s):`));
12258
+ for (const line of verdict.summary)
12259
+ console.log(pc.yellow(` • ${line}`));
12260
+ }
12261
+ };
12262
+ checkStateSyncCoherence();
12263
+ const coherenceTimer = setInterval(checkStateSyncCoherence, 60_000);
12264
+ if (typeof coherenceTimer.unref === 'function')
12265
+ coherenceTimer.unref();
12192
12266
  }
12193
12267
  }
12194
12268
  }