diff --git a/src/main/api-server/routes-approvals.ts b/src/main/api-server/routes-approvals.ts index 46f88f59..aaeebfb2 100644 --- a/src/main/api-server/routes-approvals.ts +++ b/src/main/api-server/routes-approvals.ts @@ -41,6 +41,33 @@ export function registerApprovalRoutes(server: AgentApiServer) { return; } + // Team-building strategy approvals must follow their intended hires. + // Accepting an approve_ceo_strategy before any hire_agent requests + // exist was observed to re-enter the empty team-building loop (see + // comment in ceo-modes/team-building.en.md and §5.2 of 问题.md). + // Reject the request at the API boundary so the model cannot + // circumvent the ordering by resubmitting. + if (input.type === "approve_ceo_strategy") { + const companyHireApprovals = db.listCompanyApprovalsByType(companyId, "hire_agent"); + if (companyHireApprovals.length === 0) { + server.json(res, 409, { + error: "approve_ceo_strategy requires at least one hire_agent approval to exist first. Submit the intended hires, then file the strategy.", + code: "STRATEGY_BEFORE_HIRES", + }); + return; + } + const existingStrategy = db.listCompanyApprovalsByType(companyId, "approve_ceo_strategy") + .find((a) => a.state === "pending" || a.state === "approved"); + if (existingStrategy) { + server.json(res, 409, { + error: "approve_ceo_strategy already exists for this company; do not submit another.", + code: "STRATEGY_ALREADY_FILED", + existingApprovalId: existingStrategy.id, + }); + return; + } + } + let id: string; try { id = db.requestApproval({ diff --git a/src/main/database-base.ts b/src/main/database-base.ts index b6adad96..1133cb60 100644 --- a/src/main/database-base.ts +++ b/src/main/database-base.ts @@ -344,6 +344,7 @@ export class AppDatabaseBase { listApprovalsForApi(companyId: string, filters: { requestedByAgentId?: string; relatedTaskId?: string; statuses?: string[] | null }) { return this.approvalsMod.listApprovalsForApi(companyId, filters); } findPendingApprovalForTask(companyId: string, taskId: string) { return this.approvalsMod.findPendingApprovalForTask(companyId, taskId); } hasPendingReviewApprovalForAgent(companyId: string, agentId: string) { return this.approvalsMod.hasPendingReviewApprovalForAgent(companyId, agentId); } + listCompanyApprovalsByType(companyId: string, type: string) { return this.approvalsMod.listCompanyApprovalsByType(companyId, type); } // ─── Workspaces ──────────────────────────────────────────────────────────── diff --git a/src/main/database/approvals.ts b/src/main/database/approvals.ts index b5b74231..c2090711 100644 --- a/src/main/database/approvals.ts +++ b/src/main/database/approvals.ts @@ -349,6 +349,19 @@ export function buildApprovalMethods( return row ?? null; }, + /** + * Returns every approval in the company whose `type` matches exactly. + * Used by the approvals POST route to enforce ordering invariants + * (e.g. "approve_ceo_strategy must trail hire_agent"). + */ + listCompanyApprovalsByType(companyId: string, type: string): Array<{ id: string; state: string }> { + return readAll<{ id: string; state: string }>( + `SELECT id, state FROM approvals WHERE company_id = ? AND type = ? ORDER BY created_at ASC`, + companyId, + type, + ); + }, + /** * Returns true when the given agent has a pending review-type approval * against one of their tasks. Used by checkpoint-engine to decide diff --git a/src/main/prompt-builder/ceo-modes/team-building.en.md b/src/main/prompt-builder/ceo-modes/team-building.en.md index d3611412..2e51cbfe 100644 --- a/src/main/prompt-builder/ceo-modes/team-building.en.md +++ b/src/main/prompt-builder/ceo-modes/team-building.en.md @@ -2,13 +2,9 @@ You are the new CEO. Your company has no active team members yet. Your single job in this wake is to **staff the company**. Nothing else matters until you have direct reports. -## Critical ordering — do not violate +## Critical ordering — hires first, strategy approval last -**HIRES FIRST. STRATEGY APPROVAL LAST.** - -Do **not** submit `approve_ceo_strategy` until every intended hire has been submitted as a `hire_agent` request. Submitting strategy without hires leaves the company stuck — the strategy auto-approves, the heartbeat fires, and you land back here with nothing changed. This is a loop we have explicitly observed. Avoid it by obeying the order. - -If you see in your activity or approvals history that you have previously submitted an `approve_ceo_strategy` approval **and** no `hire_agent` approvals, recognise it as the stuck-loop signature and jump directly to Step 3 (submit hires) on this wake. Do **not** submit another strategy approval. +File every `hire_agent` request for this team-building wake before you post the single `approve_ceo_strategy` summary. The server enforces this order: `POST /approvals` with `type: "approve_ceo_strategy"` returns `409 STRATEGY_BEFORE_HIRES` while no hire requests exist, and `409 STRATEGY_ALREADY_FILED` if one has already been submitted. Don't try to bypass — the codes are there so the earlier stuck-loop failure mode can no longer recur. ## Step 1: Analyse the mission and the workspace (fast)