@lovek4d/kickflip 0.1.0 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (96) hide show
  1. package/CONTEXT.md +177 -78
  2. package/README.md +46 -5
  3. package/SKILL.md +58 -37
  4. package/bin/doctor.mjs +4 -3
  5. package/bin/install.mjs +3 -3
  6. package/bin/kickflip.mjs +87 -32
  7. package/bin/statusline.mjs +4 -3
  8. package/bin/wait-events.mjs +2 -2
  9. package/board/arch-review.mjs +5 -0
  10. package/board/bin/agent.mjs +1 -1
  11. package/board/bin/board.mjs +241 -69
  12. package/board/bin/builder-stop-hook.mjs +4 -3
  13. package/board/domain.mjs +5 -3
  14. package/board/on-host.mjs +1 -0
  15. package/board/public/app-arch.js +75 -0
  16. package/board/public/app-asks.js +33 -14
  17. package/board/public/app-cards.js +242 -49
  18. package/board/public/app-core.js +39 -5
  19. package/board/public/app-header.js +24 -6
  20. package/board/public/app-lanes.js +76 -26
  21. package/board/public/app-needs.js +1 -0
  22. package/board/public/app-snapshot.js +45 -11
  23. package/board/public/app-threads.js +13 -11
  24. package/board/public/app.html +4 -1
  25. package/board/public/app.js +131 -20
  26. package/board/public/dialogs.js +114 -116
  27. package/board/public/hub-kit.js +19 -2
  28. package/board/public/hub.css +24 -18
  29. package/board/public/linear-key.js +141 -0
  30. package/board/public/router.js +6 -3
  31. package/board/public/rules-editor.js +1 -1
  32. package/board/public/shell.js +1 -1
  33. package/board/public/sidebar.css +31 -11
  34. package/board/public/sidebar.js +88 -16
  35. package/board/public/style.css +141 -46
  36. package/board/public/toasts.js +6 -4
  37. package/board/public/ui-cards.js +102 -37
  38. package/board/public/ui-header.js +17 -8
  39. package/board/public/ui-hub.js +30 -12
  40. package/board/public/ui-links.js +18 -10
  41. package/board/public/ui-order.js +18 -0
  42. package/board/public/ui-page-header.js +4 -2
  43. package/board/public/ui-questions.js +37 -16
  44. package/board/public/ui-sidebar.js +49 -11
  45. package/board/public/ui-snapshot.js +15 -9
  46. package/board/public/ui-threads.js +25 -16
  47. package/board/public/ui.js +23 -11
  48. package/board/public/view-hub-settings.js +46 -30
  49. package/board/public/view-hub.js +64 -13
  50. package/board/public/view-notes.js +7 -4
  51. package/board/public/view-polling.js +3 -3
  52. package/board/public/view-questions.js +42 -22
  53. package/board/public/view-settings.js +90 -35
  54. package/board/review.mjs +1 -0
  55. package/board/server.mjs +12 -7
  56. package/board/store.mjs +14 -6
  57. package/board/tickets.mjs +3 -3
  58. package/board/worktree.mjs +14 -13
  59. package/docs/hub-session.md +3 -3
  60. package/hooks-plugin/hooks/codex-share-auto.mjs +7 -5
  61. package/hooks-plugin/hooks/watcher-stop.mjs +41 -0
  62. package/hub/awake.mjs +1 -0
  63. package/hub/board-repo.mjs +1 -1
  64. package/hub/boards.mjs +4 -4
  65. package/hub/close-board.mjs +1 -1
  66. package/hub/duration.mjs +1 -0
  67. package/hub/github.mjs +4 -5
  68. package/hub/home.mjs +1 -1
  69. package/hub/hub.mjs +12 -10
  70. package/hub/linear.mjs +2 -2
  71. package/hub/new-board.mjs +2 -2
  72. package/hub/pr-report.mjs +1 -0
  73. package/hub/push.mjs +2 -2
  74. package/hub/readiness.mjs +2 -0
  75. package/hub/repos.mjs +3 -2
  76. package/hub/restart-board.mjs +1 -0
  77. package/hub/routes.mjs +1 -1
  78. package/hub/schedule.mjs +1 -1
  79. package/hub/sessions.mjs +2 -2
  80. package/hub/shell.mjs +2 -1
  81. package/hub/slack.mjs +2 -0
  82. package/hub/stream.mjs +12 -12
  83. package/hub/sw.js +1 -1
  84. package/hub/trust.mjs +2 -0
  85. package/hub/try.mjs +4 -3
  86. package/hub/upgrade.mjs +1 -0
  87. package/hub/usage.mjs +3 -3
  88. package/hub/views.mjs +1 -1
  89. package/package.json +1 -1
  90. package/shared/colors.css +7 -5
  91. package/shared/fake-gh.mjs +54 -0
  92. package/shared/service.mjs +16 -16
  93. package/shared/skills.mjs +2 -0
  94. package/shared/usage-gate.mjs +1 -1
  95. package/starter/public/claude.js +3 -2
  96. package/bin/web-ts.mjs +0 -2
@@ -1,7 +1,8 @@
1
1
  // Board Settings, a modal the app shell (shell.js) mounts over its board for /<route>/settings.html.
2
2
  // Saving reviewers sends a `reviewers` event {add: [name], remove: [name]}; Claude finds each new person's Slack and
3
- // GitHub accounts, writes the list with board.mjs reviewers, and replies. An unsaved edit marks [data-dirty], and the
4
- // shell asks before closing the modal then.
3
+ // GitHub accounts, writes the list with board.mjs reviewers, and replies. Tickets save on each change, and Done, ✕,
4
+ // Escape and the backdrop wait for that save to land before closing. An unsaved edit marks [data-dirty], and the shell
5
+ // asks before closing the modal then.
5
6
  (globalThis.AppViews ||= {}).settings = {
6
7
  className: 'view-settings',
7
8
  title: () => 'Board Settings',
@@ -12,7 +13,7 @@
12
13
  <p class="muted small" id="boardRepoStatus"></p>
13
14
  <h2>Tickets</h2>
14
15
  <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>
15
- <form id="ticketsForm" class="tk-section"><div id="boardTickets">Loading…</div><button type="submit" id="ticketsSave" disabled>Save</button></form>
16
+ <form id="ticketsForm" class="tk-section"><div id="boardTickets">Loading…</div></form>
16
17
  <p class="muted small" id="ticketsStatus" role="status"></p>
17
18
  <h2>Merging</h2>
18
19
  <label class="who" for="autoMerge">
@@ -27,6 +28,15 @@
27
28
  <input type="checkbox" class="toggle" id="autoBuild" disabled></label>
28
29
  <p class="muted small" id="autoBuildStatus" role="status"></p>
29
30
  <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>
31
+ <h2>Todo</h2>
32
+ <label class="who" for="autoPull"><span><span>Pull in Todo cards when idle</span>
33
+ <span class="muted small sub">On: when no implementer is running on this board, Claude takes the top Todo card that nothing blocks or holds, and grills or implements it. Off: a Todo card waits until it is unblocked, answered or bumped.</span></span>
34
+ <input type="checkbox" class="toggle" id="autoPull" disabled></label>
35
+ <p class="muted small" id="autoPullStatus" role="status"></p>
36
+ <label class="who" for="autoTriage"><span><span>Triage Todo when idle</span>
37
+ <span class="muted small sub">On: once a day, when no implementer is running and Todo changed, Claude looks for duplicate cards, cards another covers, cards ready to close and stale deps. It links cards and merges clear duplicates itself, and asks you about the rest under one topic. Off: Todo is left as it is.</span></span>
38
+ <input type="checkbox" class="toggle" id="autoTriage" disabled></label>
39
+ <p class="muted small" id="autoTriageStatus" role="status"></p>
30
40
  <h2>Reviewers</h2>
31
41
  <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>
32
42
  <div id="list"><p class="muted small">Loading…</p></div>
@@ -66,12 +76,20 @@
66
76
  const modal = $('#boardSettingsModal');
67
77
  const previous = document.activeElement;
68
78
  const unfitModal = BoardDialogs.fitSheet(modal);
69
- B.on(modal, 'cancel', (e) => { e.preventDefault(); ctx.dismiss?.(); });
79
+ let dismissing = false;
80
+ function close() {
81
+ if (tickets) saveTickets();
82
+ if (!savingTickets) { ctx.dismiss?.(); return; }
83
+ if (dismissing) return;
84
+ dismissing = true;
85
+ savingTickets.then(() => { dismissing = false; if (B.alive()) ctx.dismiss?.(); });
86
+ }
87
+ B.on(modal, 'cancel', (e) => { e.preventDefault(); close(); });
70
88
  B.on(modal, 'click', (e) => {
71
- if (e.target.closest?.('[data-settings-dismiss]')) ctx.dismiss?.();
89
+ if (e.target.closest?.('[data-settings-dismiss]')) close();
72
90
  else if (e.target === modal) {
73
91
  const rect = modal.getBoundingClientRect();
74
- if (e.clientX < rect.left || e.clientX > rect.right || e.clientY < rect.top || e.clientY > rect.bottom) ctx.dismiss?.();
92
+ if (e.clientX < rect.left || e.clientX > rect.right || e.clientY < rect.top || e.clientY > rect.bottom) close();
75
93
  }
76
94
  });
77
95
  B.atUnmount(() => { unfitModal(); modal.close(); if (previous?.isConnected) previous.focus({ preventScroll: true }); });
@@ -89,13 +107,13 @@
89
107
  const { body: d } = await get(`/_hub/close-board/${encodeURIComponent(route)}`);
90
108
  if (!B.alive()) return;
91
109
  closure = d;
92
- closeRow.disabled = d.hub === true;
93
- $('#closeBoardReason').textContent = d.hub ? 'The hub board cannot be closed while it is flagged as the hub.' : '';
110
+ closeRow.disabled = false;
111
+ $('#closeBoardReason').textContent = '';
94
112
  $('#closeBoardName').textContent = d.title;
95
113
  $('#closeBoardTypeName').textContent = d.title;
96
114
  const warnings = [...(d.prs || []).map((p) => `<li>PR #${esc(p.pr)} (${esc(p.repo)}) — ${esc(p.wave)}/${esc(p.id)}</li>`),
97
115
  ...(d.builders || []).map((b) => `<li>${esc(b.label)} — ${esc(b.key)}</li>`)];
98
- $('#closeBoardWarning').innerHTML = warnings.length ? `<h3>This board still has active work</h3><ul>${warnings.join('')}</ul><p>Running builder records will be closed with a note. PRs stay open on GitHub; this board stops polling them.</p>` : '';
116
+ $('#closeBoardWarning').innerHTML = warnings.length ? `<h3>This board still has active work</h3><ul>${warnings.join('')}</ul><p>Running implementers will be stopped with a note. PRs stay open on GitHub; this board stops polling them.</p>` : '';
99
117
  };
100
118
  loadClosure().catch(() => { if (B.alive()) $('#closeBoardReason').textContent = 'Could not load Close board. Is the host awake?'; });
101
119
  B.on(closeRow, 'click', async () => {
@@ -103,7 +121,7 @@
103
121
  closeRow.disabled = true;
104
122
  try {
105
123
  await loadClosure();
106
- if (!B.alive() || closure.hub) return;
124
+ if (!B.alive()) return;
107
125
  typed.value = '';
108
126
  closeConfirm.disabled = true;
109
127
  $('#closeBoardError').textContent = '';
@@ -119,7 +137,7 @@
119
137
  B.on(closeSheet, 'click', (e) => { if (e.target === closeSheet && !closing) closeSheet.close(); });
120
138
  B.on($('#closeBoardForm'), 'submit', async (e) => {
121
139
  e.preventDefault();
122
- if (closing || closure?.hub || typed.value !== closure?.title) return;
140
+ if (closing || typed.value !== closure?.title) return;
123
141
  closing = true;
124
142
  closeConfirm.disabled = true;
125
143
  try {
@@ -140,19 +158,13 @@
140
158
  B.atUnmount(() => { unfit?.(); closeSheet.close(); });
141
159
  const repoURL = `/_hub/board-repo/${encodeURIComponent(B.route)}`;
142
160
  let repo = {}, closeRepo = () => {};
143
- let tickets, savedTickets, savingTickets = false, ticketsTouched = false;
144
- const showRepo = () => { $('#boardRepoValue').textContent = repo.github ? `${repo.github} · ${repo.branch || 'Branch not recorded'}` : 'No GitHub repo'; };
161
+ let tickets, savedTickets, savingTickets = null, ticketsTouched = false;
162
+ const showRepo = () => { $('#boardRepoValue').textContent = repo.github || repo.remote ? `${repo.github || repo.remote} · ${repo.branch || 'Branch not recorded'}` : 'No GitHub repo'; };
145
163
  get(repoURL).then(({ body }) => {
146
164
  if (!B.alive()) return;
147
165
  repo = body;
148
166
  tickets?.render();
149
167
  if (tickets) syncTickets();
150
- if (repo.hub) {
151
- $('#boardRepoValue').textContent = 'Kickflip’s own clone';
152
- $('#boardRepoChange').hidden = true;
153
- $('#boardRepoStatus').textContent = `${repo.repo} · The hub board’s builders branch from its main.`;
154
- return;
155
- }
156
168
  showRepo();
157
169
  $('#boardRepo').disabled = false;
158
170
  }).catch(() => { $('#boardRepoValue').textContent = 'Could not load repo.'; });
@@ -172,30 +184,44 @@
172
184
  B.atUnmount(() => closeRepo());
173
185
  B.atUnmount(() => tickets?.destroy());
174
186
  const ticketsForm = $('#ticketsForm');
175
- const ticketsSave = $('#ticketsSave');
176
187
  const ticketsStatus = $('#ticketsStatus');
188
+ const unsaved = () => ticketsTouched && JSON.stringify(tickets.read()) !== JSON.stringify(savedTickets);
177
189
  function syncTickets() {
178
- const dirty = ticketsTouched && JSON.stringify(tickets.read()) !== JSON.stringify(savedTickets);
179
- if (dirty) ticketsForm.setAttribute('data-dirty', '');
190
+ if (unsaved()) ticketsForm.setAttribute('data-dirty', '');
180
191
  else ticketsForm.removeAttribute('data-dirty');
181
- ticketsSave.disabled = savingTickets || !dirty || !tickets.valid();
182
192
  }
183
- B.on(ticketsForm, 'submit', async (e) => {
184
- e.preventDefault();
185
- if (!tickets || ticketsSave.disabled || savingTickets) return;
186
- savingTickets = true;
187
- const value = tickets.read();
188
- syncTickets();
193
+ async function postTickets(value) {
189
194
  try {
190
195
  const r = await fetch(site.url('api/settings'), { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ tickets: value }) });
191
196
  const out = await r.json();
192
197
  if (!r.ok) throw new Error(out.error || 'Could not save tickets.');
193
- if (!B.alive()) return;
194
198
  savedTickets = out.settings.tickets;
195
- ticketsStatus.textContent = 'Saved.';
196
- } catch (e) { if (B.alive()) ticketsStatus.textContent = e.message || 'Could not save. Is the host awake?'; }
197
- finally { savingTickets = false; if (B.alive()) syncTickets(); }
198
- });
199
+ return true;
200
+ } catch (e) {
201
+ if (B.alive()) ticketsStatus.textContent = e.message || 'Could not save. Is the host awake?';
202
+ return false;
203
+ }
204
+ }
205
+ // One save at a time: a change made while one is out saves once it lands.
206
+ function saveTickets() {
207
+ syncTickets();
208
+ if (savingTickets || !unsaved() || !tickets.valid()) return;
209
+ ticketsStatus.innerHTML = '<span class="tk-saving"><span class="try-spin" aria-hidden="true"></span>Saving…</span>';
210
+ LinearKey.doneBusy(modal.querySelectorAll('[data-settings-dismiss]'), true);
211
+ savingTickets = (async () => {
212
+ let saved, value;
213
+ do {
214
+ value = tickets.read();
215
+ saved = await postTickets(value);
216
+ } while (saved && B.alive() && tickets.valid() && JSON.stringify(tickets.read()) !== JSON.stringify(value));
217
+ savingTickets = null;
218
+ if (!B.alive()) return;
219
+ if (saved) ticketsStatus.textContent = 'Saved';
220
+ LinearKey.doneBusy(modal.querySelectorAll('[data-settings-dismiss]'), false);
221
+ syncTickets();
222
+ })();
223
+ }
224
+ B.on(ticketsForm, 'submit', (e) => { e.preventDefault(); saveTickets(); });
199
225
  let buildKind = 'web';
200
226
  let current = [];
201
227
  const add = new Set();
@@ -255,7 +281,10 @@
255
281
  savedTickets = d.settings?.tickets || null;
256
282
  tickets = BoardDialogs.ticketsControl($('#boardTickets'), { value: savedTickets || { source: 'internal', prefix: 'INT' }, repo: () => repo.github || '', title: () => ctx.siteTitle || '',
257
283
  loadLinear: async () => (await get('/_hub/api/linear')).body,
258
- change(_value, _valid, { loaded } = {}) { if (!loaded) ticketsTouched = true; syncTickets(); }, createProject: async (team, name) => {
284
+ change(_value, _valid, { loaded, typing } = {}) {
285
+ if (!loaded) ticketsTouched = true;
286
+ if (typing) syncTickets(); else saveTickets();
287
+ }, createProject: async (team, name) => {
259
288
  const r = await fetch('/_hub/linear/projects', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ team, name }) });
260
289
  const out = await r.json().catch(() => ({}));
261
290
  if (!r.ok) throw new Error(out.error || 'Could not create the project. Is the host awake?');
@@ -266,6 +295,10 @@
266
295
  autoMerge.disabled = false;
267
296
  $('#autoBuild').checked = d.settings?.auto_build === true;
268
297
  $('#autoBuild').disabled = false;
298
+ $('#autoPull').checked = d.settings?.auto_pull === true;
299
+ $('#autoPull').disabled = false;
300
+ $('#autoTriage').checked = d.settings?.auto_triage === true;
301
+ $('#autoTriage').disabled = false;
269
302
  }).catch(() => { autoMergeStatus.textContent = ticketsStatus.textContent = 'Offline. Is the host awake?'; });
270
303
  B.on(autoMerge, 'change', async () => {
271
304
  const want = autoMerge.checked;
@@ -306,6 +339,28 @@
306
339
  } finally { if (B.alive()) toggle.disabled = false; }
307
340
  });
308
341
 
342
+ // The Todo section's toggles save with no confirmation: docs/adr/0123-an-idle-board-pulls-in-its-top-todo-card.md
343
+ // and docs/adr/0165-an-idle-board-triages-its-todo-once-a-day.md.
344
+ const todoToggle = (id, key, saved) => B.on($(`#${id}`), 'change', async () => {
345
+ const toggle = $(`#${id}`);
346
+ const want = toggle.checked;
347
+ toggle.disabled = true;
348
+ try {
349
+ const r = await fetch(site.url('api/settings'), { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ [key]: want }) });
350
+ const out = await r.json().catch(() => ({}));
351
+ if (!r.ok) throw new Error(out.error);
352
+ if (!B.alive()) return;
353
+ toggle.checked = out.settings[key] === true;
354
+ $(`#${id}Status`).textContent = saved(toggle.checked);
355
+ } catch (e) {
356
+ if (!B.alive()) return;
357
+ toggle.checked = !want;
358
+ $(`#${id}Status`).textContent = e.message || 'Could not save. Is the host awake?';
359
+ } finally { if (B.alive()) toggle.disabled = false; }
360
+ });
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
+
309
364
  // Saving writes board.json through POST api/rules, whose `rules {scope: "board"}` event wakes Claude to acknowledge
310
365
  // the change; replies show below, newest first.
311
366
  get(site.url('api/rules'), waitIn('#rules')).then(({ body: d }) => {
@@ -0,0 +1 @@
1
+ var w=Object.defineProperty;var l=(t,e)=>w(t,"name",{value:e,configurable:!0});import m from"node:fs";import p from"node:path";import d from"node:os";import{routeRecord as f}from"../hub/routes.mjs";function _(){const t=p.join(d.homedir(),".claude","slack-bridge","topics"),e=Object.create(null);let r=[];try{r=m.readdirSync(t).filter(i=>i.endsWith(".json")).sort()}catch{}for(const i of r)try{const o=JSON.parse(m.readFileSync(p.join(t,i),"utf8"));for(const[a,s]of Object.entries(o.items||{}))e[a]={...e[a],...s}}catch{}return e}l(_,"topicItems");function j(t,e,r,i={}){const o=Object.create(null),a=[e?.review_link_ts,e?.review_sent,f(t?.review_link_ts,e?.pr),f(t?.review_link_ts,r),f(i,r)?.review_link_ts];for(const s of a)for(const[u,n]of Array.isArray(s)?s.map(c=>[c,null]):Object.entries(s||{})){if(Object.hasOwn(o,u))continue;const c=typeof n=="string"?n:n?.ts;n!==null&&(typeof c!="string"||!/^\d+(?:\.\d+)?$/.test(c))||(o[u]={ts:c||null,...typeof n?.channel=="string"?{channel:n.channel}:{}})}return o}l(j,"reviewLinks");const N=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 k(t,e=[]){return[...new Map([...e,...t.reviewers||[]].map(r=>[r.name,r])).values()]}l(k,"reviewersOf");export{j as reviewLinks,N as reviewTime,k as reviewersOf,_ as topicItems};
package/board/server.mjs CHANGED
@@ -1,12 +1,17 @@
1
1
  #!/usr/bin/env node
2
- var wt=Object.defineProperty;var c=(t,e)=>wt(t,"name",{value:e,configurable:!0});import gt from"node:http";import m from"node:fs/promises";import l from"node:path";import X from"node:os";import{execFile as K}from"node:child_process";import{fileURLToPath as yt}from"node:url";import V from"node:vm";import kt from"node:zlib";import St from"node:crypto";import{promisify as bt}from"node:util";import{buildBoard as Y,ITEM_EVENTS as vt,needsAnswer as jt,refOf as D,threadOf as Ot}from"./domain.mjs";import{createReadStream as Et,watch as Pt}from"node:fs";import{BoardError as x,boardConfig as xt,readPrototypes as $t,takeFile as Tt,finishHandshake as Rt,itemSources as R,readAskLinks as F,readAskProgress as At,readBoardRules as It,readBoardSettings as Nt,readHandshake as _t,readHidden as Ht,readNotes as Ct,readWorking as B,readUpload as Dt,saveBoardRules as Ft,saveBoardSettings as Bt,saveUpload as Ut,setHidden as Mt,settingsOf as A,threadFile as Lt,wavesDir as Z}from"./store.mjs";import{changed as Jt,readHubRules as Wt}from"../hub/rules.mjs";import{boardAt as q,listenState as Gt,linksOf as zt}from"../hub/boards.mjs";import{pendingFor as Qt,shown as Xt}from"../hub/prompts.mjs";import{dropQueued as Kt}from"../hub/push.mjs";import{routeRecord as j}from"../hub/routes.mjs";import{VIEW_PAGES as Vt,shellHtml as Yt}from"../hub/shell.mjs";import{crossSite as Zt,isJSON as qt}from"../starter/cross-site.mjs";const E=l.dirname(yt(import.meta.url));async function tt(){const t=await m.stat(l.join(E,"..","hub","icon-180.png")).catch(()=>null);return`/_hub/icon-180.png?v=${t?Math.floor(t.mtimeMs).toString(36):"0"}`}c(tt,"touchIcon");async function te(t){if(!t.includes("<!--header "))return t;const e=V.createContext({});return V.runInContext(await m.readFile(l.join(E,"public","ui-page-header.js"),"utf8"),e),t.replace(/<!--header (\{.*?\})-->/g,(a,n)=>e.BoardUI.header(JSON.parse(n)))}c(te,"pageHeaders");const u=l.resolve(process.argv[2]||process.cwd()),ee=[l.join(u,"public"),l.join(E,"public"),l.join(E,"..","starter","public"),l.join(E,"..","shared")],S=l.join(u,"data"),U=l.join(S,"events.jsonl"),I=l.join(S,"replies"),ae=l.join(X.homedir(),".claude","slack-bridge","state.json"),et=l.join(X.homedir(),".claude","slack-bridge","topics");async function ne(){let t=[];try{t=(await m.readdir(et)).filter(a=>a.endsWith(".json"))}catch{}const e=Object.create(null);for(const a of t){const n=await y(l.join(et,a),{items:{}});for(const[i,s]of Object.entries(n.items||{}))e[i]={...e[i],...s}}return e}c(ne,"topicItems");const at=Number(process.env.PORT||4600),O=(process.env.PREFIX||"").replace(/\/+$/,""),nt={".html":"text/html; charset=utf-8",".css":"text/css; charset=utf-8",".js":"text/javascript; charset=utf-8",".mjs":"text/javascript; charset=utf-8",".json":"application/json; charset=utf-8",".svg":"image/svg+xml",".png":"image/png",".ico":"image/x-icon",".txt":"text/plain; charset=utf-8",".md":"text/markdown; charset=utf-8",".plist":"text/xml; charset=utf-8",".ipa":"application/octet-stream",".apk":"application/vnd.android.package-archive"},ie=bt(kt.gzip),se=/^(text\/|application\/json|image\/svg\+xml)/;async function it(t,e,a,n){const i={...a};let s=Buffer.isBuffer(n)?n:Buffer.from(n??"");if(se.test(i["content-type"]||"")&&(i.vary="accept-encoding",s.length>=1024&&/\bgzip\b/.test(t.req?.headers["accept-encoding"]||"")))try{s=await ie(s),i["content-encoding"]="gzip"}catch{}i["content-length"]=s.length,t.writeHead(e,i),t.end(t.req?.method==="HEAD"?void 0:s)}c(it,"send");const h=c((t,e,a)=>it(t,e,{"content-type":nt[".json"],"cache-control":"no-store"},JSON.stringify(a)),"json"),N=c(t=>`${Math.floor(t.mtimeMs).toString(36)}-${t.size.toString(36)}`,"versionOf"),st=/\b(src|href)="([\w.][\w./-]*\.(?:js|css))"/g;async function oe(t,e){const a=e.endsWith("/")?e:l.posix.dirname(e),n=[...new Set([...t.matchAll(st)].map(s=>s[2]))],i=new Map(await Promise.all(n.map(async s=>{const o=await ut(l.posix.join(a,s)),r=o?await m.stat(o).catch(()=>null):null;return[s,r?N(r):null]})));return t.replace(st,(s,o,r)=>i.get(r)?`${o}="${r}?v=${i.get(r)}"`:s)}c(oe,"versionAssets");const re=c((t,e)=>String(t||"").split(",").map(a=>a.trim().replace(/^W\//,"")).includes(e.replace(/^W\//,"")),"sameTag"),y=c(async(t,e)=>{try{return JSON.parse(await m.readFile(t,"utf8"))}catch{return e}},"readJSON"),$=c(async t=>{try{return(await m.readFile(t,"utf8")).split(`
3
- `).filter(Boolean).map(e=>JSON.parse(e))}catch{return[]}},"readLines"),M=c(t=>Lt(u,t),"threadFile"),L=c(async()=>{const t=await Ht(u);return(await $(U)).filter(e=>!t.has(e.id))},"shownEvents");async function T(t,e=64*1024){if(!qt(t.headers))throw Object.assign(new Error("Send the body as application/json."),{status:403});let a="";for await(const n of t)if(a+=n,a.length>e)throw Object.assign(new Error("too large"),{status:413});return a?JSON.parse(a):{}}c(T,"readBody");async function _(t,e){const a={id:"e"+Date.now().toString(36)+Math.random().toString(36).slice(2,5),ts:new Date().toISOString(),kind:t,data:e};return await m.mkdir(I,{recursive:!0}),await m.appendFile(U,JSON.stringify(a)+`
4
- `),a.id}c(_,"appendEvent");async function J(t){for(const e of["html","json"]){const a=l.join(I,`${t.id}.${e}`);try{const[n,i]=await Promise.all([m.readFile(a,"utf8"),m.stat(a)]);return{...t,reply:{[e]:e==="json"?JSON.parse(n):n,at:i.mtime.toISOString()}}}catch{}}return{...t,reply:null}}c(J,"withReply");async function ce(t,e){const a=await B(u);let n=await L();return t&&(n=n.filter(i=>i.kind===t)),Promise.all(n.reverse().slice(0,e).map(async i=>{const s=await J(i);return s.reply||!j(a,i.id)?s:{...s,working:j(a,i.id)}}))}c(ce,"listEvents");const ot=new Map,le="number,title,url,state,isDraft,reviewDecision,mergeStateStatus,mergeable,statusCheckRollup,mergedAt,headRefName,headRefOid,updatedAt,latestReviews";function ue(t,e){return new Promise(a=>{K("gh",["pr","view",String(e),"--repo",t,"--json",le],{timeout:2e4},(n,i)=>{if(n)return a(null);try{a(JSON.parse(i))}catch{a(null)}})})}c(ue,"ghPR");function de(t,e){return new Promise(a=>{K("gh",["api",`repos/${t}/pulls/${e}/comments?per_page=100`],{timeout:2e4},(n,i)=>{if(n)return a(0);try{const s=JSON.parse(i),o=new Set(s.map(r=>r.in_reply_to_id).filter(Boolean));a(s.filter(r=>!r.in_reply_to_id&&/codex/i.test(r.user?.login||"")&&!o.has(r.id)).length)}catch{a(0)}})})}c(de,"ghCodexOpen");function fe(t){const e=t||[];let a=0,n=0;for(const i of e){const s=(i.conclusion||i.state||"").toUpperCase(),o=(i.status||"").toUpperCase();["FAILURE","ERROR","CANCELLED","TIMED_OUT","ACTION_REQUIRED","STARTUP_FAILURE"].includes(s)?a++:(o&&o!=="COMPLETED"||s==="PENDING"||s==="EXPECTED")&&n++}return{total:e.length,fail:a,pending:n,state:a?"fail":n?"pending":e.length?"pass":"none"}}c(fe,"checksOf");async function pe(t,e){const a=`${t}#${e}`;let n=ot.get(a);return n||(n={at:0,value:null,loading:null},ot.set(a,n)),Date.now()-n.at>45e3&&!n.loading&&(n.loading=Promise.all([ue(t,e),de(t,e)]).then(([i,s])=>{if(i){const{statusCheckRollup:o,latestReviews:r,...g}=i,w=Object.fromEntries((r||[]).map(b=>[b.author?.login,b.state]));n.value={...g,checks:fe(o),reviews:w,codexOpen:s}}n.at=Date.now(),n.loading=null})),!n.value&&n.loading&&await n.loading,n.value}c(pe,"prState");const P=c(()=>xt(u),"config");async function me(){const t=await m.readdir(I).catch(()=>[]),e=await Promise.all(t.map(n=>m.stat(l.join(I,n)).then(i=>i.mtimeMs,()=>0))),a=Math.max(0,...e);return a?new Date(a).toISOString():null}c(me,"latestReply");async function rt(t){let e=[];try{e=(await m.readdir(t)).filter(i=>i.endsWith(".json"))}catch{}const a=await Promise.all(e.map(i=>y(l.join(t,i),null))),n=Object.create(null);return e.forEach((i,s)=>{if(!a[s])return;const[o,r="main"]=l.basename(i,".json").split("@");(n[o]||=Object.create(null))[r]=a[s]}),n}c(rt,"readAgents");async function ct(){const t=await P(),e=t.items==="waves",a=await R(u),n=(await Promise.all(a.map(async s=>{const o=await y(s.file,null);return!o||!Array.isArray(o.items)?null:{slug:s.slug,wave:o,agents:await rt(l.join(s.dir,"agents"))}}))).filter(Boolean),i=Object.create(null);return await Promise.all(n.flatMap(({slug:s,wave:o})=>o.items.map(async r=>{i[`${s}/${r.id}`]=await $(M(`${s}/${r.id}`))}))),{github:q(u)?.github,fromWaves:e,autoMerge:A(t).auto_merge,tickets:A(t).tickets,waves:n,threads:i}}c(ct,"itemSnapshot");async function he(){const t=await ct(),{waves:e,threads:a}=t,n=Object.create(null);await Promise.all(e.flatMap(({wave:r})=>{const g=t.github||r.repo;return g?r.items.filter(w=>w.pr).map(async w=>{n[`${g}#${w.pr}`]=await pe(g,w.pr)}):[]}));const i=new Set(["ask","owner-answer","grill-more","item-message"]),s=await B(u),o=await Promise.all((await L()).map(r=>i.has(r.kind)||j(s,r.id)?J(r):{...r,reply:null}));return await Promise.all(o.filter(r=>r.kind==="ask").map(async r=>{const g=await $(M(`ask:${r.id}`));g.length&&(a[`ask:${r.id}`]=g)})),{...t,route:O.slice(1)||l.basename(u),try:await y(l.join(S,"try.json"),null),main:await y(l.join(S,"main.json"),null),prs:n,events:o,working:s,askLinks:await F(u),askProgress:await At(u),checked:await y(l.join(S,"checked.json"),null),watcher:await y(l.join(S,"watcher.json"),null),replyAt:await me(),watched:(await y(l.join(Z(),"monitor.json"),{waves:[]})).waves||[],owner:await y(l.join(S,"owner.json"),{items:[]}),bridge:await y(ae,{items:{},config:{}}),topicItems:await ne()}}c(he,"snapshot");let W=!1;async function G(){if(!W){W=!0;try{const t=l.join(S,"builder-stalled.json"),e=await y(t,{}),a=Object.create(null);let n;for(const{key:i,lane:s,builder:o}of Y(await ct()).items)s==="done"||!o?.stalled||(a[i]=o.silentSince,j(e,i)!==o.silentSince&&(n||=await $(U),n.some(r=>r.kind==="builder-stalled"&&r.data?.key===i&&r.data?.silentSince===o.silentSince)||await _("builder-stalled",{key:i,silentSince:o.silentSince})));JSON.stringify(a)!==JSON.stringify(e)&&(await m.mkdir(S,{recursive:!0}),await m.writeFile(t+".tmp",JSON.stringify(a)+`
5
- `),await m.rename(t+".tmp",t))}catch(t){console.error("Builder check failed:",t.message)}finally{W=!1}}}c(G,"checkStalled");async function we(t){const e=await _t(u);if(!e?.hello?.id||e.done_at)return null;const a={events:e.ack?.id===e.hello.id,github:!!t?.find(n=>n.link==="github")?.ok};return a.events&&a.github?(await Rt(u),null):a}c(we,"setupOf");async function z(){const{title:t,reviewers:e}=await P(),a=q(u),n=a?Gt(a):"deaf",i=Y({...await he(),listen:n,prompts:a?Qt(a.route).map(Xt):[]}),s=a?zt(a,{listen:n}):null;return{generatedAt:new Date().toISOString(),title:t,reviewers:(e||[]).map(o=>o?.name).filter(o=>typeof o=="string"),...i,links:s,setup:await we(s)}}c(z,"board");async function ge(t){const[e,a]=t.split("/"),n=(await R(u)).find(o=>o.slug===e),i=n?await y(n.file,null):null,s=Array.isArray(i?.items)&&i.items.find(o=>o.id===a)||null;return{item:s,agents:s?j(await rt(l.join(n.dir,"agents")),a)||{}:{}}}c(ge,"itemOf");async function ye(t){const e=j(await F(u),t),{tickets:a}=A(await P()),n=[];for(const i of await R(u)){const s=await y(i.file,null);for(const o of Array.isArray(s?.items)?s.items:[]){const r=`${i.slug}/${o.id}`;o.from_ask===t?n.unshift({key:r,ref:D(s,o.id,a),became:!0}):r===e&&n.push({key:r,ref:D(s,o.id,a),became:!1})}}return n}c(ye,"askLinksOf");async function ke(t){const e=await y(l.join(S,"owner.json"),{items:[]}),a=new Set((e.items||[]).filter(f=>f.topic===`item:${t}`).map(f=>f.id)),{item:n,agents:i}=t.startsWith("ask:")?{item:null,agents:{}}:await ge(t),s=t.startsWith("ask:"),o=s?t.slice(4):n?.from_ask||null,r=s?[]:Object.entries(await F(u)).filter(([,f])=>f===t).map(([f])=>f),g=[o,...r].filter(Boolean),w=new Set([t,...g.map(f=>`ask:${f}`)]),b=(await L()).filter(f=>w.has(f.data?.key)&&vt.includes(f.kind)||f.kind==="owner-answer"&&a.has(f.data?.id)||f.kind==="ask"&&g.includes(f.id)||f.kind==="ask-edited"),C=(await Promise.all([...w].map(f=>$(M(f))))).flat();return Ot(t,{lines:C,events:await Promise.all(b.map(J)),owner:e,working:await B(u),fromAsk:o,links:s?await ye(o):[],agents:i,moved:r})}c(ke,"thread");const lt=c(t=>String(t).replace(/[&<>"']/g,e=>({"&":"&amp;","<":"&lt;",">":"&gt;",'"':"&quot;","'":"&#39;"})[e]),"esc");async function ut(t){for(const e of ee){let a=l.resolve(e,"."+t);if(a!==e&&!a.startsWith(e+l.sep))return null;if((await m.stat(a).catch(()=>null))?.isDirectory()&&(a=l.join(a,"index.html")),await m.stat(a).then(i=>i.isFile(),()=>!1))return a}return null}c(ut,"staticFile");const dt=l.join(E,"public","prototype.html");async function Se(t){const e=/^\/prototype-([a-z0-9-]+)\.html$/.exec(t)?.[1];return e&&(await $t(u)).find(a=>a.topic===e)||null}c(Se,"recordedPrototype");const ft=c(t=>JSON.stringify(t).replace(/</g,"\\u003c").replace(/\u2028/g,"\\u2028").replace(/\u2029/g,"\\u2029"),"scriptJSON");async function be(t){const e=await Promise.all(t.takes.map(async s=>{const o=`prototypes/${t.topic}/${s.key}.js`,r=await m.stat(Tt(u,t.topic,s.key)).catch(()=>null);return{key:s.key,label:s.label,src:r?.isFile()?`${o}?v=${N(r)}`:null,file:o}})),a=`${t.title} \xB7 prototype`,n={lead:"back",backHref:t.card?`./#item=${encodeURIComponent(t.card)}`:"./",title:a,controls:!1},i={prototypeTitle:lt(a),prototypeHeader:`<!--header ${ft(n).replace(/>/g,"\\u003e")}-->`,prototypeData:ft({topic:t.topic,title:t.title,takes:e})};return(await m.readFile(dt,"utf8")).replace(/\{\{(prototypeTitle|prototypeHeader|prototypeData)\}\}/g,(s,o)=>i[o])}c(be,"prototypePage");async function ve(t,e){try{const a=decodeURIComponent(String(t.headers["x-filename"]||""));return h(e,200,await Ut(u,t,{name:a,type:t.headers["content-type"],length:Number(t.headers["content-length"])||0}))}catch(a){if(!(a instanceof x)&&!(a instanceof URIError))throw a;return t.resume(),h(e,a.status||400,{error:a.message})}}c(ve,"upload");const je=/^(text\/html|image\/svg\+xml|application\/(xhtml\+)?xml|text\/xml)$/;async function Oe(t,e,a){const n=await Dt(u,a);if(!n){e.writeHead(404).end();return}if(e.writeHead(200,{"content-type":n.type.startsWith("text/")?`${n.type}; charset=utf-8`:n.type,"content-length":n.size,"content-disposition":`inline; filename*=UTF-8''${encodeURIComponent(n.name)}`,"cache-control":"private, max-age=86400","x-content-type-options":"nosniff",...je.test(n.type)?{"content-security-policy":"sandbox"}:{}}),t.method==="HEAD"){e.end();return}Et(n.path).pipe(e)}c(Oe,"sendUpload");const Q=new Set;function Ee(t,e){e.writeHead(200,{"content-type":"text/event-stream; charset=utf-8","cache-control":"no-store",connection:"keep-alive"}),e.write(`retry: 3000
2
+ var _t=Object.defineProperty;var c=(t,e)=>_t(t,"name",{value:e,configurable:!0});import{topicItems as Dt}from"./review.mjs";import{boardWorktree as lt}from"./worktree.mjs";import{fromHost as Ft}from"./on-host.mjs";import{archOf as Ht,readReviews as Bt,repoName as Ut,startReview as Ct}from"./arch-review.mjs";import Jt from"node:http";import y from"node:fs/promises";import p from"node:path";import{execFile as ut}from"node:child_process";import{fileURLToPath as Mt}from"node:url";import dt from"node:vm";import Lt from"node:zlib";import Wt from"node:crypto";import{promisify as Gt}from"node:util";import{buildBoard as A,ITEM_EVENTS as zt,needsAnswer as Qt,ownerActions as Vt,pullOf as Xt,refOf as X,startedByAnswer as Kt,threadOf as Yt,todoOf as Zt}from"./domain.mjs";import{localDay as qt}from"../hub/schedule.mjs";import{dropQueued as te}from"../hub/push.mjs";import{createReadStream as ee,existsSync as ae,readdirSync as ne,statSync as ie,watch as re}from"node:fs";import{BoardError as P,addTopic as se,boardConfig as oe,dueQueuedBuilds as ce,migrateFollowups as le,pickUp as ue,startQueuedBuild as de,startTappedBuild as fe,sendPush as pe,recordOwnerTap as me,readPrototypes as we,takeFile as he,finishHandshake as ge,itemSources as J,readAskLinks as K,readAskProgress as ye,readBoardRules as ke,readBoardSettings as Se,readHandshake as be,readHidden as ft,readLineupNotes as ve,readNotes as je,readWorking as Y,readUpload as Oe,resolveNoteAnswers as Pe,saveBoardRules as Ee,saveBoardSettings as Te,saveUpload as $e,setHidden as Ae,settingsOf as x,threadFile as xe,wavesDir as Re}from"./store.mjs";import{changed as Ie,readHubRules as Ne}from"../hub/rules.mjs";import{boardAt as Z,listenState as _e,linksOf as De}from"../hub/boards.mjs";import{pendingFor as Fe,shown as He}from"../hub/prompts.mjs";import{routeRecord as E}from"../hub/routes.mjs";import{FOREVER as Be,durationEnd as Ue}from"../hub/duration.mjs";import{durationChoices as Ce}from"../hub/views.mjs";import{VIEW_PAGES as Je,shellHtml as Me}from"../hub/shell.mjs";import{crossSite as Le,isJSON as We}from"../starter/cross-site.mjs";import{readUsageGate as pt}from"../shared/usage-gate.mjs";import{reviewWaits as Ge}from"../shared/skills.mjs";import{boardRepo as ze}from"../hub/board-repo.mjs";const N=p.dirname(Mt(import.meta.url));async function mt(){const t=await y.stat(p.join(N,"..","hub","icon-180.png")).catch(()=>null);return`/_hub/icon-180.png?v=${t?Math.floor(t.mtimeMs).toString(36):"0"}`}c(mt,"touchIcon");async function Qe(t){if(!t.includes("<!--header "))return t;const e=dt.createContext({});return dt.runInContext(await y.readFile(p.join(N,"public","ui-page-header.js"),"utf8"),e),t.replace(/<!--header (\{.*?\})-->/g,(n,a)=>e.BoardUI.header(JSON.parse(a)))}c(Qe,"pageHeaders");const d=p.resolve(process.argv[2]||process.cwd()),Ve=[p.join(d,"public"),p.join(N,"public"),p.join(N,"..","starter","public"),p.join(N,"..","shared")],b=p.join(d,"data"),F=p.join(b,"events.jsonl"),M=p.join(b,"replies"),wt=Number(process.env.PORT||4600),R=(process.env.PREFIX||"").replace(/\/+$/,""),ht={".html":"text/html; charset=utf-8",".css":"text/css; charset=utf-8",".js":"text/javascript; charset=utf-8",".mjs":"text/javascript; charset=utf-8",".json":"application/json; charset=utf-8",".svg":"image/svg+xml",".png":"image/png",".ico":"image/x-icon",".txt":"text/plain; charset=utf-8",".md":"text/markdown; charset=utf-8",".plist":"text/xml; charset=utf-8",".ipa":"application/octet-stream",".apk":"application/vnd.android.package-archive"},Xe=Gt(Lt.gzip),Ke=/^(text\/|application\/json|image\/svg\+xml)/;async function gt(t,e,n,a){const i={...n};let r=Buffer.isBuffer(a)?a:Buffer.from(a??"");if(Ke.test(i["content-type"]||"")&&(i.vary="accept-encoding",r.length>=1024&&/\bgzip\b/.test(t.req?.headers["accept-encoding"]||"")))try{r=await Xe(r),i["content-encoding"]="gzip"}catch{}i["content-length"]=r.length,t.writeHead(e,i),t.end(t.req?.method==="HEAD"?void 0:r)}c(gt,"send");const g=c((t,e,n)=>gt(t,e,{"content-type":ht[".json"],"cache-control":"no-store"},JSON.stringify(n)),"json"),L=c(t=>`${Math.floor(t.mtimeMs).toString(36)}-${t.size.toString(36)}`,"versionOf"),yt=/\b(src|href)="([\w.][\w./-]*\.(?:js|css))"/g;async function Ye(t,e){const n=e.endsWith("/")?e:p.posix.dirname(e),a=[...new Set([...t.matchAll(yt)].map(r=>r[2]))],i=new Map(await Promise.all(a.map(async r=>{const s=await Ot(p.posix.join(n,r)),o=s?await y.stat(s).catch(()=>null):null;return[r,o?L(o):null]})));return t.replace(yt,(r,s,o)=>i.get(o)?`${s}="${o}?v=${i.get(o)}"`:r)}c(Ye,"versionAssets");const Ze=c((t,e)=>String(t||"").split(",").map(n=>n.trim().replace(/^W\//,"")).includes(e.replace(/^W\//,"")),"sameTag"),v=c(async(t,e)=>{try{return JSON.parse(await y.readFile(t,"utf8"))}catch{return e}},"readJSON"),I=c(async t=>{try{return(await y.readFile(t,"utf8")).split(`
3
+ `).filter(Boolean).map(e=>JSON.parse(e))}catch{return[]}},"readLines"),H=c(t=>xe(d,t),"threadFile"),W=c(async()=>{const t=await ft(d);return(await I(F)).filter(e=>!t.has(e.id))},"shownEvents");async function _(t,e=64*1024){if(!We(t.headers))throw Object.assign(new Error("Send the body as application/json."),{status:403});let n="";for await(const a of t)if(n+=a,n.length>e)throw Object.assign(new Error("too large"),{status:413});return n?JSON.parse(n):{}}c(_,"readBody");const O=c(async(t,e)=>(await q([[t,e]]))[0],"appendEvent");async function q(t){const e=new Date().toISOString(),n=t.map(([a,i])=>({id:"e"+Date.now().toString(36)+Math.random().toString(36).slice(2,7),ts:e,kind:a,data:i}));return await y.mkdir(M,{recursive:!0}),await y.appendFile(F,n.map(a=>JSON.stringify(a)+`
4
+ `).join("")),n.map(a=>a.id)}c(q,"appendEvents");async function kt(t){const e=await q(t.map(i=>["owner-answer",i]));await Pe(d,t);const n=A(await Q({github:!1})),a=[...new Set(t.map(i=>Kt(n,String(i.id))).filter(Boolean))];return a.length&&await q(a.map(i=>["card-unblocked",{key:i}])),e}c(kt,"recordAnswers");async function qe(t){if(!Array.isArray(t)||!t.length)return"Send at least one answer.";const e=new Set(((await v(p.join(b,"owner.json"),{items:[]})).items||[]).map(a=>a.id)),n=t.filter(a=>!e.has(String(a?.id)));return n.length?`${n.map(a=>String(a?.id)).join(", ")} ${n.length===1?"is":"are"} no longer asked here. Reload to see what is, then send again.`:null}c(qe,"answersRefused");async function G(t){for(const e of["html","json"]){const n=p.join(M,`${t.id}.${e}`);try{const[a,i]=await Promise.all([y.readFile(n,"utf8"),y.stat(n)]);return{...t,reply:{[e]:e==="json"?JSON.parse(a):a,at:i.mtime.toISOString()}}}catch{}}return{...t,reply:null}}c(G,"withReply");async function ta(t,e){const n=await Y(d);let a=await W();return t&&(a=a.filter(i=>i.kind===t)),Promise.all(a.reverse().slice(0,e).map(async i=>{const r=await G(i);return r.reply||!E(n,i.id)?r:{...r,working:E(n,i.id)}}))}c(ta,"listEvents");const St=new Map,ea="number,title,url,state,isDraft,reviewDecision,mergeStateStatus,mergeable,statusCheckRollup,mergedAt,headRefName,headRefOid,updatedAt,latestReviews";function aa(t,e){return new Promise(n=>{ut("gh",["pr","view",String(e),"--repo",t,"--json",ea],{timeout:2e4},(a,i)=>{if(a)return n(null);try{n(JSON.parse(i))}catch{n(null)}})})}c(aa,"ghPR");function na(t,e){return new Promise(n=>{ut("gh",["api",`repos/${t}/pulls/${e}/comments?per_page=100`],{timeout:2e4},(a,i)=>{if(a)return n(0);try{const r=JSON.parse(i),s=new Set(r.map(o=>o.in_reply_to_id).filter(Boolean));n(r.filter(o=>!o.in_reply_to_id&&/codex/i.test(o.user?.login||"")&&!s.has(o.id)).length)}catch{n(0)}})})}c(na,"ghCodexOpen");function ia(t){const e=t||[];let n=0,a=0;for(const i of e){const r=(i.conclusion||i.state||"").toUpperCase(),s=(i.status||"").toUpperCase();["FAILURE","ERROR","CANCELLED","TIMED_OUT","ACTION_REQUIRED","STARTUP_FAILURE"].includes(r)?n++:(s&&s!=="COMPLETED"||r==="PENDING"||r==="EXPECTED")&&a++}return{total:e.length,fail:n,pending:a,state:n?"fail":a?"pending":e.length?"pass":"none"}}c(ia,"checksOf");async function ra(t,e){const n=`${t}#${e}`;let a=St.get(n);return a||(a={at:0,value:null,loading:null},St.set(n,a)),Date.now()-a.at>45e3&&!a.loading&&(a.loading=Promise.all([aa(t,e),na(t,e)]).then(([i,r])=>{if(i){const{statusCheckRollup:s,latestReviews:o,...l}=i,m=Object.fromEntries((o||[]).map(h=>[h.author?.login,h.state]));a.value={...l,checks:ia(s),reviews:m,codexOpen:r}}a.at=Date.now(),a.loading=null})),!a.value&&a.loading&&await a.loading,a.value}c(ra,"prState");const T=c(()=>oe(d),"config");async function sa(){const t=await y.readdir(M).catch(()=>[]),e=await Promise.all(t.map(a=>y.stat(p.join(M,a)).then(i=>i.mtimeMs,()=>0))),n=Math.max(0,...e);return n?new Date(n).toISOString():null}c(sa,"latestReply");async function bt(t){let e=[];try{e=(await y.readdir(t)).filter(i=>i.endsWith(".json"))}catch{}const n=await Promise.all(e.map(i=>v(p.join(t,i),null))),a=Object.create(null);return e.forEach((i,r)=>{if(!n[r])return;const[s,o="main"]=p.basename(i,".json").split("@");(a[s]||=Object.create(null))[o]=n[r]}),a}c(bt,"readAgents");async function z(){const t=await T(),e=t.items==="waves",n=await J(d),a=(await Promise.all(n.map(async s=>{const o=await v(s.file,null);return!o||!Array.isArray(o.items)?null:{slug:s.slug,wave:o,agents:await bt(p.join(s.dir,"agents"))}}))).filter(Boolean),i=Object.create(null);await Promise.all(a.flatMap(({slug:s,wave:o})=>o.items.map(async l=>{i[`${s}/${l.id}`]=await I(H(`${s}/${l.id}`))})));const r=Z(d);return{github:r?.github,remote:r?.remote,branch:r?.branch,reviewers:t.reviewers||[],fromWaves:e,autoMerge:x(t).auto_merge,autoBuild:x(t).auto_build,tickets:x(t).tickets,waves:a,threads:i,working:await Y(d)}}c(z,"itemSnapshot");async function Q({github:t=!0}={}){const e=await z(),{waves:n,threads:a,working:i}=e,r=Object.create(null);t&&await Promise.all(n.flatMap(({wave:l})=>{const m=e.github||l.repo;return m?l.items.filter(h=>h.pr).map(async h=>{r[`${m}#${h.pr}`]=await ra(m,h.pr)}):[]}));const s=new Set(["ask","owner-answer","grill-more","item-message","wave-triage","todo-triage"]),o=await Promise.all((await W()).map(l=>s.has(l.kind)||E(i,l.id)?G(l):{...l,reply:null}));return await Promise.all(o.filter(l=>l.kind==="ask").map(async l=>{const m=await I(H(`ask:${l.id}`));m.length&&(a[`ask:${l.id}`]=m)})),{...e,route:R.slice(1)||p.basename(d),try:await v(p.join(b,"try.json"),null),main:await v(p.join(b,"main.json"),null),prs:r,events:o,askLinks:await K(d),askProgress:await ye(d),checked:await v(p.join(b,"checked.json"),null),watcher:await v(p.join(b,"watcher.json"),null),replyAt:await sa(),owner:await v(p.join(b,"owner.json"),{items:[]}),topicItems:await Dt()}}c(Q,"snapshot");let tt=!1;async function et(){if(!tt){tt=!0;try{const t=p.join(b,"builder-stalled.json"),e=await v(t,{}),n=Object.create(null);let a;for(const{key:i,lane:r,builder:s}of A(await z()).items)r==="done"||!s?.stalled||(n[i]=s.silentSince,E(e,i)!==s.silentSince&&(a||=await I(F),a.some(o=>o.kind==="builder-stalled"&&o.data?.key===i&&o.data?.silentSince===s.silentSince)||await O("builder-stalled",{key:i,silentSince:s.silentSince})));JSON.stringify(n)!==JSON.stringify(e)&&(await y.mkdir(b,{recursive:!0}),await y.writeFile(t+".tmp",JSON.stringify(n)+`
5
+ `),await y.rename(t+".tmp",t))}catch(t){console.error("Builder check failed:",t.message)}finally{tt=!1}}}c(et,"checkStalled");async function oa(t){const e=await be(d);if(!e?.hello?.id||e.done_at)return null;const n=t?.find(i=>i.link==="github"),a={events:e.ack?.id===e.hello.id,...n||!t?{github:!!n?.ok}:{}};return a.events&&a.github!==!1?(await ge(d),null):a}c(oa,"setupOf");async function $(){const t=new Date().toISOString(),{title:e,reviewers:n}=await T(),a=Z(d),i=a?_e(a):"deaf",r=A({...await Q(),listen:i,prompts:a?Fe(a.route).map(He):[]});for(const o of r.items)o.worktree&&!ae(o.worktree)&&(o.worktree=null);const s=a?De(a,{listen:i}):null;return{generatedAt:t,title:e,reviewers:(n||[]).map(o=>o?.name).filter(o=>typeof o=="string"),...r,links:s,setup:await oa(s),holdChoices:Ce(new Date,"until you unhold it"),arch:a?Ht(await Bt(d),r.items,r.owner,{repo:Ut(ze(a.route)),waits:Ge()}):null}}c($,"board");let vt=Promise.resolve();function ca(){const t=vt.then(async()=>{const e=Z(d);if(!e)throw Object.assign(new P("This board isn\u2019t in the hub\u2019s manifest."),{status:404});const{arch:n}=await $();if(n.last?.open)return{key:n.last.key,ref:n.last.ref,existing:!0};const{key:a,data:i}=await Ct(d,e),r=await O("arch-review",i);return{key:a,id:r,existing:!1}});return vt=t.catch(()=>{}),t}c(ca,"runReview");async function la(t){const[e,n]=t.split("/"),a=(await J(d)).find(s=>s.slug===e),i=a?await v(a.file,null):null,r=Array.isArray(i?.items)&&i.items.find(s=>s.id===n)||null;return{item:r,agents:r?E(await bt(p.join(a.dir,"agents")),n)||{}:{}}}c(la,"itemOf");async function ua(t){const e=E(await K(d),t),{tickets:n}=x(await T()),a=[];for(const i of await J(d)){const r=await v(i.file,null);for(const s of Array.isArray(r?.items)?r.items:[]){const o=`${i.slug}/${s.id}`;s.from_ask===t?a.unshift({key:o,ref:X(r,s,n),became:!0}):o===e&&a.push({key:o,ref:X(r,s,n),became:!1})}}return a}c(ua,"askLinksOf");async function da(t){const e=await v(p.join(b,"owner.json"),{items:[]}),n=new Set((e.items||[]).filter(w=>w.topic===`item:${t}`).map(w=>w.id)),{item:a,agents:i}=t.startsWith("ask:")?{item:null,agents:{}}:await la(t),r=t.startsWith("ask:"),s=r?t.slice(4):a?.from_ask||null,o=r?[]:Object.entries(await K(d)).filter(([,w])=>w===t).map(([w])=>w),l=[s,...o].filter(Boolean),m=new Set([t,...l.map(w=>`ask:${w}`)]),h=(await W()).filter(w=>m.has(w.data?.key)&&zt.includes(w.kind)||w.kind==="owner-answer"&&n.has(w.data?.id)||w.kind==="ask"&&l.includes(w.id)||w.kind==="ask-edited"),j=(await Promise.all([...m].map(w=>I(H(w))))).flat();return Yt(t,{lines:j,events:await Promise.all(h.map(G)),owner:e,working:await Y(d),fromAsk:s,links:r?await ua(s):[],agents:i,moved:o})}c(da,"thread");const jt=c(t=>String(t).replace(/[&<>"']/g,e=>({"&":"&amp;","<":"&lt;",">":"&gt;",'"':"&quot;","'":"&#39;"})[e]),"esc");async function Ot(t){for(const e of Ve){let n=p.resolve(e,"."+t);if(n!==e&&!n.startsWith(e+p.sep))return null;if((await y.stat(n).catch(()=>null))?.isDirectory()&&(n=p.join(n,"index.html")),await y.stat(n).then(i=>i.isFile(),()=>!1))return n}return null}c(Ot,"staticFile");const Pt=p.join(N,"public","prototype.html");async function fa(t){const e=/^\/prototype-([a-z0-9-]+)\.html$/.exec(t)?.[1];return e&&(await we(d)).find(n=>n.topic===e)||null}c(fa,"recordedPrototype");const Et=c(t=>JSON.stringify(t).replace(/</g,"\\u003c").replace(/\u2028/g,"\\u2028").replace(/\u2029/g,"\\u2029"),"scriptJSON");async function pa(t){const e=await Promise.all(t.takes.map(async r=>{const s=`prototypes/${t.topic}/${r.key}.js`,o=await y.stat(he(d,t.topic,r.key)).catch(()=>null);return{key:r.key,label:r.label,src:o?.isFile()?`${s}?v=${L(o)}`:null,file:s}})),n=`${t.title} \xB7 prototype`,a={lead:"back",backHref:t.card?`./#item=${encodeURIComponent(t.card)}`:"./",title:n,controls:!1},i={prototypeTitle:jt(n),prototypeHeader:`<!--header ${Et(a).replace(/>/g,"\\u003e")}-->`,prototypeData:Et({topic:t.topic,title:t.title,takes:e})};return(await y.readFile(Pt,"utf8")).replace(/\{\{(prototypeTitle|prototypeHeader|prototypeData)\}\}/g,(r,s)=>i[s])}c(pa,"prototypePage");async function ma(t,e){try{const n=decodeURIComponent(String(t.headers["x-filename"]||""));return g(e,200,await $e(d,t,{name:n,type:t.headers["content-type"],length:Number(t.headers["content-length"])||0}))}catch(n){if(!(n instanceof P)&&!(n instanceof URIError))throw n;return t.resume(),g(e,n.status||400,{error:n.message})}}c(ma,"upload");const wa=/^(text\/html|image\/svg\+xml|application\/(xhtml\+)?xml|text\/xml)$/;async function ha(t,e,n){const a=await Oe(d,n);if(!a){e.writeHead(404).end();return}if(e.writeHead(200,{"content-type":a.type.startsWith("text/")?`${a.type}; charset=utf-8`:a.type,"content-length":a.size,"content-disposition":`inline; filename*=UTF-8''${encodeURIComponent(a.name)}`,"cache-control":"private, max-age=86400","x-content-type-options":"nosniff",...wa.test(a.type)?{"content-security-policy":"sandbox"}:{}}),t.method==="HEAD"){e.end();return}ee(a.path).pipe(e)}c(ha,"sendUpload");const at=new Set;function ga(t,e){e.writeHead(200,{"content-type":"text/event-stream; charset=utf-8","cache-control":"no-store",connection:"keep-alive"}),e.write(`retry: 3000
6
6
 
7
- `),Q.add(e),t.on("close",()=>Q.delete(e))}c(Ee,"stream");const pt=c(t=>{for(const e of Q)e.write(t)},"broadcast");setInterval(()=>pt(`: ping
7
+ `),at.add(e),t.on("close",()=>at.delete(e))}c(ga,"stream");const Tt=c(t=>{for(const e of at)e.write(t)},"broadcast");setInterval(()=>Tt(`: ping
8
8
 
9
- `),25e3).unref();const Pe=100,xe=500;let H=null,mt=0;function ht(){const t=Date.now();H||(mt=t),clearTimeout(H),H=setTimeout(()=>{H=null,pt(`event: change
9
+ `),25e3).unref();const ya=100,ka=500;let V=null,$t=0;function At(){const t=Date.now();V||($t=t),clearTimeout(V),V=setTimeout(()=>{V=null,Tt(`event: change
10
10
  data: ${JSON.stringify({at:new Date().toISOString()})}
11
11
 
12
- `),G()},Math.max(0,Math.min(Pe,mt+xe-t)))}c(ht,"changed");function $e(t){let e=!1;const a=c(()=>{let n;try{n=Pt(t,{recursive:!0},(i,s)=>{String(s||"").endsWith(".tmp")||ht()})}catch{e=!0,setTimeout(a,2e3).unref();return}e&&ht(),n.on("error",()=>{n.close(),e=!0,setTimeout(a,2e3).unref()})},"start");a()}c($e,"watchTree");async function Te(){const t=(await P()).items==="waves"?[Z()]:(await R(u)).map(a=>a.dir),e=[...new Set([S,...t].map(a=>l.resolve(a)))];for(const a of e.filter(n=>!e.some(i=>i!==n&&n.startsWith(i+l.sep))))$e(a)}c(Te,"watchBoard");const Re=gt.createServer(async(t,e)=>{try{if(t.method!=="GET"&&t.method!=="HEAD"&&Zt(t.headers)){e.writeHead(403,{"content-type":"text/plain; charset=utf-8","cache-control":"no-store"}),e.end("Requests must come from the app\u2019s own pages.");return}const a=new URL(t.url,"http://site");let n=a.pathname;if(O&&(n===O||n.startsWith(O+"/"))&&(n=n.slice(O.length)||"/"),n==="/_claude/health")return h(e,200,{ok:!0});if(n==="/api/board")return h(e,200,await z());if(n==="/api/links")return h(e,200,{links:(await z()).links});if(n==="/api/stream"&&t.method==="GET")return Ee(t,e);if(n==="/_claude/needs")return h(e,200,jt(await z()));if(n==="/api/thread")return h(e,200,await ke(String(a.searchParams.get("key")||"")));if(n==="/api/notes"||n==="/api/plan"){const p=String(a.searchParams.get("item")||"");try{const{wave:d,item:v,markdown:k}=await Ct(u,p);return h(e,200,{key:p,ref:D(d,v.id,A(await P()).tickets),title:v.title,markdown:k})}catch(d){if(d instanceof x)return h(e,404,{error:d.message});throw d}}if(n==="/_claude/events"&&t.method==="POST"){const p=await T(t),d=String(p.kind||"message").slice(0,64);delete p.kind;const v=await _(d,p);if(d==="owner-answer")try{await Kt([`q-${p.id}`],l.basename(u))}catch{}return h(e,200,{id:v})}if(n==="/api/rules"&&t.method==="GET")return h(e,200,{board:await It(u),hub:Wt()});if(n==="/api/rules"&&t.method==="POST"){const p=await T(t);let d;try{d=await Ft(u,p.rules,p.base??[])}catch(k){if(k instanceof x)return h(e,400,{error:k.message});throw k}const v=Jt(d.changes)?await _("rules",{scope:"board",...d.changes}):null;return h(e,200,{rules:d.rules,id:v})}if(n==="/api/settings"&&t.method==="GET")return h(e,200,{settings:await Nt(u)});if(n==="/api/try"&&t.method==="POST"){const p=await T(t);if(!p||typeof p!="object"||Array.isArray(p)||Object.keys(p).some(d=>d!=="key")||typeof p.key!="string")return h(e,400,{error:"Send only the card key."});try{const d=await fetch(`http://127.0.0.1:${process.env.KICKFLIP_HUB_PORT||4500}/_hub/build`,{method:"POST",headers:{"content-type":"application/json"},body:JSON.stringify({route:O.slice(1)||l.basename(u),key:p.key}),signal:AbortSignal.timeout(1e4)});return h(e,d.status,await d.json())}catch(d){return h(e,502,{error:`Could not start the build: ${d.message}`})}}if(n==="/api/settings"&&t.method==="POST"){const p=await T(t);let d;try{d=await Bt(u,p)}catch(k){if(k instanceof x)return h(e,400,{error:k.message});throw k}const v=d.changed.length?await _("settings",Object.fromEntries(d.changed.map(k=>[k,d.settings[k]]))):null;return h(e,200,{settings:d.settings,id:v})}if(n==="/api/hide"&&t.method==="POST"){const p=await T(t);try{await Mt(u,String(p.id||""),p.hidden!==!1)}catch(d){if(d instanceof x)return h(e,400,{error:d.message});throw d}return h(e,200,{ok:!0})}if(n==="/_claude/events"&&t.method==="GET")return h(e,200,await ce(a.searchParams.get("kind"),Math.min(500,Number(a.searchParams.get("limit")||100))));if(n==="/api/upload"&&t.method==="POST")return await ve(t,e);if(t.method!=="GET"&&t.method!=="HEAD"){e.writeHead(405).end();return}if(n.startsWith("/uploads/"))return await Oe(t,e,decodeURIComponent(n.slice(9)));if(n==="/plan.html"){e.writeHead(302,{location:`notes.html${a.search}`,"cache-control":"no-store"}).end();return}if(n==="/index.html"){e.writeHead(302,{location:`./${a.search}`,"cache-control":"no-store"}).end();return}const i=Vt.has(n.slice(1)),s=i?null:await Se(n),o=i?null:s?dt:n==="/prototype.html"?null:await ut(decodeURIComponent(n));if(!i&&!o){e.writeHead(404).end();return}const r=i?".html":l.extname(o).toLowerCase(),g=i?null:await m.stat(o);let w=i?Buffer.from(Yt({touchIcon:await tt()})):s?Buffer.from(await be(s)):await m.readFile(o);!i&&r===".html"&&(w=(await te(w.toString("utf8"))).replaceAll("{{title}}",lt((await P()).title)).replaceAll("{{touchIcon}}",await tt())),!i&&r===".html"&&(w=Buffer.from(await oe(w,n)));const b=r===".html"?`W/"${St.createHash("sha1").update(w).digest("base64url").slice(0,20)}"`:`W/"${N(g)}"`,C=r!==".html"&&a.searchParams.get("v")===N(g),f={"content-type":j(nt,r)||"application/octet-stream","cache-control":C?"public, max-age=31536000, immutable":"no-cache",etag:b};if(re(t.headers["if-none-match"],b)){e.writeHead(304,{etag:b,"cache-control":f["cache-control"],vary:"accept-encoding"}).end();return}return await it(e,200,f,w)}catch(a){e.headersSent||(a.status===403?e.writeHead(403,{"content-type":"text/plain; charset=utf-8","cache-control":"no-store"}).end(a.message):e.writeHead(a.status||(a.code==="ENOENT"?404:400)).end())}});await Te(),setInterval(G,6e4).unref(),await G(),Re.listen(at,"127.0.0.1",()=>console.log(`site on http://127.0.0.1:${at}/ (prefix ${O||"none"})`));
12
+ `),et(),ct(),rt()},Math.max(0,Math.min(ya,$t+ka-t)))}c(At,"changed");const xt=c(t=>{try{return ie(t).isDirectory()}catch{return!1}},"isFolder");function Sa(t){const e=new Map;let n=!1;const a=c(l=>{for(const[m,h]of e)(m===l||m.startsWith(l+p.sep))&&(h.close(),e.delete(m))},"unwatch"),i=c(l=>{if(e.has(l))return;const m=re(l,(h,j)=>{if(String(j||"").endsWith(".tmp")||At(),l===t&&!xt(t))return s();if(!j)return;const w=p.join(l,String(j));if(!xt(w))a(w);else try{i(w)}catch{a(w)}});e.set(l,m),m.on("error",()=>l===t?s():a(l));for(const h of ne(l,{withFileTypes:!0}))if(h.isDirectory())try{i(p.join(l,h.name))}catch{a(p.join(l,h.name))}},"watchFolder");let r=null;const s=c(()=>{r||(a(t),n=!0,r=setTimeout(()=>{r=null,o()},2e3).unref())},"retry"),o=c(()=>{try{i(t)}catch{s();return}n&&At()},"start");o()}c(Sa,"watchTree");async function ba(){const t=(await T()).items==="waves"?[Re()]:(await J(d)).map(n=>n.dir),e=[...new Set([b,...t].map(n=>p.resolve(n)))];for(const n of e.filter(a=>!e.some(i=>i!==a&&a.startsWith(i+p.sep))))Sa(n)}c(ba,"watchBoard");const va=Jt.createServer(async(t,e)=>{try{if(t.method!=="GET"&&t.method!=="HEAD"&&Le(t.headers)){e.writeHead(403,{"content-type":"text/plain; charset=utf-8","cache-control":"no-store"}),e.end("Requests must come from the app\u2019s own pages.");return}const n=new URL(t.url,"http://site");let a=n.pathname;if(R&&(a===R||a.startsWith(R+"/"))&&(a=a.slice(R.length)||"/"),a==="/_claude/health")return g(e,200,{ok:!0});if(a==="/api/board")return g(e,200,{...await $(),onHost:Ft(t)});if(a==="/api/links")return g(e,200,{links:(await $()).links});if(a==="/api/stream"&&t.method==="GET")return ga(t,e);if(a==="/_claude/needs")return g(e,200,Qt(await $()));if(a==="/api/working"){const u=A(await Q({github:!1})),f=new Map(u.items.filter(S=>S.kind==="work").map(S=>[S.key,S.ref]));return g(e,200,{working:u.workingOn.map(S=>({id:S.id,kind:S.kind,card:f.get(S.key)||null,since:S.since}))})}if(a==="/api/thread")return g(e,200,await da(String(n.searchParams.get("key")||"")));if(a==="/api/notes"&&n.searchParams.has("wave"))try{return g(e,200,await ve(d,String(n.searchParams.get("wave")),x(await T()).tickets))}catch(u){if(u instanceof P)return g(e,404,{error:u.message});throw u}if(a==="/api/notes"||a==="/api/plan"){const u=String(n.searchParams.get("item")||"");try{const{wave:f,item:S,markdown:k}=await je(d,u);return g(e,200,{key:u,ref:X(f,S,x(await T()).tickets),title:S.title,markdown:k})}catch(f){if(f instanceof P)return g(e,404,{error:f.message});throw f}}if(a==="/_claude/events"&&t.method==="POST"){const u=await _(t,524288),f=String(u.kind||"message").slice(0,64);if(delete u.kind,f==="card-hold"&&u.hold&&u.for!==void 0){const k=Ue(new Date,u.for);if(!k)return g(e,400,{error:"That hold ends in the past or more than 30 days on. Pick another time."});delete u.for,k!==Be&&(u.until=k.toISOString())}if(f==="card-build"){const k=String(u.key||""),D=await fe(d,k,()=>lt(d,k.split("/")[1]));return g(e,200,{id:D&&!D.queued?await O(f,D):null,board:await $()})}if(f==="owner-answer")return g(e,200,{id:(await kt([u]))[0],board:await $()});if(f==="owner-answers"){const k=await qe(u.answers);return k?g(e,400,{error:k}):g(e,200,{ids:await kt(u.answers),board:await $()})}const S=await O(f,u);if(f==="card-hold"||f==="card-done")try{await me(d,f,u)}catch(k){console.error(`Recording ${f} failed:`,k.message)}return g(e,200,{id:S,board:await $()})}if(a==="/api/rules"&&t.method==="GET")return g(e,200,{board:await ke(d),hub:Ne()});if(a==="/api/rules"&&t.method==="POST"){const u=await _(t);let f;try{f=await Ee(d,u.rules,u.base??[])}catch(k){if(k instanceof P)return g(e,400,{error:k.message});throw k}const S=Ie(f.changes)?await O("rules",{scope:"board",...f.changes}):null;return g(e,200,{rules:f.rules,id:S})}if(a==="/api/settings"&&t.method==="GET")return g(e,200,{settings:await Se(d)});if(a==="/api/try/main/log"&&t.method==="GET"){let u;try{u=await y.open(p.join(d,"data","try","main.log"),"r");const{size:f}=await u.stat(),S=Math.max(0,f-64*1024),k=Buffer.alloc(f-S),{bytesRead:D}=await u.read(k,0,k.length,S),C=k.subarray(0,D).toString("utf8").split(/\r?\n/);return S&&C.shift(),C.at(-1)===""&&C.pop(),g(e,200,{lines:C.slice(-40)})}catch(f){if(f.code==="ENOENT")return g(e,200,{lines:[]});throw f}finally{await u?.close()}}if(a==="/api/try"&&t.method==="POST"){const u=await _(t);if(!u||typeof u!="object"||Array.isArray(u)||Object.keys(u).some(f=>f!=="key")||typeof u.key!="string")return g(e,400,{error:"Send only the card key."});try{const f=await fetch(`http://127.0.0.1:${process.env.KICKFLIP_HUB_PORT||4500}/_hub/build`,{method:"POST",headers:{"content-type":"application/json"},body:JSON.stringify({route:R.slice(1)||p.basename(d),key:u.key}),signal:AbortSignal.timeout(1e4)});return g(e,f.status,await f.json())}catch(f){return g(e,502,{error:`Could not start the build: ${f.message}`})}}if(a==="/api/settings"&&t.method==="POST"){const u=await _(t);let f;try{f=await Te(d,u)}catch(k){if(k instanceof P)return g(e,400,{error:k.message});throw k}const S=f.changed.length?await O("settings",Object.fromEntries(f.changed.map(k=>[k,f.settings[k]]))):null;return f.changed.includes("auto_merge")&&f.settings.auto_merge&&await O("check-github",{reason:"auto-merge"}),g(e,200,{settings:f.settings,id:S})}if(a==="/api/arch-review"&&t.method==="POST"){await _(t);try{return g(e,200,await ca())}catch(u){if(u instanceof P)return g(e,u.status||400,{error:u.message});throw u}}if(a==="/api/hide"&&t.method==="POST"){const u=await _(t);try{await Ae(d,String(u.id||""),u.hidden!==!1)}catch(f){if(f instanceof P)return g(e,400,{error:f.message});throw f}return g(e,200,{ok:!0})}if(a==="/_claude/events"&&t.method==="GET")return g(e,200,await ta(n.searchParams.get("kind"),Math.min(500,Number(n.searchParams.get("limit")||100))));if(a==="/api/upload"&&t.method==="POST")return await ma(t,e);if(t.method!=="GET"&&t.method!=="HEAD"){e.writeHead(405).end();return}if(a.startsWith("/uploads/"))return await ha(t,e,decodeURIComponent(a.slice(9)));if(a==="/plan.html"){e.writeHead(302,{location:`notes.html${n.search}`,"cache-control":"no-store"}).end();return}if(a==="/index.html"){e.writeHead(302,{location:`./${n.search}`,"cache-control":"no-store"}).end();return}const i=Je.has(a.slice(1)),r=i?null:await fa(a),s=i?null:r?Pt:a==="/prototype.html"?null:await Ot(decodeURIComponent(a));if(!i&&!s){e.writeHead(404).end();return}const o=i?".html":p.extname(s).toLowerCase(),l=i?null:await y.stat(s);let m=i?Buffer.from(Me({touchIcon:await mt()})):r?Buffer.from(await pa(r)):await y.readFile(s);!i&&o===".html"&&(m=(await Qe(m.toString("utf8"))).replaceAll("{{title}}",jt((await T()).title)).replaceAll("{{touchIcon}}",await mt())),!i&&o===".html"&&(m=Buffer.from(await Ye(m,a)));const h=o===".html"?`W/"${Wt.createHash("sha1").update(m).digest("base64url").slice(0,20)}"`:`W/"${L(l)}"`,j=o!==".html"&&n.searchParams.get("v")===L(l),w={"content-type":E(ht,o)||"application/octet-stream","cache-control":j?"public, max-age=31536000, immutable":"no-cache",etag:h};if(Ze(t.headers["if-none-match"],h)){e.writeHead(304,{etag:h,"cache-control":w["cache-control"],vary:"accept-encoding"}).end();return}return await gt(e,200,w,m)}catch(n){e.headersSent||(n.status===403?e.writeHead(403,{"content-type":"text/plain; charset=utf-8","cache-control":"no-store"}).end(n.message):e.writeHead(n.status||(n.code==="ENOENT"?404:400)).end())}});let nt=!1;async function Rt(){if(!nt){nt=!0;try{const t=Date.now(),e=A({...await z(),events:await W(),now:t}).items.filter(a=>a.heldUntil&&a.lane!=="done"&&Date.parse(a.heldUntil)<=t),n=e.length?await I(F):[];for(const{key:a,heldUntil:i}of e)await ue(d,a,i,{announce:!n.some(r=>r.kind==="card-unblocked"&&r.data?.key===a&&r.data?.until===i)})}catch(t){console.error("Pick-up check failed:",t.message)}finally{nt=!1}}}c(Rt,"checkPickUps");let B=null,it=!1;function rt(){return B?(it=!0,B):(B=(async()=>{do{it=!1;try{if(pt().state==="paused")continue;for(const t of await ce(d)){const e=await de(d,t,()=>lt(d,t.split("/")[1]));e&&!e.queued&&await O("card-build",e)}}catch(t){console.error("Queued build check failed:",t.message)}}while(it)})().finally(()=>{B=null}),B)}c(rt,"checkQueued");let st=!1;async function It(){if(!st){st=!0;try{const t=x(await T()),e=await I(F),n=await ft(d),{items:a}=A({...await z(),events:e.filter(l=>!n.has(l.id)),owner:await v(p.join(b,"owner.json"),{items:[]})}),{idle:i,pullable:r,plannable:s}=Xt(a),o=i&&pt().state!=="paused";if(await ja(t,e,a,o))return;await Nt(Oa,t,e,r,o),await Nt(Pa,t,e,r.length?[]:s,o)}catch(t){console.error("Idle check failed:",t.message)}finally{st=!1}}}c(It,"checkIdle");async function ja(t,e,n,a){const i=p.join(b,"triaged.json"),r=await v(i,{}),s=Zt(n),o=c(async()=>{await y.mkdir(b,{recursive:!0}),await y.writeFile(i+".tmp",JSON.stringify({todo:s})+`
13
+ `),await y.rename(i+".tmp",i)},"save"),l=e.filter(w=>w.kind==="todo-triage").at(-1);if(l&&!(await G(l)).reply)return r.todo!==s&&await o(),!0;const m=qt(new Date);if(!t.auto_triage||!a||!s||s===r.todo||l?.data?.date===m)return!1;const h=`todo-triage-${m}`;return((await v(p.join(b,"owner.json"),{topics:[]})).topics||[]).some(w=>w.id===h)||await se(d,{id:h,title:`Todo triage ${m}`}),await O("todo-triage",{date:m,topic:h}),await o(),!0}c(ja,"checkTriage");const Oa={kind:"card-pull",file:"pulled.json",line:"Pulled in: the board was idle."},Pa={kind:"card-plan",file:"planned.json",line:"Asked for a grill or prototype: nothing in Todo was ready to build."};async function Nt({kind:t,file:e,line:n},a,i,r,s){const o=p.join(b,e),l=await v(o,{}),m=c(async u=>{await y.mkdir(b,{recursive:!0}),await y.writeFile(o+".tmp",JSON.stringify({key:u,at:new Date().toISOString()})+`
14
+ `),await y.rename(o+".tmp",o)},"save");if(!a.auto_pull){l.key&&await m(null);return}const h=i.filter(u=>u.kind===t).at(-1),j=h&&String(h.ts)>String(l.at||"")?h.data?.key:l.key;if(j&&r.includes(j))return;if(!s||!r.length){j&&await m(null);return}const w=r[0];await O(t,{key:w}),await y.mkdir(p.dirname(H(w)),{recursive:!0}),await y.appendFile(H(w),JSON.stringify({ts:new Date().toISOString(),who:"claude",text:n})+`
15
+ `),await m(w)}c(Nt,"checkPull");let U=null,ot=!1;function ct(){return U?(ot=!0,U):(U=(async()=>{do{ot=!1;try{await Ea()}catch(t){console.error("Action push check failed:",t.message)}}while(ot)})().finally(()=>{U=null}),U)}c(ct,"checkActions");async function Ea(){const t=p.join(b,"action-pushes.json"),e=await v(t,null),n=await Q(),a=Vt(A(n)),i=Object.fromEntries(a.map(r=>[r.tag,{kind:r.kind,ids:r.questions.map(s=>s.id)}]));if(e){for(const s of a){const o=E(e,s.tag),l=s.questions.filter(h=>!o?.ids?.includes(h.id));if(o?.kind===s.kind&&!l.length)continue;const m=l.length?`questions.html#${encodeURIComponent(l[0].id)}`:s.url;try{await pe(d,{kind:"action",title:s.title,body:l[0]?.question||s.body,url:`/${n.route}/${m}`,tag:s.tag,now:s.now})}catch(h){if(h instanceof P)console.error(`Push ${s.tag} not sent:`,h.message.split(`
16
+ `).at(-2)||h.message);else throw h}}const r=Object.keys(e).filter(s=>!E(i,s));r.length&&await te(r,n.route)}JSON.stringify(i)!==JSON.stringify(e)&&(await y.mkdir(b,{recursive:!0}),await y.writeFile(t+".tmp",JSON.stringify(i)+`
17
+ `),await y.rename(t+".tmp",t))}c(Ea,"pushActions");try{for(const t of await le(d))console.log(t)}catch(t){console.error("follow-up migration:",t.message)}await ba(),setInterval(et,6e4).unref(),setInterval(Rt,6e4).unref(),setInterval(It,6e4).unref(),setInterval(ct,6e4).unref(),setInterval(rt,6e4).unref(),await et(),await Rt(),await It(),ct(),rt(),va.listen(wt,"127.0.0.1",()=>console.log(`site on http://127.0.0.1:${wt}/ (prefix ${R||"none"})`));