import type { GoalTask, GoalTaskList } from "./goal-record.ts"; import { promptSafeObjective } from "./goal-contract.ts"; import { renderConfirmationTasks } from "./goal-task-confirmation.ts"; export type GoalDraftingFocus = "goal" | "sisyphus"; export interface GoalConfirmationIntentLike { focus: GoalDraftingFocus; originalTopic: string; startedAt?: number; } export interface DraftProposalInput { intent: GoalConfirmationIntentLike | null; hasUnfinishedGoal: boolean; objective: string; sisyphus?: boolean; draftId?: string; } export type DraftProposalValidation = | { ok: true; objective: string; expectedSisyphus: boolean } | { ok: false; message: string; clearDrafting?: boolean }; export type ToolGateDecision = | { block: false } | { block: true; reason: string }; // ── Shared formatting helpers ────────────────────────────────────────────── function formatModeLabel(sisyphus: boolean): string { return sisyphus ? "Sisyphus (prompt/criteria style)" : "Normal goal"; } function formatPrefixedLines(content: string): string[] { const lines: string[] = []; for (const rawLine of content.split("\n")) { const trimmed = rawLine.trim(); if (!trimmed) continue; if (trimmed.startsWith("│")) { lines.push(rawLine); } else { lines.push(`│ ${rawLine}`); } } return lines; } function formatSection(title: string, content: string): string[] { const body = formatPrefixedLines(content); return ["", `─── ${title} ───`, "", ...body]; } /** * §proposal-adjust: the "Continue chatting — keep refining" row opens an answer * editor, so the user can type the adjustment in the dialog itself. The option * label stays unchanged (existing decision semantics), which makes this hint * the only in-dialog affordance that advertises the direct input. */ const ADJUSTMENT_HINT = '│ Adjust: press Enter on "Continue chatting — keep refining" and type what to change.'; export function buildDraftConfirmationText(args: { focus: GoalDraftingFocus; originalTopic: string; objective: string; autoContinue: boolean; }): string { const lines: string[] = []; lines.push("● Goal draft ready for confirmation."); lines.push(""); lines.push("─── Draft Details ───"); lines.push(`│ Mode: ${formatModeLabel(args.focus === "sisyphus")}`); lines.push(`│ Auto-continue: ${args.autoContinue ? "yes" : "no"}`); lines.push(...formatSection("Original Topic", args.originalTopic.trim())); lines.push(...formatSection("Proposed Goal", args.objective)); lines.push("", ADJUSTMENT_HINT); return lines.join("\n"); } export function buildTweakConfirmationText(args: { currentObjective: string; newObjective: string; changeSummary: string; sisyphus: boolean; tasks?: GoalTask[]; }): string { const lines: string[] = []; lines.push("● Goal tweak ready for confirmation."); lines.push(""); lines.push("─── Draft Details ───"); lines.push(`│ Mode: ${formatModeLabel(args.sisyphus)}`); lines.push(...formatSection("Change", args.changeSummary)); lines.push(...formatSection("Current Objective", args.currentObjective)); lines.push(...formatSection("Proposed New Objective", args.newObjective)); if (args.tasks && args.tasks.length > 0) { const taskLines = renderConfirmationTasks(args.tasks, 0); lines.push(""); lines.push(`┌─ TASKS ─────────────────────────────────────┐`); for (const tl of taskLines) lines.push(tl); lines.push(`└──────────────────────────────────────────────┘`); } lines.push("", ADJUSTMENT_HINT); return lines.join("\n"); } export function evaluateDraftingToolGate(args: { toolName: string; draftingFocus?: GoalDraftingFocus | null; tweakDraftingGoalId?: string | null; activeGoalId?: string | null; proposeToolName?: string; tweakApplyToolName?: string; getGoalToolName?: string; }): ToolGateDecision { // Goal confirmation is prompt-guided, not runtime-enforced. The agent should // avoid substantive work before confirmation, but minimal reconnaissance is allowed. void args; return { block: false }; } export function validateGoalDraftProposal(input: DraftProposalInput): DraftProposalValidation { if (input.intent === null) { return { ok: false, message: "propose_goal_draft REJECTED: no /goal, /sisyphus, or /goal-tweak guided draft is in progress.", }; } const expectedSisyphus = input.intent.focus === "sisyphus"; const actualSisyphus = input.sisyphus === true; if (actualSisyphus !== expectedSisyphus) { return { ok: false, message: `propose_goal_draft REJECTED (focus gate): the user invoked ${input.intent.focus === "sisyphus" ? "/sisyphus" : "/goal"} but you passed sisyphus=${actualSisyphus}. Set sisyphus=${expectedSisyphus} to match their choice.`, }; } const objective = input.objective.trim(); if (!objective) { return { ok: false, message: "propose_goal_draft REJECTED: objective is empty." }; } return { ok: true, objective, expectedSisyphus }; } export function goalDraftingPrompt(topic: string, focus: GoalDraftingFocus): string { const safeTopic = promptSafeObjective(topic.trim() || "(no topic provided — ask the user what they want to accomplish)"); const header = focus === "sisyphus" ? "[GOAL CONFIRMATION focus=sisyphus]\nThe user invoked Sisyphus intent discussion (/sisyphus). Help turn their request into a confirmed goal contract. Do NOT start substantive work yet." : "[GOAL CONFIRMATION focus=goal]\nThe user invoked guided goal drafting (/goal). Help turn their request into a confirmed goal contract. Do NOT start substantive work yet."; const commonProtocol = [ "Confirmation protocol:", "- Treat this as a lightweight conversation with the user, not a separate long-running runtime phase.", "- If the topic is vague, ask one focused question with a recommended default. Use the installed `ask_user` tool (pi-ask) when a structured answer would help; if that tool is not available, ask in plain chat. Plain conversation is always acceptable.", "- Targeted read-only research is allowed when it helps define a better goal contract; do not start implementation before confirmation.", "- If the topic is already concrete, you may proceed directly to propose_goal_draft.", "- The goal contract should make the objective, success criteria, boundaries, constraints, and blocker rule explicit.", "- Keep grilling assumptions until the objective, success criteria, boundaries, constraints, and blocker rule are clear enough to confirm.", "- If the objective naturally decomposes into trackable milestones, you MUST include the task list in the `tasks` parameter of `propose_goal_draft` so the user can accept both goal and tasks in a single confirmation dialog. Do NOT propose the goal without tasks and then call `propose_task_list` separately.", "- For simple single-step goals, no task list is required. The `tasks` parameter can be omitted.", "- After goal creation, `propose_task_list` is still available for user-requested task additions or structural changes.", "- propose_goal_draft opens the user's Confirm / Continue Chatting dialog. Confirm creates and focuses the goal; Continue Chatting means keep refining through normal proposal cycles. The user may also type the adjustment they want directly in the Continue Chatting editor; when they do, the text arrives verbatim in the tool result while the goal stays unchanged — use it instead of asking them to restate it.", "- Call propose_goal_draft with the complete structured proposal. The tool call renderer is the scrollable, complete human presentation of the objective and task tree — do NOT duplicate the full proposal in a preceding prose message. A brief orientation sentence is fine.", "- create_goal is not a shortcut. Direct create_goal calls are rejected so the user keeps explicit say in goal creation.", ]; const goalFocusItems = [ "For /goal, propose a normal goal in this shape when ready:", "=== Goal ===", "Objective: ", "Success criteria: ", "Boundaries: ", "Constraints: ", "Verification contract: ", "If blocked: ", "Call propose_goal_draft with sisyphus=false and autoContinue=true unless the user asked otherwise.", ]; const sisyphusFocusItems = [ "For /sisyphus, remember that Sisyphus is a prompt/criteria style, not a separate step-counter mechanism.", "Propose a Sisyphus goal in this shape when ready:", "=== Sisyphus Goal ===", "Objective: ", "Success criteria: ", "Boundaries: ", "Constraints: ", "Verification contract: ", "Ordered steps: ", "If blocked / unclear / failing: ", "Sisyphus reminder: Work patiently and sequentially. No rushing, no unrequested preflight steps, no improvising around blockers.", "Call propose_goal_draft with sisyphus=true and autoContinue=true unless the user asked otherwise.", ]; return [ header, "", "Topic the user provided:", "", safeTopic, "", "", ...commonProtocol, "", ...(focus === "sisyphus" ? sisyphusFocusItems : goalFocusItems), ].join("\n"); } // ── §14 durable proposal summary ───────────────────────────────────────────── export interface ProposalSummaryArgs { objective: string; taskList?: GoalTaskList; verificationContract?: string; autoContinue: boolean; auditorEnabled: boolean; } /** * Durable proposal summary written to the terminal transcript (plan §14): * proposed objective, proposed plan (numbered top-level tasks), verification, * automatic-continuation state, and auditor state. Produced for every proposal * outcome (confirm / continue refining / cancel) so the summary is always part * of the conversation, not only the confirmation dialog. */ export function buildProposalSummary(args: ProposalSummaryArgs): string { const lines: string[] = ["Proposed objective:", args.objective.trim()]; if (args.taskList && args.taskList.tasks.length > 0) { lines.push("", "Proposed plan:"); lines.push(...renderProposalPlan(args.taskList.tasks)); } if (args.verificationContract?.trim()) { lines.push("", "Verification:", args.verificationContract.trim()); } lines.push("", `Automatic continuation: ${args.autoContinue ? "enabled" : "disabled"}`); lines.push(`Independent auditor: ${args.auditorEnabled ? "enabled" : "disabled"}`); return lines.join("\n"); } function renderProposalPlan(tasks: GoalTask[]): string[] { const lines: string[] = []; let index = 1; for (const t of tasks) { lines.push(`${index}. ${t.title}`); if (t.subtasks && t.subtasks.length > 0) { for (const child of t.subtasks) { lines.push(` - ${child.title}`); } } index++; } return lines; }