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

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