@celsoft/coordinator 0.0.0 → 0.1.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.
Files changed (131) hide show
  1. package/CHANGELOG.md +11 -0
  2. package/README.md +107 -0
  3. package/THIRD_PARTY_NOTICES.md +2005 -0
  4. package/agents/console.md +20 -0
  5. package/agents/free.md +110 -0
  6. package/build/0-JP6XL6LB.js +17 -0
  7. package/build/1-A52YZWNS.js +17 -0
  8. package/build/2-CLH2NM6F.js +32 -0
  9. package/build/3-4N467IB6.js +45 -0
  10. package/build/4-WL5H4LUW.js +29 -0
  11. package/build/_layout.svelte-KPUSX7LQ.js +27 -0
  12. package/build/_page.svelte-22H56UR6.js +7972 -0
  13. package/build/_page.svelte-CVWUNRQC.js +135 -0
  14. package/build/_page.svelte-LTZXBHMT.js +57 -0
  15. package/build/_server.ts-3KVBYN6P.js +20 -0
  16. package/build/_server.ts-3POPKVEM.js +47 -0
  17. package/build/_server.ts-5735T2V5.js +31 -0
  18. package/build/_server.ts-5VXGNWV2.js +38 -0
  19. package/build/_server.ts-645KP7WP.js +42 -0
  20. package/build/_server.ts-A3AA24Z2.js +28 -0
  21. package/build/_server.ts-CITUOEM6.js +58 -0
  22. package/build/_server.ts-I7HMTPOA.js +25 -0
  23. package/build/_server.ts-M5BQ4LLX.js +30 -0
  24. package/build/_server.ts-OB4O27HF.js +683 -0
  25. package/build/_server.ts-OJT7HE7M.js +15 -0
  26. package/build/_server.ts-OYGWVLZM.js +12 -0
  27. package/build/_server.ts-T75UVMQP.js +38 -0
  28. package/build/_server.ts-WYZYNHUZ.js +28 -0
  29. package/build/_server.ts-Z26EB47B.js +486 -0
  30. package/build/chunk-4CIG53L5.js +33 -0
  31. package/build/chunk-BJTJS3XY.js +338 -0
  32. package/build/chunk-F2CNWJZM.js +14 -0
  33. package/build/chunk-HGYE3UTU.js +416 -0
  34. package/build/chunk-KAYW2W7H.js +1311 -0
  35. package/build/chunk-LFP3UPCM.js +10 -0
  36. package/build/chunk-M44JEFEH.js +62 -0
  37. package/build/chunk-MC5AGLMY.js +39 -0
  38. package/build/chunk-NCZ77J5E.js +19798 -0
  39. package/build/chunk-NVNLDPUJ.js +2122 -0
  40. package/build/chunk-PDCON4KS.js +494 -0
  41. package/build/chunk-Q4OQNY5M.js +25 -0
  42. package/build/chunk-QCUOR2U5.js +75 -0
  43. package/build/chunk-R6RYZZWA.js +46 -0
  44. package/build/chunk-SOJTASZC.js +51 -0
  45. package/build/chunk-SU26PJVX.js +410 -0
  46. package/build/chunk-U5BSBG4R.js +506 -0
  47. package/build/chunk-UEKOZGRO.js +37 -0
  48. package/build/chunk-W6EFUWJ2.js +9 -0
  49. package/build/chunk-WOPREK6Q.js +117 -0
  50. package/build/chunk-WWJ3F4QS.js +127 -0
  51. package/build/chunk-ZGZJEEE3.js +1091 -0
  52. package/build/client/_app/immutable/assets/0.BGa5ZNDM.css +1 -0
  53. package/build/client/_app/immutable/chunks/BUrrLYDt.js +2 -0
  54. package/build/client/_app/immutable/chunks/BgFr_EyZ.js +4 -0
  55. package/build/client/_app/immutable/chunks/Bjy-W4x2.js +82 -0
  56. package/build/client/_app/immutable/chunks/C8qZab0y.js +56 -0
  57. package/build/client/_app/immutable/chunks/C90fLsA5.js +4 -0
  58. package/build/client/_app/immutable/chunks/CRzLMJPS.js +1 -0
  59. package/build/client/_app/immutable/chunks/DLT1iIlz.js +2 -0
  60. package/build/client/_app/immutable/chunks/pIrVVOVY.js +2 -0
  61. package/build/client/_app/immutable/entry/app.0DERehLW.js +3 -0
  62. package/build/client/_app/immutable/entry/payload.DSmR2FwN.js +2 -0
  63. package/build/client/_app/immutable/entry/start.B7srFiqh.js +3 -0
  64. package/build/client/_app/immutable/nodes/0.aD9vJoxR.js +2 -0
  65. package/build/client/_app/immutable/nodes/1.Br4FL91i.js +2 -0
  66. package/build/client/_app/immutable/nodes/2.vl6OYfu3.js +8 -0
  67. package/build/client/_app/immutable/nodes/3.VYqJOLAF.js +2 -0
  68. package/build/client/_app/immutable/nodes/4.jF9UVlFN.js +2 -0
  69. package/build/client/_app/version.json +1 -0
  70. package/build/client/apple-touch-icon.png +0 -0
  71. package/build/client/favicon.ico +0 -0
  72. package/build/client/favicon.svg +48 -0
  73. package/build/client/icon-192.png +0 -0
  74. package/build/client/icon-512.png +0 -0
  75. package/build/client/icon-maskable-512.png +0 -0
  76. package/build/client/icon.svg +44 -0
  77. package/build/client/manifest.webmanifest +15 -0
  78. package/build/error.svelte-JXZDY65F.js +51 -0
  79. package/build/handler.js +716 -0
  80. package/build/hooks.server-34IMHRKC.js +43 -0
  81. package/build/instance-NP3FJIWW.js +2967 -0
  82. package/client/client.mjs +28 -0
  83. package/client/hooks.mjs +7 -0
  84. package/client/register.mjs +5 -0
  85. package/dist/cli.js +62826 -0
  86. package/dist/port.js +36691 -0
  87. package/package.json +20 -1
  88. package/skills/agents/.claude-plugin/plugin.json +5 -0
  89. package/skills/agents/hooks/hooks.json +1 -0
  90. package/skills/agents/hooks/register.ts +57 -0
  91. package/skills/agents/skills/bus-work/SKILL.md +66 -0
  92. package/skills/agents/skills/plan/SKILL.md +98 -0
  93. package/skills/console/.claude-plugin/plugin.json +5 -0
  94. package/skills/console/hooks/hooks.json +1 -0
  95. package/skills/console/hooks/register.ts +10 -0
  96. package/skills/console/skills/bus/SKILL.md +61 -0
  97. package/skills/default/.claude-plugin/plugin.json +5 -0
  98. package/skills/default/skills/git-workflow/SKILL.md +294 -0
  99. package/skills/default/skills/git-workflow/mise-tasks/_lib.sh +107 -0
  100. package/skills/default/skills/git-workflow/mise-tasks/clone +36 -0
  101. package/skills/default/skills/git-workflow/mise-tasks/repo-list +8 -0
  102. package/skills/default/skills/git-workflow/mise-tasks/unpushed +26 -0
  103. package/skills/default/skills/git-workflow/mise-tasks/worktree-add +60 -0
  104. package/skills/default/skills/github/SKILL.md +208 -0
  105. package/skills/default/skills/github/scripts/gh-org +196 -0
  106. package/skills/default/skills/plan-page/SKILL.md +300 -0
  107. package/skills/default/skills/plan-page/assets/index.html +854 -0
  108. package/skills/default/skills/plan-page/scripts/inline +46 -0
  109. package/skills/default/skills/plan-page/scripts/stage +26 -0
  110. package/skills/default/skills/secrets/SKILL.md +134 -0
  111. package/skills/default/skills/secrets/scripts/secrets-check +87 -0
  112. package/skills/default/skills/writing-style/SKILL.md +45 -0
  113. package/skills/extra/.claude-plugin/plugin.json +5 -0
  114. package/skills/extra/skills/artifacts/SKILL.md +52 -0
  115. package/skills/extra/skills/github-actions/SKILL.md +78 -0
  116. package/skills/extra/skills/github-actions/scan +32 -0
  117. package/skills/extra/skills/markdown-review/SKILL.md +70 -0
  118. package/skills/extra/skills/markdown-review/assets/index.html +201 -0
  119. package/skills/extra/skills/markdown-review/scripts/stage +36 -0
  120. package/skills/extra/skills/mise-usage/SKILL.md +155 -0
  121. package/skills/extra/skills/plugin-authoring/SKILL.md +317 -0
  122. package/skills/extra/skills/project-conventions/SKILL.md +64 -0
  123. package/skills/extra/skills/questions/SKILL.md +156 -0
  124. package/skills/extra/skills/questions/assets/index.html +376 -0
  125. package/skills/extra/skills/questions/scripts/stage +26 -0
  126. package/skills/extra/skills/serve/SKILL.md +165 -0
  127. package/skills/extra/skills/serve/scripts/serve +268 -0
  128. package/skills/extra/skills/serve/scripts/server.py +345 -0
  129. package/skills/extra/skills/skill-dependencies/SKILL.md +65 -0
  130. package/skills/extra/skills/skill-maintenance/SKILL.md +101 -0
  131. package/skills/extra/skills/ticketing/SKILL.md +125 -0
@@ -0,0 +1,376 @@
1
+ <title>Open Questions</title>
2
+ <style>
3
+ :root {
4
+ --bg: #f5f7f8; --surface: #ffffff; --fg: #17212b; --muted: #5b6875; --line: #d9e0e5;
5
+ --accent: #0f6b6e; --accent-soft: #e0f0ef; --note: #8a5a00; --note-soft: #fbf0d9;
6
+ --reply: #eef2f7; --pro: #e6f4ea; --pro-fg: #1e6b35; --con: #fbe9e7; --con-fg: #9a3412;
7
+ }
8
+ @media (prefers-color-scheme: dark) {
9
+ :root:not([data-theme="light"]) {
10
+ --bg: #11171c; --surface: #182128; --fg: #e4eaee; --muted: #94a2ae; --line: #2a3640;
11
+ --accent: #4fc1bf; --accent-soft: #163634; --note: #e8b45a; --note-soft: #3a2c12;
12
+ --reply: #1d2a33; --pro: #15301f; --pro-fg: #7fd69a; --con: #3a1d17; --con-fg: #f4a48c; color-scheme: dark;
13
+ }
14
+ }
15
+ :root[data-theme="dark"] {
16
+ --bg: #11171c; --surface: #182128; --fg: #e4eaee; --muted: #94a2ae; --line: #2a3640;
17
+ --accent: #4fc1bf; --accent-soft: #163634; --note: #e8b45a; --note-soft: #3a2c12;
18
+ --reply: #1d2a33; --pro: #15301f; --pro-fg: #7fd69a; --con: #3a1d17; --con-fg: #f4a48c; color-scheme: dark;
19
+ }
20
+ * { box-sizing: border-box; }
21
+ body {
22
+ margin: 0; background: var(--bg); color: var(--fg); padding: 0 16px 48px;
23
+ font: 15px/1.5 -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
24
+ }
25
+ main { max-width: 760px; margin: 0 auto; display: flex; flex-direction: column; gap: 28px; }
26
+ header.top { padding-top: 32px; display: flex; flex-direction: column; gap: 6px; }
27
+ h1 { font-size: 26px; line-height: 1.2; margin: 0; }
28
+ .lede { color: var(--muted); margin: 0; max-width: 62ch; }
29
+ .problem { margin: 12px 0 0; padding: 10px 12px; border-left: 3px solid var(--accent); background: var(--bg); border-radius: 0 6px 6px 0; max-width: 62ch; }
30
+ .problem strong { display: block; font-size: 12px; letter-spacing: .04em; text-transform: uppercase; color: var(--muted); margin-bottom: 2px; }
31
+ .tally {
32
+ position: sticky; top: 0; z-index: 5; background: var(--bg);
33
+ display: flex; flex-wrap: wrap; gap: 8px 16px; align-items: center; padding-block: 10px;
34
+ border-bottom: 1px solid var(--line); font-variant-numeric: tabular-nums; font-size: 14px;
35
+ }
36
+ .meter { flex: 1 1 160px; height: 6px; background: var(--line); border-radius: 3px; overflow: hidden; min-width: 120px; }
37
+ .meter > div { height: 100%; background: var(--accent); width: 0; transition: width .25s ease; }
38
+ .status-line { color: var(--muted); font-size: 13px; }
39
+ .done {
40
+ font: inherit; font-size: 14px; font-weight: 600; color: #fff; background: var(--accent);
41
+ border: 0; border-radius: 6px; padding: 6px 12px; cursor: pointer;
42
+ }
43
+ .done:disabled { opacity: .6; cursor: default; }
44
+ section { display: flex; flex-direction: column; gap: 12px; }
45
+ section > h2 { font-size: 13px; letter-spacing: .08em; text-transform: uppercase; color: var(--muted); margin: 0; font-weight: 600; }
46
+ section > p.group-note { margin: -4px 0 0; color: var(--muted); font-size: 14px; }
47
+ .item {
48
+ background: var(--surface); border: 1px solid var(--line); border-radius: 10px;
49
+ padding: 16px; display: flex; flex-direction: column; gap: 12px;
50
+ }
51
+ .item.approved { border-color: var(--accent); box-shadow: 0 0 0 1px var(--accent); }
52
+ .item h3 { margin: 0; font-size: 17px; line-height: 1.3; }
53
+ .item p { margin: 0; }
54
+ .item .why { color: var(--muted); font-size: 14px; }
55
+ .item .why::before { content: "Why this came up: "; font-weight: 600; color: var(--fg); }
56
+ .item .versus { font-size: 14px; border-left: 3px solid var(--line); padding-left: 10px; }
57
+ .item .versus::before { content: "Compared with: "; font-weight: 600; }
58
+ .tradeoffs { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; }
59
+ .side { border-radius: 8px; padding: 10px 12px; }
60
+ .side h4 { margin: 0 0 4px; font-size: 12px; letter-spacing: .06em; text-transform: uppercase; }
61
+ .side ul { margin: 0; padding-left: 18px; font-size: 14px; }
62
+ .side li + li { margin-top: 3px; }
63
+ .side.pros { background: var(--pro); }
64
+ .side.pros h4 { color: var(--pro-fg); }
65
+ .side.cons { background: var(--con); }
66
+ .side.cons h4 { color: var(--con-fg); }
67
+ .side .none { color: var(--muted); font-size: 14px; }
68
+ .reply { background: var(--reply); border-radius: 8px; padding: 8px 10px; font-size: 14px; }
69
+ .reply::before { content: "Claude: "; font-weight: 600; }
70
+ .answer { border-top: 1px solid var(--line); padding-top: 12px; display: flex; flex-direction: column; gap: 8px; }
71
+ .approve { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }
72
+ .approve label { display: inline-flex; align-items: center; gap: 8px; font-weight: 600; cursor: pointer; }
73
+ .approve input { width: 20px; height: 20px; margin: 0; accent-color: var(--accent); cursor: pointer; }
74
+ .chips { display: flex; gap: 6px; flex-wrap: wrap; }
75
+ .chip { font-size: 12px; padding: 1px 8px; border-radius: 10px; background: var(--line); color: var(--muted); }
76
+ .chip.ok { background: var(--accent-soft); color: var(--accent); }
77
+ .chip.c { background: var(--note-soft); color: var(--note); }
78
+ .answer .label { font-size: 13px; color: var(--muted); }
79
+ @media (max-width: 560px) { .tradeoffs { grid-template-columns: 1fr; } }
80
+ textarea {
81
+ width: 100%; min-height: 38px; resize: vertical; font: inherit; font-size: 14px;
82
+ color: var(--fg); background: var(--bg); border: 1px solid var(--line); border-radius: 6px; padding: 7px 9px;
83
+ }
84
+ textarea:focus-visible, input:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
85
+ code { font: .88em ui-monospace, SFMono-Regular, Menlo, monospace; background: var(--bg); padding: 0 4px; border-radius: 3px; overflow-wrap: anywhere; }
86
+ .notice { background: var(--note-soft); color: var(--note); padding: 10px 12px; border-radius: 6px; font-size: 14px; }
87
+ .empty { color: var(--muted); }
88
+ @media (prefers-reduced-motion: reduce) { .meter > div { transition: none; } }
89
+ </style>
90
+
91
+ <main>
92
+ <header class="top">
93
+ <h1 id="title">Open Questions</h1>
94
+ <p class="lede" id="intro">Approve a proposal as written, or comment to change it or ask about it. Answers save as you type, and Claude reads them back.</p>
95
+ <p class="problem" id="problem" hidden><strong>Problem to solve</strong><span id="problem-text"></span></p>
96
+ </header>
97
+ <div class="tally" aria-live="polite">
98
+ <span><strong id="n-approved">0</strong> of <span id="n-total">0</span> approved</span>
99
+ <span><strong id="n-commented">0</strong> with comments</span>
100
+ <div class="meter" aria-hidden="true"><div id="meter"></div></div>
101
+ <span class="status-line" id="status">Loading</span>
102
+ <button class="done" id="done" type="button" hidden>Done, tell Claude</button>
103
+ </div>
104
+ <div id="notice" class="notice" hidden></div>
105
+ <div id="groups"></div>
106
+ </main>
107
+
108
+ <script>
109
+ let GROUPS = [];
110
+ let ALL = [];
111
+ const state = {};
112
+ const timers = {};
113
+ let db = null;
114
+
115
+ // Item text may use `code` and **bold**; everything else is plain text.
116
+ function inline(target, text) {
117
+ target.replaceChildren();
118
+ for (const part of String(text ?? "").split(/(`[^`]+`|\*\*[^*]+\*\*)/)) {
119
+ if (!part) continue;
120
+ let node;
121
+ if (part.startsWith("`") && part.endsWith("`") && part.length > 1) {
122
+ node = document.createElement("code");
123
+ node.textContent = part.slice(1, -1);
124
+ } else if (part.startsWith("**") && part.endsWith("**") && part.length > 3) {
125
+ node = document.createElement("strong");
126
+ node.textContent = part.slice(2, -2);
127
+ } else {
128
+ node = document.createTextNode(part);
129
+ }
130
+ target.append(node);
131
+ }
132
+ }
133
+
134
+ function notice(text) {
135
+ const el = document.getElementById("notice");
136
+ el.textContent = text;
137
+ el.hidden = false;
138
+ }
139
+
140
+ function render() {
141
+ const root = document.getElementById("groups");
142
+ root.replaceChildren();
143
+ if (ALL.length === 0) {
144
+ const p = document.createElement("p");
145
+ p.className = "empty";
146
+ p.textContent = "Nothing is open right now.";
147
+ root.append(p);
148
+ }
149
+ for (const g of GROUPS) {
150
+ if (!g.items?.length) continue;
151
+ const sec = document.createElement("section");
152
+ const h2 = document.createElement("h2");
153
+ h2.textContent = g.title ?? "";
154
+ sec.append(h2);
155
+ if (g.note) {
156
+ const note = document.createElement("p");
157
+ note.className = "group-note";
158
+ inline(note, g.note);
159
+ sec.append(note);
160
+ }
161
+ for (const it of g.items) {
162
+ const el = document.createElement("div");
163
+ el.className = "item";
164
+ el.id = "item-" + it.id;
165
+
166
+ const h3 = document.createElement("h3");
167
+ inline(h3, it.title);
168
+ el.append(h3);
169
+ for (const [key, cls] of [["why", "why"], ["proposal", "proposal"], ["versus", "versus"]]) {
170
+ if (!it[key]) continue;
171
+ const p = document.createElement("p");
172
+ p.className = cls;
173
+ inline(p, it[key]);
174
+ el.append(p);
175
+ }
176
+
177
+ const tradeoffs = document.createElement("div");
178
+ tradeoffs.className = "tradeoffs";
179
+ for (const [key, heading] of [["pros", it.versus ? "Better than that" : "Pros"], ["cons", it.versus ? "Worse than that" : "Cons"]]) {
180
+ const side = document.createElement("div");
181
+ side.className = "side " + key;
182
+ const h4 = document.createElement("h4");
183
+ h4.textContent = heading;
184
+ side.append(h4);
185
+ const points = Array.isArray(it[key]) ? it[key] : [];
186
+ if (points.length) {
187
+ const ul = document.createElement("ul");
188
+ for (const point of points) {
189
+ const li = document.createElement("li");
190
+ inline(li, point);
191
+ ul.append(li);
192
+ }
193
+ side.append(ul);
194
+ } else {
195
+ const none = document.createElement("p");
196
+ none.className = "none";
197
+ none.textContent = "None";
198
+ side.append(none);
199
+ }
200
+ tradeoffs.append(side);
201
+ }
202
+ el.append(tradeoffs);
203
+
204
+ if (it.reply) {
205
+ const reply = document.createElement("p");
206
+ reply.className = "reply";
207
+ inline(reply, it.reply);
208
+ el.append(reply);
209
+ }
210
+
211
+ const answer = document.createElement("div");
212
+ answer.className = "answer";
213
+ const approve = document.createElement("div");
214
+ approve.className = "approve";
215
+ const label = document.createElement("label");
216
+ const cb = document.createElement("input");
217
+ cb.type = "checkbox";
218
+ cb.addEventListener("change", () => save(it.id, { approved: cb.checked }));
219
+ label.append(cb, document.createTextNode("Approve"));
220
+ const chips = document.createElement("div");
221
+ chips.className = "chips";
222
+ approve.append(label, chips);
223
+
224
+ const commentLabel = document.createElement("label");
225
+ commentLabel.className = "label";
226
+ commentLabel.textContent = "Comment";
227
+ const tx = document.createElement("textarea");
228
+ tx.id = "tx-" + it.id;
229
+ commentLabel.htmlFor = tx.id;
230
+ tx.rows = 2;
231
+ tx.placeholder = "Change it, ask about it, or pick another option";
232
+ tx.addEventListener("input", () => {
233
+ clearTimeout(timers[it.id]);
234
+ timers[it.id] = setTimeout(() => save(it.id, { comment: tx.value }), 700);
235
+ });
236
+ answer.append(approve, commentLabel, tx);
237
+ el.append(answer);
238
+ sec.append(el);
239
+ }
240
+ root.append(sec);
241
+ }
242
+ ALL.forEach((it) => paint(it.id));
243
+ tally();
244
+ }
245
+
246
+ function paint(id) {
247
+ const el = document.getElementById("item-" + id);
248
+ if (!el) return;
249
+ const s = state[id] || {};
250
+ const tx = el.querySelector("textarea");
251
+ el.querySelector(".approve input").checked = !!s.approved;
252
+ if (document.activeElement !== tx) tx.value = s.comment || "";
253
+ el.classList.toggle("approved", !!s.approved);
254
+ const chips = el.querySelector(".chips");
255
+ chips.replaceChildren();
256
+ const add = (text, cls) => {
257
+ const span = document.createElement("span");
258
+ span.className = "chip " + cls;
259
+ span.textContent = text;
260
+ chips.append(span);
261
+ };
262
+ add(s.approved ? "Approved" : "Open", s.approved ? "ok" : "");
263
+ if ((s.comment || "").trim()) add("Commented", "c");
264
+ tally();
265
+ }
266
+
267
+ function tally() {
268
+ const approved = ALL.filter((it) => state[it.id]?.approved).length;
269
+ const commented = ALL.filter((it) => (state[it.id]?.comment || "").trim()).length;
270
+ document.getElementById("n-approved").textContent = approved;
271
+ document.getElementById("n-total").textContent = ALL.length;
272
+ document.getElementById("n-commented").textContent = commented;
273
+ document.getElementById("meter").style.width = (ALL.length ? (100 * approved) / ALL.length : 100) + "%";
274
+ }
275
+
276
+ async function save(id, patch) {
277
+ state[id] = { ...(state[id] || {}), ...patch };
278
+ paint(id);
279
+ if (!db) return;
280
+ const status = document.getElementById("status");
281
+ status.textContent = "Saving";
282
+ try {
283
+ await db.collection("responses").doc(id).set({
284
+ approved: !!state[id].approved,
285
+ comment: state[id].comment || "",
286
+ updatedAt: new Date().toISOString(),
287
+ });
288
+ status.textContent = "Saved";
289
+ } catch {
290
+ status.textContent = "Not saved";
291
+ }
292
+ }
293
+
294
+ // Tells the Claude session watching this page that the viewer has finished answering.
295
+ async function offerDone() {
296
+ const button = document.getElementById("done");
297
+ let comments = null;
298
+ try {
299
+ comments = await window.claude?.use?.("comments");
300
+ } catch {
301
+ comments = null;
302
+ }
303
+ if (!comments?.sendToClaude) return;
304
+ const check = async () => {
305
+ try {
306
+ return (await comments.canSendToClaude()) === "available";
307
+ } catch {
308
+ return false;
309
+ }
310
+ };
311
+ button.hidden = !(await check());
312
+ button.addEventListener("click", async () => {
313
+ const status = document.getElementById("status");
314
+ if (!(await check())) {
315
+ status.textContent = "Claude is not listening";
316
+ return;
317
+ }
318
+ button.disabled = true;
319
+ try {
320
+ const anchor = await comments.anchorFor(document.getElementById("title"));
321
+ await comments.sendToClaude({ anchor, text: "Done answering. Read the responses and settle them." });
322
+ status.textContent = "Sent to Claude";
323
+ } catch {
324
+ status.textContent = "Could not reach Claude";
325
+ } finally {
326
+ button.disabled = false;
327
+ }
328
+ });
329
+ }
330
+
331
+ (async () => {
332
+ const status = document.getElementById("status");
333
+ try {
334
+ const data = await (await fetch("questions.json", { cache: "no-cache" })).json();
335
+ if (data.title) {
336
+ document.getElementById("title").textContent = data.title;
337
+ document.title = data.title;
338
+ }
339
+ if (data.intro) inline(document.getElementById("intro"), data.intro);
340
+ if (data.problem) {
341
+ inline(document.getElementById("problem-text"), data.problem);
342
+ document.getElementById("problem").hidden = false;
343
+ }
344
+ GROUPS = Array.isArray(data.groups) ? data.groups : [];
345
+ ALL = GROUPS.flatMap((g) => g.items || []);
346
+ } catch (error) {
347
+ status.textContent = "Not loaded";
348
+ return notice("Could not read questions.json: " + error.message);
349
+ }
350
+ render();
351
+ offerDone();
352
+
353
+ try {
354
+ db = await window.claude?.use?.("db");
355
+ } catch {
356
+ db = null;
357
+ }
358
+ if (!db) {
359
+ status.textContent = "Not saving";
360
+ return notice("Saving is unavailable in this view, so ticks and comments will not be kept. Open the artifact signed in to record them.");
361
+ }
362
+ db.collection("responses").onSnapshot(
363
+ (snap) => {
364
+ for (const d of snap.docs) {
365
+ const data = d.data() || {};
366
+ state[d.id] = { approved: !!data.approved, comment: data.comment || "" };
367
+ paint(d.id);
368
+ }
369
+ status.textContent = "Up to date";
370
+ },
371
+ () => {
372
+ status.textContent = "Not saving";
373
+ },
374
+ );
375
+ })();
376
+ </script>
@@ -0,0 +1,26 @@
1
+ #!/usr/bin/env bash
2
+ #
3
+ # stage: prepare the questions page for one topic.
4
+ #
5
+ # Usage: stage <out-dir>
6
+ # Copies the renderer and, when missing, writes an empty questions.json to fill in.
7
+ # Prints the two paths to publish, one per line.
8
+
9
+ set -euo pipefail
10
+
11
+ if [ $# -ne 1 ]; then
12
+ echo "usage: stage <out-dir>" >&2
13
+ exit 2
14
+ fi
15
+
16
+ out=$1
17
+ here=$(cd "$(dirname "$0")/.." && pwd)
18
+
19
+ mkdir -p "$out"
20
+ cp "$here/assets/index.html" "$out/index.html"
21
+ if [ ! -f "$out/questions.json" ]; then
22
+ printf '{\n "title": "",\n "intro": "",\n "groups": []\n}\n' > "$out/questions.json"
23
+ fi
24
+
25
+ echo "PAGE: $out/index.html"
26
+ echo "QUESTIONS: $out/questions.json"
@@ -0,0 +1,165 @@
1
+ ---
2
+ name: serve
3
+ description: "Serve local HTML or static assets over HTTP with live reload, on localhost or reachable from another device over the LAN or Tailscale, so a page can be opened in a real browser instead of as a file:// URL. Use when the user asks to serve, preview, or open a local page or static site, or when something you built needs a real HTTP origin (fetch, modules, CORS, service workers). Not for running an app's own dev server: use that project's start command instead."
4
+ verified: 2026-08-24
5
+ ---
6
+
7
+ # Serve a local file over HTTP
8
+
9
+ One command, no setup, no config file, no dependency install. `scripts/serve` runs a
10
+ detached python3 server per target.
11
+
12
+ ## The one rule: name what gets served
13
+
14
+ **The server serves exactly what your `--allow` patterns match, and nothing else. There are
15
+ no other content checks.** No gitignore awareness, no dotfile skipping, no secret detection.
16
+ A pattern that reaches a `.env`, a private key, or `.git/` will serve it, to whoever can
17
+ reach the port.
18
+
19
+ So **you** choose the patterns, and you choose them narrowly. This is a real decision, not a
20
+ formality: get it wrong and you publish a credential.
21
+
22
+ 1. **Start from the entry file.** `--allow 'report.html'` is a complete, safe answer for a
23
+ single page.
24
+ 2. **Add only the asset paths that page actually needs**, scoped to a directory:
25
+ `--allow 'assets/**' --allow 'css/*.css'`.
26
+ 3. **Never reach for `**` or `**/*`** to save yourself from reading the page. Serving a whole
27
+ tree is the failure mode this design exists to prevent.
28
+ 4. **Look before you widen.** `ls` the directory you are about to match. If a pattern would
29
+ pull in anything you cannot name and vouch for, tighten it.
30
+
31
+ Running with no `--allow` at all serves the entire tree under the root. The wrapper prints
32
+ `WARNING: no --allow given; the entire tree under ROOT is served.` Treat that warning as a
33
+ bug in your invocation. Do it only if the user explicitly asked for a whole directory and
34
+ you have told them plainly what is in it.
35
+
36
+ ## Start
37
+
38
+ ```bash
39
+ scripts/serve start <file-or-dir> --allow '<glob>' [--allow '<glob>' ...]
40
+ ```
41
+
42
+ - A **file** serves its parent directory and auto-scopes the allowlist to just that file.
43
+ `serve start report.html` is already narrow; no `--allow` needed.
44
+ - A **directory** needs explicit patterns. Patterns are globs relative to the root, with
45
+ stdlib `glob` semantics (`*`, `?`, `[abc]`, and `**` for recursive).
46
+ - `index.html` is used as the directory index when a pattern matches it.
47
+ - Prints `URL:`, `ID:`, `ROOT:`, `RELOAD:`, `BIND:`, `ALLOW:`, `MATCHED:`, `IDLE_MINUTES:`,
48
+ `LOG:`, one per line.
49
+
50
+ **Check `MATCHED:` before handing over the URL.** It is the file count your patterns
51
+ resolved to. A number far larger than you expected means a pattern is too broad: stop and
52
+ tighten it rather than shipping the URL.
53
+
54
+ Give the user the `URL:` line. Do not open the browser yourself unless asked.
55
+
56
+ Flags:
57
+
58
+ | Flag | Effect |
59
+ | --- | --- |
60
+ | `--allow GLOB` | Serve files matching this glob, relative to the root. Repeatable. |
61
+ | `--idle MIN` | Shut down after MIN minutes with no requests. Default 30. |
62
+ | `--no-reload` | Serve bytes verbatim, no injected script. |
63
+ | `--bind WHERE` | `local` (default), `lan`, `tailscale`, or a literal address. |
64
+
65
+ **Leave `--idle` alone.** The 30 minute default is what stops a forgotten server running
66
+ for days. `--idle 0` disables shutdown entirely and is almost never what you want: a server
67
+ left on `lan` or `tailscale` with no timeout stays reachable to other machines indefinitely.
68
+ Pass it only when the user explicitly asks for a server that outlives the session, and say
69
+ plainly that it will run until stopped.
70
+
71
+ **The script owns the port.** It picks the first free one and puts it in the URL it prints.
72
+ Never choose, check, or mention a port: just relay the `URL:` line.
73
+
74
+ ## Choosing the bind target
75
+
76
+ Run this first and offer the user the targets it reports as available:
77
+
78
+ ```bash
79
+ scripts/serve hosts
80
+ ```
81
+
82
+ It prints one line per target (`local: 127.0.0.1 localhost`, `lan: <ip> <ip>`,
83
+ `tailscale: <ip> <magicdns-name>`) or `UNAVAILABLE` for one that cannot be resolved
84
+ (no default route, Tailscale not installed or logged out).
85
+
86
+ **Ask with `AskUserQuestion`, not in prose**, offering only the available targets:
87
+
88
+ - **Local (recommended):** `127.0.0.1`, this machine only.
89
+ - **LAN:** reachable from other devices on the same network, e.g. a phone on the same wifi.
90
+ - **Tailscale:** reachable from your tailnet devices anywhere, over the MagicDNS name.
91
+
92
+ Skip the prompt only when the user already named the target ("serve it on tailscale", "just
93
+ localhost").
94
+
95
+ Each target binds **that address only**, so a Tailscale server is not exposed to the LAN and
96
+ vice versa. Widen the bind and the allowlist matters more, not less: tighten patterns before
97
+ choosing `lan` or `tailscale`.
98
+
99
+ ## Manage
100
+
101
+ ```bash
102
+ scripts/serve hosts # available bind targets
103
+ scripts/serve list # id, url, root for every live server
104
+ scripts/serve stop <id|port> # or: stop all
105
+ scripts/serve log <id|port> # recent requests; -n N for more lines
106
+ ```
107
+
108
+ Servers outlive the session, so `list` first rather than starting a second one for the same
109
+ directory. Dead entries are reaped automatically.
110
+
111
+ ## Live reload
112
+
113
+ Reload is **on by default**. The server injects a small script before `</body>` of every HTML
114
+ response; it long-polls `/__reload`, and the browser reloads when any **allowed** file
115
+ changes. So editing a file is enough: never tell the user to refresh, and never restart the
116
+ server after an edit.
117
+
118
+ Consequences worth knowing:
119
+
120
+ - **Patterns are re-evaluated as it runs.** A file created later that matches an existing
121
+ pattern is served, and triggers a reload, with no restart. A pattern is a standing
122
+ instruction, not a one-time snapshot: another reason to keep it narrow.
123
+ - **HTML is modified in flight.** Use `--no-reload` when byte-for-byte output matters.
124
+ - **An open tab keeps the server alive**, because its polling counts as traffic. The idle
125
+ timer fires once the last tab closes.
126
+ - **Reload polls are excluded from the log**, so `serve log` shows only real requests.
127
+
128
+ ## Reading the log to debug a page
129
+
130
+ `serve log` is the fastest way to find out why a page looks wrong. Two lines matter:
131
+
132
+ - **`404`** on an asset: the file is missing, or your patterns do not cover it. Check the
133
+ patterns first; a missing stylesheet is usually an allowlist gap, not a missing file.
134
+ - **`BLOCKED "<path>" (no allow pattern matches)`:** the page asked for something outside
135
+ the allowlist. Widen deliberately and narrowly, or leave it blocked.
136
+
137
+ ## What the server guarantees on its own
138
+
139
+ Only these, and they are about paths, not content:
140
+
141
+ - **Requests cannot escape the root.** `../` and percent-encoded traversal are refused.
142
+ - **A match that resolves outside the root is dropped**, so a symlink in the tree cannot
143
+ be used to reach `/etc/hosts` or a sibling repo.
144
+ - **Only files are served.** Directories resolve to an index or a listing of allowed files.
145
+
146
+ Everything else is your patterns. The server will not second-guess them.
147
+
148
+ ## Failure
149
+
150
+ `scripts/serve` reports `FAIL: <reason>` and exits non-zero.
151
+
152
+ - **`python3 not found`:** install it with `xcode-select --install` (the Command Line Tools
153
+ carry it) or `brew install python`.
154
+ - **`no free port`:** everything in 8000-8099 is taken; `serve list` and `serve stop`
155
+ something.
156
+ - **`bind target ... is unavailable`:** run `serve hosts` and pick a target it lists.
157
+ - **`server did not start`:** the last 20 log lines are printed above the failure.
158
+
159
+ ## Not this skill
160
+
161
+ - **A project's own dev server** (Vite, Next, Django, `mise run dev`): run that instead;
162
+ this serves static files only, with no build step and no API.
163
+ - **Sharing a page with someone outside your network:** publish an Artifact, or deploy it to
164
+ a static host. `--bind lan` reaches the local network only and `--bind tailscale`
165
+ reaches your tailnet only; neither is a public URL.