Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -601,15 +601,20 @@ To dispute: send `{ "id": "<fact-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)

Expand Down Expand Up @@ -957,7 +962,7 @@ Why bad: 45% ambiguity means nearly half the requirements are unclear. The mathe

<Escalation_And_Stop_Conditions>
- **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
Expand All @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<void>();
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");
Expand Down
46 changes: 40 additions & 6 deletions packages/coding-agent/test/deep-interview-skill-contract.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)", () => {
Expand Down
Loading