From fcae8c506d9d9fe58d0c2b04e5c193f0f8de5d11 Mon Sep 17 00:00:00 2001 From: jennyf19 <19942418+jennyf19@users.noreply.github.com> Date: Sat, 18 Jul 2026 19:58:49 -0700 Subject: [PATCH] fix(the-workshop): add HTTP error boundary and make workshop detection non-terminal MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two follow-up review findings: - signals-dashboard: the async createServer callback had no top-level error boundary, so a malformed %-encoded path, a stash write on a read-only workshop, or a scan failure rejected a promise the server never awaits — hanging the request and risking an unhandled-rejection crash. Wrap the handler and return a controlled 500. - workshop-create: Path A treated finding a marker (desks/, CAIRN.md, etc.) as 'just use it', an early stop that left partially initialized workshops incomplete. Make it detection-only and continue scaffolding whatever is missing, per the 'only add what's missing' principle. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 26faf13e-639c-4a21-ac05-c0dc2bff7c62 --- extensions/signals-dashboard/extension.mjs | 13 +++++++++++++ skills/workshop-create/SKILL.md | 8 +++++--- 2 files changed, 18 insertions(+), 3 deletions(-) diff --git a/extensions/signals-dashboard/extension.mjs b/extensions/signals-dashboard/extension.mjs index 8647e05f..e15e0ded 100644 --- a/extensions/signals-dashboard/extension.mjs +++ b/extensions/signals-dashboard/extension.mjs @@ -649,6 +649,7 @@ function renderDashboard(signals, stashed) { async function startServer(instanceId, workshopDir) { const server = createServer(async (req, res) => { + try { const url = new URL(req.url, `http://${req.headers.host}`); if (req.method === "POST" && url.pathname.startsWith("/api/") && isCrossSiteRequest(req)) { @@ -708,6 +709,18 @@ async function startServer(instanceId, workshopDir) { const stashed = await readStash(workshopDir); res.setHeader("Content-Type", "text/html; charset=utf-8"); res.end(renderDashboard(signals, stashed)); + } catch (err) { + // Top-level boundary: never leave a request hanging or let a + // rejection become an unhandled crash — e.g. malformed %-encoding + // in the path, a read-only workshop on a stash write, or a scan + // failure. Return a controlled error instead. + if (!res.headersSent) { + res.writeHead(500, { "Content-Type": "application/json" }); + res.end(JSON.stringify({ ok: false, error: "internal_error" })); + } else { + try { res.end(); } catch {} + } + } }); await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve)); const address = server.address(); diff --git a/skills/workshop-create/SKILL.md b/skills/workshop-create/SKILL.md index 0f12c8cb..f3e18b88 100644 --- a/skills/workshop-create/SKILL.md +++ b/skills/workshop-create/SKILL.md @@ -21,9 +21,11 @@ The operator already has a folder they want to use. Maybe it's a repo they cloned, maybe it's a local project folder. 1. **Confirm the path exists.** If not, ask the operator for a valid path. -2. **Check if it's already a workshop.** Look for `desks/` or `classroom/` - folders, a `workshop.md`, `CAIRN.md`, or `hands-up.md`. If any exist, - it's already a workshop — just use it. +2. **Detect existing workshop markers.** Look for `desks/` or `classroom/` + folders, a `workshop.md`, `CAIRN.md`, or `hands-up.md`. Finding any of + these tells you this is an existing workshop — but this is detection + only, not a stopping point. Continue to the next step and add whatever + is missing; never overwrite what is already there. 3. **Scaffold the workshop structure** (only what's missing): ``` /