@lovek4d/kickflip 0.3.1 → 0.5.0
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/CONTEXT.md +170 -118
- package/README.md +23 -14
- package/SKILL.md +40 -38
- package/bin/doctor.mjs +4 -4
- package/bin/install.mjs +3 -3
- package/bin/kickflip.mjs +34 -26
- package/bin/wait-events.mjs +2 -2
- package/board/arch-review.mjs +1 -1
- package/board/auto-review.mjs +2 -0
- package/board/bin/board.mjs +213 -74
- package/board/domain.mjs +8 -5
- package/board/finish.mjs +4 -0
- package/board/headless.mjs +4 -0
- package/board/merge3.mjs +1 -0
- package/board/on-host.mjs +1 -1
- package/board/permissions.mjs +2 -0
- package/board/public/app-arch.js +2 -2
- package/board/public/app-asks.js +39 -103
- package/board/public/app-banner.js +2 -2
- package/board/public/app-cards.js +166 -73
- package/board/public/app-core.js +2 -23
- package/board/public/app-header.js +12 -5
- package/board/public/app-lanes.js +201 -47
- package/board/public/app-needs.js +2 -1
- package/board/public/app-reader.js +174 -0
- package/board/public/app-snapshot.js +2 -2
- package/board/public/app-threads.js +53 -42
- package/board/public/app.html +4 -1
- package/board/public/app.js +78 -96
- package/board/public/attach.js +39 -3
- package/board/public/dialogs.js +59 -14
- package/board/public/hub-kit.js +9 -6
- package/board/public/hub.css +6 -18
- package/board/public/linear-key.js +6 -4
- package/board/public/log.html +48 -0
- package/board/public/new-task.js +121 -0
- package/board/public/prompts.js +9 -8
- package/board/public/prototype-kit.js +6 -6
- package/board/public/prototype-switcher.js +13 -5
- package/board/public/prototype.html +1 -0
- package/board/public/router.js +8 -7
- package/board/public/rules-editor.js +3 -4
- package/board/public/shell.js +24 -6
- package/board/public/sidebar.css +22 -1
- package/board/public/sidebar.js +64 -31
- package/board/public/style.css +245 -64
- package/board/public/toasts.js +2 -2
- package/board/public/ui-cards.js +205 -75
- package/board/public/ui-enums.js +6 -5
- package/board/public/ui-header.js +10 -10
- package/board/public/ui-hub.js +4 -4
- package/board/public/ui-markdown.js +1 -1
- package/board/public/ui-order.js +9 -6
- package/board/public/ui-page-header.js +10 -7
- package/board/public/ui-patch.js +53 -0
- package/board/public/ui-phase-bars.js +17 -0
- package/board/public/ui-prompts.js +8 -8
- package/board/public/ui-questions.js +61 -29
- package/board/public/ui-sidebar.js +54 -24
- package/board/public/ui-snapshot.js +20 -9
- package/board/public/ui-threads.js +40 -21
- package/board/public/ui.js +116 -18
- package/board/public/view-frame.js +2 -1
- package/board/public/view-hub-settings.js +25 -18
- package/board/public/view-hub.js +53 -12
- package/board/public/view-notes.js +1 -1
- package/board/public/view-questions.js +142 -62
- package/board/public/view-settings.js +189 -52
- package/board/review.mjs +1 -1
- package/board/server.mjs +20 -12
- package/board/store.mjs +15 -12
- package/board/tickets.mjs +3 -3
- package/board/worktree.mjs +21 -15
- package/docs/hub-session.md +4 -4
- package/hooks-plugin/hooks/codex-share-auto.mjs +1 -1
- package/hooks-plugin/hooks/watcher-stop.mjs +2 -2
- package/hub/board-repo.mjs +1 -1
- package/hub/boards.mjs +4 -4
- package/hub/close-board.mjs +1 -1
- package/hub/front-door.mjs +20 -0
- package/hub/github.mjs +4 -4
- package/hub/home.mjs +1 -1
- package/hub/hub.mjs +11 -14
- package/hub/incoming.mjs +2 -0
- package/hub/linear.mjs +2 -2
- package/hub/live.mjs +2 -2
- package/hub/new-board.mjs +2 -2
- package/hub/owner-checks.mjs +1 -0
- package/hub/permission-denied-hook.mjs +2 -0
- package/hub/pr-report.mjs +3 -1
- package/hub/presence.mjs +1 -1
- package/hub/prompts.mjs +1 -1
- package/hub/push.mjs +2 -2
- package/hub/readiness.mjs +2 -2
- package/hub/repos.mjs +3 -3
- package/hub/restart-board.mjs +1 -1
- package/hub/schedule.mjs +1 -1
- package/hub/sessions.mjs +2 -2
- package/hub/sw.js +1 -1
- package/hub/try.mjs +8 -4
- package/hub/usage.mjs +3 -3
- package/hub/views.mjs +1 -1
- package/package.json +5 -1
- package/shared/card-prs.mjs +1 -0
- package/shared/colors.css +5 -4
- package/shared/fake-gh.mjs +59 -8
- package/shared/owner-check.mjs +3 -0
- package/shared/repos.mjs +1 -1
- package/shared/tailscale.mjs +1 -0
- package/shared/usage-gate.mjs +1 -1
- package/board/public/view-polling.js +0 -87
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// Board Settings, a modal the app shell (shell.js) mounts over its board for /<route>/settings.html.
|
|
2
|
-
// Saving reviewers sends a `reviewers` event {add: [name], remove: [name]};
|
|
3
|
-
//
|
|
2
|
+
// Saving reviewers sends a `reviewers` event {add: [name], remove: [name]}; the server removes names itself and the list
|
|
3
|
+
// redraws from its answer, while Claude finds each new person's Slack and GitHub accounts, adds them with board.mjs
|
|
4
|
+
// reviewers, and replies. Tickets save on each change, and Done, ✕,
|
|
4
5
|
// Escape and the backdrop wait for that save to land before closing. An unsaved edit marks [data-dirty], and the shell
|
|
5
6
|
// asks before closing the modal then.
|
|
6
7
|
(globalThis.AppViews ||= {}).settings = {
|
|
@@ -14,29 +15,50 @@
|
|
|
14
15
|
<h2>Tickets</h2>
|
|
15
16
|
<p class="muted small">Every card on this board has a ticket from here. A new card gets a new ticket unless you name one.</p>
|
|
16
17
|
<form id="ticketsForm" class="tk-section"><div id="boardTickets">Loading…</div></form>
|
|
18
|
+
<div class="tk-inline" id="ticketsKey" hidden></div>
|
|
17
19
|
<p class="muted small" id="ticketsStatus" role="status"></p>
|
|
18
|
-
<h2>
|
|
19
|
-
<label class="who" for="
|
|
20
|
-
<span
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
<
|
|
20
|
+
<h2>Auto</h2>
|
|
21
|
+
<label class="who" for="autoPull"><span><span>Auto implement Todo cards</span>
|
|
22
|
+
<span class="muted small sub">Claude starts a Todo card as soon as it’s ready and nothing blocks or holds it, while an implementer slot is free.</span></span>
|
|
23
|
+
<input type="checkbox" class="toggle" id="autoPull" disabled></label>
|
|
24
|
+
<p class="muted small" id="autoPullStatus" role="status"></p>
|
|
25
|
+
<label class="who" for="maxImplementers"><span><span>Maximum parallel implementers</span>
|
|
26
|
+
<span class="muted small sub">Cards building or waiting in Orchestrator review count. Grills and cards waiting on you don’t.</span></span>
|
|
27
|
+
<input type="number" class="count" id="maxImplementers" min="1" max="99" step="1" inputmode="numeric" disabled></label>
|
|
28
|
+
<p class="muted small" id="maxImplementersStatus" role="status"></p>
|
|
29
|
+
<label class="who" for="autoBuild"><span><span>Auto serve</span>
|
|
27
30
|
<span class="muted small sub" id="autoBuildSub">${BoardUI.esc(BoardUI.tryWords('web').autoSub)}</span></span>
|
|
28
31
|
<input type="checkbox" class="toggle" id="autoBuild" disabled></label>
|
|
29
32
|
<p class="muted small" id="autoBuildStatus" role="status"></p>
|
|
30
|
-
<
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
33
|
+
<label class="who" for="autoReview"><span><span>Auto review</span>
|
|
34
|
+
<span class="muted small sub">Claude reviews teammates’ PRs that touch this board’s work. May use a lot of usage.</span></span>
|
|
35
|
+
<input type="checkbox" class="toggle" id="autoReview" disabled></label>
|
|
36
|
+
<p class="muted small" id="autoReviewStatus" role="status"></p>
|
|
37
|
+
<div id="prsRow" hidden><label class="who" for="prs"><span><span>PRs</span>
|
|
38
|
+
<span class="muted small sub">Each card opens a PR. Off, no PR opens: each card lands on main and main is pushed.</span></span>
|
|
39
|
+
<input type="checkbox" class="toggle" id="prs" disabled></label>
|
|
40
|
+
<p class="muted small" id="prsStatus" role="status"></p></div>
|
|
41
|
+
<label class="who" for="autoMerge"><span><span>Auto-merge</span>
|
|
42
|
+
<span class="muted small sub" id="autoMergeSub">Claude merges a PR once it is approved, green and merges cleanly.</span></span>
|
|
43
|
+
<input type="checkbox" class="toggle" id="autoMerge" disabled></label>
|
|
44
|
+
<p class="muted small" id="autoMergeStatus" role="status"></p>
|
|
45
|
+
<div id="skipDraftRow" hidden><label class="who" for="skipDraft"><span><span>Skip the draft</span>
|
|
46
|
+
<span class="muted small sub">A builder’s PR opens as a draft and is marked ready for review once CI passes, so it goes to reviewers without waiting for you.</span></span>
|
|
47
|
+
<input type="checkbox" class="toggle" id="skipDraft" disabled></label>
|
|
48
|
+
<p class="muted small">With Auto-merge on too, a builder’s PR can merge once reviewers approve it, without you seeing it.</p>
|
|
49
|
+
<p class="muted small" id="skipDraftStatus" role="status"></p></div>
|
|
50
|
+
<label class="who" for="autoTriage"><span><span>Triage overnight</span>
|
|
51
|
+
<span class="muted small sub">Once a night, when two or more cards wait on your answers, Claude merges their overlapping questions and duplicate cards, and tidies Todo.</span></span>
|
|
38
52
|
<input type="checkbox" class="toggle" id="autoTriage" disabled></label>
|
|
39
53
|
<p class="muted small" id="autoTriageStatus" role="status"></p>
|
|
54
|
+
<label class="who" for="idleNotification"><span><span>Idle notification</span>
|
|
55
|
+
<span class="muted small sub">Pushes once when the board has been idle for 15 minutes, with what closed since the last one.</span></span>
|
|
56
|
+
<input type="checkbox" class="toggle" id="idleNotification" disabled></label>
|
|
57
|
+
<p class="muted small" id="idleNotificationStatus" role="status"></p>
|
|
58
|
+
<p class="muted small">The hub runs <code>.kickflip/build</code> from each card’s branch and from main. Claude wrote it when it set up this board; if builds break, file a card.</p>
|
|
59
|
+
<h2>Permissions</h2>
|
|
60
|
+
<div id="permissions"><p class="muted small">Loading…</p></div>
|
|
61
|
+
<div id="reviewersSection" hidden>
|
|
40
62
|
<h2>Reviewers</h2>
|
|
41
63
|
<p class="muted small">Everyone here gets each PR link on this board once it's ready, and the weekday digest. Someone you add also gets the links of PRs already in review.</p>
|
|
42
64
|
<div id="list"><p class="muted small">Loading…</p></div>
|
|
@@ -46,6 +68,7 @@
|
|
|
46
68
|
</form>
|
|
47
69
|
<div class="row" style="margin-top:12px"><span class="muted small" id="saveStatus"></span><button class="primary" id="save" disabled>Save</button></div>
|
|
48
70
|
<div class="replies" id="replies"></div>
|
|
71
|
+
</div>
|
|
49
72
|
<h2>Rules</h2>
|
|
50
73
|
<p class="muted small">Standing instructions for this board's Claude session. It sees them each time it wakes, and says what changes when you save.</p>
|
|
51
74
|
<div id="rules"><p class="muted small">Loading…</p></div>
|
|
@@ -139,11 +162,15 @@
|
|
|
139
162
|
e.preventDefault();
|
|
140
163
|
if (closing || typed.value !== closure?.title) return;
|
|
141
164
|
closing = true;
|
|
142
|
-
|
|
143
|
-
|
|
165
|
+
// Close board waits until the hub has stopped the board's session and server.
|
|
166
|
+
const post = (async () => {
|
|
144
167
|
const r = await fetch('/_hub/close-board', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ route, name: typed.value }) });
|
|
145
168
|
const out = await r.json();
|
|
146
169
|
if (!r.ok) throw new Error(out.error || 'Could not close board.');
|
|
170
|
+
return out;
|
|
171
|
+
})();
|
|
172
|
+
try {
|
|
173
|
+
const out = await BoardUI.waitOn(closeConfirm, post);
|
|
147
174
|
if (!B.alive()) return;
|
|
148
175
|
closeSheet.close();
|
|
149
176
|
ctx.sidebarChanged?.();
|
|
@@ -159,7 +186,20 @@
|
|
|
159
186
|
const repoURL = `/_hub/board-repo/${encodeURIComponent(B.route)}`;
|
|
160
187
|
let repo = {}, closeRepo = () => {};
|
|
161
188
|
let tickets, savedTickets, savingTickets = null, ticketsTouched = false;
|
|
162
|
-
|
|
189
|
+
// PRs shows on a board on GitHub that builds from its shared clone; a local board never opens PRs. Skip the draft and
|
|
190
|
+
// Reviewers show only while cards open PRs, and Auto-merge says what it merges
|
|
191
|
+
// (docs/adr/2026-11-04-1913-a-board-can-skip-the-draft.md, docs/adr/2026-11-04-2100-a-land-waits-for-your-merge.md).
|
|
192
|
+
let prsSetting = true;
|
|
193
|
+
const opensPRs = () => Boolean(repo.github) && !repo.local && prsSetting;
|
|
194
|
+
const syncPRs = () => {
|
|
195
|
+
$('#prsRow').hidden = !repo.github || Boolean(repo.local);
|
|
196
|
+
$('#skipDraftRow').hidden = $('#reviewersSection').hidden = !opensPRs();
|
|
197
|
+
$('#autoMergeSub').textContent = opensPRs() ? 'Claude merges a PR once it is approved, green and merges cleanly.' : 'Lands each verified card on main without waiting for your Merge.';
|
|
198
|
+
};
|
|
199
|
+
const showRepo = () => {
|
|
200
|
+
$('#boardRepoValue').textContent = repo.github || repo.remote ? `${repo.github || repo.remote} · ${repo.branch || 'Branch not recorded'}` : 'No GitHub repo';
|
|
201
|
+
syncPRs();
|
|
202
|
+
};
|
|
163
203
|
get(repoURL).then(({ body }) => {
|
|
164
204
|
if (!B.alive()) return;
|
|
165
205
|
repo = body;
|
|
@@ -202,11 +242,21 @@
|
|
|
202
242
|
return false;
|
|
203
243
|
}
|
|
204
244
|
}
|
|
245
|
+
// A save waits on the control last tapped or changed, found by selector since ticketsControl redraws it, and every
|
|
246
|
+
// other tickets control is disabled meanwhile (ui.js waitOn).
|
|
247
|
+
let ticketsTapped = '[data-tk-source][aria-pressed="true"]';
|
|
248
|
+
const tapped = (e) => {
|
|
249
|
+
const source = e.target.closest?.('[data-tk-source]');
|
|
250
|
+
if (source) ticketsTapped = `[data-tk-source="${source.dataset.tkSource}"]`;
|
|
251
|
+
else if (e.target.dataset?.tk) ticketsTapped = `[data-tk="${e.target.dataset.tk}"]`;
|
|
252
|
+
};
|
|
253
|
+
B.on(ticketsForm, 'click', tapped, true);
|
|
254
|
+
B.on(ticketsForm, 'change', tapped, true);
|
|
205
255
|
// One save at a time: a change made while one is out saves once it lands.
|
|
206
256
|
function saveTickets() {
|
|
207
257
|
syncTickets();
|
|
208
258
|
if (savingTickets || !unsaved() || !tickets.valid()) return;
|
|
209
|
-
ticketsStatus.
|
|
259
|
+
ticketsStatus.textContent = '';
|
|
210
260
|
LinearKey.doneBusy(modal.querySelectorAll('[data-settings-dismiss]'), true);
|
|
211
261
|
savingTickets = (async () => {
|
|
212
262
|
let saved, value;
|
|
@@ -220,6 +270,8 @@
|
|
|
220
270
|
LinearKey.doneBusy(modal.querySelectorAll('[data-settings-dismiss]'), false);
|
|
221
271
|
syncTickets();
|
|
222
272
|
})();
|
|
273
|
+
BoardUI.waitOn(ticketsForm.querySelector(ticketsTapped) || ticketsForm.querySelector('[data-tk-source][aria-pressed="true"]'), savingTickets,
|
|
274
|
+
{ group: ticketsForm.querySelectorAll('button, select, input') });
|
|
223
275
|
}
|
|
224
276
|
B.on(ticketsForm, 'submit', (e) => { e.preventDefault(); saveTickets(); });
|
|
225
277
|
let buildKind = 'web';
|
|
@@ -263,24 +315,28 @@
|
|
|
263
315
|
B.on($('#save'), 'click', async () => {
|
|
264
316
|
const status = $('#saveStatus');
|
|
265
317
|
try {
|
|
266
|
-
await site.send('reviewers', { add: [...add], remove: [...remove] });
|
|
318
|
+
const out = await BoardUI.waitOn($('#save'), site.send('reviewers', { add: [...add], remove: [...remove] }));
|
|
319
|
+
status.textContent = add.size ? 'Sent. Claude updates the list and replies below.' : 'Saved';
|
|
320
|
+
current = out.board.reviewers;
|
|
267
321
|
add.clear(); remove.clear(); render();
|
|
268
|
-
|
|
269
|
-
} catch { status.textContent = 'Could not send. Is the host awake?'; }
|
|
322
|
+
} catch (e) { status.textContent = e?.reason || 'Could not send. Is the host awake?'; }
|
|
270
323
|
});
|
|
271
324
|
B.watch((events) => {
|
|
272
325
|
$('#replies').innerHTML = events.map((e) => `<div class="reply">${e.reply ? (e.reply.html ?? '') : '<span class="muted small">Waiting for Claude…</span>'}</div>`).join('');
|
|
273
326
|
}, { kind: 'reviewers' });
|
|
274
327
|
|
|
275
328
|
// The toggle saves at once through POST api/settings, which writes board.json itself, so it takes effect with no
|
|
276
|
-
// session awake (docs/adr/
|
|
329
|
+
// session awake (docs/adr/2026-10-29-1752-auto-merge-is-a-board-setting-off-by-default.md).
|
|
277
330
|
const autoMerge = $('#autoMerge');
|
|
278
331
|
const autoMergeStatus = $('#autoMergeStatus');
|
|
332
|
+
let savedMax = '';
|
|
279
333
|
get(site.url('api/settings'), waitIn('#autoMergeStatus')).then(({ body: d }) => {
|
|
280
334
|
if (!B.alive()) return;
|
|
281
335
|
savedTickets = d.settings?.tickets || null;
|
|
282
336
|
tickets = BoardDialogs.ticketsControl($('#boardTickets'), { value: savedTickets || { source: 'internal', prefix: 'INT' }, repo: () => repo.github || '', title: () => ctx.siteTitle || '',
|
|
283
337
|
loadLinear: async () => (await get('/_hub/api/linear')).body,
|
|
338
|
+
keyState: async () => (await get('/_hub/api/settings')).body.linear, keySlot: $('#ticketsKey'),
|
|
339
|
+
keyBusy: (on) => LinearKey.doneBusy(modal.querySelectorAll('[data-settings-dismiss]'), on),
|
|
284
340
|
change(_value, _valid, { loaded, typing } = {}) {
|
|
285
341
|
if (!loaded) ticketsTouched = true;
|
|
286
342
|
if (typing) syncTickets(); else saveTickets();
|
|
@@ -297,69 +353,150 @@
|
|
|
297
353
|
$('#autoBuild').disabled = false;
|
|
298
354
|
$('#autoPull').checked = d.settings?.auto_pull === true;
|
|
299
355
|
$('#autoPull').disabled = false;
|
|
356
|
+
$('#maxImplementers').value = savedMax = String(d.settings?.max_implementers);
|
|
357
|
+
$('#maxImplementers').disabled = false;
|
|
300
358
|
$('#autoTriage').checked = d.settings?.auto_triage === true;
|
|
301
359
|
$('#autoTriage').disabled = false;
|
|
360
|
+
$('#autoReview').checked = d.settings?.auto_review === true;
|
|
361
|
+
$('#autoReview').disabled = false;
|
|
362
|
+
$('#idleNotification').checked = d.settings?.idle_notification === true;
|
|
363
|
+
$('#idleNotification').disabled = false;
|
|
364
|
+
$('#skipDraft').checked = d.settings?.skip_draft === true;
|
|
365
|
+
$('#skipDraft').disabled = false;
|
|
366
|
+
$('#prs').checked = prsSetting = d.settings?.prs !== false;
|
|
367
|
+
$('#prs').disabled = false;
|
|
368
|
+
syncPRs();
|
|
302
369
|
}).catch(() => { autoMergeStatus.textContent = ticketsStatus.textContent = 'Offline. Is the host awake?'; });
|
|
370
|
+
// An Auto toggle keeps its face, disabled with a spinner beside it (ui.js waitOn), while POST api/settings saves it.
|
|
371
|
+
// → the saved settings
|
|
372
|
+
const saveToggle = (toggle, body) => BoardUI.waitOn(toggle, (async () => {
|
|
373
|
+
const r = await fetch(site.url('api/settings'), { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify(body) });
|
|
374
|
+
const out = await r.json().catch(() => ({}));
|
|
375
|
+
if (!r.ok) throw new Error(out.error);
|
|
376
|
+
return out.settings;
|
|
377
|
+
})());
|
|
303
378
|
B.on(autoMerge, 'change', async () => {
|
|
304
379
|
const want = autoMerge.checked;
|
|
305
|
-
|
|
380
|
+
const ask = opensPRs() ? 'Turn on auto-merge? Claude merges each approved PR on this board once CI is green and it merges cleanly, without asking you.'
|
|
381
|
+
: 'Turn on auto-merge? Each verified card lands on main without asking you.';
|
|
382
|
+
if (want && !(await BoardDialogs.confirm(ask))) {
|
|
306
383
|
autoMerge.checked = false;
|
|
307
384
|
return;
|
|
308
385
|
}
|
|
309
386
|
if (!B.alive()) return;
|
|
310
|
-
autoMerge.disabled = true;
|
|
311
387
|
try {
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
autoMerge.checked = out.settings.auto_merge === true;
|
|
316
|
-
autoMergeStatus.textContent = autoMerge.checked ? 'Saved. Claude merges approved, green PRs itself.' : 'Saved. Approved, green PRs wait for your Merge.';
|
|
388
|
+
autoMerge.checked = (await saveToggle(autoMerge, { auto_merge: want })).auto_merge === true;
|
|
389
|
+
autoMergeStatus.textContent = opensPRs() ? (autoMerge.checked ? 'Saved. Claude merges approved, green PRs itself.' : 'Saved. Approved, green PRs wait for your Merge.')
|
|
390
|
+
: autoMerge.checked ? 'Saved. Each verified card lands on main by itself.' : 'Saved. Each verified card waits for your Merge.';
|
|
317
391
|
} catch (e) {
|
|
318
392
|
autoMerge.checked = !want;
|
|
319
393
|
autoMergeStatus.textContent = e.message || 'Could not save. Is the host awake?';
|
|
320
394
|
}
|
|
321
|
-
autoMerge.disabled = false;
|
|
322
395
|
});
|
|
323
396
|
|
|
324
397
|
B.on($('#autoBuild'), 'change', async () => {
|
|
325
398
|
const toggle = $('#autoBuild');
|
|
326
399
|
const want = toggle.checked;
|
|
327
|
-
toggle.disabled = true;
|
|
328
400
|
try {
|
|
329
|
-
const
|
|
330
|
-
const out = await r.json();
|
|
331
|
-
if (!r.ok) throw new Error(out.error);
|
|
401
|
+
const settings = await saveToggle(toggle, { auto_build: want });
|
|
332
402
|
if (!B.alive()) return;
|
|
333
|
-
toggle.checked =
|
|
403
|
+
toggle.checked = settings.auto_build === true;
|
|
334
404
|
$('#autoBuildStatus').textContent = BoardUI.tryWords(buildKind).saved(toggle.checked);
|
|
335
405
|
} catch (e) {
|
|
336
406
|
if (!B.alive()) return;
|
|
337
407
|
toggle.checked = !want;
|
|
338
408
|
$('#autoBuildStatus').textContent = e.message || 'Could not save. Is the host awake?';
|
|
339
|
-
}
|
|
409
|
+
}
|
|
340
410
|
});
|
|
341
411
|
|
|
342
|
-
//
|
|
343
|
-
//
|
|
344
|
-
|
|
412
|
+
// These Auto toggles save with no confirmation: docs/adr/2026-11-02-1754-an-idle-board-pulls-in-its-top-todo-card.md,
|
|
413
|
+
// docs/adr/2026-11-04-1900-triage-runs-once-overnight-on-cards-waiting-on-your-answers.md and
|
|
414
|
+
// docs/adr/2026-11-03-1731-each-board-reviews-incoming-prs-that-touch-its-work.md; Idle notification too:
|
|
415
|
+
// docs/adr/2026-11-04-0257-an-idle-board-pushes-once-when-it-has-gone-idle.md.
|
|
416
|
+
const autoToggle = (id, key, saved) => B.on($(`#${id}`), 'change', async () => {
|
|
345
417
|
const toggle = $(`#${id}`);
|
|
346
418
|
const want = toggle.checked;
|
|
347
|
-
toggle.disabled = true;
|
|
348
419
|
try {
|
|
349
|
-
const
|
|
350
|
-
const out = await r.json().catch(() => ({}));
|
|
351
|
-
if (!r.ok) throw new Error(out.error);
|
|
420
|
+
const settings = await saveToggle(toggle, { [key]: want });
|
|
352
421
|
if (!B.alive()) return;
|
|
353
|
-
toggle.checked =
|
|
422
|
+
toggle.checked = settings[key] === true;
|
|
354
423
|
$(`#${id}Status`).textContent = saved(toggle.checked);
|
|
355
424
|
} catch (e) {
|
|
356
425
|
if (!B.alive()) return;
|
|
357
426
|
toggle.checked = !want;
|
|
358
427
|
$(`#${id}Status`).textContent = e.message || 'Could not save. Is the host awake?';
|
|
359
|
-
}
|
|
428
|
+
}
|
|
429
|
+
});
|
|
430
|
+
autoToggle('autoPull', 'auto_pull', (on) => (on ? 'Saved. Claude starts ready Todo cards while an implementer slot is free.' : 'Saved. Todo cards wait for you.'));
|
|
431
|
+
|
|
432
|
+
// docs/adr/2026-11-04-0057-auto-implement-fills-in-progress-up-to-a-cap.md: Maximum parallel implementers saves on
|
|
433
|
+
// change, waiting on the server like a toggle; a number outside 1 to 99 or a refusal puts the saved one back.
|
|
434
|
+
B.on($('#maxImplementers'), 'change', async () => {
|
|
435
|
+
const input = $('#maxImplementers');
|
|
436
|
+
const status = $('#maxImplementersStatus');
|
|
437
|
+
const want = /^\d+$/.test(input.value) ? Number(input.value) : 0;
|
|
438
|
+
if (want < 1 || want > 99) { input.value = savedMax; status.textContent = 'Pick a whole number from 1 to 99.'; return; }
|
|
439
|
+
try {
|
|
440
|
+
const settings = await saveToggle(input, { max_implementers: want });
|
|
441
|
+
if (!B.alive()) return;
|
|
442
|
+
input.value = savedMax = String(settings.max_implementers);
|
|
443
|
+
status.textContent = `Saved. Up to ${settings.max_implementers === 1 ? '1 card builds or waits' : `${settings.max_implementers} cards build or wait`} in Orchestrator review at once.`;
|
|
444
|
+
} catch (e) {
|
|
445
|
+
if (!B.alive()) return;
|
|
446
|
+
input.value = savedMax;
|
|
447
|
+
status.textContent = e.message || 'Could not save. Is the host awake?';
|
|
448
|
+
}
|
|
449
|
+
});
|
|
450
|
+
autoToggle('autoReview', 'auto_review', (on) => (on ? 'Saved. When idle, Claude reviews incoming PRs that touch this board’s work.' : 'Saved. Claude leaves incoming PRs alone.'));
|
|
451
|
+
autoToggle('idleNotification', 'idle_notification', (on) => (on ? 'Saved. One push once the board has been idle for 15 minutes.' : 'Saved. No push when the board goes idle.'));
|
|
452
|
+
autoToggle('autoTriage', 'auto_triage', (on) => (on ? 'Saved. Once a night, when cards wait on your answers, Claude triages them.' : 'Saved. Questions and Todo are left as they are.'));
|
|
453
|
+
autoToggle('skipDraft', 'skip_draft', (on) => (on ? 'Saved. A builder’s draft PR is marked ready for review once CI passes.' : 'Saved. A builder’s draft PR waits for your review.'));
|
|
454
|
+
// PRs off needs Push main after a land, which the server checks; its refusal shows in the status line.
|
|
455
|
+
autoToggle('prs', 'prs', (on) => {
|
|
456
|
+
prsSetting = on;
|
|
457
|
+
syncPRs();
|
|
458
|
+
return on ? 'Saved. Each card opens a PR.' : 'Saved. Each card lands on main with no PR, and main is pushed.';
|
|
459
|
+
});
|
|
460
|
+
|
|
461
|
+
// Each switch writes or removes one allow rule in the board folder's .claude/settings.local.json through POST
|
|
462
|
+
// api/permissions (docs/adr/2026-11-03-1921-board-settings-grant-a-boards-session-permissions.md); the hub lets only
|
|
463
|
+
// the owner reach the board (docs/adr/2026-11-04-2156-the-whole-hub-answers-only-the-owner.md). A tap waits on the server with the
|
|
464
|
+
// switch as it was and a spinner beside it, then draws the server's answer.
|
|
465
|
+
const permissions = $('#permissions');
|
|
466
|
+
const REFUSED = {
|
|
467
|
+
denied: (command) => `Auto mode still refused this once: ${command}. Approve it under /permissions, Recently blocked.`,
|
|
468
|
+
asked: (command) => `The session still asked before running ${command}, so it hasn’t picked the rule up yet. It applies once the session restarts.`,
|
|
469
|
+
};
|
|
470
|
+
const permRow = (p) => `<label class="who" for="perm-${esc(p.id)}"><span><span>${esc(p.name)}</span>
|
|
471
|
+
<span class="muted small sub">${esc(p.allows)} Writes <code>${esc(p.rule)}</code></span></span>
|
|
472
|
+
<span class="toggle-slot"><span class="try-spin toggle-spin" aria-hidden="true"></span><input type="checkbox" class="toggle" data-perm="${esc(p.id)}" id="perm-${esc(p.id)}"${p.on ? ' checked' : ''}></span></label>
|
|
473
|
+
<p class="muted small" id="perm-${esc(p.id)}Status" role="status"></p>
|
|
474
|
+
${p.refused ? `<p class="muted small refused-note">${esc(REFUSED[p.refused.how](p.refused.command))}</p>` : ''}`;
|
|
475
|
+
const drawPermissions = (d) => {
|
|
476
|
+
permissions.innerHTML = d.permissions.map(permRow).join('');
|
|
477
|
+
};
|
|
478
|
+
get(site.url('api/permissions'), waitIn('#permissions')).then(({ ok, body }) => {
|
|
479
|
+
if (!B.alive()) return;
|
|
480
|
+
if (!ok) { permissions.innerHTML = `<p class="muted small">${esc(body.error || 'Could not load permissions.')}</p>`; return; }
|
|
481
|
+
drawPermissions(body);
|
|
482
|
+
}).catch(() => { if (B.alive()) permissions.innerHTML = '<p class="muted">Offline. Is the host awake?</p>'; });
|
|
483
|
+
B.on(permissions, 'change', async (e) => {
|
|
484
|
+
const toggle = e.target.closest?.('input[data-perm]');
|
|
485
|
+
if (!toggle) return;
|
|
486
|
+
const id = toggle.dataset.perm;
|
|
487
|
+
const on = toggle.checked;
|
|
488
|
+
toggle.checked = !on;
|
|
489
|
+
try {
|
|
490
|
+
const r = await BoardUI.waitOn(toggle, fetch(site.url('api/permissions'), { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ id, on }) }));
|
|
491
|
+
const out = await r.json().catch(() => ({}));
|
|
492
|
+
if (!r.ok) throw new Error(out.error);
|
|
493
|
+
if (!B.alive()) return;
|
|
494
|
+
drawPermissions(out);
|
|
495
|
+
$(`#perm-${id}Status`).textContent = on ? 'Saved. The session runs it without asking.' : 'Saved. The session asks first again.';
|
|
496
|
+
} catch (error) {
|
|
497
|
+
if (B.alive()) $(`#perm-${id}Status`).textContent = error.message || 'Could not save. Is the host awake?';
|
|
498
|
+
}
|
|
360
499
|
});
|
|
361
|
-
todoToggle('autoPull', 'auto_pull', (on) => (on ? 'Saved. When no implementer runs, Claude pulls in the top Todo card.' : 'Saved. Todo cards wait until they are unblocked, answered or bumped.'));
|
|
362
|
-
todoToggle('autoTriage', 'auto_triage', (on) => (on ? 'Saved. Once a day, when the board is idle and Todo changed, Claude triages it.' : 'Saved. Todo is left as it is.'));
|
|
363
500
|
|
|
364
501
|
// Saving writes board.json through POST api/rules, whose `rules {scope: "board"}` event wakes Claude to acknowledge
|
|
365
502
|
// the change; replies show below, newest first.
|
package/board/review.mjs
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
var
|
|
1
|
+
var d=Object.defineProperty;var l=(t,e)=>d(t,"name",{value:e,configurable:!0});import m from"node:fs";import p from"node:path";import w from"node:os";import{routeRecord as a}from"../hub/routes.mjs";import{storedPRs as b}from"../shared/card-prs.mjs";function N(){const t=p.join(w.homedir(),".claude","slack-bridge","topics"),e=Object.create(null);let r=[];try{r=m.readdirSync(t).filter(o=>o.endsWith(".json")).sort()}catch{}for(const o of r)try{const i=JSON.parse(m.readFileSync(p.join(t,o),"utf8"));for(const[f,s]of Object.entries(i.items||{}))e[f]={...e[f],...s}}catch{}return e}l(N,"topicItems");function k(t,e,r,o={}){const i=Object.create(null),f=[e?.review_link_ts,e?.review_sent,a(t?.review_link_ts,b(e)[0]?.number),a(t?.review_link_ts,r),a(o,r)?.review_link_ts];for(const s of f)for(const[u,n]of Array.isArray(s)?s.map(c=>[c,null]):Object.entries(s||{})){if(Object.hasOwn(i,u))continue;const c=typeof n=="string"?n:n?.ts;n!==null&&(typeof c!="string"||!/^\d+(?:\.\d+)?$/.test(c))||(i[u]={ts:c||null,...typeof n?.channel=="string"?{channel:n.channel}:{}})}return i}l(k,"reviewLinks");const g=l(t=>{const e=t?Number(t)*1e3:NaN;return Number.isFinite(e)&&!Number.isNaN(new Date(e).getTime())?new Date(e).toISOString():null},"reviewTime");function S(t,e=[]){return[...new Map([...e,...t.reviewers||[]].map(r=>[r.name,r])).values()]}l(S,"reviewersOf");export{k as reviewLinks,g as reviewTime,S as reviewersOf,N as topicItems};
|