{"record":{"id":"ee39f3ecaf45005a","repo":"jackwener/OpenCLI","slug":"detached-head-checkout-a-branch-first","errorCode":null,"errorMessage":"Detached HEAD — checkout a branch first","messagePattern":"Detached HEAD — checkout a branch first","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"autoresearch/engine.ts","lineNumber":103,"sourceCode":"\n  /** Phase 0: Precondition checks */\n  private checkPreconditions(): void {\n    // Git repo exists\n    try { execStrict('git rev-parse --git-dir'); }\n    catch { throw new Error('Not a git repository'); }\n\n    // Clean working tree\n    const status = exec('git status --porcelain');\n    if (status) throw new Error(`Working tree not clean:\\n${status}`);\n\n    // No stale locks\n    if (existsSync(join(ROOT, '.git', 'index.lock'))) {\n      throw new Error('Stale .git/index.lock found — remove it first');\n    }\n\n    // Not detached HEAD\n    try { execStrict('git symbolic-ref HEAD'); }\n    catch { throw new Error('Detached HEAD — checkout a branch first'); }\n  }\n\n  /** Phase 5: Run verify command and extract metric */\n  private runVerify(): number | null {\n    this.log('  verify...');\n    const output = exec(this.config.verify, { timeout: 300_000 });\n    return extractMetric(output);\n  }\n\n  /** Phase 5.5: Run guard command */\n  private runGuard(): boolean {\n    if (!this.config.guard) return true;\n    this.log('  guard...');\n    try {\n      execStrict(this.config.guard, { timeout: 300_000 });\n      return true;\n    } catch {\n      return false;","sourceCodeStart":85,"sourceCodeEnd":121,"githubUrl":"https://github.com/jackwener/OpenCLI/blob/49907e53dc3ade5c223ff0c4c2c2785687cec4e6/autoresearch/engine.ts#L85-L121","documentation":"At src/daemon.ts:496 the daemon's HTTP server (httpServer, declared from createServer) wraps every handleRequest invocation with a catch that discards the error and replies with a bare 500 and no body: res.writeHead(500); res.end(). Clients therefore see an empty message — the response carries status 500 but no error JSON explaining what failed. The declared symbol region is this server-creation site; the empty message is the deliberate (but unhelpful) swallowing of the underlying rejection.","triggerScenarios":"Any HTTP request to the daemon where handleRequest's promise rejects — malformed JSON body, handler bug, or thrown DaemonCommandFailure outside the handled branch — triggering the .catch(() => { res.writeHead(500); res.end(); }) fallback with an empty response body.","commonSituations":"A client sends an unparseable POST body to a daemon endpoint; an internal handler throws before the route-level try/catch at line 479 can respond; the request is aborted mid-handler causing a downstream rejection; a bug in a new route escapes the existing error mapping.","solutions":["Check daemon logs at the time of the request — the rejection is logged server-side even though the client sees an empty 500.","Change the catch handler to send a JSON error body (e.g. jsonResponse(res, 500, { ok:false, error: message })) so failures are diagnosable from the client side.","Reproduce the request with curl and verify the payload matches the endpoint's expected schema (valid JSON, correct fields).","Ensure routes wrap their logic in the existing try/catch that maps DaemonCommandFailure and timeouts to proper status codes (400/408)."],"exampleFix":"// before\nhandleRequest(req, res).catch(() => { res.writeHead(500); res.end(); });\n// after\nhandleRequest(req, res).catch((err) => {\n  jsonResponse(res, 500, { ok: false, error: err instanceof Error ? err.message : 'Internal error' });\n});","handlingStrategy":"validation","validationCode":"const body = JSON.stringify(payload);\nJSON.parse(body); // fail fast client-side before hitting the daemon\nconst res = await fetch(url, { method: 'POST', body });\nif (res.status === 500 && (await res.text()).length === 0) {\n  console.error('Daemon returned empty 500 — check daemon logs for the swallowed handleRequest rejection');\n}","typeGuard":"function isDaemonErrorResponse(res: Response): res is Response & { status: 400 | 408 | 500 } {\n  return res.status >= 400;\n}","tryCatchPattern":"const res = await fetch(daemonUrl, { method: 'POST', body });\nif (!res.ok) {\n  const text = await res.text();\n  if (!text) throw new Error(`Daemon 500 with empty body (src/daemon.ts:496) — inspect daemon logs`);\n  const { error } = JSON.parse(text);\n  throw new Error(error);\n}","preventionTips":["Validate request payloads against the endpoint schema before sending to the daemon.","Watch daemon logs in development — the client-side empty 500 hides the real error there.","Patch the .catch handler to return a JSON error body so failures are self-describing.","Distinguish status codes: 408 = timeout, 400 = invalid request, empty 500 = swallowed internal rejection."],"tags":["http","daemon","empty-response","server-error"],"backgroundTag":"empty-500-response","analyzedSha":"49907e53dc3ade5c223ff0c4c2c2785687cec4e6","analyzedAt":"2026-08-29T08:14:47.543Z","schemaVersion":2},"datasetVersion":"2026-08-29T12:17:43.993Z"}