@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.
- package/CHANGELOG.md +11 -0
- package/README.md +107 -0
- package/THIRD_PARTY_NOTICES.md +2005 -0
- package/agents/console.md +20 -0
- package/agents/free.md +110 -0
- package/build/0-JP6XL6LB.js +17 -0
- package/build/1-A52YZWNS.js +17 -0
- package/build/2-CLH2NM6F.js +32 -0
- package/build/3-4N467IB6.js +45 -0
- package/build/4-WL5H4LUW.js +29 -0
- package/build/_layout.svelte-KPUSX7LQ.js +27 -0
- package/build/_page.svelte-22H56UR6.js +7972 -0
- package/build/_page.svelte-CVWUNRQC.js +135 -0
- package/build/_page.svelte-LTZXBHMT.js +57 -0
- package/build/_server.ts-3KVBYN6P.js +20 -0
- package/build/_server.ts-3POPKVEM.js +47 -0
- package/build/_server.ts-5735T2V5.js +31 -0
- package/build/_server.ts-5VXGNWV2.js +38 -0
- package/build/_server.ts-645KP7WP.js +42 -0
- package/build/_server.ts-A3AA24Z2.js +28 -0
- package/build/_server.ts-CITUOEM6.js +58 -0
- package/build/_server.ts-I7HMTPOA.js +25 -0
- package/build/_server.ts-M5BQ4LLX.js +30 -0
- package/build/_server.ts-OB4O27HF.js +683 -0
- package/build/_server.ts-OJT7HE7M.js +15 -0
- package/build/_server.ts-OYGWVLZM.js +12 -0
- package/build/_server.ts-T75UVMQP.js +38 -0
- package/build/_server.ts-WYZYNHUZ.js +28 -0
- package/build/_server.ts-Z26EB47B.js +486 -0
- package/build/chunk-4CIG53L5.js +33 -0
- package/build/chunk-BJTJS3XY.js +338 -0
- package/build/chunk-F2CNWJZM.js +14 -0
- package/build/chunk-HGYE3UTU.js +416 -0
- package/build/chunk-KAYW2W7H.js +1311 -0
- package/build/chunk-LFP3UPCM.js +10 -0
- package/build/chunk-M44JEFEH.js +62 -0
- package/build/chunk-MC5AGLMY.js +39 -0
- package/build/chunk-NCZ77J5E.js +19798 -0
- package/build/chunk-NVNLDPUJ.js +2122 -0
- package/build/chunk-PDCON4KS.js +494 -0
- package/build/chunk-Q4OQNY5M.js +25 -0
- package/build/chunk-QCUOR2U5.js +75 -0
- package/build/chunk-R6RYZZWA.js +46 -0
- package/build/chunk-SOJTASZC.js +51 -0
- package/build/chunk-SU26PJVX.js +410 -0
- package/build/chunk-U5BSBG4R.js +506 -0
- package/build/chunk-UEKOZGRO.js +37 -0
- package/build/chunk-W6EFUWJ2.js +9 -0
- package/build/chunk-WOPREK6Q.js +117 -0
- package/build/chunk-WWJ3F4QS.js +127 -0
- package/build/chunk-ZGZJEEE3.js +1091 -0
- package/build/client/_app/immutable/assets/0.BGa5ZNDM.css +1 -0
- package/build/client/_app/immutable/chunks/BUrrLYDt.js +2 -0
- package/build/client/_app/immutable/chunks/BgFr_EyZ.js +4 -0
- package/build/client/_app/immutable/chunks/Bjy-W4x2.js +82 -0
- package/build/client/_app/immutable/chunks/C8qZab0y.js +56 -0
- package/build/client/_app/immutable/chunks/C90fLsA5.js +4 -0
- package/build/client/_app/immutable/chunks/CRzLMJPS.js +1 -0
- package/build/client/_app/immutable/chunks/DLT1iIlz.js +2 -0
- package/build/client/_app/immutable/chunks/pIrVVOVY.js +2 -0
- package/build/client/_app/immutable/entry/app.0DERehLW.js +3 -0
- package/build/client/_app/immutable/entry/payload.DSmR2FwN.js +2 -0
- package/build/client/_app/immutable/entry/start.B7srFiqh.js +3 -0
- package/build/client/_app/immutable/nodes/0.aD9vJoxR.js +2 -0
- package/build/client/_app/immutable/nodes/1.Br4FL91i.js +2 -0
- package/build/client/_app/immutable/nodes/2.vl6OYfu3.js +8 -0
- package/build/client/_app/immutable/nodes/3.VYqJOLAF.js +2 -0
- package/build/client/_app/immutable/nodes/4.jF9UVlFN.js +2 -0
- package/build/client/_app/version.json +1 -0
- package/build/client/apple-touch-icon.png +0 -0
- package/build/client/favicon.ico +0 -0
- package/build/client/favicon.svg +48 -0
- package/build/client/icon-192.png +0 -0
- package/build/client/icon-512.png +0 -0
- package/build/client/icon-maskable-512.png +0 -0
- package/build/client/icon.svg +44 -0
- package/build/client/manifest.webmanifest +15 -0
- package/build/error.svelte-JXZDY65F.js +51 -0
- package/build/handler.js +716 -0
- package/build/hooks.server-34IMHRKC.js +43 -0
- package/build/instance-NP3FJIWW.js +2967 -0
- package/client/client.mjs +28 -0
- package/client/hooks.mjs +7 -0
- package/client/register.mjs +5 -0
- package/dist/cli.js +62826 -0
- package/dist/port.js +36691 -0
- package/package.json +20 -1
- package/skills/agents/.claude-plugin/plugin.json +5 -0
- package/skills/agents/hooks/hooks.json +1 -0
- package/skills/agents/hooks/register.ts +57 -0
- package/skills/agents/skills/bus-work/SKILL.md +66 -0
- package/skills/agents/skills/plan/SKILL.md +98 -0
- package/skills/console/.claude-plugin/plugin.json +5 -0
- package/skills/console/hooks/hooks.json +1 -0
- package/skills/console/hooks/register.ts +10 -0
- package/skills/console/skills/bus/SKILL.md +61 -0
- package/skills/default/.claude-plugin/plugin.json +5 -0
- package/skills/default/skills/git-workflow/SKILL.md +294 -0
- package/skills/default/skills/git-workflow/mise-tasks/_lib.sh +107 -0
- package/skills/default/skills/git-workflow/mise-tasks/clone +36 -0
- package/skills/default/skills/git-workflow/mise-tasks/repo-list +8 -0
- package/skills/default/skills/git-workflow/mise-tasks/unpushed +26 -0
- package/skills/default/skills/git-workflow/mise-tasks/worktree-add +60 -0
- package/skills/default/skills/github/SKILL.md +208 -0
- package/skills/default/skills/github/scripts/gh-org +196 -0
- package/skills/default/skills/plan-page/SKILL.md +300 -0
- package/skills/default/skills/plan-page/assets/index.html +854 -0
- package/skills/default/skills/plan-page/scripts/inline +46 -0
- package/skills/default/skills/plan-page/scripts/stage +26 -0
- package/skills/default/skills/secrets/SKILL.md +134 -0
- package/skills/default/skills/secrets/scripts/secrets-check +87 -0
- package/skills/default/skills/writing-style/SKILL.md +45 -0
- package/skills/extra/.claude-plugin/plugin.json +5 -0
- package/skills/extra/skills/artifacts/SKILL.md +52 -0
- package/skills/extra/skills/github-actions/SKILL.md +78 -0
- package/skills/extra/skills/github-actions/scan +32 -0
- package/skills/extra/skills/markdown-review/SKILL.md +70 -0
- package/skills/extra/skills/markdown-review/assets/index.html +201 -0
- package/skills/extra/skills/markdown-review/scripts/stage +36 -0
- package/skills/extra/skills/mise-usage/SKILL.md +155 -0
- package/skills/extra/skills/plugin-authoring/SKILL.md +317 -0
- package/skills/extra/skills/project-conventions/SKILL.md +64 -0
- package/skills/extra/skills/questions/SKILL.md +156 -0
- package/skills/extra/skills/questions/assets/index.html +376 -0
- package/skills/extra/skills/questions/scripts/stage +26 -0
- package/skills/extra/skills/serve/SKILL.md +165 -0
- package/skills/extra/skills/serve/scripts/serve +268 -0
- package/skills/extra/skills/serve/scripts/server.py +345 -0
- package/skills/extra/skills/skill-dependencies/SKILL.md +65 -0
- package/skills/extra/skills/skill-maintenance/SKILL.md +101 -0
- 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.
|