instar 1.3.494 → 1.3.496
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 +67 -0
- package/dashboard/mandates.js +147 -3
- package/dist/core/PostUpdateMigrator.d.ts.map +1 -1
- package/dist/core/PostUpdateMigrator.js +18 -0
- package/dist/core/PostUpdateMigrator.js.map +1 -1
- package/dist/scaffold/templates.d.ts.map +1 -1
- package/dist/scaffold/templates.js +1 -0
- package/dist/scaffold/templates.js.map +1 -1
- package/dist/server/CapabilityIndex.d.ts.map +1 -1
- package/dist/server/CapabilityIndex.js +2 -1
- package/dist/server/CapabilityIndex.js.map +1 -1
- package/dist/server/routes.d.ts.map +1 -1
- package/dist/server/routes.js +25 -0
- package/dist/server/routes.js.map +1 -1
- package/package.json +1 -1
- package/src/data/builtin-manifest.json +63 -63
- package/src/scaffold/templates.ts +1 -0
- package/upgrades/1.3.496.md +66 -0
- package/upgrades/side-effects/mandate-grant-form.md +79 -0
- package/upgrades/side-effects/multi-machine-seamlessness-ws42.md +59 -0
package/dashboard/index.html
CHANGED
|
@@ -217,6 +217,20 @@
|
|
|
217
217
|
already names the machine, so a slightly dimmed row is enough hint. */
|
|
218
218
|
.session-item.remote { opacity: 0.9; cursor: pointer; }
|
|
219
219
|
.session-item.remote:hover { opacity: 1; }
|
|
220
|
+
/* WS4.2: explicit per-machine state when a pool machine has no session
|
|
221
|
+
tiles — an idle machine must never look identical to a broken one.
|
|
222
|
+
Plain flex row; inherits the sidebar width (mobile-safe). */
|
|
223
|
+
.machine-status-row {
|
|
224
|
+
display: flex;
|
|
225
|
+
align-items: center;
|
|
226
|
+
gap: 8px;
|
|
227
|
+
padding: 8px 12px;
|
|
228
|
+
font-size: 11px;
|
|
229
|
+
color: var(--text-dim, #8b8fa3);
|
|
230
|
+
border-top: 1px dashed rgba(139, 143, 163, 0.25);
|
|
231
|
+
}
|
|
232
|
+
.machine-status-row.offline .machine-status-text { color: #f87171; }
|
|
233
|
+
.machine-status-row.online .machine-status-text { opacity: 0.85; }
|
|
220
234
|
|
|
221
235
|
.type-badge {
|
|
222
236
|
padding: 1px 6px;
|
|
@@ -809,6 +823,12 @@
|
|
|
809
823
|
.mnd-btn { padding: 6px 14px; cursor: pointer; }
|
|
810
824
|
.mnd-btn-danger { color: #f85149; }
|
|
811
825
|
.mnd-dead-note { font-size: 13px; color: var(--text-dim); font-style: italic; }
|
|
826
|
+
.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); }
|
|
830
|
+
.mnd-grant-row { display: flex; gap: 8px; align-items: center; flex-wrap: wrap; margin: 8px 0 4px; }
|
|
831
|
+
.mnd-grant-field { min-width: 130px; max-width: 100%; padding: 6px 8px; }
|
|
812
832
|
.mnd-notice { min-height: 20px; font-size: 14px; color: #3fb950; }
|
|
813
833
|
.mnd-issue-form { display: flex; flex-direction: column; gap: 14px; max-width: 640px; padding: 12px 0; }
|
|
814
834
|
.mnd-issue-form label { display: flex; flex-direction: column; gap: 4px; font-size: 15px; color: var(--text-bright); }
|
|
@@ -3697,6 +3717,10 @@
|
|
|
3697
3717
|
// Kept separate from `sessions` because the WebSocket replaces that array
|
|
3698
3718
|
// with LOCAL sessions on every broadcast — merging would be wiped each tick.
|
|
3699
3719
|
let remoteSessions = [];
|
|
3720
|
+
// WS4.2 (multi-machine-seamlessness spec, F7): pool machine inventory for the
|
|
3721
|
+
// per-machine empty-state strip. Populated only when the pool is enabled with
|
|
3722
|
+
// 2+ machines — single-machine agents never see the strip (strict no-op).
|
|
3723
|
+
let poolMachinesView = [];
|
|
3700
3724
|
let selfMachineNickname = null;
|
|
3701
3725
|
let activeSession = null;
|
|
3702
3726
|
let activeMachineId = null; // non-null when the active session streams from a peer (§2.2)
|
|
@@ -3958,6 +3982,9 @@
|
|
|
3958
3982
|
empty.style.display = 'flex';
|
|
3959
3983
|
// Clear any existing session items
|
|
3960
3984
|
list.querySelectorAll('.session-item').forEach(el => el.remove());
|
|
3985
|
+
// WS4.2: even with zero sessions anywhere, each pool machine still gets
|
|
3986
|
+
// an explicit state row — an idle machine must never look broken.
|
|
3987
|
+
renderMachineStatusStrip(list, all);
|
|
3961
3988
|
return;
|
|
3962
3989
|
}
|
|
3963
3990
|
|
|
@@ -4045,6 +4072,37 @@
|
|
|
4045
4072
|
${telemetryHtml}
|
|
4046
4073
|
`;
|
|
4047
4074
|
}
|
|
4075
|
+
|
|
4076
|
+
// WS4.2: per-machine state strip after the tiles.
|
|
4077
|
+
renderMachineStatusStrip(list, all);
|
|
4078
|
+
}
|
|
4079
|
+
|
|
4080
|
+
// WS4.2 (multi-machine-seamlessness spec, F7 — 2026-06-12 live incident):
|
|
4081
|
+
// a machine with zero running sessions used to render as NOTHING, identical
|
|
4082
|
+
// to a broken or unreachable machine. Render an explicit state row per pool
|
|
4083
|
+
// machine that has no session tiles: "online — no active sessions" when its
|
|
4084
|
+
// heartbeat is live, "not reachable — last seen <t>" when it is not. Pool
|
|
4085
|
+
// disabled or single-machine → poolMachinesView is empty → zero rows, zero
|
|
4086
|
+
// behavior change (single-machine strict no-op, spec invariant 6).
|
|
4087
|
+
function machineStatusInfo(m) {
|
|
4088
|
+
if (m.online) return { cls: 'online', text: 'online — no active sessions' };
|
|
4089
|
+
const seen = m.selfReportedLastSeen ? new Date(m.selfReportedLastSeen).getTime() : null;
|
|
4090
|
+
const rel = seen ? fmtRelTime(seen) : 'unknown';
|
|
4091
|
+
return { cls: 'offline', text: `not reachable — last seen ${rel}` };
|
|
4092
|
+
}
|
|
4093
|
+
function renderMachineStatusStrip(list, allSessions) {
|
|
4094
|
+
list.querySelectorAll('.machine-status-row').forEach(el => el.remove());
|
|
4095
|
+
if (!poolMachinesView.length) return;
|
|
4096
|
+
const busy = new Set(allSessions.map(s => s.machineNickname || s.machineId).filter(Boolean));
|
|
4097
|
+
for (const m of poolMachinesView) {
|
|
4098
|
+
const label = m.nickname || m.machineId || 'unnamed machine';
|
|
4099
|
+
if (busy.has(m.nickname) || busy.has(m.machineId)) continue; // has tiles — badge already names it
|
|
4100
|
+
const info = machineStatusInfo(m);
|
|
4101
|
+
const row = document.createElement('div');
|
|
4102
|
+
row.className = `machine-status-row ${info.cls}`;
|
|
4103
|
+
row.innerHTML = `<span class="machine-badge">${escapeHtml(label)}</span><span class="machine-status-text">${escapeHtml(info.text)}</span>`;
|
|
4104
|
+
list.appendChild(row);
|
|
4105
|
+
}
|
|
4048
4106
|
}
|
|
4049
4107
|
|
|
4050
4108
|
// ── Terminal ──────────────────────────────────────────────
|
|
@@ -4600,6 +4658,15 @@
|
|
|
4600
4658
|
remoteSessions = (j.sessions || []).filter(s => s.remote === true && (s.status === 'running' || s.status === 'starting'));
|
|
4601
4659
|
renderSessionList();
|
|
4602
4660
|
} catch { /* best-effort — peers may be offline; next tick retries */ }
|
|
4661
|
+
// WS4.2: refresh the machine inventory on the same cadence so the sessions
|
|
4662
|
+
// view can say "online — no active sessions" vs "not reachable" per machine.
|
|
4663
|
+
try {
|
|
4664
|
+
const pool = await apiFetch('/pool');
|
|
4665
|
+
poolMachinesView = (pool && pool.enabled && Array.isArray(pool.machines) && pool.machines.length >= 2)
|
|
4666
|
+
? pool.machines
|
|
4667
|
+
: [];
|
|
4668
|
+
renderSessionList();
|
|
4669
|
+
} catch { /* best-effort — strip simply stays absent until the next tick */ }
|
|
4603
4670
|
}
|
|
4604
4671
|
function startPoolSessionsPolling() {
|
|
4605
4672
|
if (poolPollTimer) return;
|
package/dashboard/mandates.js
CHANGED
|
@@ -20,6 +20,28 @@ const AUTHORITIES_TEMPLATE = JSON.stringify([
|
|
|
20
20
|
{ action: 'sign-code-review', bounds: { artifact: 'migration-port', mutual: true } },
|
|
21
21
|
], null, 2);
|
|
22
22
|
|
|
23
|
+
// Mirrors FLOOR_ACTIONS in src/permissions/RolePolicy.ts — the enumerated
|
|
24
|
+
// never-discretionary actions a user grant can lift. Kept in sync by the
|
|
25
|
+
// dashboard-mandatesTab test, which compares this list against the source enum.
|
|
26
|
+
const FLOOR_ACTIONS = [
|
|
27
|
+
'money-movement',
|
|
28
|
+
'prod-deploy',
|
|
29
|
+
'credential-access',
|
|
30
|
+
'destructive-data',
|
|
31
|
+
'external-send',
|
|
32
|
+
'grant-authority',
|
|
33
|
+
];
|
|
34
|
+
|
|
35
|
+
// Mobile-first: the operator picks a duration, never types a timestamp.
|
|
36
|
+
// The submit handler clamps to the mandate's own expiry (a grant can never
|
|
37
|
+
// outlive the mandate that carries it — the server enforces the same rule).
|
|
38
|
+
const GRANT_DURATIONS = [
|
|
39
|
+
{ label: '15 minutes', minutes: 15 },
|
|
40
|
+
{ label: '1 hour', minutes: 60 },
|
|
41
|
+
{ label: '4 hours', minutes: 240 },
|
|
42
|
+
{ label: '24 hours', minutes: 1440 },
|
|
43
|
+
];
|
|
44
|
+
|
|
23
45
|
function esc(s) {
|
|
24
46
|
return String(s ?? '').replace(/[&<>"']/g, (c) => ({
|
|
25
47
|
'&': '&', '<': '<', '>': '>', '"': '"', "'": ''',
|
|
@@ -39,7 +61,57 @@ function fmtWhen(iso) {
|
|
|
39
61
|
} catch { return String(iso); }
|
|
40
62
|
}
|
|
41
63
|
|
|
42
|
-
|
|
64
|
+
function userLabel(slackUserId, slackUsers) {
|
|
65
|
+
const u = (slackUsers || []).find((x) => x.slackUserId === slackUserId);
|
|
66
|
+
return u ? `${u.name} (${slackUserId})` : slackUserId;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** The grants a mandate already carries, in operator language. */
|
|
70
|
+
export function renderGrants(m, slackUsers) {
|
|
71
|
+
const grants = Array.isArray(m.grants) ? m.grants : [];
|
|
72
|
+
if (grants.length === 0) return '';
|
|
73
|
+
const rows = grants.map((g) => {
|
|
74
|
+
const expired = Date.parse(g.expiresAt) < Date.now();
|
|
75
|
+
const badge = expired
|
|
76
|
+
? '<span class="mnd-badge mnd-dead">expired</span>'
|
|
77
|
+
: '<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>`;
|
|
79
|
+
}).join('');
|
|
80
|
+
return `<div class="mnd-grants-head">User grants this mandate carries:</div><ul class="mnd-grants">${rows}</ul>`;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
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.
|
|
88
|
+
*/
|
|
89
|
+
export function renderGrantForm(m, slackUsers) {
|
|
90
|
+
const users = (slackUsers || []).filter((u) => u.slackUserId);
|
|
91
|
+
const granteeField = users.length > 0
|
|
92
|
+
? `<select class="mnd-grant-field" data-grant-user="${esc(m.id)}">${
|
|
93
|
+
users.map((u) => `<option value="${esc(u.slackUserId)}">${esc(u.name)}${u.orgRole ? ` — ${esc(u.orgRole)}` : ''}</option>`).join('')
|
|
94
|
+
}</select>`
|
|
95
|
+
: `<input type="text" class="mnd-grant-field" data-grant-user="${esc(m.id)}" placeholder="Slack user id (e.g. U0…)" />`;
|
|
96
|
+
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('')
|
|
98
|
+
}</select>`;
|
|
99
|
+
const durationField = `<select class="mnd-grant-field" data-grant-duration="${esc(m.id)}">${
|
|
100
|
+
GRANT_DURATIONS.map((d) => `<option value="${d.minutes}"${d.minutes === 60 ? ' selected' : ''}>${d.label}</option>`).join('')
|
|
101
|
+
}</select>`;
|
|
102
|
+
return `<details class="mnd-grant-details"><summary class="mnd-grant-summary">Grant a user a floor action (PIN required)</summary>
|
|
103
|
+
<div class="mnd-grant-row">
|
|
104
|
+
${granteeField}
|
|
105
|
+
${actionField}
|
|
106
|
+
${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>
|
|
109
|
+
</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>`;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export function renderMandates(list, slackUsers = []) {
|
|
43
115
|
if (!Array.isArray(list) || list.length === 0) {
|
|
44
116
|
return '<div class="mnd-empty">No mandates issued. The gate is deny-by-default: every delegated agent action refuses until you issue one.</div>';
|
|
45
117
|
}
|
|
@@ -60,6 +132,7 @@ export function renderMandates(list) {
|
|
|
60
132
|
<button class="mnd-btn mnd-btn-danger" data-revoke="${esc(m.id)}">Revoke</button>
|
|
61
133
|
</div>`
|
|
62
134
|
: `<div class="mnd-dead-note">${m.revoked ? `revoked ${fmtWhen(m.revoked.at)} — ${esc(m.revoked.reason)}` : `expired ${fmtWhen(m.expiresAt)}`}</div>`;
|
|
135
|
+
const grantUi = state === 'active' ? renderGrantForm(m, slackUsers) : '';
|
|
63
136
|
return `<div class="mnd-card">
|
|
64
137
|
<div class="mnd-card-head">
|
|
65
138
|
<span class="mnd-scope">${esc(m.scope)}</span>
|
|
@@ -68,6 +141,8 @@ export function renderMandates(list) {
|
|
|
68
141
|
</div>
|
|
69
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>
|
|
70
143
|
<ul class="mnd-authorities">${authorities}</ul>
|
|
144
|
+
${renderGrants(m, slackUsers)}
|
|
145
|
+
${grantUi}
|
|
71
146
|
${revokeUi}
|
|
72
147
|
</div>`;
|
|
73
148
|
}).join('');
|
|
@@ -129,12 +204,18 @@ export function createController({ doc, els, fetchImpl }) {
|
|
|
129
204
|
|
|
130
205
|
let refreshErrorShown = false;
|
|
131
206
|
let autoOpenedIssueForm = false;
|
|
207
|
+
// Last-fetched state the grant submit handler needs: the mandate list (for
|
|
208
|
+
// the expiry clamp) — kept here, never re-derived from the DOM.
|
|
209
|
+
let lastMandates = [];
|
|
132
210
|
|
|
133
211
|
async function refresh() {
|
|
134
212
|
try {
|
|
135
|
-
const [mand, audit] = await Promise.all([
|
|
213
|
+
const [mand, audit, users] = await Promise.all([
|
|
136
214
|
fetchJson('/mandate'),
|
|
137
215
|
fetchJson('/mandate/audit?limit=200'),
|
|
216
|
+
// Registered Slack users feed the grant form's person picker. A failure
|
|
217
|
+
// here must never take down the tab — the form degrades to a text input.
|
|
218
|
+
fetchJson('/permissions/users').catch(() => ({ status: 0, body: null })),
|
|
138
219
|
]);
|
|
139
220
|
if (mand.status === 503) {
|
|
140
221
|
els.list.innerHTML = '<div class="mnd-empty">Mandate engine unavailable on this server (older version or init failure).</div>';
|
|
@@ -143,7 +224,9 @@ export function createController({ doc, els, fetchImpl }) {
|
|
|
143
224
|
return;
|
|
144
225
|
}
|
|
145
226
|
const mandates = mand.body?.mandates;
|
|
146
|
-
|
|
227
|
+
lastMandates = Array.isArray(mandates) ? mandates : [];
|
|
228
|
+
const slackUsers = users.status === 200 && Array.isArray(users.body?.users) ? users.body.users : [];
|
|
229
|
+
els.list.innerHTML = renderMandates(mandates, slackUsers);
|
|
147
230
|
els.audit.innerHTML = renderAudit(audit.body);
|
|
148
231
|
if (els.stamp) els.stamp.textContent = 'updated ' + new Date().toLocaleTimeString();
|
|
149
232
|
// Nothing issued yet → the issue form IS the page's call to action;
|
|
@@ -155,6 +238,7 @@ export function createController({ doc, els, fetchImpl }) {
|
|
|
155
238
|
// A persistent refresh error from a server restart-gap heals itself.
|
|
156
239
|
if (refreshErrorShown) { clearNote(); refreshErrorShown = false; }
|
|
157
240
|
wireRevokeButtons();
|
|
241
|
+
wireGrantButtons();
|
|
158
242
|
} catch (e) {
|
|
159
243
|
refreshErrorShown = true;
|
|
160
244
|
note('refresh failed: ' + (e?.message ?? e) + ' — retrying automatically.', 'error');
|
|
@@ -184,6 +268,66 @@ export function createController({ doc, els, fetchImpl }) {
|
|
|
184
268
|
});
|
|
185
269
|
}
|
|
186
270
|
|
|
271
|
+
function wireGrantButtons() {
|
|
272
|
+
els.list.querySelectorAll('[data-grant]').forEach((btn) => {
|
|
273
|
+
btn.onclick = async () => {
|
|
274
|
+
const id = btn.getAttribute('data-grant');
|
|
275
|
+
const userEl = els.list.querySelector(`[data-grant-user="${id}"]`);
|
|
276
|
+
const actionEl = els.list.querySelector(`[data-grant-action="${id}"]`);
|
|
277
|
+
const durEl = els.list.querySelector(`[data-grant-duration="${id}"]`);
|
|
278
|
+
const pinEl = els.list.querySelector(`[data-grant-pin="${id}"]`);
|
|
279
|
+
const grantedTo = (userEl?.value ?? '').trim();
|
|
280
|
+
const floorAction = actionEl?.value ?? 'prod-deploy';
|
|
281
|
+
const pin = pinEl?.value ?? '';
|
|
282
|
+
const problems = [];
|
|
283
|
+
if (!grantedTo) problems.push('• Pick (or type) who the grant is for.');
|
|
284
|
+
if (!pin) problems.push('• Type your dashboard PIN — granting a floor action is a human action; agent credentials are refused.');
|
|
285
|
+
if (problems.length > 0) {
|
|
286
|
+
note('Not granted — fix the following first:\n' + problems.join('\n'), 'error');
|
|
287
|
+
return;
|
|
288
|
+
}
|
|
289
|
+
// A grant can never outlive its mandate — clamp client-side so the
|
|
290
|
+
// operator's pick always succeeds (the server enforces the same rule
|
|
291
|
+
// by rejection; rejection is a worse experience than a shorter window).
|
|
292
|
+
const minutes = Number(durEl?.value ?? 60) || 60;
|
|
293
|
+
const mandate = lastMandates.find((m) => m.id === id);
|
|
294
|
+
const mandateExpiryMs = mandate ? Date.parse(mandate.expiresAt) : NaN;
|
|
295
|
+
let expiryMs = Date.now() + minutes * 60_000;
|
|
296
|
+
let clamped = false;
|
|
297
|
+
if (!isNaN(mandateExpiryMs) && expiryMs > mandateExpiryMs) { expiryMs = mandateExpiryMs; clamped = true; }
|
|
298
|
+
btn.disabled = true;
|
|
299
|
+
try {
|
|
300
|
+
const { status, body } = await fetchJson(`/mandate/${encodeURIComponent(id)}/grants`, {
|
|
301
|
+
method: 'POST',
|
|
302
|
+
headers: { 'Content-Type': 'application/json' },
|
|
303
|
+
body: JSON.stringify({
|
|
304
|
+
pin,
|
|
305
|
+
grants: [{
|
|
306
|
+
floorAction,
|
|
307
|
+
grantedTo,
|
|
308
|
+
authorizedBy: 'operator (dashboard PIN)',
|
|
309
|
+
expiresAt: new Date(expiryMs).toISOString(),
|
|
310
|
+
}],
|
|
311
|
+
}),
|
|
312
|
+
});
|
|
313
|
+
if (status === 201) {
|
|
314
|
+
note(`✓ Grant signed — ${grantedTo} may ${floorAction} until ${fmtWhen(new Date(expiryMs).toISOString())}${clamped ? ' (shortened to the mandate’s own expiry — a grant can never outlive its mandate)' : ''}.`, 'success');
|
|
315
|
+
await refresh();
|
|
316
|
+
} else {
|
|
317
|
+
note(`Not granted — the server refused: ${body?.error ?? `HTTP ${status}`}`, 'error');
|
|
318
|
+
}
|
|
319
|
+
} catch (e) {
|
|
320
|
+
// A network failure must neither strand the typed PIN (cleared in
|
|
321
|
+
// finally) nor fail silently.
|
|
322
|
+
note(`Not granted — request failed: ${e?.message ?? e}. Nothing was signed; try again.`, 'error');
|
|
323
|
+
} finally {
|
|
324
|
+
if (pinEl) pinEl.value = ''; // never retain the PIN — on ANY path
|
|
325
|
+
btn.disabled = false;
|
|
326
|
+
}
|
|
327
|
+
};
|
|
328
|
+
});
|
|
329
|
+
}
|
|
330
|
+
|
|
187
331
|
// Validate EVERYTHING client-side and report every problem at once —
|
|
188
332
|
// never POST a request we know the server will refuse, and never let the
|
|
189
333
|
// operator discover a missing field one transient error at a time.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"PostUpdateMigrator.d.ts","sourceRoot":"","sources":["../../src/core/PostUpdateMigrator.ts"],"names":[],"mappings":"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
|
|
1
|
+
{"version":3,"file":"PostUpdateMigrator.d.ts","sourceRoot":"","sources":["../../src/core/PostUpdateMigrator.ts"],"names":[],"mappings":"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"}
|
|
@@ -5027,6 +5027,24 @@ Create worktrees for collaborator repos with \`instar worktree create <branch>\`
|
|
|
5027
5027
|
patched = true;
|
|
5028
5028
|
result.upgraded.push('CLAUDE.md: added Coordination Mandate awareness section');
|
|
5029
5029
|
}
|
|
5030
|
+
// Phone-first floor grants (Mobile-Complete Operator Actions, instar#1080)
|
|
5031
|
+
// — agents that already carry the Coordination Mandate section must learn
|
|
5032
|
+
// to point operators at the Mandates-tab grant form, never at a terminal
|
|
5033
|
+
// command. Content-sniffed on the bullet's distinctive lead; inserted
|
|
5034
|
+
// inside the existing section when its anchor line is intact, appended
|
|
5035
|
+
// otherwise so a hand-edited section still gains the guidance.
|
|
5036
|
+
if (content.includes('/mandate/evaluate') && !content.includes('User floor-action grants are phone-first')) {
|
|
5037
|
+
const grantBullet = `- **User floor-action grants are phone-first.** When the operator needs to grant a USER a floor action (e.g. "let Mia prod-deploy for an hour"), the Mandates tab carries a grant form on every active mandate: pick the person (from the registered-user list), pick the action and duration, type the PIN, tap Grant. Send them the dashboard link — NEVER a terminal command or a hand-built API call (Mobile-Complete Operator Actions). The grant is signed into the mandate, clamped to the mandate's expiry, and voided by revoking the mandate.`;
|
|
5038
|
+
const grantAnchor = 'point them at the dashboard **Mandates tab** (issue/revoke forms + the decision audit live there).';
|
|
5039
|
+
if (content.includes(grantAnchor)) {
|
|
5040
|
+
content = content.replace(grantAnchor, grantAnchor + '\n' + grantBullet);
|
|
5041
|
+
}
|
|
5042
|
+
else {
|
|
5043
|
+
content += '\n' + grantBullet + '\n';
|
|
5044
|
+
}
|
|
5045
|
+
patched = true;
|
|
5046
|
+
result.upgraded.push('CLAUDE.md: added phone-first floor-grant guidance to the Coordination Mandate section');
|
|
5047
|
+
}
|
|
5030
5048
|
// ReviewExchange protocol (coordination-mandate spec §7 G2.3) — Agent
|
|
5031
5049
|
// Awareness backfill. Content-sniffed on the distinctive route prefix.
|
|
5032
5050
|
if (!content.includes('/review-exchange')) {
|