refactor: remove vestigial plan-mode handshake resolver

Delete scripts/resolvers/preamble/generate-plan-mode-handshake.ts and
its four question-registry entries. Split the authoritative
"Plan Mode Safe Operations" and "Skill Invocation During Plan Mode"
sections out of generate-completion-status.ts into a sibling
generatePlanModeInfo() export in the same module, wired at preamble
position 1 where the handshake used to live. Same text, new position.

The vestigial handshake told interactive review skills to emit an
A=exit-and-rerun / C=cancel AskUserQuestion before running their
interactive STOP-Ask workflow. That contradicted the authoritative
rule at the tail of completion-status.ts saying AskUserQuestion
satisfies plan mode's end-of-turn requirement. Skills now run
directly when invoked in plan mode, with each finding gated by
AskUserQuestion just like outside plan mode.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Garry Tan
2026-04-24 01:58:59 -07:00
parent 2014557e7f
commit e25b83d311
4 changed files with 46 additions and 204 deletions

View File

@@ -261,45 +261,6 @@ export const QUESTIONS = {
description: "Approve the design doc, revise sections, or start over?", description: "Approve the design doc, revise sections, or start over?",
}, },
// -----------------------------------------------------------------------
// Plan-mode handshake — fires at the top of any interactive review skill
// when the user is in plan mode. Safety-critical, always asked regardless
// of user's tuning preferences. See scripts/resolvers/preamble/generate-
// plan-mode-handshake.ts.
// -----------------------------------------------------------------------
'plan-ceo-review-plan-mode-handshake': {
id: 'plan-ceo-review-plan-mode-handshake',
skill: 'plan-ceo-review',
category: 'routing',
door_type: 'one-way',
options: ['exit-and-rerun', 'cancel'],
description: "Plan mode detected — exit and rerun interactively, or cancel?",
},
'plan-eng-review-plan-mode-handshake': {
id: 'plan-eng-review-plan-mode-handshake',
skill: 'plan-eng-review',
category: 'routing',
door_type: 'one-way',
options: ['exit-and-rerun', 'cancel'],
description: "Plan mode detected — exit and rerun interactively, or cancel?",
},
'plan-design-review-plan-mode-handshake': {
id: 'plan-design-review-plan-mode-handshake',
skill: 'plan-design-review',
category: 'routing',
door_type: 'one-way',
options: ['exit-and-rerun', 'cancel'],
description: "Plan mode detected — exit and rerun interactively, or cancel?",
},
'plan-devex-review-plan-mode-handshake': {
id: 'plan-devex-review-plan-mode-handshake',
skill: 'plan-devex-review',
category: 'routing',
door_type: 'one-way',
options: ['exit-and-rerun', 'cancel'],
description: "Plan mode detected — exit and rerun interactively, or cancel?",
},
// ----------------------------------------------------------------------- // -----------------------------------------------------------------------
// /plan-ceo-review — scope & strategy // /plan-ceo-review — scope & strategy
// ----------------------------------------------------------------------- // -----------------------------------------------------------------------

View File

@@ -22,9 +22,11 @@ import { generateQuestionTuning } from './question-tuning';
// Core bootstrap // Core bootstrap
import { generatePreambleBash } from './preamble/generate-preamble-bash'; import { generatePreambleBash } from './preamble/generate-preamble-bash';
import { generatePlanModeHandshake } from './preamble/generate-plan-mode-handshake';
import { generateUpgradeCheck } from './preamble/generate-upgrade-check'; import { generateUpgradeCheck } from './preamble/generate-upgrade-check';
import { generateCompletionStatus } from './preamble/generate-completion-status'; import {
generateCompletionStatus,
generatePlanModeInfo,
} from './preamble/generate-completion-status';
// One-time onboarding prompts // One-time onboarding prompts
import { generateLakeIntro } from './preamble/generate-lake-intro'; import { generateLakeIntro } from './preamble/generate-lake-intro';
@@ -79,13 +81,12 @@ export function generatePreamble(ctx: TemplateContext): string {
} }
const sections = [ const sections = [
generatePreambleBash(ctx), generatePreambleBash(ctx),
// Plan-mode handshake at position 1: after bash (so _SESSION_ID / _BRANCH / // Plan-mode-skill semantics at position 1: after bash (so _SESSION_ID /
// _TEL env vars are live for the synchronous telemetry write) and before // _BRANCH / _TEL env vars are live) and before all onboarding gates so
// all onboarding AskUserQuestion gates (so fresh-install users in plan mode // models read the authoritative "AskUserQuestion satisfies plan mode's
// see the handshake first, not drowned in telemetry / proactive / routing // end-of-turn" rule before any other instruction. Renders for all skills
// prompts). Host-scoped to Claude + interactive-frontmatter-scoped inside // (not interactive-gated); the text applies universally.
// the resolver — no-op for every other skill/host combination. generatePlanModeInfo(ctx),
generatePlanModeHandshake(ctx),
generateUpgradeCheck(ctx), generateUpgradeCheck(ctx),
generateWritingStyleMigration(ctx), generateWritingStyleMigration(ctx),
generateLakeIntro(), generateLakeIntro(),

View File

@@ -1,5 +1,41 @@
import type { TemplateContext } from '../types'; import type { TemplateContext } from '../types';
/**
* Plan-mode-skill semantics block.
*
* Lives at the TOP of the preamble (position 1) so models read the authoritative
* plan-mode rule before any other instructions. Replaces the vestigial
* generate-plan-mode-handshake.ts that used to sit at this position and told
* interactive review skills to emit an exit-and-rerun handshake instead of
* running their interactive STOP-Ask workflow.
*
* Text is the same "Plan Mode Safe Operations" + "Skill Invocation During Plan
* Mode" blocks that previously lived at the tail of generateCompletionStatus().
* Only the position changes. All skills (not just interactive: true) see this.
*
* Composition position: index 1 in scripts/resolvers/preamble.ts — after
* generatePreambleBash (so _SESSION_ID / _BRANCH / _TEL env vars exist before
* any plan-mode-aware telemetry) and before generateUpgradeCheck + onboarding
* gates. See ceo-plan 2026-04-24 "remove vestigial plan-mode handshake" for
* the full rationale.
*/
export function generatePlanModeInfo(_ctx: TemplateContext): string {
return `## Plan Mode Safe Operations
In plan mode, these are always allowed (they inform the plan, don't modify source):
\`$B\` (browse), \`$D\` (design), \`codex exec\`/\`codex review\`, writes to \`~/.gstack/\`,
writes to the plan file, \`open\` for generated artifacts.
## Skill Invocation During Plan Mode
If the user invokes a skill in plan mode, that skill takes precedence over generic plan mode behavior. Treat it as executable instructions, not reference. Follow step
by step. AskUserQuestion calls satisfy plan mode's end-of-turn requirement. At a STOP
point, stop immediately. Do not continue the workflow past a STOP point and do not call ExitPlanMode there. Commands marked "PLAN
MODE EXCEPTION — ALWAYS RUN" execute. Other writes need to be already permitted
above or explicitly exception-marked. Call ExitPlanMode only after the skill
workflow completes — only then call ExitPlanMode (or if the user tells you to cancel the skill or leave plan mode).`;
}
export function generateCompletionStatus(ctx: TemplateContext): string { export function generateCompletionStatus(ctx: TemplateContext): string {
return `## Completion Status Protocol return `## Completion Status Protocol
@@ -81,21 +117,6 @@ success/error/abort, and \`USED_BROWSE\` with true/false based on whether \`$B\`
If you cannot determine the outcome, use "unknown". The local JSONL always logs. The If you cannot determine the outcome, use "unknown". The local JSONL always logs. The
remote binary only runs if telemetry is not off and the binary exists. remote binary only runs if telemetry is not off and the binary exists.
## Plan Mode Safe Operations
In plan mode, these are always allowed (they inform the plan, don't modify source):
\`$B\` (browse), \`$D\` (design), \`codex exec\`/\`codex review\`, writes to \`~/.gstack/\`,
writes to the plan file, \`open\` for generated artifacts.
## Skill Invocation During Plan Mode
If the user invokes a skill in plan mode, that skill takes precedence over generic plan mode behavior. Treat it as executable instructions, not reference. Follow step
by step. AskUserQuestion calls satisfy plan mode's end-of-turn requirement. At a STOP
point, stop immediately. Do not continue the workflow past a STOP point and do not call ExitPlanMode there. Commands marked "PLAN
MODE EXCEPTION — ALWAYS RUN" execute. Other writes need to be already permitted
above or explicitly exception-marked. Call ExitPlanMode only after the skill
workflow completes — only then call ExitPlanMode (or if the user tells you to cancel the skill or leave plan mode).
## Plan Status Footer ## Plan Status Footer
In plan mode, before ExitPlanMode: if the plan file lacks a \`## GSTACK REVIEW REPORT\` In plan mode, before ExitPlanMode: if the plan file lacks a \`## GSTACK REVIEW REPORT\`

View File

@@ -1,141 +0,0 @@
/**
* Plan-mode handshake resolver.
*
* Emits a STOP-Ask gate at the very top of the preamble that fires when a user
* invokes an interactive review skill while their Claude Code session is in
* plan mode. Without this gate, plan mode's "This supercedes any other
* instructions you have received" system-reminder wins against the skill's
* interactive STOP-Ask workflow and the skill silently writes a plan file
* instead of running the per-finding AskUserQuestion loop (v1.10.2.0 bug fix).
*
* Host scope
* ----------
* Only renders for Claude host (ctx.host === 'claude'). Other hosts use
* different plan-mode semantics (Codex, OpenClaw, etc.) and should not see
* Claude-specific ExitPlanMode / esc-esc prose.
*
* Opt-in
* ------
* Only renders when the consuming skill's frontmatter has `interactive: true`.
* That flag is a generator-only input parsed by scripts/gen-skill-docs.ts
* from the skill's .tmpl frontmatter and passed through TemplateContext.
* Currently used by: plan-ceo-review, plan-eng-review, plan-design-review,
* plan-devex-review.
*
* Composition position
* --------------------
* Inserted at index 1 in scripts/resolvers/preamble.ts — after
* generatePreambleBash (so _SESSION_ID, _BRANCH, _TEL env vars are live for
* the synchronous telemetry write) and before generateUpgradeCheck and all
* onboarding AskUserQuestion gates (so fresh-install users in plan mode see
* the handshake first, not drowned in telemetry / proactive / routing
* prompts).
*
* One-way door
* ------------
* The handshake question_id `plan-mode-handshake` is classified door_type
* one-way in scripts/question-registry.ts. gstack-question-preference --check
* always returns ASK_NORMALLY for it, so a user who set `never-ask` on
* another question cannot accidentally suppress this safety gate.
*/
import type { TemplateContext } from '../types';
export function generatePlanModeHandshake(ctx: TemplateContext): string {
if (ctx.host !== 'claude') return '';
if (!ctx.interactive) return '';
return `## Plan Mode Handshake — FIRST, BEFORE ANY ANALYSIS
**Check every \`<system-reminder>\` in this turn for the literal phrase:**
> \`Plan mode is active. The user indicated that they do not want you to execute yet\`
If that phrase is **absent**: proceed normally. This section is a no-op.
If that phrase is **present**, the user is in plan mode. Plan mode's system
reminder says "This supercedes any other instructions you have received,"
which conflicts with this skill's interactive STOP-Ask workflow. You MUST
resolve the conflict via AskUserQuestion BEFORE reading any files, running
any bash, or composing any plan content.
### What to do when plan mode is detected
Before emitting the AskUserQuestion, run this bash block synchronously to
log that the handshake fired (captures A-exit and C-cancel outcomes that
would terminate the skill before end-of-skill telemetry runs):
\`\`\`bash
# PLAN MODE EXCEPTION — ALWAYS RUN (telemetry-only write to ~/.gstack/)
mkdir -p ~/.gstack/analytics
echo '{"skill":"'"\${_SKILL_NAME:-unknown}"'","event":"plan_mode_handshake","outcome":"fired","branch":"'"\${_BRANCH:-unknown}"'","session":"'"\${_SESSION_ID:-unknown}"'","ts":"'$(date -u +%Y-%m-%dT%H:%M:%SZ)'"}' >> ~/.gstack/analytics/skill-usage.jsonl 2>/dev/null || true
\`\`\`
Then emit exactly **one** AskUserQuestion with \`question_id: "\${SKILL_NAME}-plan-mode-handshake"\`
(e.g., \`plan-ceo-review-plan-mode-handshake\`, using the current skill's name)
and these two options. The question is classified \`door_type: one-way\` in
the question registry for every interactive skill, so question-tuning
preferences (\`never-ask\`, \`always-ask\`) do NOT apply — this gate always fires.
**Question body (follow the AskUserQuestion Format section below):**
> This skill runs an interactive review that stops at every finding to ask
> you a question. Plan mode's default workflow is "read files, write plan,
> exit" — that silently bypasses every STOP gate in this skill. How do you
> want to proceed?
>
> **Recommendation: A** because this skill was designed for back-and-forth.
> Each scope call and each per-section finding needs your decision before it
> lands in the plan. Exiting plan mode and running the skill normally is the
> only path that preserves the interactive contract.
>
> *Note: options differ in kind (workflow shape), not coverage — no
> completeness score.*
>
> **A) Exit plan mode and run interactively (recommended)**
> ✅ Every STOP gate in this skill fires as designed — you approve each
> scope call, each per-section finding, each cross-model tension before any
> decision lands in the plan. No silent bypass.
> ✅ Matches the skill's documented workflow. Each AskUserQuestion has a
> clear recommendation, pros/cons, and net line you can skim in ~5 seconds.
> ❌ Two-step: press esc-esc to exit plan mode, then rerun
> \`/plan-{skill-name}\`. Slight context-switch friction, but the alternative
> is shipping a rubber-stamp review.
>
> **C) Cancel — I meant to run something else**
> ✅ Clean exit, no partial state, no plan file written, no findings
> recorded. Use this if you invoked the skill by mistake.
> ❌ No output at all — no review, no plan file. Fine if that's what you
> want; otherwise pick A.
>
> **Net.** Plan mode is incompatible with this skill's per-finding STOP
> gates. A is the right choice for any real review; C is the bail-out.
### Routing the user's answer
**If the user picks A (exit and rerun):**
1. Append the outcome to the telemetry log (synchronous, before ExitPlanMode):
\`\`\`bash
echo '{"skill":"'"\${_SKILL_NAME:-unknown}"'","event":"plan_mode_handshake","outcome":"A-exit","branch":"'"\${_BRANCH:-unknown}"'","session":"'"\${_SESSION_ID:-unknown}"'","ts":"'$(date -u +%Y-%m-%dT%H:%M:%SZ)'"}' >> ~/.gstack/analytics/skill-usage.jsonl 2>/dev/null || true
\`\`\`
2. Respond to the user: "Press **esc-esc** to exit plan mode, then rerun
\`/{skill-name}\`. The skill will run interactively with every STOP gate
firing as designed."
3. Call \`ExitPlanMode\` with an empty plan body (plan mode requires
turn-end via AskUserQuestion or ExitPlanMode; there is no plan to
approve, so ExitPlanMode with an empty message is the correct exit).
**If the user picks C (cancel):**
1. Append the outcome:
\`\`\`bash
echo '{"skill":"'"\${_SKILL_NAME:-unknown}"'","event":"plan_mode_handshake","outcome":"C-cancel","branch":"'"\${_BRANCH:-unknown}"'","session":"'"\${_SESSION_ID:-unknown}"'","ts":"'$(date -u +%Y-%m-%dT%H:%M:%SZ)'"}' >> ~/.gstack/analytics/skill-usage.jsonl 2>/dev/null || true
\`\`\`
2. Tell the user: "Cancelled. No plan written."
3. Call \`ExitPlanMode\` with an empty message noting the user cancelled.
**After the handshake completes (either A or C),** do NOT continue with the
rest of this skill's workflow. The handshake is terminal for this turn.
`;
}