instar 1.3.519 → 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.
- package/dashboard/index.html +40 -10
- package/dashboard/mandates.js +115 -34
- package/package.json +1 -1
- package/scripts/instar-dev-precommit.js +46 -0
- package/scripts/lib/operator-surface.mjs +54 -0
- package/skills/instar-dev/templates/side-effects-artifact.md +15 -0
- package/src/data/builtin-manifest.json +2 -2
- package/upgrades/1.3.520.md +50 -0
- package/upgrades/side-effects/operator-surface-quality-and-mandates-redesign.md +200 -0
package/dashboard/index.html
CHANGED
|
@@ -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
|
-
|
|
819
|
-
|
|
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-
|
|
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:
|
|
828
|
-
.mnd-grant-
|
|
829
|
-
|
|
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);
|
|
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">
|
|
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>
|
package/dashboard/mandates.js
CHANGED
|
@@ -61,12 +61,72 @@ function fmtWhen(iso) {
|
|
|
61
61
|
} catch { return String(iso); }
|
|
62
62
|
}
|
|
63
63
|
|
|
64
|
-
|
|
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 ?
|
|
67
|
+
return u ? u.name : slackUserId;
|
|
67
68
|
}
|
|
68
69
|
|
|
69
|
-
|
|
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
|
-
|
|
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">
|
|
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
|
|
85
|
-
*
|
|
86
|
-
*
|
|
87
|
-
*
|
|
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 `<
|
|
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">
|
|
111
|
-
</
|
|
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
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
const
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
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}">${
|
|
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
|
-
<
|
|
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>
|
package/package.json
CHANGED
|
@@ -32,6 +32,7 @@ import { checkEli16Overview, MIN_ELI16_CHARS } from './eli16-overview-check.mjs'
|
|
|
32
32
|
import { verifyProposalDerivedRunbooks } from '../skills/instar-dev/scripts/verify-proposal-derived-runbook.mjs';
|
|
33
33
|
import { classifyTier, decideRequirementSet } from './lib/classify-tier.mjs';
|
|
34
34
|
import { recognizeConvergence } from './lib/convergence-recognition.mjs';
|
|
35
|
+
import { isOperatorSurfaceFile, artifactAddressesOperatorSurfaceQuality } from './lib/operator-surface.mjs';
|
|
35
36
|
|
|
36
37
|
// Report-Backed Converging Audit (docs/specs/CONVERGING-AUDIT-DEFAULT.md, Part B).
|
|
37
38
|
// The precommit reads NO config file and runs pre-compile, so it cannot import
|
|
@@ -884,6 +885,7 @@ if (!promotionGateResult.ok) {
|
|
|
884
885
|
// ─── Pass ────────────────────────────────────────────────────────────────
|
|
885
886
|
|
|
886
887
|
assertFrameworkGenerality(inScopeFiles, validTrace.trace);
|
|
888
|
+
assertOperatorSurfaceQuality(staged, validTrace.trace);
|
|
887
889
|
|
|
888
890
|
console.error(
|
|
889
891
|
`[instar-dev-precommit] OK — trace ${path.basename(validTrace.entry.file)} covers ${inScopeFiles.length} in-scope file(s), artifact ${validTrace.trace.artifactPath} verified, spec ${spec} is converged + approved` +
|
|
@@ -930,6 +932,49 @@ function assertFrameworkGenerality(inScopeFiles, trace) {
|
|
|
930
932
|
}
|
|
931
933
|
}
|
|
932
934
|
|
|
935
|
+
// Operator-Surface Quality review gate (docs/STANDARDS-REGISTRY.md →
|
|
936
|
+
// "Operator-Surface Quality", CMT-1434). A change touching an operator surface
|
|
937
|
+
// (dashboard renderers/markup, approval pages, grant/secret forms) must answer
|
|
938
|
+
// the operator-surface-quality question in the side-effects artifact IN WRITING:
|
|
939
|
+
// does the surface lead with its primary action, expose zero raw internals,
|
|
940
|
+
// de-emphasize destructive actions, and work at phone width? A "reachable but
|
|
941
|
+
// bad" surface passes Mobile-Complete and still fails the operator (the
|
|
942
|
+
// 2026-06-12 "abysmal" Mandates-grant-form lesson) — this makes the quality
|
|
943
|
+
// question unskippable. Operator-surface files are NOT in the gate's inScope set
|
|
944
|
+
// (they live under dashboard/), so we scan the full STAGED set, not inScopeFiles.
|
|
945
|
+
// Scoped tight so a non-surface commit pays nothing. Companion to
|
|
946
|
+
// assertFrameworkGenerality.
|
|
947
|
+
function assertOperatorSurfaceQuality(stagedFiles, trace) {
|
|
948
|
+
const touched = (stagedFiles || []).filter(isOperatorSurfaceFile);
|
|
949
|
+
if (touched.length === 0) return;
|
|
950
|
+
const artifactRel = trace.sideEffectsPath || trace.artifactPath;
|
|
951
|
+
if (!artifactRel) return; // a missing artifact is already blocked upstream
|
|
952
|
+
const artifactAbs = path.resolve(ROOT, artifactRel);
|
|
953
|
+
if (!fs.existsSync(artifactAbs)) return;
|
|
954
|
+
const content = fs.readFileSync(artifactAbs, 'utf8');
|
|
955
|
+
// The artifact must engage the operator-surface-quality question (the §6b
|
|
956
|
+
// section seeded by skills/instar-dev/templates/side-effects-artifact.md).
|
|
957
|
+
if (!artifactAddressesOperatorSurfaceQuality(content)) {
|
|
958
|
+
blockCommit(
|
|
959
|
+
touched,
|
|
960
|
+
[
|
|
961
|
+
'Operator-Surface Quality review gate:',
|
|
962
|
+
` ${touched.join(', ')} change an OPERATOR SURFACE,`,
|
|
963
|
+
' but the side-effects artifact never answers the operator-surface-quality',
|
|
964
|
+
' question — a surface can be phone-reachable (Mobile-Complete) and still be',
|
|
965
|
+
' unusable (the 2026-06-12 "abysmal" Mandates grant-form lesson, CMT-1434).',
|
|
966
|
+
'',
|
|
967
|
+
' Add the "## 6b. Operator-surface quality" section and answer in writing:',
|
|
968
|
+
' does the surface (1) lead with its primary action, (2) expose zero raw',
|
|
969
|
+
' internals as primary content, (3) de-emphasize destructive actions, and',
|
|
970
|
+
' (4) read in plain language at phone width? (Standard:',
|
|
971
|
+
' docs/STANDARDS-REGISTRY.md → "Operator-Surface Quality". Template:',
|
|
972
|
+
' skills/instar-dev/templates/side-effects-artifact.md §6b.)',
|
|
973
|
+
].join('\n'),
|
|
974
|
+
);
|
|
975
|
+
}
|
|
976
|
+
}
|
|
977
|
+
|
|
933
978
|
function blockCommit(files, reason) {
|
|
934
979
|
console.error('');
|
|
935
980
|
console.error('╔════════════════════════════════════════════════════════════════════╗');
|
|
@@ -1125,6 +1170,7 @@ function enforceTier1(trace, traceFile) {
|
|
|
1125
1170
|
}
|
|
1126
1171
|
|
|
1127
1172
|
assertFrameworkGenerality(inScopeFiles, trace);
|
|
1173
|
+
assertOperatorSurfaceQuality(staged, trace);
|
|
1128
1174
|
|
|
1129
1175
|
console.error(
|
|
1130
1176
|
`[instar-dev-precommit] OK (Tier 1) — trace ${traceName} covers ${inScopeFiles.length} in-scope file(s), ` +
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* operator-surface.mjs — pure decision logic for the Operator-Surface Quality
|
|
3
|
+
* review gate in the instar-dev commit gate (docs/STANDARDS-REGISTRY.md →
|
|
4
|
+
* "Operator-Surface Quality", CMT-1434).
|
|
5
|
+
*
|
|
6
|
+
* Two pure predicates, extracted so the gate's load-bearing decisions are
|
|
7
|
+
* unit-testable without git/fs mocking (the classify-tier.mjs pattern):
|
|
8
|
+
*
|
|
9
|
+
* - isOperatorSurfaceFile(path): is this staged file an operator surface — a
|
|
10
|
+
* dashboard renderer/markup file, an approval page, or a grant/secret form?
|
|
11
|
+
* - artifactAddressesOperatorSurfaceQuality(content): does the side-effects
|
|
12
|
+
* artifact engage the operator-surface-quality question in writing?
|
|
13
|
+
*
|
|
14
|
+
* SIGNAL-FREE: these never block; the gate (instar-dev-precommit.js) reads them
|
|
15
|
+
* and decides. Keeping them pure means both sides of every boundary are pinned
|
|
16
|
+
* by tests (Testing Integrity → semantic-correctness).
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* An operator surface is anything a person uses to authorize, decide, or act:
|
|
21
|
+
* - dashboard renderer / markup files (dashboard/*.js, dashboard/*.html), AND
|
|
22
|
+
* - approval pages / one-time-approval links / secret-drop forms (a file whose
|
|
23
|
+
* basename starts with approval / operator-approval / secret-drop).
|
|
24
|
+
* Build/test/spec siblings (*.test.js, *.spec.js) are NOT surfaces.
|
|
25
|
+
*/
|
|
26
|
+
export const OPERATOR_SURFACE_RE =
|
|
27
|
+
/^dashboard\/.+\.(?:js|html)$|(?:^|\/)(?:approval|operator-approval|secret-drop)[^/]*\.(?:js|html|ts)$/i;
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* True when a staged file path is an operator surface.
|
|
31
|
+
* @param {string} file repo-relative path
|
|
32
|
+
* @returns {boolean}
|
|
33
|
+
*/
|
|
34
|
+
export function isOperatorSurfaceFile(file) {
|
|
35
|
+
const f = String(file ?? '');
|
|
36
|
+
if (!f) return false;
|
|
37
|
+
// A test/spec file that happens to live under dashboard/ or match the approval
|
|
38
|
+
// basename is NOT itself an operator surface — it's a guard for one.
|
|
39
|
+
if (/\.(?:test|spec)\.(?:js|ts|mjs)$/.test(f)) return false;
|
|
40
|
+
return OPERATOR_SURFACE_RE.test(f);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* The artifact engages the operator-surface-quality question when it carries the
|
|
45
|
+
* §6b heading/phrase (seeded by side-effects-artifact.md). The gate's job is to
|
|
46
|
+
* ensure the question is structurally PRESENT for every operator-surface change —
|
|
47
|
+
* an agent that deletes/skips the section (or writes a bespoke artifact omitting
|
|
48
|
+
* it) is blocked. (Same strength as the framework-generality gate.)
|
|
49
|
+
* @param {string} content the side-effects artifact text
|
|
50
|
+
* @returns {boolean}
|
|
51
|
+
*/
|
|
52
|
+
export function artifactAddressesOperatorSurfaceQuality(content) {
|
|
53
|
+
return /operator[- ]surface quality/i.test(String(content ?? ''));
|
|
54
|
+
}
|
|
@@ -101,6 +101,21 @@
|
|
|
101
101
|
|
|
102
102
|
---
|
|
103
103
|
|
|
104
|
+
## 6b. Operator-surface quality (Operator-Surface Quality standard)
|
|
105
|
+
|
|
106
|
+
**REQUIRED whenever this change touches an operator surface** — a dashboard renderer/markup file (`dashboard/*.js`, `dashboard/*.html`), an approval page, or a grant/revoke/secret-drop form. The pre-commit gate (`scripts/instar-dev-precommit.js`) refuses the commit if this section is missing when an operator-surface file is staged. Reachable-but-bad still fails the operator (the 2026-06-12 "abysmal" Mandates-grant-form lesson, CMT-1434): Mobile-Complete asks *can they do it from a phone?*; this asks *is it good when they do?*
|
|
107
|
+
|
|
108
|
+
Answer each in writing (a "no" or unjustified "n/a" blocks the commit):
|
|
109
|
+
|
|
110
|
+
1. **Leads with the primary action?** The thing the operator came to do is visible and actionable on arrival — never collapsed behind a toggle, below the fold, or after explanatory prose.
|
|
111
|
+
2. **Zero raw internals as primary content?** No JSON blobs, fingerprints, UUIDs, hashes, or enum/slug values shown as headline content — only human language; identifiers de-emphasized as support metadata when genuinely needed.
|
|
112
|
+
3. **Destructive actions de-emphasized?** Revoke/delete/stop is visually quieter than the constructive primary action and never appears above it.
|
|
113
|
+
4. **Plain language + phone width?** Labels/states read the way a non-engineer would say them; verified at phone width — real tap targets, readable type, no horizontal scroll, no truncated table hiding the answer.
|
|
114
|
+
|
|
115
|
+
[Specific findings per criterion. "Grant form renders open as the card's primary block (mnd-grant-block); Revoke demoted to a collapsed mnd-revoke-details below it; bounds/fingerprints humanized, raw ids kept only on the muted 'For support' line; audit table stacks at ≤640px." If this change touches NO operator surface, state: "No operator surface — not applicable."]
|
|
116
|
+
|
|
117
|
+
---
|
|
118
|
+
|
|
104
119
|
## 7. Multi-machine posture (Cross-Machine Coherence)
|
|
105
120
|
|
|
106
121
|
**When this agent runs on MORE THAN ONE machine, what is this feature's posture?**
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "./builtin-manifest.schema.json",
|
|
3
3
|
"schemaVersion": 1,
|
|
4
|
-
"generatedAt": "2026-06-13T09:
|
|
5
|
-
"instarVersion": "1.3.
|
|
4
|
+
"generatedAt": "2026-06-13T09:21:46.780Z",
|
|
5
|
+
"instarVersion": "1.3.520",
|
|
6
6
|
"entryCount": 201,
|
|
7
7
|
"entries": {
|
|
8
8
|
"hook:session-start": {
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# Upgrade Guide — vNEXT
|
|
2
|
+
|
|
3
|
+
<!-- assembled-by: assemble-next-md -->
|
|
4
|
+
<!-- bump: minor -->
|
|
5
|
+
|
|
6
|
+
## What Changed
|
|
7
|
+
|
|
8
|
+
Two cohesive changes (topic 22367, CMT-1434):
|
|
9
|
+
|
|
10
|
+
1. **Mandates dashboard tab redesign.** The Mandates card now leads with the Grant
|
|
11
|
+
form as its primary, always-open block; Revoke is demoted to a quiet, collapsed
|
|
12
|
+
control beneath it. Raw JSON bounds, agent fingerprints, and scope slugs are
|
|
13
|
+
replaced with a plain-language summary sentence; identifiers survive only on a
|
|
14
|
+
muted "For support" line. Existing grants read as plain English ("Adam Admin can
|
|
15
|
+
deploy to production until 9:37 PM — authorized by you"), and the decision-audit
|
|
16
|
+
table stacks into labelled rows at phone width so the reason column is never
|
|
17
|
+
truncated. Renderer + markup + CSS only — the mandate API, payloads, and
|
|
18
|
+
PIN-never-retained discipline are unchanged. Ships via `dashboard/` static
|
|
19
|
+
serving, so it reaches every agent through the normal update path.
|
|
20
|
+
|
|
21
|
+
2. **New constitutional standard: Operator-Surface Quality** (sibling to
|
|
22
|
+
Mobile-Complete Operator Actions) in `docs/STANDARDS-REGISTRY.md`. Where
|
|
23
|
+
Mobile-Complete asks "can the operator do this from a phone?", this asks "is it
|
|
24
|
+
actually good when they do?" — lead with the primary action, zero raw internals,
|
|
25
|
+
de-emphasized destructive actions, plain language, phone-width layout. It lands
|
|
26
|
+
with teeth: a new operator-surface-quality question in the instar-dev
|
|
27
|
+
side-effects review template, and a pre-commit gate
|
|
28
|
+
(`scripts/instar-dev-precommit.js`) that blocks any commit touching an operator
|
|
29
|
+
surface unless the review answers it. The standard names that gate, so the
|
|
30
|
+
Standards-Enforcement-Coverage audit classifies it as an enforced gate.
|
|
31
|
+
|
|
32
|
+
## What to Tell Your User
|
|
33
|
+
|
|
34
|
+
- **A cleaner Mandates screen on your phone**: "I gave the Mandates tab a real
|
|
35
|
+
overhaul. The action you actually came to do — granting someone permission — is
|
|
36
|
+
now front and centre, the risky Revoke button is tucked quietly out of the way,
|
|
37
|
+
and the screen reads in plain English instead of raw data. The history list also
|
|
38
|
+
finally fits your phone without cutting off the reason column."
|
|
39
|
+
- **A new quality bar for everything you touch**: "I also turned this into a
|
|
40
|
+
standing rule for myself: any screen you use to approve or decide something has to
|
|
41
|
+
be genuinely good to use on your phone, not just technically reachable. I added an
|
|
42
|
+
automatic check that holds me to it whenever I build one of those screens."
|
|
43
|
+
|
|
44
|
+
## Summary of New Capabilities
|
|
45
|
+
|
|
46
|
+
| Capability | How to Use |
|
|
47
|
+
|-----------|-----------|
|
|
48
|
+
| Redesigned Mandates tab (humanized, mobile-first) | Open the Mandates tab in the dashboard |
|
|
49
|
+
| Operator-Surface Quality standard | docs/STANDARDS-REGISTRY.md (constitution) |
|
|
50
|
+
| Operator-surface-quality pre-commit gate | automatic during instar-dev work |
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
# Side-Effects Review — Operator-Surface Quality standard + Mandates-tab redesign
|
|
2
|
+
|
|
3
|
+
**Version / slug:** `operator-surface-quality-and-mandates-redesign`
|
|
4
|
+
**Date:** `2026-06-12`
|
|
5
|
+
**Author:** `Echo (instar dev agent)`
|
|
6
|
+
**Second-pass reviewer:** `not required (Tier 1)`
|
|
7
|
+
|
|
8
|
+
## Summary of the change
|
|
9
|
+
|
|
10
|
+
Two cohesive changes from one operator finding (2026-06-12, topic 22367, CMT-1434):
|
|
11
|
+
the freshly-shipped Mandates grant form was mobile-*reachable* (per Mobile-Complete
|
|
12
|
+
Operator Actions) yet unusable ("abysmal"). This (A) redesigns the Mandates
|
|
13
|
+
dashboard card to be genuinely good, and (B) lands a new constitutional standard,
|
|
14
|
+
**Operator-Surface Quality**, with a real enforcement gate so reachable-but-bad
|
|
15
|
+
cannot ship again.
|
|
16
|
+
|
|
17
|
+
Files touched:
|
|
18
|
+
- `dashboard/mandates.js` — renderers: grant form is now the card's primary,
|
|
19
|
+
always-open block (was wrapped in a collapsed `<details>`); revoke demoted to a
|
|
20
|
+
quiet collapsed control ordered after grant; JSON bounds / fingerprints / scope
|
|
21
|
+
slugs replaced by a plain-language summary, with ids kept only on a muted "For
|
|
22
|
+
support" line; grants list humanized; audit cells carry `data-label` for
|
|
23
|
+
mobile stacking.
|
|
24
|
+
- `dashboard/index.html` — shortened the explanatory wall to one line + a
|
|
25
|
+
collapsible "What is this?"; added CSS for the primary grant block, the demoted
|
|
26
|
+
revoke, the plain summary, and a `@media (max-width:640px)` rule that stacks the
|
|
27
|
+
audit table so the reason column is never truncated.
|
|
28
|
+
- `docs/STANDARDS-REGISTRY.md` — new "Operator-Surface Quality" article (Interaction
|
|
29
|
+
family), naming `scripts/instar-dev-precommit.js` as its gate.
|
|
30
|
+
- `skills/instar-dev/templates/side-effects-artifact.md` — new §6b operator-surface
|
|
31
|
+
quality question.
|
|
32
|
+
- `scripts/instar-dev-precommit.js` + `scripts/lib/operator-surface.mjs` — the gate
|
|
33
|
+
(`assertOperatorSurfaceQuality`) + its pure decision predicates.
|
|
34
|
+
- Tests: `tests/unit/dashboard-mandateGrantForm.test.ts`,
|
|
35
|
+
`tests/unit/dashboard-mandatesTab.test.ts`,
|
|
36
|
+
`tests/unit/standards-enforcement-auditor.test.ts`,
|
|
37
|
+
`tests/unit/operator-surface-gate.test.ts`.
|
|
38
|
+
|
|
39
|
+
## Decision-point inventory
|
|
40
|
+
|
|
41
|
+
- `assertOperatorSurfaceQuality` (scripts/instar-dev-precommit.js) — **add** — a new
|
|
42
|
+
scoped review gate: when a commit touches an operator surface, the side-effects
|
|
43
|
+
artifact must answer the operator-surface-quality question or the commit blocks.
|
|
44
|
+
- `isOperatorSurfaceFile` / `artifactAddressesOperatorSurfaceQuality`
|
|
45
|
+
(scripts/lib/operator-surface.mjs) — **add** — the gate's two pure predicates.
|
|
46
|
+
- Mandates renderers (dashboard/mandates.js) — **modify** — presentation only; no
|
|
47
|
+
change to the grant/revoke/issue API calls, payloads, or PIN discipline.
|
|
48
|
+
|
|
49
|
+
---
|
|
50
|
+
|
|
51
|
+
## 1. Over-block
|
|
52
|
+
|
|
53
|
+
**What legitimate inputs does this change reject that it shouldn't?**
|
|
54
|
+
|
|
55
|
+
The only block/allow surface added is the pre-commit gate. It fires ONLY when a
|
|
56
|
+
staged file matches an operator surface (`dashboard/*.js|html`, or an
|
|
57
|
+
approval/secret-drop/operator-approval form) AND the side-effects artifact lacks
|
|
58
|
+
the operator-surface-quality phrase. A legitimate operator-surface change whose
|
|
59
|
+
review genuinely answers §6b passes. A change touching no operator surface is never
|
|
60
|
+
evaluated. The detector explicitly excludes `*.test.*`/`*.spec.*` siblings so a
|
|
61
|
+
test guarding a surface isn't itself treated as the surface.
|
|
62
|
+
|
|
63
|
+
## 2. Under-block
|
|
64
|
+
|
|
65
|
+
**What failure modes does this still miss?**
|
|
66
|
+
|
|
67
|
+
A commit that touches an operator surface but stages NO in-scope file (src/,
|
|
68
|
+
scripts/, .husky/, skill) never reaches the gate at all — the precommit only runs
|
|
69
|
+
its body when an in-scope file is staged. This is the SAME boundary the sibling
|
|
70
|
+
`assertFrameworkGenerality` gate has (it fires only when its src surface is
|
|
71
|
+
touched). A pure dashboard-only commit (dashboard/ is not in-scope) would not be
|
|
72
|
+
gated. Accepted for v1: the dashboard surface is authored by instar-dev work that
|
|
73
|
+
near-always co-stages an in-scope file, and the standard + template question still
|
|
74
|
+
guide the review. Widening the gate's trigger to operator surfaces directly is a
|
|
75
|
+
tracked follow-up, not silently dropped.
|
|
76
|
+
|
|
77
|
+
The gate also checks for *presence* of the §6b engagement, not the substantive
|
|
78
|
+
quality of the four answers — that judgment stays with the reviewer (Signal vs.
|
|
79
|
+
Authority: the gate signals the question must be present; the mind answers it).
|
|
80
|
+
|
|
81
|
+
## 3. Level-of-abstraction fit
|
|
82
|
+
|
|
83
|
+
**Is this at the right layer?**
|
|
84
|
+
|
|
85
|
+
Yes. The gate is a brittle path/text detector that ENFORCES a written answer; it
|
|
86
|
+
holds no semantic judgment about whether the UI is actually good — that is the
|
|
87
|
+
reviewer's call. This is the correct split: the detector ensures the question is
|
|
88
|
+
asked; the full-context mind answers it. It feeds the existing side-effects review
|
|
89
|
+
flow rather than running parallel to it, mirroring `assertFrameworkGenerality`.
|
|
90
|
+
|
|
91
|
+
## 4. Signal vs authority compliance
|
|
92
|
+
|
|
93
|
+
**Required reference:** docs/signal-vs-authority.md
|
|
94
|
+
|
|
95
|
+
- [x] No — this change has no block/allow surface that makes a *semantic* judgment.
|
|
96
|
+
The gate's only authority is "the operator-surface-quality section must be present
|
|
97
|
+
in the artifact for an operator-surface change" — a structural presence check, not
|
|
98
|
+
a content-quality verdict. The quality verdict stays with the human/agent reviewer.
|
|
99
|
+
|
|
100
|
+
## 5. Interactions
|
|
101
|
+
|
|
102
|
+
- **Shadowing:** the new gate runs alongside `assertFrameworkGenerality` at both the
|
|
103
|
+
Tier-1 and Tier-2 pass sites; neither shadows the other (different surfaces,
|
|
104
|
+
different artifacts checks). Both run before `process.exit(0)`.
|
|
105
|
+
- **Double-fire:** the gate is invoked once per pass path; only one pass path
|
|
106
|
+
executes per commit (Tier-1 lite exits, Tier-2 falls through). No double-fire.
|
|
107
|
+
- **Races:** none — pure synchronous file reads at commit time.
|
|
108
|
+
- **Feedback loops:** none.
|
|
109
|
+
|
|
110
|
+
The dashboard renderer changes are pure presentation: the grant/revoke/issue
|
|
111
|
+
controller, fetch calls, payloads, and PIN-never-retained discipline are untouched
|
|
112
|
+
(verified — all existing controller tests pass unchanged).
|
|
113
|
+
|
|
114
|
+
## 6. External surfaces
|
|
115
|
+
|
|
116
|
+
- Other agents on the same machine? No.
|
|
117
|
+
- Other users of the install base? Yes — the Mandates dashboard tab is the visible
|
|
118
|
+
change. It ships via the package's `dashboard/` (served by `express.static`), so
|
|
119
|
+
it reaches every agent through the normal release → auto-update path. No
|
|
120
|
+
agent-installed-file rewrite is involved for the dashboard.
|
|
121
|
+
- External systems? No.
|
|
122
|
+
- Persistent state? No.
|
|
123
|
+
- **Operator surface (Mobile-Complete Operator Actions):** the Mandates grant/revoke
|
|
124
|
+
actions already have a phone-completable surface (this very tab). This change
|
|
125
|
+
improves that surface; it adds no new API-only operator action.
|
|
126
|
+
|
|
127
|
+
**Migration parity (the one agent-installed file I touched —
|
|
128
|
+
`skills/instar-dev/templates/side-effects-artifact.md`):** this template is deployed
|
|
129
|
+
into dev-agent homes and is already migrated by
|
|
130
|
+
`migrateMultiMachinePostureReviewDimension`. I deliberately did NOT add a new
|
|
131
|
+
`PostUpdateMigrator` migration, and this is a recorded decision, not a silent skip:
|
|
132
|
+
(1) the BINDING enforcement is the pre-commit gate in `scripts/`, which is
|
|
133
|
+
instar-source-repo tooling run from `.husky` — it is NOT a deployed runtime file and
|
|
134
|
+
works for ALL instar-dev work regardless of any installed template copy; (2) the gate
|
|
135
|
+
is self-teaching — its block message names the exact §6b section to add, so no
|
|
136
|
+
developer is left in a silent-fail state (the Migration Parity standard's actual
|
|
137
|
+
concern: "a feature that only works for new agents"); (3) new agents get §6b via
|
|
138
|
+
`installBuiltinSkills`; existing agents NOT yet on the multi-machine template get it
|
|
139
|
+
automatically because the bundled file the existing migration re-copies now contains
|
|
140
|
+
§6b; only an already-multi-machine dev agent misses the template PROMPT, and the gate
|
|
141
|
+
covers it. Touching `PostUpdateMigrator` (fleet machinery the codebase declares
|
|
142
|
+
"never Tier-1") to ship a convenience prompt to that narrow residual is
|
|
143
|
+
disproportionate. Tracked: CMT-1434.
|
|
144
|
+
|
|
145
|
+
## 7. Multi-machine posture (Cross-Machine Coherence)
|
|
146
|
+
|
|
147
|
+
**machine-local BY DESIGN.** The change is a stateless dashboard renderer + a
|
|
148
|
+
commit-time repo gate. The dashboard reads pool-wide mandate/audit state from the
|
|
149
|
+
server it is served by (no new per-machine state); the gate runs in whichever
|
|
150
|
+
checkout the developer commits from. It emits no user-facing notices, holds no
|
|
151
|
+
durable state, and generates no URLs — so there is nothing to replicate, proxy, or
|
|
152
|
+
strand on a topic transfer. Framework generality: not applicable — this touches
|
|
153
|
+
neither the session launch/inject abstraction nor messaging delivery.
|
|
154
|
+
|
|
155
|
+
## 8. Rollback cost
|
|
156
|
+
|
|
157
|
+
Pure code change. Revert the renderer/markup/CSS and the gate; ship as a patch. No
|
|
158
|
+
persistent state, no data migration, no agent-state repair. During the rollback
|
|
159
|
+
window users would simply see the prior Mandates card again — no functional
|
|
160
|
+
regression (the underlying mandate API is unchanged throughout).
|
|
161
|
+
|
|
162
|
+
## Conclusion
|
|
163
|
+
|
|
164
|
+
The review produced one design refinement: extracting the gate's decision logic into
|
|
165
|
+
a pure, unit-tested lib (`scripts/lib/operator-surface.mjs`) rather than inlining
|
|
166
|
+
regexes in the precommit, so both sides of each boundary are pinned by tests and the
|
|
167
|
+
gate's wiring is verifiable. The recorded migration-parity decision (no new
|
|
168
|
+
`PostUpdateMigrator` migration, with explicit reasoning) is the one item flagged for
|
|
169
|
+
the operator's awareness. Clear to ship as a Tier-1 change.
|
|
170
|
+
|
|
171
|
+
## Operator-surface quality (Operator-Surface Quality standard) — §6b self-review
|
|
172
|
+
|
|
173
|
+
This change IS an operator surface (`dashboard/mandates.js`, `dashboard/index.html`),
|
|
174
|
+
so it is held to the standard it introduces:
|
|
175
|
+
|
|
176
|
+
1. **Leads with the primary action?** Yes. The Grant form renders as an open,
|
|
177
|
+
titled `mnd-grant-block` — never inside a collapsed `<details>`. On a mandate
|
|
178
|
+
with no grants it is the visible call to action.
|
|
179
|
+
2. **Zero raw internals as primary content?** Yes. The card headline is a
|
|
180
|
+
de-slugified scope title; the body is a plain-language summary. No JSON bounds,
|
|
181
|
+
no fingerprints, no slugs as primary content (asserted: the card body contains no
|
|
182
|
+
`{"` substring and no raw bounds keys). Ids/fingerprints/slugs survive only on a
|
|
183
|
+
muted "For support" line.
|
|
184
|
+
3. **Destructive actions de-emphasized?** Yes. Revoke is a quiet, collapsed
|
|
185
|
+
`mnd-revoke-details` ordered AFTER the Grant block in the DOM; the danger button
|
|
186
|
+
is no longer bold/featured (asserted: grant index < revoke index).
|
|
187
|
+
4. **Plain language + phone width?** Yes. Grants read "Adam Admin can deploy to
|
|
188
|
+
production until … — authorized by you"; action slugs are humanized; the audit
|
|
189
|
+
table stacks into labelled rows at ≤640px so the reason column is never
|
|
190
|
+
truncated.
|
|
191
|
+
|
|
192
|
+
## Evidence pointers
|
|
193
|
+
|
|
194
|
+
- `tests/unit/dashboard-mandateGrantForm.test.ts` — grant form NOT in collapsed
|
|
195
|
+
`<details>`; no `{"` JSON in card body; revoke ordered after grant; humanized
|
|
196
|
+
grants; mobile audit labels.
|
|
197
|
+
- `tests/unit/operator-surface-gate.test.ts` — both predicates (both sides of each
|
|
198
|
+
boundary) + wiring-integrity (the gate is called on both pass paths, not a no-op).
|
|
199
|
+
- `tests/unit/standards-enforcement-auditor.test.ts` — the new standard classifies as
|
|
200
|
+
`gate` with zero dangling refs over the real registry.
|