import { codexGoalMode, expectedCodexObjective, isFinalRunCompletionCandidate } from "./goal-status.js"; import type { UlwLoopCodexGoalMode, UlwLoopItem, UlwLoopPlan, UlwLoopSuccessCriterion } from "./types.js"; export interface CodexCreateGoalPayload { readonly objective: string; } export interface UlwLoopGoalInstruction { readonly text: string; readonly json: CodexCreateGoalPayload; } export function buildCodexGoalInstruction(args: { readonly plan: UlwLoopPlan; readonly goal: UlwLoopItem; readonly isFinal?: boolean; }): UlwLoopGoalInstruction { const mode = codexGoalMode(args.plan); const createGoal = buildCreateGoalPayload(args.plan, args.goal); const isFinal = args.isFinal ?? isFinalRunCompletionCandidate(args.plan, args.goal); return { text: buildText(mode, args.plan, args.goal, createGoal, isFinal), json: createGoal }; } function buildCreateGoalPayload(plan: UlwLoopPlan, goal: UlwLoopItem): CodexCreateGoalPayload { return { objective: expectedCodexObjective(plan, goal) }; } function buildText( mode: UlwLoopCodexGoalMode, plan: UlwLoopPlan, goal: UlwLoopItem, createGoal: CodexCreateGoalPayload, isFinal: boolean, ): string { return joinLines([ mode === "aggregate" ? "UlwLoop aggregate-goal handoff" : "UlwLoop active-goal handoff", `Mode: ${mode}`, `Plan: ${plan.goalsPath}`, `Ledger: ${plan.ledgerPath}`, `Goal: ${goal.id} — ${goal.title}`, "", ...activeGoalLines(goal), "", ...successCriteriaLines(goal.successCriteria), "", "Codex goal integration constraints:", "- Use the create_goal payload exactly as rendered: objective only.", "- Goals are unlimited. Do not add numeric limits.", ...modeConstraintLines(mode, isFinal), finalSection(plan, goal, isFinal, mode === "aggregate"), ...checkpointLines(plan, mode), "", "create_goal payload:", JSON.stringify(createGoal, null, 2), ]); } function modeConstraintLines(mode: UlwLoopCodexGoalMode, isFinal: boolean): readonly string[] { if (mode === "per_story") { return [ "- First call get_goal. If no active goal exists, call create_goal with the payload below.", "- If a different active Codex goal exists, finish/checkpoint that goal before starting this ulw-loop.", "- Work only this goal until its completion audit passes.", ]; } return [ "- Codex goal = the whole omo ulw-loop run; OMO G001/G002/etc. = ledger stories.", "- First call get_goal. If no active goal exists, call create_goal with the aggregate payload below.", "- If get_goal reports the same aggregate objective as active, continue this OMO story without creating a new Codex goal.", "- If a different active or incomplete Codex goal exists, finish/checkpoint that goal before starting this ulw-loop.", isFinal ? "- This is the final story; update_goal is allowed only after the mandatory quality gate passes." : "- This is not the final story: do not call update_goal yet; the aggregate Codex goal must remain active while later OMO stories remain.", ]; } function checkpointLines(plan: UlwLoopPlan, mode: UlwLoopCodexGoalMode): readonly string[] { const failureLine = `- If blocked or failed, checkpoint with --status failed and the failure evidence; rerun complete-goals${sessionOption(plan)} --retry-failed to resume.`; if (mode === "per_story") return [failureLine]; return [ "- Checkpoint this OMO story with a fresh get_goal snapshot whose objective matches the aggregate payload.", failureLine, ]; } function activeGoalLines(goal: UlwLoopItem): readonly string[] { return ["Active goal:", `- id: ${goal.id}`, `- title: ${goal.title}`, `- objective: ${goal.objective}`]; } function successCriteriaLines(criteria: readonly UlwLoopSuccessCriterion[]): readonly string[] { if (criteria.length === 0) return ["Success criteria:", "- No success criteria recorded for this goal."]; return ["Success criteria:", ...criteria.map(formatCriterionLine)]; } function formatCriterionLine(criterion: UlwLoopSuccessCriterion): string { const remainingWork = criterion.status === "pending" ? " remaining work:" : ""; return `-${remainingWork} [${criterion.id}] (${criterion.userModel}) ${criterion.scenario} — expect: ${criterion.expectedEvidence} — status: ${criterion.status}`; } function finalSection(plan: UlwLoopPlan, goal: UlwLoopItem, isFinal: boolean, aggregate: boolean): string { if (!isFinal) return "- This is not the final ulw-loop story; do not run the final ai-slop-cleaner/code-review gate yet."; const option = sessionOption(plan); const blockerCommand = `omo ulw-loop record-review-blockers${option} --goal-id ${goal.id} --title "Resolve final code-review blockers" --objective "" --evidence "" --codex-goal-json ""`; const checkpointCommand = `omo ulw-loop checkpoint${option} --goal-id ${goal.id} --status complete --evidence "" --codex-goal-json "" --quality-gate-json ""`; return joinLines([ "Final story — run mandatory quality gate before update_goal:", "- Run ai-slop-cleaner on changed files even when it is a no-op, rerun verification, then run the code review (spawn_agent(agent_type=\"codex-ultrawork-reviewer\", fork_turns=\"none\", ...); fall back to agent_type=\"worker\" with a scoped reviewer assignment if unavailable).", "- If the final review is not APPROVE with architect status CLEAR, do not call update_goal. Record blocker work first:", ` ${blockerCommand}`, aggregate ? '- If the final review is clean, call update_goal({status: "complete"}), call get_goal again, then checkpoint the aggregate story:' : '- If the final review is clean, call update_goal({status: "complete"}), call get_goal again, then checkpoint:', ` ${checkpointCommand}`, ]); } function sessionOption(plan: UlwLoopPlan): string { const prefix = ".omo/ulw-loop/"; const suffix = "/goals.json"; if (!plan.goalsPath.startsWith(prefix) || !plan.goalsPath.endsWith(suffix)) return ""; const sessionId = plan.goalsPath.slice(prefix.length, -suffix.length); return sessionId.length === 0 ? "" : ` --session-id ${sessionId}`; } function joinLines(lines: readonly string[]): string { return lines.join("\n"); }