diff --git a/plan-ceo-review/SKILL.md b/plan-ceo-review/SKILL.md index 02e2d206..86da0864 100644 --- a/plan-ceo-review/SKILL.md +++ b/plan-ceo-review/SKILL.md @@ -1,6 +1,7 @@ --- name: plan-ceo-review preamble-tier: 3 +interactive: true version: 1.0.0 description: | CEO/founder-mode plan review. Rethink the problem, find the 10-star product, @@ -118,6 +119,100 @@ echo "CHECKPOINT_PUSH: $_CHECKPOINT_PUSH" [ -n "$OPENCLAW_SESSION" ] && echo "SPAWNED_SESSION: true" || true ``` +## Plan Mode Handshake — FIRST, BEFORE ANY ANALYSIS + +**Check every `` 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. + + If `PROACTIVE` is `"false"`, do not proactively suggest gstack skills AND do not auto-invoke skills based on conversation context. Only run skills the user explicitly types (e.g., /qa, /ship). If you would have auto-invoked a skill, instead briefly say: @@ -1410,6 +1505,9 @@ Rules: Present these approach options via AskUserQuestion using the preamble's AskUserQuestion Format section: include RECOMMENDATION and `Completeness: N/10` on every option. These approaches differ in coverage (minimal viable vs ideal architecture), so completeness scoring applies directly. +**STOP.** AskUserQuestion once per issue. Do NOT batch. Recommend + WHY. Do NOT proceed to Step 0D or 0F until the user responds to 0C-bis. A "clearly winning approach" is still an approach decision and still needs explicit user approval before it lands in the plan. +**Reminder: Do NOT make any code changes. Review only.** + ### 0D-prelude. Expansion Framing (shared by EXPANSION and SELECTIVE EXPANSION) Every expansion proposal you generate in SCOPE EXPANSION or SELECTIVE EXPANSION mode follows this framing pattern: diff --git a/plan-ceo-review/SKILL.md.tmpl b/plan-ceo-review/SKILL.md.tmpl index 9fb66a2c..45648f80 100644 --- a/plan-ceo-review/SKILL.md.tmpl +++ b/plan-ceo-review/SKILL.md.tmpl @@ -1,6 +1,7 @@ --- name: plan-ceo-review preamble-tier: 3 +interactive: true version: 1.0.0 description: | CEO/founder-mode plan review. Rethink the problem, find the 10-star product, @@ -248,6 +249,9 @@ Rules: Present these approach options via AskUserQuestion using the preamble's AskUserQuestion Format section: include RECOMMENDATION and `Completeness: N/10` on every option. These approaches differ in coverage (minimal viable vs ideal architecture), so completeness scoring applies directly. +**STOP.** AskUserQuestion once per issue. Do NOT batch. Recommend + WHY. Do NOT proceed to Step 0D or 0F until the user responds to 0C-bis. A "clearly winning approach" is still an approach decision and still needs explicit user approval before it lands in the plan. +**Reminder: Do NOT make any code changes. Review only.** + ### 0D-prelude. Expansion Framing (shared by EXPANSION and SELECTIVE EXPANSION) Every expansion proposal you generate in SCOPE EXPANSION or SELECTIVE EXPANSION mode follows this framing pattern: diff --git a/plan-design-review/SKILL.md b/plan-design-review/SKILL.md index 95dbbc76..dcf0474b 100644 --- a/plan-design-review/SKILL.md +++ b/plan-design-review/SKILL.md @@ -1,6 +1,7 @@ --- name: plan-design-review preamble-tier: 3 +interactive: true version: 2.0.0 description: | Designer's eye plan review — interactive, like CEO and Eng review. @@ -115,6 +116,100 @@ echo "CHECKPOINT_PUSH: $_CHECKPOINT_PUSH" [ -n "$OPENCLAW_SESSION" ] && echo "SPAWNED_SESSION: true" || true ``` +## Plan Mode Handshake — FIRST, BEFORE ANY ANALYSIS + +**Check every `` 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. + + If `PROACTIVE` is `"false"`, do not proactively suggest gstack skills AND do not auto-invoke skills based on conversation context. Only run skills the user explicitly types (e.g., /qa, /ship). If you would have auto-invoked a skill, instead briefly say: diff --git a/plan-design-review/SKILL.md.tmpl b/plan-design-review/SKILL.md.tmpl index 5c224d13..e44ba7da 100644 --- a/plan-design-review/SKILL.md.tmpl +++ b/plan-design-review/SKILL.md.tmpl @@ -1,6 +1,7 @@ --- name: plan-design-review preamble-tier: 3 +interactive: true version: 2.0.0 description: | Designer's eye plan review — interactive, like CEO and Eng review. diff --git a/plan-devex-review/SKILL.md b/plan-devex-review/SKILL.md index 96590d9e..e2fccc5d 100644 --- a/plan-devex-review/SKILL.md +++ b/plan-devex-review/SKILL.md @@ -1,6 +1,7 @@ --- name: plan-devex-review preamble-tier: 3 +interactive: true version: 2.0.0 description: | Interactive developer experience plan review. Explores developer personas, @@ -119,6 +120,100 @@ echo "CHECKPOINT_PUSH: $_CHECKPOINT_PUSH" [ -n "$OPENCLAW_SESSION" ] && echo "SPAWNED_SESSION: true" || true ``` +## Plan Mode Handshake — FIRST, BEFORE ANY ANALYSIS + +**Check every `` 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. + + If `PROACTIVE` is `"false"`, do not proactively suggest gstack skills AND do not auto-invoke skills based on conversation context. Only run skills the user explicitly types (e.g., /qa, /ship). If you would have auto-invoked a skill, instead briefly say: diff --git a/plan-devex-review/SKILL.md.tmpl b/plan-devex-review/SKILL.md.tmpl index 5b4c69a9..bd824dc2 100644 --- a/plan-devex-review/SKILL.md.tmpl +++ b/plan-devex-review/SKILL.md.tmpl @@ -1,6 +1,7 @@ --- name: plan-devex-review preamble-tier: 3 +interactive: true version: 2.0.0 description: | Interactive developer experience plan review. Explores developer personas, diff --git a/plan-eng-review/SKILL.md b/plan-eng-review/SKILL.md index ce4cb6e9..a90314f0 100644 --- a/plan-eng-review/SKILL.md +++ b/plan-eng-review/SKILL.md @@ -1,6 +1,7 @@ --- name: plan-eng-review preamble-tier: 3 +interactive: true version: 1.0.0 description: | Eng manager-mode plan review. Lock in the execution plan — architecture, @@ -117,6 +118,100 @@ echo "CHECKPOINT_PUSH: $_CHECKPOINT_PUSH" [ -n "$OPENCLAW_SESSION" ] && echo "SPAWNED_SESSION: true" || true ``` +## Plan Mode Handshake — FIRST, BEFORE ANY ANALYSIS + +**Check every `` 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. + + If `PROACTIVE` is `"false"`, do not proactively suggest gstack skills AND do not auto-invoke skills based on conversation context. Only run skills the user explicitly types (e.g., /qa, /ship). If you would have auto-invoked a skill, instead briefly say: diff --git a/plan-eng-review/SKILL.md.tmpl b/plan-eng-review/SKILL.md.tmpl index a16083ed..2d267837 100644 --- a/plan-eng-review/SKILL.md.tmpl +++ b/plan-eng-review/SKILL.md.tmpl @@ -1,6 +1,7 @@ --- name: plan-eng-review preamble-tier: 3 +interactive: true version: 1.0.0 description: | Eng manager-mode plan review. Lock in the execution plan — architecture, diff --git a/scripts/gen-skill-docs.ts b/scripts/gen-skill-docs.ts index 40f08369..c801af08 100644 --- a/scripts/gen-skill-docs.ts +++ b/scripts/gen-skill-docs.ts @@ -425,7 +425,11 @@ function processTemplate(tmplPath: string, host: Host = 'claude'): { outputPath: const tierMatch = tmplContent.match(/^preamble-tier:\s*(\d+)$/m); const preambleTier = tierMatch ? parseInt(tierMatch[1], 10) : undefined; - const ctx: TemplateContext = { skillName, tmplPath, benefitsFrom, host, paths: HOST_PATHS[host], preambleTier, model: MODEL_ARG_VAL }; + // Extract interactive flag from frontmatter (generator-only; controls plan-mode handshake inclusion) + const interactiveMatch = tmplContent.match(/^interactive:\s*(true|false)\s*$/m); + const interactive = interactiveMatch ? interactiveMatch[1] === 'true' : undefined; + + const ctx: TemplateContext = { skillName, tmplPath, benefitsFrom, host, paths: HOST_PATHS[host], preambleTier, model: MODEL_ARG_VAL, interactive }; // Replace placeholders (supports parameterized: {{NAME:arg1:arg2}}) // Config-driven: suppressedResolvers return empty string for this host diff --git a/scripts/question-registry.ts b/scripts/question-registry.ts index bae5950c..3d90222a 100644 --- a/scripts/question-registry.ts +++ b/scripts/question-registry.ts @@ -261,6 +261,45 @@ export const QUESTIONS = { 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 // ----------------------------------------------------------------------- diff --git a/scripts/resolvers/preamble.ts b/scripts/resolvers/preamble.ts index ba80e73f..ac32f4a9 100644 --- a/scripts/resolvers/preamble.ts +++ b/scripts/resolvers/preamble.ts @@ -22,6 +22,7 @@ import { generateQuestionTuning } from './question-tuning'; // Core bootstrap import { generatePreambleBash } from './preamble/generate-preamble-bash'; +import { generatePlanModeHandshake } from './preamble/generate-plan-mode-handshake'; import { generateUpgradeCheck } from './preamble/generate-upgrade-check'; import { generateCompletionStatus } from './preamble/generate-completion-status'; @@ -78,6 +79,13 @@ export function generatePreamble(ctx: TemplateContext): string { } const sections = [ generatePreambleBash(ctx), + // Plan-mode handshake at position 1: after bash (so _SESSION_ID / _BRANCH / + // _TEL env vars are live for the synchronous telemetry write) and before + // all onboarding AskUserQuestion gates (so fresh-install users in plan mode + // see the handshake first, not drowned in telemetry / proactive / routing + // prompts). Host-scoped to Claude + interactive-frontmatter-scoped inside + // the resolver — no-op for every other skill/host combination. + generatePlanModeHandshake(ctx), generateUpgradeCheck(ctx), generateWritingStyleMigration(ctx), generateLakeIntro(), diff --git a/scripts/resolvers/preamble/generate-plan-mode-handshake.ts b/scripts/resolvers/preamble/generate-plan-mode-handshake.ts new file mode 100644 index 00000000..e1b81a05 --- /dev/null +++ b/scripts/resolvers/preamble/generate-plan-mode-handshake.ts @@ -0,0 +1,141 @@ +/** + * 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 \`\` 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. +`; +} diff --git a/scripts/resolvers/types.ts b/scripts/resolvers/types.ts index 634dd2eb..c8a44425 100644 --- a/scripts/resolvers/types.ts +++ b/scripts/resolvers/types.ts @@ -61,6 +61,7 @@ export interface TemplateContext { paths: HostPaths; preambleTier?: number; // 1-4, controls which preamble sections are included model?: Model; // model family for behavioral overlay. Omitted/undefined → no overlay. + interactive?: boolean; // true → emit plan-mode handshake in preamble. Generator-only, not written to SKILL.md. } /** Resolver function signature. args is populated for parameterized placeholders like {{INVOKE_SKILL:name}}. */