Skip to content

Overview as decision control center: structured questions with inline choice/text answers that re-trigger the run - #177

Open
danthebaker wants to merge 1 commit into
mainfrom
agent/se-52-a0f15e59
Open

Overview as decision control center: structured questions with inline choice/text answers that re-trigger the run#177
danthebaker wants to merge 1 commit into
mainfrom
agent/se-52-a0f15e59

Conversation

@danthebaker

Copy link
Copy Markdown
Contributor

Implements Overview as decision control center: structured questions with inline choice/text answers that re-trigger the run.

@danthebaker
danthebaker requested review from a team as code owners August 8, 2026 02:25
Comment on lines +525 to +651
router.post('/decisions/:id/answer', async (req, res) => {
if (!rateLimit(`se-admin:decision-answer:${clientIp(req)}`, 30, 60_000)) {
return res.status(429).json({ error: { code: 'rate_limited', message: 'Too many requests' } });
}
const id = req.params.id;
if (!UUID.test(id)) return res.status(400).json({ error: 'bad id' });
const { data: decision } = await supabase.from('se_decisions').select('*').eq('id', id).maybeSingle();
if (!decision) return res.status(404).json({ error: 'not found' });
if (decision.status !== 'pending') {
return res.status(409).json({ error: { code: 'already_answered', message: `decision is ${decision.status}` } });
}
const { data: run } = await supabase.from('se_runs')
.select('id, site_id, project_id, status, kind, archived_at, current_phase, error, repo_owner, repo_name, issue_number')
.eq('id', decision.run_id).maybeSingle();
if (!run) return res.status(404).json({ error: 'run not found' });
if (await denyIfNotApprover(req, res, run)) return; // Advance action

// ── Validate the answer shape against the decision's kind ──────────────────────────────
let optionId: string | null = null;
let text = '';
if (decision.kind === 'choice') {
optionId = typeof req.body?.option_id === 'string' ? req.body.option_id : null;
const valid = (decision.options ?? []).some((o: any) => o?.id === optionId);
if (!optionId || !valid) return res.status(400).json({ error: { code: 'invalid_option', message: 'option_id must match one of the decision options' } });
text = sanitizeAnswerText(req.body?.text);
} else {
text = sanitizeAnswerText(req.body?.text);
if (!text) return res.status(400).json({ error: { code: 'empty_answer', message: 'text is required' } });
}

// A true architecture decision if the run is still at (or past) the architecture gate — the run's
// status, not decision.kind alone, disambiguates this from a coincidentally-shaped choice decision.
const isArchitecture = ARCH_STATES.includes(run.status) && ARCH_ANSWER_OPTIONS.has(optionId ?? '');
if (isArchitecture && (optionId === 'request_changes' || optionId === 'reject') && !text) {
return res.status(400).json({ error: { code: 'empty_answer', message: 'text is required for this option' } });
}

// Non-architecture origin: re-derive the same classification the resume route uses, and reject
// outright if it resolves to config_blocked — that class is not agent-discussable.
let originKind: string | null = null;
let gateDetail: any = null;
if (!isArchitecture) {
const { data: prs } = await supabase.from('se_run_prs').select('state').eq('run_id', run.id);
originKind = classifyDecision(run, prs ?? []);
if (originKind === 'config_blocked' || originKind == null) {
return res.status(400).json({ error: { code: 'not_answerable', message: 'This block is a configuration/credential issue — resolve it in Setup, then resume the run.' } });
}
if (originKind === 'review_blocked') {
const { data: gate } = await supabase.from('se_gates')
.select('detail').eq('run_id', run.id).eq('gate', 'adversarial_review').order('created_at', { ascending: false }).limit(1).maybeSingle();
gateDetail = gate?.detail ?? null;
}
}

const actorId = authorOf(req);
const answer = decision.kind === 'choice' ? { option_id: optionId, text: text || undefined } : { text };
// A distilled review_blocked decision (workers/review.ts's distillDecision) can ALSO be
// kind:'choice' with custom option ids/labels, not just the fixed architecture options. The
// resumed agent needs to see WHICH option was picked, not just the optional free-text reason —
// so build the human-readable summary from the option's label whenever one was selected.
const chosenLabel = decision.kind === 'choice' ? (decision.options ?? []).find((o: any) => o.id === optionId)?.label ?? optionId : null;
const answerSummary = chosenLabel ? (text ? `${chosenLabel} — ${text}` : chosenLabel) : text;

// ── CAS the decision to answered BEFORE acting — a lost race means someone else already
// answered it, so bail out rather than double-resume the run. ─────────────────────────────
const { data: racedDecision, error: decisionError } = await supabase.from('se_decisions')
.update({ status: 'answered', answer, answered_by: actorId, answered_at: new Date().toISOString() })
.eq('id', id).eq('status', 'pending')
.select().single();
if (decisionError) return res.status(500).json({ error: 'update failed' });
if (!racedDecision) return res.status(409).json({ error: { code: 'already_answered', message: 'decision was already answered' } });

let actionResult: any = { ok: true };
let auditNote = `Answered decision: "${decision.question}" → `;
if (isArchitecture) {
if (optionId === 'approve') {
if (run.status !== 'architecture_in_review') {
actionResult = { status: 409, error: { code: 'not_finalized', message: 'Finalize (commit) the proposal before approving.' } };
} else {
actionResult = await approveArchitecture(supabase, null, run, { actorId, enqueueJob });
}
auditNote += 'approved.';
} else if (optionId === 'request_changes') {
actionResult = await resumeRunForDecision(supabase, null, run, 'architecture', {
actorId, enqueueJob, note: `Architecture changes requested by admin: ${text}`,
});
auditNote += `requested changes — ${text}`;
} else {
const { data: raced } = await supabase.from('se_runs')
.update({ status: 'blocked', error: `architecture proposal rejected: ${text}`, acting_user_id: actorId })
.eq('id', run.id).eq('status', run.status)
.select('id');
actionResult = (!raced || raced.length === 0)
? { status: 409, error: { code: 'state_changed', message: 'Run state changed — refresh and retry if still needed.' } }
: { ok: true };
auditNote += `rejected — ${text}`;
}
} else if (originKind === 'review_blocked') {
actionResult = await resumeRunForDecision(supabase, null, run, 'spec', {
actorId, enqueueJob, extraJobData: { objections: gateDetail?.objections ?? [] },
note: `Answered by admin: ${answerSummary}`,
});
auditNote += answerSummary;
} else {
// pr_closed_partial (the only remaining non-architecture, non-config_blocked DecisionKind).
actionResult = await resumeRunForDecision(supabase, null, run, 'revise', {
actorId, enqueueJob, note: `Answered by admin: ${answerSummary}`,
});
auditNote += answerSummary;
}

if (actionResult?.error) {
// Roll the decision back to pending — the action didn't take effect, so the question is still open.
await supabase.from('se_decisions').update({ status: 'pending', answer: null, answered_by: null, answered_at: null }).eq('id', id);
return res.status(actionResult.status ?? 500).json({ error: actionResult.error });
}

if (run.issue_number) {
try {
const project = await getProject(supabase, run.project_id);
if (project?.githubToken) await githubClient(project.githubToken).postComment(run.repo_owner, run.repo_name, run.issue_number, auditNote);
} catch { /* best-effort */ }
}

const { data: freshRun } = await supabase.from('se_runs').select('*').eq('id', run.id).maybeSingle();
res.json({ decision: racedDecision, run: freshRun });
});
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants