/** * `decision` — record, list and close the questions put to the operator. * * Moved out of cli.ts's switch by the per-verb module split (#462); * only the case wrapper, the injected `ctx` lookups and the imports * changed from the original bodies. */ import type { CommandContext } from "./context.ts"; import { findProject, loadConfig } from "../config.ts"; import { CONDITION_FORMS, parseCondition } from "../decisions.ts"; import { dbPath, openStore } from "../store.ts"; import { validateQuestionShape } from "../ask.ts"; export async function decisionCommand(ctx: CommandContext): Promise { const sub = ctx.argv[1]; const project = findProject(loadConfig(), ctx.projectFlag); const store = openStore(dbPath()); try { if (sub === "open") { const question = ctx.flag("question")?.trim(); if (question === undefined || question.length === 0 || question.startsWith("--")) { process.stderr.write( "omp-conductor: decision open needs --question with the question you sent\n", ); process.exit(2); } const shape = validateQuestionShape(question); if (!shape.ok) { process.stderr.write(`omp-conductor: decision open: ${shape.problem}\n`); process.exit(2); } const condition = ctx.flag("resolves-when")?.trim(); if (condition !== undefined && parseCondition(condition) === undefined) { process.stderr.write( `omp-conductor: --resolves-when must be one of:\n${CONDITION_FORMS.map((f) => ` ${f}`).join("\n")}\n`, ); process.exit(2); } const blocks = ctx.flag("blocks")?.trim(); const decision = store.createDecision({ project: project.name, question, ...(blocks === undefined || blocks.length === 0 ? {} : { blocks }), ...(condition === undefined ? {} : { condition }), at: Date.now(), }); process.stdout.write( `decision ${decision.id} recorded — resolve with: omp-conductor decision resolve ${decision.id} --answer "..."\n`, ); return; } if (sub === "resolve" || sub === "withdraw") { const id = ctx.argv[2]; if (id === undefined || id.startsWith("--")) { process.stderr.write(`omp-conductor: decision ${sub} needs the decision id\n`); process.exit(2); } const answered = sub === "resolve"; const text = answered ? ctx.flag("answer")?.trim() : ctx.flag("reason")?.trim(); if (answered && (text === undefined || text.length === 0)) { process.stderr.write("omp-conductor: decision resolve needs --answer with what was decided\n"); process.exit(2); } const ok = store.resolveDecision( id, answered ? "answered" : "withdrawn", text === undefined || text.length === 0 ? "withdrawn" : text, Date.now(), ); if (!ok) { // A decision that is not open is a different mistake from an id that // never existed, and the operator can only act on one of them. process.stderr.write( `omp-conductor: no open decision ${id} for ${project.name} — it was answered, withdrawn or expired, or the id is wrong\n`, ); process.exit(1); } process.stdout.write(`decision ${id} ${answered ? "answered" : "withdrawn"}\n`); return; } if (sub === "list" || sub === undefined) { // Only rows a human must answer. Watches would render identically here and // were the whole reason an operator read the orchestrator's own reminder as // a question aimed at them (#459) — `watch list` shows those. const open = store.openDecisions(project.name).filter((d) => d.kind !== "watch"); const json = ctx.argv.includes("--json"); const now = Date.now(); const decisions = open.map((d) => ({ id: d.id, ageHours: Math.max(0, Math.round((now - d.askedAt) / 3_600_000)), blocks: d.blocks ?? null, condition: d.condition === undefined ? null : d.conditionMetAt === undefined ? "pending" : "met", question: d.question, })); if (json) { process.stdout.write(`${JSON.stringify({ project: project.name, decisions }, null, 2)}\n`); return; } if (decisions.length === 0) { process.stdout.write("no open decisions\n"); return; } for (const decision of decisions) { process.stdout.write( `${decision.id} ${decision.ageHours}h blocks:${decision.blocks ?? "-"} ` + `condition:${decision.condition ?? "-"} ${decision.question}\n`, ); } return; } process.stderr.write( `omp-conductor: unknown decision subcommand "${sub}" — expected open, resolve, withdraw or list\n`, ); process.exit(2); } finally { store.close(); } }