fix(the-workshop): add HTTP error boundary and make workshop detection non-terminal

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
This commit is contained in:
jennyf19
2026-07-18 19:58:49 -07:00
parent fa1f154155
commit fcae8c506d
2 changed files with 18 additions and 3 deletions
@@ -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();
+5 -3
View File
@@ -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):
```
<path>/