{"record":{"id":"b48eaefc9f8f05d0","repo":"nexu-io/open-design","slug":"proposal-proposal-id-is-proposal-status-not","errorCode":null,"errorMessage":"proposal ${proposal.id} is ${proposal.status}, not reviewable","messagePattern":"proposal (.+?) is (.+?), not reviewable","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"apps/daemon/src/automation-proposals.ts","lineNumber":123,"sourceCode":"      ? input.sourcePacketIds.filter((id): id is string => typeof id === 'string' && id.length > 0)\n      : [],\n    ...(typeof input.automationRunId === 'string' ? { automationRunId: input.automationRunId } : {}),\n    ...(typeof input.targetRef === 'string' ? { targetRef: input.targetRef } : {}),\n    patch: input.patch,\n    ...(typeof input.confidence === 'number' ? { confidence: input.confidence } : {}),\n    ...(input.compressionReport ? { compressionReport: input.compressionReport } : {}),\n    ...(input.metadata === undefined ? {} : { metadata: input.metadata }),\n  };\n  const proposals = await listAutomationProposals(dataDir, { status: 'all' });\n  const next = proposals.filter((existing) => existing.id !== proposal.id);\n  next.push(proposal);\n  await writeProposals(dataDir, next);\n  return proposal;\n}\n\nfunction assertReviewable(proposal: AutomationEvolutionProposal): void {\n  if (proposal.status === 'pending-review' || proposal.status === 'draft') return;\n  throw new Error(`proposal ${proposal.id} is ${proposal.status}, not reviewable`);\n}\n\nfunction safeMemoryType(value: unknown): MemoryType {\n  return typeof value === 'string' && VALID_MEMORY_TYPES.has(value as MemoryType)\n    ? (value as MemoryType)\n    : 'project';\n}\n\nfunction parseJsonPatchAfter(proposal: AutomationEvolutionProposal): Record<string, unknown> {\n  if (proposal.patch.format !== 'json') return {};\n  const after = proposal.patch.after;\n  if (typeof after !== 'string' || !after.trim()) return {};\n  try {\n    const parsed = JSON.parse(after);\n    return parsed && typeof parsed === 'object' && !Array.isArray(parsed) ? parsed : {};\n  } catch {\n    throw new Error('proposal patch.after is not valid JSON');\n  }","sourceCodeStart":105,"sourceCodeEnd":141,"githubUrl":"https://github.com/nexu-io/open-design/blob/5be4028344c2eb4c667c5a97bda8f750c5597ef7/apps/daemon/src/automation-proposals.ts#L105-L141","documentation":"Thrown by assertReviewable() when a proposal's status is neither 'pending-review' nor 'draft'. Only those two states may be applied or rejected; proposals already applied, rejected, superseded, or failed cannot be acted on again. The guard runs in both applyAutomationProposal and rejectAutomationProposal after the proposal is fetched.","triggerScenarios":"Calling applyAutomationProposal() or rejectAutomationProposal() on a proposal whose status is 'applied', 'rejected', 'superseded', or 'failed'. Common when a UI action is double-clicked or a stale proposal id is reused.","commonSituations":"Reviewer double-submits (apply then apply again); two reviewers acting concurrently; client retrying after a timeout when the first call already succeeded; automated loop reprocessing an old proposal list.","solutions":["Refresh the proposal list and check status before offering apply/reject actions.","Disable the action in the UI once status leaves 'pending-review'/'draft'.","Treat this error as a no-op in idempotent callers (log and skip) rather than retrying."],"exampleFix":"// before — unconditional apply\nawait applyAutomationProposal(dataDir, id);\n// after — guard on status\nconst p = await getAutomationProposal(dataDir, id);\nif (p && (p.status === 'pending-review' || p.status === 'draft')) {\n  await applyAutomationProposal(dataDir, id);\n}","handlingStrategy":"validation","validationCode":"const proposal = await getAutomationProposal(dataDir, id);\nif (!proposal) throw new Error('automation proposal not found');\nif (proposal.status !== 'pending-review' && proposal.status !== 'draft') {\n  throw new Error(`proposal ${id} is ${proposal.status}, not reviewable`);\n}","typeGuard":"function isReviewable(status: string): boolean {\n  return status === 'pending-review' || status === 'draft';\n}","tryCatchPattern":"try {\n  await applyAutomationProposal(dataDir, id);\n} catch (e) {\n  if (/not reviewable/.test((e as Error).message)) {\n    // already actioned; refresh UI, do not retry\n  }\n}","preventionTips":["Refresh proposal status before offering apply/reject actions.","Disable actions once status leaves pending-review/draft.","Treat 'not reviewable' as a no-op in idempotent callers."],"tags":["automation","proposals","state-machine","validation","daemon"],"backgroundTag":null,"analyzedSha":"5be4028344c2eb4c667c5a97bda8f750c5597ef7","analyzedAt":"2026-08-12T12:03:58.812Z","schemaVersion":2},"datasetVersion":"2026-08-12T18:17:37.767Z"}