feat: add gates ensuring discuss-phase decisions are translated to plans and verified (closes #2492) (#2611)
* feat(#2492): add gates ensuring discuss-phase decisions are translated and verified Two gates close the loop between CONTEXT.md `<decisions>` and downstream work, fixing #2492: - Plan-phase **translation gate** (BLOCKING). After requirements coverage, refuses to mark a phase planned when a trackable decision is not cited (by id `D-NN` or by 6+-word phrase) in any plan's `must_haves`, `truths`, or body. Failure message names each missed decision with id, category, text, and remediation paths. - Verify-phase **validation gate** (NON-BLOCKING). Searches plans, SUMMARY.md, files modified, and recent commit subjects for each trackable decision. Misses are written to VERIFICATION.md as a warning section but do not change verification status. Asymmetry is deliberate — fuzzy-match miss should not fail an otherwise green phase. Shared helper `parseDecisions()` lives in `sdk/src/query/decisions.ts` so #2493 can consume the same parser. Decisions opt out of both gates via `### Claude's Discretion` heading or `[informational]` / `[folded]` / `[deferred]` tags. Both gates skip silently when `workflow.context_coverage_gate=false` (default `true`). Closes #2492 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(#2492): make plan-phase decision gate actually block (review F1, F8, F9, F10, F15) - F1: replace `${context_path}` with `${CONTEXT_PATH}` in the plan-phase gate snippet so the BLOCKING gate receives a non-empty path. The variable was defined in Step 4 (`CONTEXT_PATH=$(_gsd_field "$INIT" ...)`) and the gate snippet referenced the lowercase form, leaving the gate to run with an empty path argument and silently skip. - F15: wrap the SDK call with `jq -e '.data.passed == true' || exit 1` so failure halts the workflow instead of being printed and ignored. The verify-phase counterpart deliberately keeps no exit-1 (non-blocking by design) and now carries an inline note documenting the asymmetry. - F10: tag the JSON example fence as `json` and the options-list fence as `text` (MD040). - F8/F9: anchor the heading-presence test regexes to `^## 13[a-z]?\\.` so prose substrings like "Requirements Coverage Gate" mentioned in body text cannot satisfy the assertion. Added two new regression tests (variable-name match, exit-1 guard) so a future revert is caught. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(#2492): tighten decision-coverage gates against false positives and config drift (review F3,F4,F5,F6,F7,F16,F18,F19) - F3: forward `workstream` arg through both gate handlers so workstream-scoped `workflow.context_coverage_gate=false` actually skips. Added negative test that creates a workstream config disabling the gate while the root config has it enabled and asserts the workstream call is skipped. - F4: restrict the plan-phase haystack to designated sections — front-matter `must_haves` / `truths` / `objective` plus body sections under headings matching `must_haves|truths|tasks|objective`. HTML comments and fenced code blocks are stripped before extraction so a commented-out citation or a literal example never counts as coverage. Verify-phase keeps the broader artifact-wide haystack by design (non-blocking). - F5: reject decisions with fewer than 6 normalized words from soft-matching (previously only rejected when the resulting phrase was under 12 chars AFTER slicing — too lenient). Short decisions now require an explicit `D-NN` citation, with regression tests for the boundary. - F6: walk every `*-SUMMARY.md` independently and use `matchAll` with the `/g` flag so multiple `files_modified:` blocks across multiple summaries are all aggregated. Previously only the first block in the concatenated string was parsed, silently dropping later plans' files. - F7: validate every `files_modified` path stays inside `projectDir` after resolution (rejects absolute paths, `../` traversal). Cap each file read at 256 KB. Skipped paths emit a stderr warning naming the entry. - F16: validate `workflow.context_coverage_gate` is boolean in `loadGateConfig`; warn loudly on numeric or other-shaped values and default to ON. Mirrors the schema-vs-loadConfig validation gap from #2609. - F18: bump verify-phase `git log -n` cap from 50 to 200 so longer-running phases are not undercounted. Documented as a precision-vs-recall tradeoff appropriate for a non-blocking gate. - F19: tighten `QueryResult` / `QueryHandler` to be parameterized (`<T = unknown>`). Drops the `as unknown as Record<string, unknown>` casts in the gate handlers and surfaces shape mismatches at compile time for callers that pass a typed `data` value. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(#2492): harden decisions parser and verify-phase glob (review F11,F12,F13,F14,F17,F20) - F11: strip fenced code blocks from CONTEXT.md before searching for `<decisions>` so an example block inside ``` ``` is not mis-parsed. - F12: accept tab-indented continuation lines (previously required a leading space) so decisions split with `\t` continue cleanly. - F13: parse EVERY `<decisions>` block in the file via `matchAll`, not just the first. CONTEXT.md may legitimately carry more than one block. - F14: `decisions.parse` handler now resolves a relative path against `projectDir` — symmetric with the gate handlers — and still accepts absolute paths. - F17: replace `ls "${PHASE_DIR}"/*-CONTEXT.md | head -1` in verify-phase.md with a glob loop (ShellCheck SC2012 fix). Also avoids spawning an extra subprocess and survives filenames with whitespace. - F20: extend the unicode quote-stripping in the discretion-heading match to cover U+2018/2019/201A/201B and the U+201C-F double-quote variants plus backtick, so any rendering of "Claude's Discretion" collapses to the same key. Each fix has a regression test in `decisions.test.ts`. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1310,6 +1310,72 @@ Options:
|
||||
|
||||
If `TEXT_MODE` is true, present as a plain-text numbered list (options already shown in the block above). Otherwise use AskUserQuestion to present the options.
|
||||
|
||||
## 13a. Decision Coverage Gate
|
||||
|
||||
After the requirements coverage gate passes, verify that every trackable
|
||||
decision captured by discuss-phase in CONTEXT.md `<decisions>` is referenced
|
||||
by at least one plan. This is the **translation gate** from issue #2492 —
|
||||
its job is to refuse to mark a phase planned when a discuss-phase decision
|
||||
silently dropped on the way into the plans.
|
||||
|
||||
**Skip if** `workflow.context_coverage_gate` is explicitly set to `false`
|
||||
(absent key = enabled). Also skip if no CONTEXT.md exists for this phase
|
||||
(nothing to translate) or if its `<decisions>` block is empty.
|
||||
|
||||
```bash
|
||||
GATE_CFG=$(gsd-sdk query config-get workflow.context_coverage_gate 2>/dev/null || echo "true")
|
||||
if [ "$GATE_CFG" != "false" ]; then
|
||||
GATE_RESULT=$(gsd-sdk query check.decision-coverage-plan "${PHASE_DIR}" "${CONTEXT_PATH}")
|
||||
# BLOCKING: refuse to mark phase planned when a trackable decision is uncovered.
|
||||
# `passed: true` covers both real-pass and skipped cases (gate disabled / no CONTEXT.md /
|
||||
# no trackable decisions). Verify-phase counterpart deliberately omits this exit-1 — that
|
||||
# gate is non-blocking by design (review finding F15).
|
||||
echo "$GATE_RESULT" | jq -e '.data.passed == true' >/dev/null || {
|
||||
echo "$GATE_RESULT" | jq -r '.data.message'
|
||||
exit 1
|
||||
}
|
||||
fi
|
||||
```
|
||||
|
||||
The handler returns JSON:
|
||||
```json
|
||||
{
|
||||
"passed": true,
|
||||
"skipped": false,
|
||||
"total": 2,
|
||||
"covered": 2,
|
||||
"uncovered": [ { "id": "D-01", "text": "...", "category": "..." } ],
|
||||
"message": "..."
|
||||
}
|
||||
```
|
||||
|
||||
**If `passed` is true (or `skipped` is true):** Display
|
||||
`✓ Decision coverage: {M}/{N} CONTEXT.md decisions covered by plans` (or
|
||||
`(skipped — gate disabled)` / `(skipped — no decisions)`) and proceed to
|
||||
step 13b.
|
||||
|
||||
**If `passed` is false:** Display the handler's `message` block. It already
|
||||
names each uncovered decision (`D-NN | category | text`) and tells the user
|
||||
what to do — cite the id in a relevant plan's `must_haves` / `truths`, or
|
||||
move the decision under `### Claude's Discretion` / tag it `[informational]`
|
||||
if it should not be tracked. Then offer:
|
||||
|
||||
```text
|
||||
Options:
|
||||
1. Re-plan to cover missing decisions (recommended)
|
||||
2. Edit CONTEXT.md to mark dropped decisions as [informational] / Discretion
|
||||
3. Proceed anyway — accept the coverage gap
|
||||
```
|
||||
|
||||
If `TEXT_MODE` is true, present as a plain-text numbered list. Otherwise use
|
||||
AskUserQuestion. Selecting "Proceed anyway" continues to step 13b but
|
||||
records the override in STATE.md so verify-phase can re-surface it.
|
||||
|
||||
**Why this gate blocks:** failing here is cheap. The plans are the contract
|
||||
between discuss-phase and execute-phase; if a decision isn't visible in any
|
||||
plan, no executor will implement it. Catching that now beats discovering it
|
||||
after thousands of dollars of execution.
|
||||
|
||||
## 13b. Record Planning Completion in STATE.md
|
||||
|
||||
After plans pass all gates, record that planning is complete so STATE.md reflects the new phase status:
|
||||
|
||||
@@ -183,6 +183,57 @@ grep -E "Phase ${PHASE_NUM}" .planning/REQUIREMENTS.md 2>/dev/null || true
|
||||
For each requirement: parse description → identify supporting truths/artifacts → status: ✓ SATISFIED / ✗ BLOCKED / ? NEEDS HUMAN.
|
||||
</step>
|
||||
|
||||
<step name="verify_decisions">
|
||||
**Decision coverage validation gate (issue #2492).**
|
||||
|
||||
After requirements coverage, also check that each trackable CONTEXT.md
|
||||
`<decisions>` entry shows up somewhere in the shipped artifacts (plans,
|
||||
SUMMARY.md, files modified by the phase, or recent commit subjects on the
|
||||
phase branch).
|
||||
|
||||
This gate is **non-blocking / warning only** by deliberate asymmetry with
|
||||
the plan-phase translation gate. The plan-phase gate already blocked at
|
||||
translation time, so by the time verification runs every decision has
|
||||
either been translated or explicitly deferred. This gate's job is to
|
||||
surface decisions that *were* translated but vanished during execution —
|
||||
that's a soft signal because "honors a decision" is a fuzzy substring
|
||||
heuristic, and we don't want a paraphrase miss to fail an otherwise good
|
||||
phase.
|
||||
|
||||
**Skip if** `workflow.context_coverage_gate` is explicitly set to `false`
|
||||
(absent key = enabled). Also skip cleanly when CONTEXT.md is missing or has
|
||||
no `<decisions>` block.
|
||||
|
||||
```bash
|
||||
GATE_CFG=$(gsd-sdk query config-get workflow.context_coverage_gate 2>/dev/null || echo "true")
|
||||
if [ "$GATE_CFG" != "false" ]; then
|
||||
# Discover the phase CONTEXT.md via glob expansion rather than `ls | head`
|
||||
# (review F17 / ShellCheck SC2012). Globs preserve filenames containing
|
||||
# spaces and avoid an extra subprocess.
|
||||
CONTEXT_PATH=""
|
||||
for f in "${PHASE_DIR}"/*-CONTEXT.md; do
|
||||
[ -e "$f" ] && CONTEXT_PATH="$f" && break
|
||||
done
|
||||
DECISION_RESULT=$(gsd-sdk query check.decision-coverage-verify "${PHASE_DIR}" "${CONTEXT_PATH}")
|
||||
fi
|
||||
```
|
||||
|
||||
The handler returns JSON `{ skipped, blocking: false, total, honored,
|
||||
not_honored: [...], message }`.
|
||||
|
||||
**Reporting:** Append the handler's `message` (a `### Decision Coverage`
|
||||
section) to VERIFICATION.md regardless of outcome — even when all
|
||||
decisions are honored, recording the count helps reviewers spot drift over
|
||||
time. Set `decision_coverage` in the verification result to
|
||||
`{honored, total, not_honored: [...]}` so downstream tooling can read it.
|
||||
|
||||
**Status impact:** none. The decision gate does NOT influence the
|
||||
`gaps_found` / `human_needed` / `passed` decision tree in
|
||||
`determine_status`. Its findings are warnings the user reviews and may act
|
||||
on by re-opening the phase or by acknowledging the decision was abandoned
|
||||
intentionally.
|
||||
</step>
|
||||
|
||||
<step name="behavioral_verification">
|
||||
**Run the project's test suite and CLI commands to verify behavior, not just structure.**
|
||||
|
||||
@@ -479,6 +530,7 @@ Orchestrator routes: `passed` → update_roadmap | `gaps_found` → create/execu
|
||||
- [ ] All artifacts checked at all three levels
|
||||
- [ ] All key links verified
|
||||
- [ ] Requirements coverage assessed (if applicable)
|
||||
- [ ] CONTEXT.md decisions checked against shipped artifacts (#2492 — non-blocking)
|
||||
- [ ] Anti-patterns scanned and categorized
|
||||
- [ ] Test quality audited (disabled tests, circular patterns, assertion strength, provenance)
|
||||
- [ ] Human verification items identified
|
||||
|
||||
Reference in New Issue
Block a user