* test(#3827): new-mode routing approval gate before roadmapper (failing first) * fix(#3827): new-mode routing gate — classify approval no longer authorizes scaffold writes The route_new_mode step delegated to gsd-roadmapper (PROJECT.md, REQUIREMENTS.md, ROADMAP.md, STATE.md creation + commit) with no gate: the discovery gate approved classification only, and the zero-conflict branch said 'proceed to routing silently'. Merge mode previews its diff and gates via approve-revise-abort; new mode now shows the exact destinations and requires Create planning setup | Keep synthesized intel only | Abort, per the skill contract's routing-gate requirement. The keep-intel-only choice is the analysis-only path the issue asks for: no roadmapper, no destination writes, intel preserved; finalize labels it 'new (intel only)' and points at /gsd:new-project instead of plan-phase. Ambiguous gate answers re-ask once, then treat as Abort — never infer Create. Zero-conflict wording is mode-aware: silence about conflicts is not authorization to write. Emitted-Drift-Ack-Growth: ingest-docs.md — #3827: +routing gate display block, AskUserQuestion contract, three disposition branches (incl. the intel-only no-write path), ambiguous-answer rule, intel-only finalize line, mode-aware zero-conflict pointer; a real behavioral gate, not incidental bloat. * chore(#3827): changeset fragment (pr number backfilled after PR creation) * chore(#3827): backfill changeset PR number (4037) --------- Co-authored-by: sim <sim@local>
This commit is contained in:
5
.changeset/vivid-pumas-squeak.md
Normal file
5
.changeset/vivid-pumas-squeak.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
type: Fixed
|
||||
pr: 4037
|
||||
---
|
||||
gsd-ingest-docs new mode now requires an explicit routing approval (Create planning setup | Keep synthesized intel only | Abort) before creating the planning scaffold — approving document classification no longer also authorizes scaffold creation and commit (#3827)
|
||||
@@ -256,7 +256,7 @@ On Abort: exit cleanly with "Ingest cancelled. Staged intel preserved at `.plann
|
||||
|
||||
**If BLOCKERS = 0 and WARNINGS = 0:**
|
||||
|
||||
Proceed to routing silently, or optionally display `GSD > No conflicts. Auto-resolved: {N}.`
|
||||
Optionally display `GSD > No conflicts. Auto-resolved: {N}.` Absence of conflicts is not authorization to write: proceed to the routing gate for the active mode — new mode's routing gate (next step) or merge mode's merge-diff approve-revise-abort gate — which decides whether destination files are created.
|
||||
|
||||
</step>
|
||||
|
||||
@@ -266,6 +266,32 @@ Proceed to routing silently, or optionally display `GSD > No conflicts. Auto-res
|
||||
|
||||
Audit PROJECT.md field requirements that `gsd-roadmapper` expects. For fields derivable from `.planning/intel/SYNTHESIS.md` (project scope, goals/non-goals, constraints, locked decisions), synthesize from the intel. For fields NOT derivable (project name, developer-facing success metric, target runtime), prompt via `AskUserQuestion` one at a time — minimal question set, no interrogation.
|
||||
|
||||
**Routing gate (#3827): approval to classify documents is not approval to write the planning scaffold.** Before delegating, display the exact destinations and require an explicit choice:
|
||||
|
||||
```
|
||||
Routing — create the planning setup now?
|
||||
|
||||
.planning/PROJECT.md (new)
|
||||
.planning/REQUIREMENTS.md (new)
|
||||
.planning/ROADMAP.md (new)
|
||||
.planning/STATE.md (new)
|
||||
```
|
||||
|
||||
Use `AskUserQuestion`:
|
||||
- question: "Routing — create the planning setup now?"
|
||||
- header: "Routing"
|
||||
- options: Create planning setup | Keep synthesized intel only | Abort
|
||||
|
||||
**Text mode:** numbered list (1/2/3) with the same three choices.
|
||||
|
||||
On **Create planning setup**: continue to the `gsd-roadmapper` delegation below.
|
||||
|
||||
On **Keep synthesized intel only**: analysis-only ingest. Do NOT invoke `gsd-roadmapper`; write no destination files. The staged intel under `.planning/intel/` is preserved and committed by `finalize` (substitute its actual file set — no PROJECT.md/REQUIREMENTS.md/ROADMAP.md/STATE.md lines). Display the completion banner with mode `new (intel only)`.
|
||||
|
||||
On **Abort**: exit cleanly with "Ingest cancelled. Staged intel preserved at `.planning/intel/`."
|
||||
|
||||
Any other response (freeform/"Other"): re-ask once; if still ambiguous, treat as Abort. Never infer Create from an ambiguous answer.
|
||||
|
||||
Delegate to `gsd-roadmapper` (runs in a subagent — no output until it returns, ~1–5 min; expected, not a freeze):
|
||||
|
||||
```
|
||||
@@ -334,11 +360,11 @@ Display completion:
|
||||
```
|
||||
|
||||
Show:
|
||||
- Mode ran (new or merge)
|
||||
- Mode ran (new, new (intel only), or merge)
|
||||
- Docs ingested (count + type breakdown)
|
||||
- Decisions locked, requirements created, constraints captured
|
||||
- Conflict report path (`.planning/INGEST-CONFLICTS.md`)
|
||||
- Next step: `/gsd:plan-phase 1` (new mode) or `/gsd:plan-phase N` (merge, pointing at the first newly-added phase)
|
||||
- Next step: `/gsd:plan-phase 1` (new) or `/gsd:plan-phase N` (merge, pointing at the first newly-added phase); for intel-only runs there is no roadmap yet — point at `/gsd:new-project` (or a later re-run with `--mode merge` once scaffold files exist), never `/gsd:plan-phase`
|
||||
|
||||
</step>
|
||||
|
||||
|
||||
@@ -188,6 +188,58 @@ describe('ingest-docs workflow content', () => {
|
||||
);
|
||||
});
|
||||
|
||||
test('#3827 gate: new mode requires a routing approval BEFORE the roadmapper delegation', () => {
|
||||
// The gate text and the delegation must both exist, and the gate must
|
||||
// come first — an approval gate placed after the subagent call would
|
||||
// authorize nothing.
|
||||
const gateIdx = content.indexOf('Routing — create the planning setup now?');
|
||||
const delegateIdx = content.indexOf('subagent_type: "gsd-roadmapper"');
|
||||
assert.ok(gateIdx !== -1, 'new mode must display a routing gate question before delegating');
|
||||
assert.ok(delegateIdx !== -1, 'new mode must still delegate to gsd-roadmapper');
|
||||
assert.ok(gateIdx < delegateIdx, 'the routing gate must precede the roadmapper delegation');
|
||||
|
||||
// The gate display must show the user exactly what will be written
|
||||
// (#3827: the classification approval must not also authorize scaffold
|
||||
// creation). Window is the DISPLAY BLOCK only (gate question → the
|
||||
// AskUserQuestion spec), so the assertions can't pass off text from the
|
||||
// option-disposition branches below.
|
||||
const askIdx = content.indexOf('Use `AskUserQuestion`', gateIdx);
|
||||
assert.ok(askIdx !== -1 && askIdx < delegateIdx, 'gate must specify its AskUserQuestion contract');
|
||||
const displayBlock = content.slice(gateIdx, askIdx);
|
||||
for (const dest of ['.planning/PROJECT.md', '.planning/REQUIREMENTS.md', '.planning/ROADMAP.md', '.planning/STATE.md']) {
|
||||
assert.ok(
|
||||
displayBlock.includes(dest),
|
||||
`routing gate display block must name destination file ${dest}`
|
||||
);
|
||||
}
|
||||
|
||||
// Three-way choice, including the analysis-only exit the issue asks for.
|
||||
const gateWindow = content.slice(gateIdx, delegateIdx);
|
||||
assert.ok(gateWindow.includes('Create planning setup'), 'gate must offer to create the scaffold');
|
||||
assert.ok(gateWindow.includes('Keep synthesized intel only'), 'gate must offer an analysis-only path');
|
||||
assert.ok(gateWindow.includes('Abort'), 'gate must offer abort');
|
||||
// "Keep intel only" must skip the roadmapper (no scaffold writes).
|
||||
const keepIdx = content.indexOf('Keep synthesized intel only');
|
||||
assert.ok(
|
||||
keepIdx !== -1 && content.slice(keepIdx, delegateIdx).includes('Do NOT invoke'),
|
||||
'the keep-intel-only branch must explicitly skip the roadmapper delegation'
|
||||
);
|
||||
});
|
||||
|
||||
test('#3827 gate: zero-conflict branch routes to the gate, never silently into writes', () => {
|
||||
const zeroIdx = content.indexOf('If BLOCKERS = 0 and WARNINGS = 0');
|
||||
assert.ok(zeroIdx !== -1, 'workflow must keep the zero-conflict branch');
|
||||
const zeroWindow = content.slice(zeroIdx, zeroIdx + 400);
|
||||
assert.ok(
|
||||
!zeroWindow.toLowerCase().includes('silently'),
|
||||
'the zero-conflict branch must not authorize silent routing; it hands control to the routing gate'
|
||||
);
|
||||
assert.ok(
|
||||
zeroWindow.includes('proceed to the routing gate'),
|
||||
'zero-conflict branch must hand control to the routing gate'
|
||||
);
|
||||
});
|
||||
|
||||
test('rejects --resolve interactive in v1', () => {
|
||||
const lower = content.toLowerCase();
|
||||
assert.ok(
|
||||
|
||||
Reference in New Issue
Block a user