fix(#4600): explicit --converge overrides the convergence feature gate (#4771)

* 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 <sim@local>
This commit is contained in:
Tom Boucher
2026-09-15 15:53:42 -04:00
committed by GitHub
parent 779f67cb11
commit 0d6bf19bf1
12 changed files with 127 additions and 37 deletions

View File

@@ -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)

View File

@@ -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`.

View File

@@ -11,7 +11,7 @@ allowed-tools:
- Agent
- Skill
- AskUserQuestion
requires: [phase, review]
requires: [phase, review, autonomous]
---
<objective>
@@ -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.
</context>
<process>

View File

@@ -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 |

View File

@@ -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 `<decisions>` 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 |

View File

@@ -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.
---

View File

@@ -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
```
<!-- gsd:section id="converge-fail-fast" when="state:plan-strategy-converge" -->
@@ -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
</success_criteria>

View File

@@ -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.

View File

@@ -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).
</process>
<success_criteria>
- [ ] 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

View File

@@ -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`.

View File

@@ -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.
</context>
<process>

View File

@@ -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)',
);
});