diff --git a/packages/coding-agent/src/defaults/gjc/skills/deep-interview/SKILL.md b/packages/coding-agent/src/defaults/gjc/skills/deep-interview/SKILL.md index 4352974c58..21c7f2e50f 100644 --- a/packages/coding-agent/src/defaults/gjc/skills/deep-interview/SKILL.md +++ b/packages/coding-agent/src/defaults/gjc/skills/deep-interview/SKILL.md @@ -601,15 +601,20 @@ To dispute: send `{ "id": "", "disputed": true }`. To supersede: send ` **`write --input`** — same `{"state":{…}}` shape and same merge semantics as a staged `merge-state` apply, committed in one step. `write --reset --input` replaces the whole `state` with the payload (the locked `intent_contract` is re-attached automatically); use it only for deliberate re-initialization. -### Step 2f: Check Tiered Confirmation Cadence +### Step 2f: Check Continuation Contract (issue #4589) -Confirmation cadence is tiered by round, adopted from ouroboros's ooo interview, while the hard safety cap is retained: +An ordinary answered round NEVER asks for generic continuation approval. After scoring, persisting the round, and reporting progress, continue directly to the next weakest-dimension question until a legitimate terminal condition occurs. Generic "continue?"-style or continue/cancel/clear choices are NOT interview-round prompts; surfacing a generic continuation ask after an ordinary answered round converts the ambiguity gate into per-answer consent friction and is a contract violation. -- **Rounds 1-3 (auto-continue)**: minimum context gathering — proceed to the next question without a "continue?" prompt. -- **Rounds 4-15 (ask to continue)**: after each round, ask "Continue, or proceed with current clarity ({score}%)?" so the user controls depth. -- **Rounds 16+ (diminishing-returns warning)**: keep asking "Continue?" but prefix a diminishing-returns warning: "We're at {n} rounds (ambiguity: {score}%); each further round yields less. Continue or proceed?" -- **Round 3+ early exit**: still allow immediate exit if the user says "enough", "let's go", "build it". -- **Round 100 (hard cap)**: "Maximum interview rounds reached. Proceeding with current clarity level ({score}%)." The tiered cadence never removes this hard safety cap. +Legitimate terminal conditions — the ONLY places the interview may stop or ask about stopping: + +1. **Threshold + closure gates**: ambiguity ≤ the resolved threshold AND the Phase 4 closure audit and one-sentence Restate gate have passed. Then crystallize the spec and present the Phase 5 execution options. +2. **Explicit user exit**: preserve the two exit-intent classes in any session language: + - **Hard cancellation**: "stop", "cancel", "abort", or equivalent stops immediately at any round and saves state for resume. Never turn a hard cancellation into a clarifying question. + - **Early proceed**: "enough", "let's go", "build it", or equivalent stops with the early-exit warning from round 3+ when ambiguity > threshold. Before round 3, ask one targeted clarifying question about what the user wants changed instead; do not treat that early-proceed intent as a hard cancellation. +3. **Invocation/resume suitability ambiguity only**: the Phase 0.5 continue/cancel/clear choice exists solely at the invocation boundary when existing state already contains rounds, topology, spec, or handoff metadata. It is never re-asked inside an active interview. +4. **Bounded continuation safety recovery**: the Round 100 hard cap ("Maximum interview rounds reached. Proceeding with current clarity level ({score}%).") or the runtime's bounded continuation budget being exhausted. These are safety stops, not consent prompts. + +The user always keeps passive exit control: any answer, option, or free-text reply can carry an exit intent. Hard cancellations are honored immediately; early-proceed intents follow their round-3 safety rule above. Depth control comes from answering the questions themselves or exiting explicitly — not from per-round continue? interruptions. ## Phase 3: Lateral Review Panel (milestone-triggered) @@ -957,7 +962,7 @@ Why bad: 45% ambiguity means nearly half the requirements are unclear. The mathe - **Hard cap at 100 rounds**: Proceed with whatever clarity exists, noting the risk -- **Tiered confirmation cadence**: rounds 1-3 auto-continue, rounds 4-15 ask to continue, rounds 16+ ask with a diminishing-returns warning +- **Continuation contract**: ordinary answered rounds auto-continue to the next weakest-dimension question with no generic continue/cancel/clear ask; stopping is reserved for threshold + closure gates, explicit user exit, invocation/resume suitability, or bounded safety recovery - **Early exit (round 3+)**: Allow with warning if ambiguity > threshold - **User says "stop", "cancel", "abort"**: Stop immediately, save state for resume - **Ambiguity stalls** (same score +-0.05 for 3 rounds): Activate Ontologist mode to reframe @@ -976,6 +981,7 @@ Why bad: 45% ambiguity means nearly half the requirements are unclear. The mathe - [ ] Free-text answers passed the Refine gate; dialectic rhythm guard forced a user question after 3 agent-resolved answers; any auto-answer threshold crossing explicitly confirmed - [ ] Closure / Acceptance Guard and the one-sentence Restate gate both passed before crystallization - [ ] Interview reached ambiguity ≤ threshold OR an explicit early exit with warning +- [ ] Ordinary answered rounds auto-continued to the next weakest-dimension question with no generic continue/cancel/clear ask; any stop matched a legitimate terminal condition (threshold + closure gates, explicit user exit, invocation/resume suitability, or bounded safety recovery) - [ ] Spec persisted to `.gjc/_session-{sessionid}/specs/deep-interview-{slug}.md` exactly via the GJC CLI (no direct `.gjc/` edits without force override), covering every active topology component plus goal/constraints/acceptance criteria/clarity/ontology/transcript - [ ] Spec metadata includes the auto/lateral counters (`auto_researched_rounds`, `auto_answered_rounds`, `lateral_reviews`, `refined_rounds`, `architect_failures`, `lateral_panel_failures`) - [ ] Execution bridge presented via `ask`; execution invoked only after explicit approval through a public workflow entrypoint (never direct implementation); state cleaned up after handoff diff --git a/packages/coding-agent/test/agent-session-deep-interview-continuation.test.ts b/packages/coding-agent/test/agent-session-deep-interview-continuation.test.ts index 1dc08ed87f..e9497331e8 100644 --- a/packages/coding-agent/test/agent-session-deep-interview-continuation.test.ts +++ b/packages/coding-agent/test/agent-session-deep-interview-continuation.test.ts @@ -564,6 +564,79 @@ describe("AgentSession deep-interview continuation", () => { ), ); }); + it("auto-continues an ordinary answered round past the retired tier boundary without a generic continuation approval ask (#4589)", async () => { + await activateWorkflow("deep-interview"); + // Round 6 sits inside the retired "rounds 4-15 ask to continue" band; an + // ordinary answered round there must still auto-continue the interview + // (score, persist, next weakest-dimension question) instead of surfacing a + // generic continue/cancel/clear choice. + const ask = new AskTool({ + cwd: tempDir.path(), + hasUI: true, + settings: Settings.isolated(), + getSessionFile: () => null, + getSessionSpawns: () => "*", + getSessionId: () => sessionManager.getSessionId(), + getDeepInterviewAskStage: () => "post-topology", + } as ToolSession); + const context = { + hasUI: true, + ui: { + select: async (_prompt: string, options: string[]) => options.find(option => option.includes("Timeline")), + }, + abort: () => {}, + } as unknown as AgentToolContext; + await ask.execute( + "ordinary-answered-round", + { + questions: [ + { + id: "goal-clarity", + question: "Which outcome does the first release target?", + options: [{ label: "Budget" }, { label: "Timeline" }], + deepInterview: { round: 6, component: "Scope", dimension: "Goal", ambiguity: 0.58 }, + }, + ], + }, + undefined, + undefined, + context, + ); + const state = JSON.parse( + await Bun.file(modeStatePath(tempDir.path(), sessionManager.getSessionId(), "deep-interview")).text(), + ); + expect(state.state.rounds).toEqual([ + expect.objectContaining({ round: 6, question_id: "goal-clarity", selected_options: ["Timeline"] }), + ]); + + const continued = Promise.withResolvers(); + const continueSpy = vi.spyOn(session.agent, "continue").mockImplementation(async () => continued.resolve()); + const assistant = { + ...createAssistantMessage("Round 6 complete. Ambiguity 58% -> 44%. Next: Constraints."), + timestamp: 2, + }; + session.agent.emitExternalEvent({ type: "turn_start" }); + session.agent.emitExternalEvent({ type: "message_end", message: assistant }); + session.agent.emitExternalEvent({ type: "agent_end", messages: [assistant] }); + await continued.promise; + + // The stop gate continued the interview contract: score/persist, then the + // next weakest-dimension question via the ask tool. + expect(continueSpy).toHaveBeenCalledTimes(1); + const [reminder] = developerReminders(); + expect(reminder).toContain("score and persist the answered round"); + expect(reminder).toContain("use the ask tool for the next question"); + // The continuation reminder must be the interview contract itself, never a + // generic continue/cancel/clear approval surface. + expect(reminder).not.toMatch(/continue, or proceed with current clarity/i); + expect(reminder).not.toMatch(/ask(?:ing)? to continue/i); + // No generic continue/cancel/clear choice surface: the only stop exits the + // reminder names are the legitimate terminals (crystallize, handoff, or an + // explicit user cancellation), not a mid-interview consent ask. + expect(reminder).not.toMatch(/continue\?/i); + expect(reminder).not.toMatch(/clear the workflow/i); + expect(reminder).not.toMatch(/whether to continue/i); + }); it("atomically commits only two overlapping ordinary-stop reservations", async () => { await activateWorkflow("deep-interview"); diff --git a/packages/coding-agent/test/deep-interview-skill-contract.test.ts b/packages/coding-agent/test/deep-interview-skill-contract.test.ts index a1ba294e06..c9de67373c 100644 --- a/packages/coding-agent/test/deep-interview-skill-contract.test.ts +++ b/packages/coding-agent/test/deep-interview-skill-contract.test.ts @@ -151,13 +151,47 @@ describe("deep-interview ask clarification contract", () => { }); describe("deep-interview ouroboros ooo-interview parity port", () => { - it("documents the tiered confirmation cadence while keeping the hard cap (feature B)", () => { - expect(skill).toMatch(/Tiered Confirmation Cadence/i); - expect(skill).toMatch(/Rounds 1-3 \(auto-continue\)/i); - expect(skill).toMatch(/Rounds 4-15 \(ask to continue\)/i); - expect(skill).toMatch(/Rounds 16\+ \(diminishing-returns warning\)/i); - expect(skill).toMatch(/never removes this hard safety cap/i); + it("documents the continuation contract: ordinary answered rounds auto-continue without a generic approval ask (issue #4589)", () => { + expect(skill).toMatch(/Step 2f: Check Continuation Contract/i); + expect(skill).toMatch(/ordinary answered round NEVER asks for generic continuation approval/i); + // The regression this pins: the retired tiered cadence asked + // "Continue, or proceed with current clarity" after every ordinary round 4-15 + // (and 16+), turning the ambiguity gate into per-answer consent friction. + expect(skill).not.toMatch(/ask to continue/i); + expect(skill).not.toMatch(/Continue, or proceed with current clarity/i); + expect(skill).not.toMatch(/Rounds 4-15/i); + expect(skill).not.toMatch(/diminishing-returns warning/); + + const steps = extractSection(skill, "Steps"); + const continuationIndex = steps.indexOf("### Step 2f: Check Continuation Contract"); + expect(continuationIndex).toBeGreaterThanOrEqual(0); + const continuation = steps.slice(continuationIndex, steps.indexOf("## Phase 3:")); + // All four legitimate terminal conditions are documented. + expect(continuation).toMatch(/Threshold \+ closure gates/); + expect(continuation).toMatch(/ambiguity ≤ the resolved threshold/); + expect(continuation).toMatch(/Explicit user exit/); + expect(continuation).toMatch(/Invocation\/resume suitability ambiguity only/); + expect(continuation).toMatch(/Bounded continuation safety recovery/); + // The resume choice is invocation-boundary-only and never re-asked mid-interview. + expect(continuation).toMatch(/never re-asked inside an active interview/); + // Hard cancellations are never delayed by the pre-round-3 early-proceed guard. + expect(continuation).toMatch(/Hard cancellation/); + expect(continuation).toMatch(/stops immediately at any round/); + expect(continuation).toMatch(/Never turn a hard cancellation into a clarifying question/); + expect(continuation).toMatch(/Early proceed/); + expect(continuation).toMatch(/Before round 3, ask one targeted clarifying question/); + expect(continuation).toMatch(/do not treat that early-proceed intent as a hard cancellation/); + // Passive exit control is preserved with the two intent classes kept distinct. + expect(continuation).toMatch(/any answer, option, or free-text reply can carry an exit intent/); + expect(continuation).toMatch(/Hard cancellations are honored immediately/); + }); + + it("keeps the hard cap and explicit early-exit controls after retiring the tiered cadence (issue #4589)", () => { expect(skill).toContain("Round 100"); + expect(skill).toMatch(/Hard cap at 100 rounds/i); + expect(skill).toMatch(/Early exit \(round 3\+\)/i); + expect(skill).toMatch(/User says "stop", "cancel", "abort"/i); + expect(skill).toMatch(/stop immediately, save state for resume/i); }); it("documents advisory fanout lanes distinct from the milestone panel (feature C)", () => {