From 0d6bf19bf137af3b5ed248c01645c8356bac40b8 Mon Sep 17 00:00:00 2001 From: Tom Boucher Date: Tue, 15 Sep 2026 15:53:42 -0400 Subject: [PATCH] fix(#4600): explicit --converge overrides the convergence feature gate (#4771) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * test(#4600): pin explicit-flag-overrides-gate precedence * fix(#4600): explicit --converge overrides the convergence feature gate PLAN_STRATEGY=converge is set only by an explicit --converge/--cross-ai, so gating it on workflow.plan_review_convergence made an explicit operator flag lose to a config default and stop the run with a question-shaped success. The config remains the default for non-flag invocation; the flag now wins, and the step says so instead of gate-and-exit. * test(#4600): pin the precedence contract sentences as written * fix(#4600): keep the precedence sentence on one line The pinned contract phrase wrapped across a line break, so the writer-contract assertion could not match it. * fix(#4600): document the flag-overrides-gate precedence on user surfaces commands/gsd/autonomous.md and docs/COMMANDS.md still said --converge requires workflow.plan_review_convergence=true; both now state the override. Changeset typed Changed with the docs update alongside. * docs(#4600): backfill changeset PR number * fix(#4600): restore the convergence gate mention in user surfaces * test(#4600): pin the dispatched convergence run against the config gate * fix(#4600): override the convergence gate on the dispatched run Emitted-Drift-Ack-Growth: autonomous.md — #4600: the converge dispatch appends --override-gate inside the PLAN_STRATEGY conditional, and the precedence sentences replace the stale fail-fast instruction Emitted-Drift-Ack-Growth: plan-review-convergence.md — #4600: config gate 1.5 honors an explicit --override-gate dispatch (token-anchored) while the standalone veto and config-get default are preserved --------- Co-authored-by: sim --- .changeset/silly-wasps-run.md | 5 ++ commands/gsd/autonomous.md | 2 +- commands/gsd/plan-review-convergence.md | 5 +- docs/COMMANDS.md | 2 +- docs/CONFIGURATION.md | 2 +- docs/how-to/run-phases-autonomously.md | 4 +- gsd-core/workflows/autonomous.md | 11 ++- .../autonomous/steps/converge-fail-fast.md | 27 ++---- gsd-core/workflows/plan-review-convergence.md | 18 +++- skills/gsd-autonomous/SKILL.md | 2 +- skills/gsd-plan-review-convergence/SKILL.md | 3 +- tests/autonomous-converge.test.cjs | 83 +++++++++++++++++-- 12 files changed, 127 insertions(+), 37 deletions(-) create mode 100644 .changeset/silly-wasps-run.md diff --git a/.changeset/silly-wasps-run.md b/.changeset/silly-wasps-run.md new file mode 100644 index 000000000..3586c0bf5 --- /dev/null +++ b/.changeset/silly-wasps-run.md @@ -0,0 +1,5 @@ +--- +type: Changed +pr: 4771 +--- +**`/gsd-autonomous --converge` now overrides the convergence config gate** — an explicit `--converge` or `--cross-ai` enables plan-review convergence for that run even when `workflow.plan_review_convergence` is `false`, instead of stopping with an enable instruction; without the flag, planning runs `gsd-plan-phase` as before, and the gate continues to govern standalone `/gsd-plan-review-convergence`. (#4600) diff --git a/commands/gsd/autonomous.md b/commands/gsd/autonomous.md index beffbdc03..6c7329cff 100644 --- a/commands/gsd/autonomous.md +++ b/commands/gsd/autonomous.md @@ -37,7 +37,7 @@ Optional flags: - `--to N` — stop after phase N completes (halt instead of advancing to next phase). - `--only N` — execute only phase N (single-phase mode). - `--interactive` — run discuss inline with questions (not auto-answered), then dispatch plan→execute as background agents. Keeps the main context lean while preserving user input on decisions. -- `--converge` — run each phase's planning step through `gsd-plan-review-convergence` instead of plain `gsd-plan-phase`. Requires `workflow.plan_review_convergence=true`. +- `--converge` — run each phase's planning step through `gsd-plan-review-convergence` instead of plain `gsd-plan-phase`. Works even when `workflow.plan_review_convergence` is `false` — the explicit flag overrides the gate; the gate (`workflow.plan_review_convergence=true` enables it) governs the standalone `gsd-plan-review-convergence` command. Without the flag, planning runs `gsd-plan-phase`. - `--cross-ai` — compatibility alias for `--converge`. When `--converge` or `--cross-ai` is set, reviewer selector flags supported by `gsd-plan-review-convergence` may be passed through: `--codex`, `--claude`, `--opencode`, `--ollama`, `--lm-studio`, `--llama-cpp`, `--all`, and `--max-cycles N`. diff --git a/commands/gsd/plan-review-convergence.md b/commands/gsd/plan-review-convergence.md index df390f313..f27415889 100644 --- a/commands/gsd/plan-review-convergence.md +++ b/commands/gsd/plan-review-convergence.md @@ -11,7 +11,7 @@ allowed-tools: - Agent - Skill - AskUserQuestion -requires: [phase, review] +requires: [phase, review, autonomous] --- @@ -55,7 +55,8 @@ Phase number: extracted from $ARGUMENTS (required) - `--max-cycles N` — Maximum replan→review cycles (default: 3) **Feature gate:** This command requires `workflow.plan_review_convergence=true`. Enable with: -`gsd config-set workflow.plan_review_convergence true` +`gsd config-set workflow.plan_review_convergence true`. A dispatch carrying `--override-gate` — +how `/gsd:autonomous --converge` invokes this workflow (#4600) — bypasses the gate for that run. diff --git a/docs/COMMANDS.md b/docs/COMMANDS.md index 5d65d662a..a135a4665 100644 --- a/docs/COMMANDS.md +++ b/docs/COMMANDS.md @@ -1033,7 +1033,7 @@ Run all remaining phases autonomously. | `--to N` | Stop after completing a specific phase number | | `--only N` | Restrict execution to phase N; lifecycle step is skipped | | `--interactive` | Lean context with user input | -| `--converge` | Route each planning step through `/gsd-plan-review-convergence`; requires `workflow.plan_review_convergence=true` | +| `--converge` | Route each planning step through `/gsd-plan-review-convergence`; the explicit flag overrides the gate — works even when `workflow.plan_review_convergence` is `false` (the gate `workflow.plan_review_convergence=true` governs standalone `/gsd-plan-review-convergence`); without it, planning runs `gsd-plan-phase` | | `--cross-ai` | Alias for `--converge` | | Reviewer flags | With `--converge`, pass through every reviewer lane flag: `--claude`, `--codex`, `--coderabbit`, `--opencode`, `--qwen`, `--cursor`, `--agy` / `--antigravity`, `--ollama`, `--lm-studio`, `--llama-cpp`, `--kimi-code`, `--all`, and `--max-cycles N` | | `--text` | Replace `AskUserQuestion` prompts with plain numbered lists | diff --git a/docs/CONFIGURATION.md b/docs/CONFIGURATION.md index 8d5a240fd..89c96774f 100644 --- a/docs/CONFIGURATION.md +++ b/docs/CONFIGURATION.md @@ -526,7 +526,7 @@ All workflow toggles follow the **absent = enabled** pattern. If a key is missin | `workflow.plan_bounce_script` | string | (none) | Path to the external script invoked for plan bounce validation. Receives the PLAN.md path as its first argument. Required when `plan_bounce` is `true`. Added in v1.36 | | `workflow.plan_bounce_passes` | number | `2` | Number of sequential bounce passes to run. Each pass feeds the previous pass's output back into the validator. Higher values increase rigor at the cost of latency. Added in v1.36 | | `workflow.post_planning_gaps` | boolean | `true` | Unified post-planning gap report (#2493). After all plans are generated and committed, scans REQUIREMENTS.md and CONTEXT.md `` against every PLAN.md in the phase directory, then prints one `Source \| Item \| Status` table. Word-boundary matching (REQ-1 vs REQ-10) and natural sort (REQ-02 before REQ-10). Non-blocking — informational report only. Set to `false` to skip Step 13e of plan-phase. | -| `workflow.plan_review_convergence` | boolean | `false` | Enable the `/gsd-plan-review-convergence` command. Disabled by default — the command exits with an enable instruction when this key is `false`. The command automates the manual plan→review→replan loop: it spawns configured reviewers (Codex, Claude, Antigravity, OpenCode, Ollama, LM Studio, llama.cpp), counts unresolved HIGH concerns and actionable MEDIUM/LOW findings via the CYCLE_SUMMARY contract, replans with `--reviews` feedback, and repeats until converged or max cycles reached. Enable with `gsd config-set workflow.plan_review_convergence true`. Added in v1.39 | +| `workflow.plan_review_convergence` | boolean | `false` | Enable the `/gsd-plan-review-convergence` command. Disabled by default — the command exits with an enable instruction when this key is `false`. The command automates the manual plan→review→replan loop: it spawns configured reviewers (Codex, Claude, Antigravity, OpenCode, Ollama, LM Studio, llama.cpp), counts unresolved HIGH concerns and actionable MEDIUM/LOW findings via the CYCLE_SUMMARY contract, replans with `--reviews` feedback, and repeats until converged or max cycles reached. Enable with `gsd config-set workflow.plan_review_convergence true`. A `/gsd-autonomous --converge` dispatch overrides this gate for its own run (#4600); standalone invocation stays gated. Added in v1.39 | | `workflow.plan_chunked` | boolean | `false` | Enable chunked planning mode. When `true` (or when `--chunked` flag is passed to `/gsd-plan-phase`), the orchestrator splits the single long-lived planner Task into a short outline Task followed by N short per-plan Tasks (~3-5 min each). Each plan is committed individually for crash resilience. If a Task hangs and the terminal is force-killed, rerunning with `--chunked` resumes from the last completed plan. Particularly useful on Windows where long-lived Tasks may hang on stdio. See [`planning.chunked_parallel`](#planning-settings) to dispatch the per-plan Tasks concurrently instead of one at a time. Added in v1.38 | | `workflow.code_review_command` | string | (none) | Shell command for external code review integration in `/gsd-ship`. Receives changed file paths via stdin. Non-zero exit blocks the ship workflow. Added in v1.36 | | `workflow.tdd_mode` | boolean | `false` | Enable TDD pipeline as a first-class execution mode. When `true`, the planner aggressively applies `type: tdd` to eligible tasks (business logic, APIs, validations, algorithms) and the executor enforces RED/GREEN/REFACTOR gate sequence. An end-of-phase collaborative review checkpoint verifies gate compliance. Added in v1.36 | diff --git a/docs/how-to/run-phases-autonomously.md b/docs/how-to/run-phases-autonomously.md index 22b7d5b8a..ee5575ef5 100644 --- a/docs/how-to/run-phases-autonomously.md +++ b/docs/how-to/run-phases-autonomously.md @@ -59,6 +59,8 @@ If the phase is already complete, autonomous mode exits immediately with a messa Use `--converge` when you want each phase to run the plan-review convergence loop before execution. Both `/gsd-autonomous` and `/gsd-progress --next --auto` support this flag. ```bash +# Needed only for /gsd-progress --next --converge and standalone /gsd-plan-review-convergence +# (/gsd-autonomous --converge overrides the gate for its own run): gsd config-set workflow.plan_review_convergence true # Via autonomous (multi-phase or single-phase): @@ -72,7 +74,7 @@ gsd config-set workflow.plan_review_convergence true `--cross-ai` is accepted as an alias for `--converge`. Reviewer flags supported by `/gsd-plan-review-convergence` pass through unchanged, including `--codex`, `--claude`, `--opencode`, `--ollama`, `--lm-studio`, `--llama-cpp`, `--all`, and `--max-cycles N`. -If `workflow.plan_review_convergence` is not enabled, the command stops before planning and prints the enable command instead of silently falling back to regular planning. +An explicit `--converge` on `/gsd-autonomous` overrides the gate for that run: convergence runs even when `workflow.plan_review_convergence` is `false`, and without the flag autonomous plans with `gsd-plan-phase`. The gate still governs `/gsd-progress --next --converge` and standalone `/gsd-plan-review-convergence`, which stop with the enable command when it is not enabled. --- diff --git a/gsd-core/workflows/autonomous.md b/gsd-core/workflows/autonomous.md index 3c6b788e3..2d692fed7 100644 --- a/gsd-core/workflows/autonomous.md +++ b/gsd-core/workflows/autonomous.md @@ -75,7 +75,7 @@ if [[ "$INIT_AUTONOMOUS" == @file:* ]]; then INIT_AUTONOMOUS=$(cat "${INIT_AUTON Extract `section_manifest` from `INIT_AUTONOMOUS` (used by the `converge-*` sections below and in step 3). -If `PLAN_STRATEGY` is `converge`, fail fast unless the existing convergence feature gate is enabled: +If `PLAN_STRATEGY` is `converge`, the dispatch below carries `--override-gate` (#4600): the operator's explicit `--converge`/`--cross-ai` overrides the convergence feature gate for this run. Without the flag, `PLAN_STRATEGY` is `local` and this block never appends it. ```bash # Lane flags derived from the declared roster (#2800/#2272); --all and --text are convergence @@ -94,6 +94,13 @@ if echo "$ARGUMENTS" | grep -qE '\-\-max-cycles\s+[0-9]+'; then MAX_CYCLES_ARG=$(echo "$ARGUMENTS" | grep -oE '\-\-max-cycles\s+[0-9]+' | awk '{print $2}') CONVERGENCE_ARGS="${CONVERGENCE_ARGS} --max-cycles ${MAX_CYCLES_ARG}" fi + +# #4600: the dispatched convergence workflow re-checks the feature gate in its own §1.5 — +# an explicit --converge/--cross-ai must override it, so mark this dispatch explicitly. +# Conditional on PLAN_STRATEGY: a local-strategy run must never carry the override. +if [ "${PLAN_STRATEGY}" = "converge" ]; then + CONVERGENCE_ARGS="${CONVERGENCE_ARGS} --override-gate" +fi ``` @@ -841,7 +848,7 @@ When any phase operation fails or a blocker is detected, present 3 options via A - [ ] `--interactive` compatible with `--only`, `--from`, and `--to` flags - [ ] `--converge` routes planning through `gsd-plan-review-convergence` - [ ] `--cross-ai` is accepted as an alias for `--converge` -- [ ] `--converge` fails fast with enable instructions when `workflow.plan_review_convergence=false` +- [ ] `--converge` overrides `workflow.plan_review_convergence=false` for the run — the dispatch carries `--override-gate`, which the convergence workflow's §1.5 gate honors (#4600) - [ ] `--converge` forwards reviewer selector flags and `--max-cycles N` - [ ] Default autonomous planning remains `gsd-plan-phase` when convergence is not requested diff --git a/gsd-core/workflows/autonomous/steps/converge-fail-fast.md b/gsd-core/workflows/autonomous/steps/converge-fail-fast.md index ed5b9c625..2c1a65809 100644 --- a/gsd-core/workflows/autonomous/steps/converge-fail-fast.md +++ b/gsd-core/workflows/autonomous/steps/converge-fail-fast.md @@ -1,21 +1,12 @@ ## Converge Fail-Fast -When `PLAN_STRATEGY` is `converge`, fail fast unless the existing convergence feature gate is enabled: +#4600: an explicit `--converge` / `--cross-ai` on the command line OVERRIDES the `workflow.plan_review_convergence` config gate. `PLAN_STRATEGY` is `converge` only when the +operator explicitly passed one of those flags, so this run performs plan-review convergence +regardless of the config value. The dispatched `gsd-plan-review-convergence` invocation carries `--override-gate` so its own §1.5 config gate cannot veto this run either. -```bash -_GSD_SHIM_NAME="gsd-tools.cjs"; _GSD_RUNTIME_ROOT="${RUNTIME_DIR:-$(git rev-parse --show-toplevel 2>/dev/null || pwd)}"; GSD_TOOLS="${_GSD_RUNTIME_ROOT}/gsd-core/bin/${_GSD_SHIM_NAME}"; _gsd_at() { for _p; do if [ -f "$_p" ]; then GSD_TOOLS="$_p"; return 0; fi; done; return 1; }; if _gsd_at "${_GSD_RUNTIME_ROOT}/gsd-core/bin/${_GSD_SHIM_NAME}" "${_GSD_RUNTIME_ROOT}/.claude/gsd-core/bin/${_GSD_SHIM_NAME}" "${_GSD_RUNTIME_ROOT}/.codex/gsd-core/bin/${_GSD_SHIM_NAME}"; then gsd_run() { node "$GSD_TOOLS" "$@"; }; elif unset -f gsd_run; _G="$(command -v gsd_run)"; then GSD_TOOLS="$_G"; gsd_run() { "$GSD_TOOLS" "$@"; }; elif _gsd_at "${CLAUDE_CONFIG_DIR:-$HOME/.claude}/gsd-core/bin/${_GSD_SHIM_NAME}" "${HERMES_HOME:-$HOME/.hermes}/gsd-core/bin/${_GSD_SHIM_NAME}" "${CURSOR_CONFIG_DIR:-$HOME/.cursor}/gsd-core/bin/${_GSD_SHIM_NAME}" "${CODEX_HOME:-$HOME/.codex}/gsd-core/bin/${_GSD_SHIM_NAME}" "${GEMINI_CONFIG_DIR:-$HOME/.gemini}/gsd-core/bin/${_GSD_SHIM_NAME}" "${COPILOT_CONFIG_DIR:-$HOME/.copilot}/gsd-core/bin/${_GSD_SHIM_NAME}" "${WINDSURF_CONFIG_DIR:-$HOME/.codeium/windsurf}/gsd-core/bin/${_GSD_SHIM_NAME}" "${AUGMENT_CONFIG_DIR:-$HOME/.augment}/gsd-core/bin/${_GSD_SHIM_NAME}" "${TRAE_CONFIG_DIR:-$HOME/.trae}/gsd-core/bin/${_GSD_SHIM_NAME}" "${QWEN_CONFIG_DIR:-$HOME/.qwen}/gsd-core/bin/${_GSD_SHIM_NAME}" "${CODEBUDDY_CONFIG_DIR:-$HOME/.codebuddy}/gsd-core/bin/${_GSD_SHIM_NAME}" "${CLINE_CONFIG_DIR:-$HOME/.cline}/gsd-core/bin/${_GSD_SHIM_NAME}" "${GROK_AGENTS_HOME:-$HOME/.agents}/gsd-core/bin/${_GSD_SHIM_NAME}" "${ANTIGRAVITY_CONFIG_DIR:-$HOME/.gemini/antigravity}/gsd-core/bin/${_GSD_SHIM_NAME}" "${OPENCODE_CONFIG_DIR:-${XDG_CONFIG_HOME:-$HOME/.config}/opencode}/gsd-core/bin/${_GSD_SHIM_NAME}" "${KILO_CONFIG_DIR:-${XDG_CONFIG_HOME:-$HOME/.config}/kilo}/gsd-core/bin/${_GSD_SHIM_NAME}"; then gsd_run() { node "$GSD_TOOLS" "$@"; }; else echo "ERROR: gsd-tools.cjs not found at $GSD_TOOLS and gsd_run is not on PATH. Run: npx -y @opengsd/gsd-core@latest --claude --local" >&2; exit 1; fi; GSD_IDENTITY_STATUS=unverified; case "$(gsd_run runtime-identity --raw 2>/dev/null || true)" in '{"packageName":"@opengsd/gsd-core"'*'}') GSD_IDENTITY_STATUS=ok;; esac; export GSD_IDENTITY_STATUS; [ "$GSD_IDENTITY_STATUS" = ok ] || echo "WARNING: \"$GSD_TOOLS\" did not prove it is @opengsd/gsd-core - it is either a different package or an @opengsd/gsd-core older than the runtime-identity verb. See docs/how-to/diagnose-a-foreign-gsd-tools.md" >&2; if [ -n "${CLAUDE_ENV_FILE:-}" ] && [ -n "${GSD_TOOLS:-}" ]; then printf "export PATH='%s':\"\$PATH\"\n" "${GSD_TOOLS%/*}" >> "$CLAUDE_ENV_FILE" 2>/dev/null || true; fi -if [ "$PLAN_STRATEGY" = "converge" ]; then - CONVERGENCE_ENABLED=$(gsd_run query config-get workflow.plan_review_convergence --raw 2>/dev/null || echo "false") - if [ "$CONVERGENCE_ENABLED" != "true" ]; then - printf '%s\n' \ - 'gsd-autonomous --converge is disabled (workflow.plan_review_convergence=false).' \ - '' \ - 'Enable plan convergence with:' \ - '' \ - ' gsd config-set workflow.plan_review_convergence true' \ - '' \ - 'Then re-run the autonomous command with --converge.' - exit 1 - fi -fi -``` +For autonomous, the config is not consulted on the non-flag path at all: without the flag, `PLAN_STRATEGY` is `local` and planning routes through `gsd-plan-phase`. The gate (`workflow.plan_review_convergence`) governs standalone `/gsd:plan-review-convergence` and `/gsd:progress --next --converge`. + +Nothing to enforce here — proceed directly to planning with convergence. Do not prompt, do not +attempt to `config-set` the gate on the operator's behalf, and do not downgrade to +non-converge planning: silently changing the plan-review contract is the outcome this step +must never produce. diff --git a/gsd-core/workflows/plan-review-convergence.md b/gsd-core/workflows/plan-review-convergence.md index dc66afdbf..0338da549 100644 --- a/gsd-core/workflows/plan-review-convergence.md +++ b/gsd-core/workflows/plan-review-convergence.md @@ -36,11 +36,25 @@ echo "$ARGUMENTS" | grep -qE '\-\-ws\s+\S+' && GSD_WS=$(echo "$ARGUMENTS" | grep ## 1.5. Config Gate (feature disabled by default) +An explicit dispatch from `/gsd:autonomous --converge` carries `--override-gate` (#4600): the +operator's explicit flag overrides this gate for that run. Standalone invocation carries no such +flag and remains gated by the config below. + ```bash _GSD_SHIM_NAME="gsd-tools.cjs"; _GSD_RUNTIME_ROOT="${RUNTIME_DIR:-$(git rev-parse --show-toplevel 2>/dev/null || pwd)}"; GSD_TOOLS="${_GSD_RUNTIME_ROOT}/gsd-core/bin/${_GSD_SHIM_NAME}"; _gsd_at() { for _p; do if [ -f "$_p" ]; then GSD_TOOLS="$_p"; return 0; fi; done; return 1; }; if _gsd_at "${_GSD_RUNTIME_ROOT}/gsd-core/bin/${_GSD_SHIM_NAME}" "${_GSD_RUNTIME_ROOT}/.claude/gsd-core/bin/${_GSD_SHIM_NAME}" "${_GSD_RUNTIME_ROOT}/.codex/gsd-core/bin/${_GSD_SHIM_NAME}"; then gsd_run() { node "$GSD_TOOLS" "$@"; }; elif unset -f gsd_run; _G="$(command -v gsd_run)"; then GSD_TOOLS="$_G"; gsd_run() { "$GSD_TOOLS" "$@"; }; elif _gsd_at "${CLAUDE_CONFIG_DIR:-$HOME/.claude}/gsd-core/bin/${_GSD_SHIM_NAME}" "${HERMES_HOME:-$HOME/.hermes}/gsd-core/bin/${_GSD_SHIM_NAME}" "${CURSOR_CONFIG_DIR:-$HOME/.cursor}/gsd-core/bin/${_GSD_SHIM_NAME}" "${CODEX_HOME:-$HOME/.codex}/gsd-core/bin/${_GSD_SHIM_NAME}" "${GEMINI_CONFIG_DIR:-$HOME/.gemini}/gsd-core/bin/${_GSD_SHIM_NAME}" "${COPILOT_CONFIG_DIR:-$HOME/.copilot}/gsd-core/bin/${_GSD_SHIM_NAME}" "${WINDSURF_CONFIG_DIR:-$HOME/.codeium/windsurf}/gsd-core/bin/${_GSD_SHIM_NAME}" "${AUGMENT_CONFIG_DIR:-$HOME/.augment}/gsd-core/bin/${_GSD_SHIM_NAME}" "${TRAE_CONFIG_DIR:-$HOME/.trae}/gsd-core/bin/${_GSD_SHIM_NAME}" "${QWEN_CONFIG_DIR:-$HOME/.qwen}/gsd-core/bin/${_GSD_SHIM_NAME}" "${CODEBUDDY_CONFIG_DIR:-$HOME/.codebuddy}/gsd-core/bin/${_GSD_SHIM_NAME}" "${CLINE_CONFIG_DIR:-$HOME/.cline}/gsd-core/bin/${_GSD_SHIM_NAME}" "${GROK_AGENTS_HOME:-$HOME/.agents}/gsd-core/bin/${_GSD_SHIM_NAME}" "${ANTIGRAVITY_CONFIG_DIR:-$HOME/.gemini/antigravity}/gsd-core/bin/${_GSD_SHIM_NAME}" "${OPENCODE_CONFIG_DIR:-${XDG_CONFIG_HOME:-$HOME/.config}/opencode}/gsd-core/bin/${_GSD_SHIM_NAME}" "${KILO_CONFIG_DIR:-${XDG_CONFIG_HOME:-$HOME/.config}/kilo}/gsd-core/bin/${_GSD_SHIM_NAME}"; then gsd_run() { node "$GSD_TOOLS" "$@"; }; else echo "ERROR: gsd-tools.cjs not found at $GSD_TOOLS and gsd_run is not on PATH. Run: npx -y @opengsd/gsd-core@latest --claude --local" >&2; exit 1; fi; GSD_IDENTITY_STATUS=unverified; case "$(gsd_run runtime-identity --raw 2>/dev/null || true)" in '{"packageName":"@opengsd/gsd-core"'*'}') GSD_IDENTITY_STATUS=ok;; esac; export GSD_IDENTITY_STATUS; [ "$GSD_IDENTITY_STATUS" = ok ] || echo "WARNING: \"$GSD_TOOLS\" did not prove it is @opengsd/gsd-core - it is either a different package or an @opengsd/gsd-core older than the runtime-identity verb. See docs/how-to/diagnose-a-foreign-gsd-tools.md" >&2; if [ -n "${CLAUDE_ENV_FILE:-}" ] && [ -n "${GSD_TOOLS:-}" ]; then printf "export PATH='%s':\"\$PATH\"\n" "${GSD_TOOLS%/*}" >> "$CLAUDE_ENV_FILE" 2>/dev/null || true; fi -CONVERGENCE_ENABLED=$(gsd_run query config-get workflow.plan_review_convergence --raw 2>/dev/null || echo "false") +# --override-gate is appended ONLY by the autonomous --converge dispatch (#4600); it must be +# matched token-anchored so no other argument can carry it in. +if echo "$ARGUMENTS" | grep -qE '(^|[[:space:]])--override-gate([[:space:]]|$)'; then + CONVERGENCE_ENABLED="override" +else + CONVERGENCE_ENABLED=$(gsd_run query config-get workflow.plan_review_convergence --raw 2>/dev/null || echo "false") +fi ``` +**If `CONVERGENCE_ENABLED` is `"override"`:** note it and continue — "Convergence was requested +explicitly (`--converge` on the autonomous command); the `workflow.plan_review_convergence` gate +is bypassed for this run (#4600)." + **If `CONVERGENCE_ENABLED` is not `"true"`:** Display and exit: ```text @@ -576,7 +590,7 @@ After plan-phase completes → go back to **step 5a** (review again). -- [ ] Config gate checked before running — exits with enable instructions if workflow.plan_review_convergence is false +- [ ] Config gate checked before running — exits with enable instructions if workflow.plan_review_convergence is false (an explicit `--override-gate` dispatch — how `/gsd:autonomous --converge` invokes this workflow — bypasses it for that run, #4600) - [ ] Initial planning via inline Skill("gsd-plan-phase") if no plans exist — NOT wrapped in Agent() (bug #936: depth-1 Agent has no Agent tool) - [ ] Review via Agent → Skill("gsd-review") — isolated Agent is correct; gsd-review is a Bash leaf with no sub-agent spawns; {GSD_WS} forwarded - [ ] Replan via inline Skill("gsd-plan-phase --reviews") — NOT wrapped in Agent(); inline lets plan-phase spawn gsd-planner/gsd-plan-checker at depth 1 diff --git a/skills/gsd-autonomous/SKILL.md b/skills/gsd-autonomous/SKILL.md index 30cad73a3..b383eecf2 100644 --- a/skills/gsd-autonomous/SKILL.md +++ b/skills/gsd-autonomous/SKILL.md @@ -36,7 +36,7 @@ Optional flags: - `--to N` — stop after phase N completes (halt instead of advancing to next phase). - `--only N` — execute only phase N (single-phase mode). - `--interactive` — run discuss inline with questions (not auto-answered), then dispatch plan→execute as background agents. Keeps the main context lean while preserving user input on decisions. -- `--converge` — run each phase's planning step through `gsd-plan-review-convergence` instead of plain `gsd-plan-phase`. Requires `workflow.plan_review_convergence=true`. +- `--converge` — run each phase's planning step through `gsd-plan-review-convergence` instead of plain `gsd-plan-phase`. Works even when `workflow.plan_review_convergence` is `false` — the explicit flag overrides the gate; the gate (`workflow.plan_review_convergence=true` enables it) governs the standalone `gsd-plan-review-convergence` command. Without the flag, planning runs `gsd-plan-phase`. - `--cross-ai` — compatibility alias for `--converge`. When `--converge` or `--cross-ai` is set, reviewer selector flags supported by `gsd-plan-review-convergence` may be passed through: `--codex`, `--claude`, `--opencode`, `--ollama`, `--lm-studio`, `--llama-cpp`, `--all`, and `--max-cycles N`. diff --git a/skills/gsd-plan-review-convergence/SKILL.md b/skills/gsd-plan-review-convergence/SKILL.md index 9c90235f5..058569bc7 100644 --- a/skills/gsd-plan-review-convergence/SKILL.md +++ b/skills/gsd-plan-review-convergence/SKILL.md @@ -55,7 +55,8 @@ Phase number: extracted from $ARGUMENTS (required) - `--max-cycles N` — Maximum replan→review cycles (default: 3) **Feature gate:** This command requires `workflow.plan_review_convergence=true`. Enable with: -`gsd config-set workflow.plan_review_convergence true` +`gsd config-set workflow.plan_review_convergence true`. A dispatch carrying `--override-gate` — +how `/gsd-autonomous --converge` invokes this workflow (#4600) — bypasses the gate for that run. diff --git a/tests/autonomous-converge.test.cjs b/tests/autonomous-converge.test.cjs index cc7df0224..e0d251cf3 100644 --- a/tests/autonomous-converge.test.cjs +++ b/tests/autonomous-converge.test.cjs @@ -26,6 +26,7 @@ const STEP_FAIL_FAST_PATH = path.join(REPO_ROOT, 'gsd-core', 'workflows', 'auton const STEP_DISPATCH_BG_PATH = path.join(REPO_ROOT, 'gsd-core', 'workflows', 'autonomous', 'steps', 'converge-dispatch-bg.md'); const STEP_DISPATCH_INLINE_PATH = path.join(REPO_ROOT, 'gsd-core', 'workflows', 'autonomous', 'steps', 'converge-dispatch-inline.md'); const STEP_LOOP_PATH = path.join(REPO_ROOT, 'gsd-core', 'workflows', 'autonomous', 'steps', 'converge-loop.md'); +const CONVERGENCE_WORKFLOW_PATH = path.join(REPO_ROOT, 'gsd-core', 'workflows', 'plan-review-convergence.md'); function read(filePath) { return fs.readFileSync(filePath, 'utf8'); @@ -56,27 +57,95 @@ describe('autonomous --converge flag (#711)', () => { assert.match(workflow, /converge\|cross-ai/, 'workflow should accept --converge and --cross-ai'); }); - test('workflow fails fast when convergence is requested but disabled', () => { - // #2994: this check lives in the converge-fail-fast step file now + test('explicit --converge overrides the config gate (#4600)', () => { + // #2994: this contract lives in the converge-fail-fast step file now // (state:plan-strategy-converge) — the host only carries the gated // conditional-read stub. + // #4600: an explicit `--converge`/`--cross-ai` (PLAN_STRATEGY=converge is + // set by nothing else) must WIN over `workflow.plan_review_convergence` + // — the config is the default for non-flag invocation, not a veto over an + // explicit operator request. The step must therefore not gate on the + // config at all, and must state the precedence so a runtime agent + // executes it as written. const workflow = read(WORKFLOW_PATH); const step = read(STEP_FAIL_FAST_PATH); assert.match( workflow, /gsd:section id="converge-fail-fast" when="state:plan-strategy-converge"/, - 'workflow should gate the fail-fast check behind state:plan-strategy-converge', + 'workflow should keep the step behind state:plan-strategy-converge', ); - assert.match( + assert.doesNotMatch( step, /config-get workflow\.plan_review_convergence/, - 'converge-fail-fast step should check workflow.plan_review_convergence before planning', + 'the step must not gate an explicit flag on workflow.plan_review_convergence (#4600)', + ); + assert.doesNotMatch(step, /exit 1/, 'the step must not stop an explicit-flag run'); + assert.match( + step, + /OVERRIDES the `workflow\.plan_review_convergence` config gate/, + 'the step must state that the explicit flag overrides the config gate', ); assert.match( step, - /gsd config-set workflow\.plan_review_convergence true/, - 'converge-fail-fast step should print the enable command instead of silently downgrading', + /invocation carries `--override-gate`/, + 'the step must state that the dispatched convergence run carries the override flag (#4600)', + ); + assert.match( + step, + /without the flag, `PLAN_STRATEGY` is `local`/, + 'the step must state that the config is not consulted on the non-flag autonomous path', + ); + }); + + test('the dispatched convergence run bypasses the config gate (#4600)', () => { + // End-to-end contract: the fail-fast step proceeding is not enough — the + // dispatched gsd-plan-review-convergence workflow has its own §1.5 gate + // that would veto the same run one step later. The autonomous dispatch + // must carry an explicit override the gate honors; the veto itself stays + // for standalone invocation, where the config gate is documented behavior. + const workflow = read(WORKFLOW_PATH); + const convergence = read(CONVERGENCE_WORKFLOW_PATH); + const howTo = read(HOW_TO_PATH); + + assert.match( + workflow, + /--override-gate/, + 'the autonomous converge dispatch must carry --override-gate so the dispatched run cannot be vetoed by the config gate (#4600)', + ); + assert.match( + convergence, + /--override-gate/, + 'plan-review-convergence must honor a --override-gate dispatch instead of failing fast (#4600)', + ); + assert.match( + convergence, + /gsd-plan-review-convergence is disabled \(workflow\.plan_review_convergence=false\)/, + 'the §1.5 veto must remain for standalone invocation, where the config gate decides (#4600)', + ); + assert.doesNotMatch( + workflow, + /fail fast unless the existing convergence feature gate/, + 'the host must not instruct a runtime agent to fail fast on the gate before the converge step (#4600)', + ); + assert.match( + howTo, + /overrides the gate for that run/, + 'the how-to must document that an explicit --converge overrides the gate on /gsd-autonomous (#4600)', + ); + // Security-review constraint: the override must be appended conditionally on the converge + // strategy (never ride on local-strategy runs) and parsed token-anchored by §1.5. + // Search from the conditional: the host prose at the precedence sentence also names the + // flag, so a bare indexOf would resolve there and the ordering check could never pass. + const conditionalAt = workflow.indexOf('if [ "${PLAN_STRATEGY}" = "converge" ]; then'); + const overrideAt = workflow.indexOf('--override-gate', conditionalAt); + assert.ok( + conditionalAt !== -1 && overrideAt !== -1, + 'the PLAN_STRATEGY=converge conditional must exist and append --override-gate (#4600)', + ); + assert.ok( + convergence.includes('(^|[[:space:]])--override-gate([[:space:]]|$)'), + '§1.5 must match --override-gate token-anchored so no other argument can carry it (#4600)', ); });