* fix(#3724): stop advisory Dimension 3b findings from forcing the revision loop Dimension 3b (undeclared/temporal coupling, #1954) is spec'd "never a blocker" but tagged severity: warning — the tier plan-phase's revision loop counts as must-fix — and the planner is never taught the rule, so every multi-wave phase touching shared mutable state replans at least once, and intentionally coupled plans re-flag identically every iteration to the stall prompt. Three coordinated changes: - gsd-plan-checker: retag 3b to severity: info, the tier references/revision-loop.md already exempts by design; recognize a coupling_justified frontmatter declaration in the Do-NOT-flag list so deliberate pairs converge. Additions are offset by trimming 3b motivation prose — the checker sits 45 bytes under its LARGE hard cap. - plan-phase step 12: INFO-only accept — an issues block with zero BLOCKER/WARNING entries accepts the plan and surfaces the advisories instead of re-entering the revision loop. Real blockers and warnings still gate unconditionally. - gsd-planner: slim pointer in assign_waves to the new progressive-disclosure reference gsd-core/references/planner-coupling.md (the planner sits 19 chars under its own cap), which carries the shared-mutable-state rule and the coupling_justified escape hatch so first-pass plans avoid the finding when the coupling is unintentional. Documented the coupling_justified field in docs/reference/plan-md.md. Growth acks per #2914; inventory manifest and install-tree fixtures regenerated for the new reference file. Closes #3724 Claude-Session: https://claude.ai/code/session_01GshUzpGjoxiw6uNRiFMHvM * test(#3724): pin Dimension 3b at severity: info The severity retag makes the old assertion (severity: warning) stale; lock the advisory tier from both directions — info must be present, warning must not — so a future edit cannot silently re-arm the revision-loop trigger. Refs #3724 Claude-Session: https://claude.ai/code/session_01GshUzpGjoxiw6uNRiFMHvM * chore(#3724): changeset fragment for PR #3758 Refs #3724 Claude-Session: https://claude.ai/code/session_01GshUzpGjoxiw6uNRiFMHvM * docs(#3724): roster planner-coupling.md in docs/INVENTORY.md The new reference was enumerated in the manifest and all 19 install-tree fixtures but missing its row in the Modular Planner Decomposition table — the roster half the manifest-sync test cannot check. (Review Blocker.) Refs #3724 Claude-Session: https://claude.ai/code/session_01GshUzpGjoxiw6uNRiFMHvM * test(#3724): cover all four acceptance criteria (review round 1) - plan-checker-coupling: the 3b severity assertion is now a PARITY check deriving the exempt tier from revision-loop.md's flow instead of hardcoding info — editing either side alone reds the suite. New describe pins the other three criteria: plan-phase's INFO-only accept clause (proven failing-first), the BLOCKER + WARNING count staying intact, the coupling_justified Do-NOT-flag exemption + fix_hint, and the planner pointer + planner-coupling.md content. - ack fragment: $comment's plan-phase figure corrected to +79B; the 2775 pin note carried forward into the gsd-planner.md entry, updated for upstream's #3761/#3764 Rule-paragraph anchor (which this diff leaves verbatim). The parallel-dependent-plans re-anchor this commit originally carried was superseded by upstream #3764 during review; this branch no longer touches that file. Refs #3724 Claude-Session: https://claude.ai/code/session_01GshUzpGjoxiw6uNRiFMHvM * fix(#3724): review round 2 — align the stance enumeration, complete the template contract MAJOR: <adversarial_stance>'s severity enumeration gains the INFO bullet so it agrees with Dimension 3b's 'ALWAYS INFO' mandate instead of contradicting it. Funded by extracting the inline <examples> block to the new progressive- disclosure reference gsd-core/references/plan-checker-examples.md (@-inlined from the same spot; #1949 precedent), which also restores the 3b motivation clause round 1 traded away (Nit 4) and nets the agent file SMALLER than base (49107 -> 48486) — the extraction the byte pressure was owed. MINOR: gsd-core/templates/phase-prompt.md now carries coupling_justified, and the field's shape becomes one 'plan-id: reason' string per coupled peer so a plan justified against two peers can express it; docs/reference/plan-md.md's Type column names the shape. NIT: the 3409 ack's plan-phase entry no longer calls the #1168 workflow ratchet an 'XL tier'. Acks and derived artifacts updated accordingly (checker entry removed — a shrink needs no ack; INVENTORY roster row + regen:derived for the new file). Claude-Session: https://claude.ai/code/session_01GshUzpGjoxiw6uNRiFMHvM * test(#3724): derive the 3b negative severity assertion (review round 2) Every severity token in the 3b span must BE the tier revision-loop.md exempts, replacing the hardcoded severity:warning negative — if the loop's exemption ever moves, the failure names the real conflict instead of blaming the agent file with a mutually-unsatisfiable pair. Claude-Session: https://claude.ai/code/session_01GshUzpGjoxiw6uNRiFMHvM * fix(#3724): refit the planner coupling pointer under the char cap Upstream #3299 (PR #3390) grew agents/gsd-planner.md to 49146 chars at the base, leaving 5 chars of headroom where the +16-char pointer was measured against 13 more. The pointer prose shortens to 'Non-file coupling:' — 49150 chars, back under the strict 49152-char cap — and the ack figures follow. The @-path the tests pin is unchanged. Claude-Session: https://claude.ai/code/session_01GshUzpGjoxiw6uNRiFMHvM * fix(#3724): re-home the plan-phase ack after the #3823 spent-fragment sweep Upstream #3078/#3823 deleted all fully-spent ack fragments, including 3409-unreachable-guard-arms.json, which carried this PR's plan-phase.md +79B append. Per the collision remedy that sweep added: take the deletion and home the still-live entry in this PR's own fragment. Figures re-measured at this merge base (90871 -> 90950 LF bytes). Claude-Session: https://claude.ai/code/session_01GshUzpGjoxiw6uNRiFMHvM * fix(#3724): absorb the spent #3172 plan-phase fragment into this PR's ack Upstream #3825 shipped 3172-stated-failing-direction.json naming only plan-phase.md, now spent at the base — colliding with this PR's live plan-phase entry. Per the #3003 pattern the fully-spent single-path fragment is deleted and this fragment stays the path's one source; figures re-measured at this base (93073 -> 93152 LF bytes). Claude-Session: https://claude.ai/code/session_01GshUzpGjoxiw6uNRiFMHvM * fix(#3724): review round 3 — true up the ack figures, restore the wave comment The fragment's absolute sizes are re-measured and anchored to basee40e9670(planner 47259 -> 47330 chars, checker 45537 -> 44916 B, plan-phase 91186 -> 91265 LF bytes), with a note that absolutes rot as next moves — the deltas are the durable claims. The round-1 removal of the '# Implicit dependency: files_modified overlap forces a later wave.' pseudocode comment offset headroom base drift had already returned, so it is restored (findings 2-3). Changeset gains the (#3724) backlink (finding 4). Claude-Session: https://claude.ai/code/session_01GshUzpGjoxiw6uNRiFMHvM * fix(#3724): review round 4 — close the verify-work surface, harden the boundaries BLOCKER: verify-work.md's verify_gap_plans is the second multi-plan consumer of the checker's sentinels, and its ISSUES FOUND handler entered revision_loop with zero severity parsing — the guaranteed replan #3724 fixed in plan-phase, alive on the gap-closure surface. The handler now counts BLOCKER + WARNING and accepts INFO-only returns with advisories displayed. The checker's INFO stance bullet is reworded to the claim that is true everywhere ('revision gates count only BLOCKER + WARNING'). Minor 1: plan-phase's iteration_count >= 3 arm recounts severities, so an INFO-only third check accepts instead of halting on a '0 issues remain' user gate. Minor 2: the coupling_justified exemption now requires the entry to NAME the other plan, closing the blanket-suppression reading. Nit 1: INVENTORY row states the extraction buys cap headroom, not context. Nit 2: the advisory display gains a concrete format on both surfaces. Ack fragment re-anchored at baseddde001a: verify-work.md +264B (new entry), plan-phase.md +395B, checker still net negative (-512B). Claude-Session: https://claude.ai/code/session_01GshUzpGjoxiw6uNRiFMHvM * test(#3724): pin the verify-work accept and the iteration-cap boundary (review round 4) Two wiring assertions: verify_gap_plans' ISSUES FOUND handler gates on BLOCKER + WARNING and accepts INFO-only blocks, and plan-phase's iteration_count >= 3 arm recounts severities instead of gating advisories — the limit+1 boundary of the gate this PR fixes. Claude-Session: https://claude.ai/code/session_01GshUzpGjoxiw6uNRiFMHvM * fix(#3724): review round 5 — fail closed at the gates, surface the advisory Blocker 1: the checker's step-10 status rule routes an INFO-only result to ## ISSUES FOUND (with a new ### Advisories (info) template section and a severity-aware recommendation) so the orchestrator receives the block and displays the advisory instead of silently accepting a bare PASSED. Blockers 2+3: all three gate surfaces (plan-phase step 12 both arms, verify-work verify_gap_plans) carry one canonical clause verbatim — an entry whose severity is missing or unrecognized counts as a BLOCKER (fail closed) — making the accept condition an explicit-INFO whitelist while keeping issue_count coherent for stall math. Major 1: the INFO stance bullet scopes its claim to the plan-phase and verify-work gates (quick mode's loop still revises on any ISSUES FOUND). Major 2: INVENTORY row and ack $comment state the extraction's real trade (readability, +0.6 KB eager runtime context), not a cap remedy. Minor 1: plan-md.md marks coupling_justified as prompt convention, unvalidated. Nit 1: ack absolutes re-anchored at base 1e67ec97; checker now +120B and acked. Claude-Session: https://claude.ai/code/session_01GshUzpGjoxiw6uNRiFMHvM * test(#3724): pin the round-5 contract — fail-closed parity, INFO-only return shape New: three-surface verbatim parity test for the fail-closed clause (Blockers 2+3); checker return-contract test for the INFO-only ## ISSUES FOUND route and advisories section (Blocker 1). All seven newly pinned tokens are absent at f3a5682d, so each new assertion fails pre-fix. Updated: accept-clause regexes track the explicit-INFO whitelist wording; the severity sweep scopes to the span's fenced yaml examples via yamlSeverityTiers (round-5 Minor 3, applied to the blocker negative too); the iteration-cap comment states it is a prose pin, not an executed boundary check (Minor 4); splitLines call sites document the line-pin coupling (Nit 2). Claude-Session: https://claude.ai/code/session_01GshUzpGjoxiw6uNRiFMHvM * fix(#3724): adopt next's line wrap in the 3b motivation clause — drops a wrap-only hunk from the diff Byte-identical content; the wrap difference was an artifact of the round-1 base adaptation predating upstream's #3003 landing. Claude-Session: https://claude.ai/code/session_01GshUzpGjoxiw6uNRiFMHvM * fix(#3724): review round 7 — gate every checker consumer, not just the two audited ones Blocker: quick/steps/plan-checker-loop.md (issue-named in #3724) gets the same canonical fail-closed clause and explicit-INFO whitelist accept as plan-phase/verify-work — an INFO-only result proceeds instead of entering quick mode's revision loop. Major: import.md plan_validate handles the checker return by severity (INFO-only never blocks an import) and is added to agent-contracts.md's consumer enumeration, which had omitted it. The checker's INFO stance bullet drops the quick-mode carve-out — the claim is universally true again now that every consuming gate is severity-aware. Minor: an applied coupling_justified exemption is surfaced as its own info advisory so a stale one-sided declaration stays observable. Nit: plan-phase's revision-iteration Display line is explicitly conditioned on not having already proceeded to step 13. Claude-Session: https://claude.ai/code/session_01GshUzpGjoxiw6uNRiFMHvM Emitted-Drift-Ack-Growth: import.md — #3724 round 7: the plan_validate step's checker-return handler becomes severity-aware — counts BLOCKER + WARNING failing closed and accepts an explicitly-INFO-only return with advisories displayed instead of blocking the import * test(#3724): pin the round-7 surfaces — five-gate parity, quick/import accepts, exemption visibility The verbatim fail-closed parity test extends to quick/steps/plan-checker-loop.md and import.md plan_validate; new assertions pin quick mode's INFO-only proceed, import's never-blocks accept, import.md's presence in agent-contracts.md's consumer row, and the surfaced coupling_justified exemption advisory. All four newly pinned token families are absent at the pre-fix head, so each new assertion fails first. Claude-Session: https://claude.ai/code/session_01GshUzpGjoxiw6uNRiFMHvM --------- Co-authored-by: Tom Boucher <trekkie@nomorestars.com>
This commit is contained in:
5
.changeset/gallant-newts-squeak.md
Normal file
5
.changeset/gallant-newts-squeak.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
type: Fixed
|
||||
pr: 3758
|
||||
---
|
||||
**Advisory plan-checker findings no longer force a replan** — Dimension 3b (undeclared same-wave coupling, #1954) is retagged to the advisory `info` tier, plan-phase accepts INFO-only checker results instead of entering the revision loop, and planners can declare deliberate coupling with a new optional `coupling_justified` plan-frontmatter field that the checker recognizes — so multi-wave phases stop paying a guaranteed extra planner pass and intentionally coupled plans converge instead of stalling. (#3724)
|
||||
@@ -39,6 +39,7 @@ You are NOT the executor or verifier — you verify plans WILL work before execu
|
||||
**Required finding classification:** Every issue must carry an explicit severity:
|
||||
- **BLOCKER** — the phase goal will not be achieved if this is not fixed before execution
|
||||
- **WARNING** — quality or maintainability is degraded; fix recommended but execution can proceed
|
||||
- **INFO** — advisory; every consuming gate counts only BLOCKER + WARNING, so INFO alone never forces a revision or blocks acceptance (#3724)
|
||||
Issues without a severity classification are not valid output.
|
||||
</adversarial_stance>
|
||||
|
||||
@@ -231,19 +232,24 @@ Execution; strong-but-local coupling inside one plan is fine):
|
||||
**Do NOT flag:** both sides only READ it, or it is immutable; the pair already overlaps in
|
||||
`files_modified` or `files_deleted` (report that once, on the file axis); the plans sit in a different wave, which
|
||||
already orders them; two tasks inside one plan; a vague same-subsystem claim naming no
|
||||
resource; incompatible *transformations* of one entity — that is Dimension 9.
|
||||
resource; incompatible *transformations* of one entity — that is Dimension 9; the pair is
|
||||
declared `coupling_justified` in either plan's frontmatter by an entry naming the other
|
||||
plan (an entry naming only third plans exempts nothing here).
|
||||
|
||||
**Severity: ALWAYS WARNING, never a blocker.** Coupling is sometimes intentional; the finding
|
||||
lets the planner declare the edge, move a plan to a later wave, or justify the pair.
|
||||
**Severity: ALWAYS INFO, never a blocker.** Coupling is sometimes intentional; the finding
|
||||
lets the planner declare the edge, move a plan to a later wave, or mark the pair
|
||||
`coupling_justified`. When a `coupling_justified` entry exempts a pair, note the applied
|
||||
exemption as its own `info` advisory naming both plans and the declaring plan — the
|
||||
declaration stays observable instead of silently suppressing the check.
|
||||
|
||||
```yaml
|
||||
issue:
|
||||
dimension: dependency_correctness
|
||||
severity: warning
|
||||
severity: info
|
||||
description: "Plans 02 and 03 are both Wave 1 with no depends_on, but 02 writes config key
|
||||
auth.session_ttl and 03 reads it"
|
||||
plans: ["02", "03"]
|
||||
fix_hint: "Declare depends_on, move 03 to a later wave, or justify either order"
|
||||
fix_hint: "Declare depends_on, move 03 to a later wave, or set coupling_justified"
|
||||
```
|
||||
|
||||
## Dimension 4: Key Links Planned
|
||||
@@ -861,9 +867,9 @@ Thresholds: 2-3 tasks/plan good, 4 warning, 5+ blocker (split required).
|
||||
|
||||
## Step 10: Determine Overall Status
|
||||
|
||||
**passed:** All requirements covered, all tasks complete, dependency graph valid, key links planned, scope within budget, must_haves properly derived.
|
||||
**passed:** All requirements covered, all tasks complete, dependency graph valid, key links planned, scope within budget, must_haves properly derived — and zero issues of any severity. An INFO-only result is NOT `passed`.
|
||||
|
||||
**issues_found:** One or more blockers or warnings. Plans need revision.
|
||||
**issues_found:** One or more issues of ANY severity, including INFO-only. Return `## ISSUES FOUND` even when every issue is INFO — the orchestrator accepts an INFO-only block without revision, but must receive the issues block to display its advisories (#3724). Plans need revision only when blockers or warnings are present.
|
||||
|
||||
Severities: `blocker` (must fix), `warning` (should fix), `info` (suggestions).
|
||||
|
||||
@@ -871,40 +877,7 @@ Severities: `blocker` (must fix), `warning` (should fix), `info` (suggestions).
|
||||
|
||||
<examples>
|
||||
|
||||
## Scope Exceeded (most common miss)
|
||||
|
||||
**Plan 01 analysis:**
|
||||
```
|
||||
Tasks: 5
|
||||
Files modified: 12
|
||||
- prisma/schema.prisma
|
||||
- src/app/api/auth/login/route.ts
|
||||
- src/app/api/auth/logout/route.ts
|
||||
- src/app/api/auth/refresh/route.ts
|
||||
- src/middleware.ts
|
||||
- src/lib/auth.ts
|
||||
- src/lib/jwt.ts
|
||||
- src/components/LoginForm.tsx
|
||||
- src/components/LogoutButton.tsx
|
||||
- src/app/login/page.tsx
|
||||
- src/app/dashboard/page.tsx
|
||||
- src/types/auth.ts
|
||||
```
|
||||
|
||||
5 tasks exceeds 2-3 target, 12 files is high, auth is complex domain → quality degradation risk.
|
||||
|
||||
```yaml
|
||||
issue:
|
||||
dimension: scope_sanity
|
||||
severity: blocker
|
||||
description: "Plan 01 has 5 tasks with 12 files - exceeds context budget"
|
||||
plan: "01"
|
||||
metrics:
|
||||
tasks: 5
|
||||
files: 12
|
||||
estimated_context: "~80%"
|
||||
fix_hint: "Split into: 01 (schema + API), 02 (middleware + lib), 03 (UI components)"
|
||||
```
|
||||
@~/.claude/gsd-core/references/plan-checker-examples.md
|
||||
|
||||
</examples>
|
||||
|
||||
@@ -993,13 +966,20 @@ Plans verified. Run `/gsd:execute-phase {phase}` to proceed.
|
||||
- Plan: {plan}
|
||||
- Fix: {fix_hint}
|
||||
|
||||
### Advisories (info)
|
||||
|
||||
**1. [{dimension}] {description}**
|
||||
- Plan: {plan}
|
||||
- Fix: {fix_hint}
|
||||
|
||||
### Structured Issues
|
||||
|
||||
(YAML issues list using format from Issue Format above)
|
||||
|
||||
### Recommendation
|
||||
|
||||
{N} blocker(s) require revision. Returning to planner with feedback.
|
||||
{N} blocker(s), {M} warning(s) require revision. Returning to planner with feedback.
|
||||
(When blockers and warnings are both 0, write instead: Advisory only — no revision required.)
|
||||
```
|
||||
|
||||
</structured_returns>
|
||||
|
||||
@@ -754,6 +754,8 @@ for each plan B in plan_order:
|
||||
```
|
||||
|
||||
**Rule:** Same-wave plans must have zero `files_modified`/`files_deleted` overlap. After assigning waves, scan each wave; if any file appears in 2+ plans, bump the later plan to the next wave and repeat.
|
||||
|
||||
Non-file coupling: @~/.claude/gsd-core/references/planner-coupling.md
|
||||
</step>
|
||||
|
||||
<step name="group_into_plans">
|
||||
|
||||
@@ -251,8 +251,10 @@
|
||||
"nyquist-compliance.md",
|
||||
"offer-next.md",
|
||||
"phase-argument-parsing.md",
|
||||
"plan-checker-examples.md",
|
||||
"planner-antipatterns.md",
|
||||
"planner-chunked.md",
|
||||
"planner-coupling.md",
|
||||
"planner-failing-direction.md",
|
||||
"planner-gap-closure.md",
|
||||
"planner-graphify-auto-update.md",
|
||||
|
||||
@@ -379,6 +379,7 @@ Full roster at `gsd-core/references/*.md`. References are shared knowledge docum
|
||||
| `api-coverage.md` | API-coverage gate reference (full-coverage-by-default) for the `ai-integration` capability's `verify:pre` blocking gate (#1562) — matrix format, trigger, tuning, detector CLI, and the seal-time outcome table naming every pass/block arm including `scope_unavailable` (#3909). |
|
||||
| `ai-frameworks.md` | AI framework decision-matrix reference for `gsd-framework-selector`. |
|
||||
| `executor-examples.md` | Worked examples for the gsd-executor agent. |
|
||||
| `plan-checker-examples.md` | Worked example for the gsd-plan-checker agent (Scope Exceeded), moved out of the inline `<examples>` block to keep worked examples structurally separate from the agent contract, matching the other agents' reference layout. `@`-inlined at load (eager), so this costs ~0.6 KB of runtime context versus keeping it inline — a readability trade, not a size-cap remedy (#3724). |
|
||||
| `doc-conflict-engine.md` | Shared conflict-detection contract for ingest/import workflows. |
|
||||
| `execute-mvp-tdd.md` | Runtime gate semantics for execute-phase under MVP+TDD — pre-task failing-test verification, end-of-phase blocking review. |
|
||||
| `mvp-concepts.md` | Cross-reference index for the six MVP-related reference files; maps each file to its purpose and which workflow loads it. |
|
||||
@@ -428,6 +429,7 @@ The `gsd-planner` agent is decomposed into a core agent plus reference modules t
|
||||
| `planner-interface-context.md` | Interface context rules for executors — how to extract key interfaces/types/exports from existing code and document new interfaces that downstream plans will consume. |
|
||||
| `planner-load-graph-context.md` | Planner's load_graph_context step: knowledge-graph freshness + dependency-context query via the gsd_run launcher (extracted from gsd-planner.md). |
|
||||
| `planner-verify-command-grounding.md` | Verify Command Grounding rules (#2401): inherit `prior_verify_commands` verbatim when the story repeats, prefer `npm --prefix <dir> run <script>` over `cd <dir> && npm run <script>`, and ground every authored path. |
|
||||
| `planner-coupling.md` | Same-wave shared-mutable-state coupling rules (#3724): when non-file coupling requires `depends_on` or a re-wave, and the `coupling_justified` frontmatter declaration that plan-checker Dimension 3b recognizes for deliberately order-independent pairs. |
|
||||
| `planner-failing-direction.md` | Stated Failing Direction rules (#3172): every runnable `<automated>` carries a `<fails_when>` sibling naming an observable failure signal, the pairing and placeholder rules, the `MISSING` sentinel exemption, and the authoring test ("if this command were silently doing nothing, what would tell me?"). |
|
||||
| `skeleton-template.md` | SKELETON.md template emitted for new-project Walking Skeleton (Phase 1 + `--mvp`). |
|
||||
| `user-story-template.md` | User story format for MVP planning — "As a / I want to / So that" structured fields. |
|
||||
|
||||
@@ -72,6 +72,7 @@ must_haves:
|
||||
| `depends_on` | Yes | array of plan IDs | Plans this plan must wait for. Empty array = wave 1. Accepts three forms, resolved in order: the full plan id (`"03-01-auth-hardening"`), the canonical phase-plan prefix (`"03-01"`), or the bare plan number (`"01"`, #3897) — which resolves to the sibling plan in the **same phase** whose canonical id ends `-01`. The bare form is in-phase only; it never resolves across phases. If two plans in the same phase share a bare form, the first one (by sorted plan-file order) wins — deterministic, but arbitrary when the collision happens, so prefer the full or canonical form when phase has any short-form collision risk. Example: `["03-01"]` means this plan runs after Plan 01 in Phase 3; from within Phase 3 itself, `["01"]` means the same thing. |
|
||||
| `files_modified` | Yes | array of paths | Every file this plan creates or modifies. Used by the plan-checker to detect same-wave file conflicts and by execute-phase for merge tracking. |
|
||||
| `files_deleted` | No | array of paths | Every file this plan deliberately **removes**. The post-wave cleanup gauntlet blocks the merge of any executor branch whose diff deletes a file — a net against a mass-deletion accident — and this field is the opt-in that names the exceptions. Matching is exact per path after separator normalization: a declared path merges, an undeclared one still blocks that plan's entry (and only that entry). There are no globs and no directory prefixes, so a declaration can never authorize more than it literally lists. Omit the field and the guard's original unconditional block stays in force, which is why absence is always the safe default (#3003). Counts toward same-wave conflict detection alongside `files_modified`: a plan deleting a file another plan in the same wave is editing is the sharpest conflict there is — one branch removes what the other is writing — so the two plans are pushed into different waves regardless of which side holds the deletion. |
|
||||
| `coupling_justified` | No | array of `"plan-id: reason"` strings | One entry per deliberately coupled same-wave peer, e.g. `["03-02: both append independent config keys"]` — declares that the coupling with that plan through a shared mutable resource (config key, table, migration, env var) is deliberate and order-independent. The plan-checker's Dimension 3b recognizes the declaration and does not flag the pair, so intentionally coupled plans can pass verification without serializing waves. The `"plan-id: reason"` shape is a prompt-level convention read by the checker, not a schema — the plan parser (`src/plan-document.cts`) neither validates nor rejects the field, so a typo'd plan-id silently exempts nothing (#3724). |
|
||||
| `autonomous` | Yes | boolean | `true` when all tasks are type `auto`. `false` when the plan contains any `checkpoint:*` task that requires human interaction. |
|
||||
| `requirements` | Yes | array of IDs | Requirement IDs from ROADMAP.md that this plan addresses. Every phase requirement ID must appear in at least one plan's `requirements` field. Empty arrays are a BLOCKER. |
|
||||
| `user_setup` | No | array of objects | External-service setup steps that Claude cannot automate (account creation, secret retrieval, dashboard configuration). When present, execute-phase generates a `USER-SETUP.md` checklist for the developer. |
|
||||
|
||||
@@ -15,7 +15,7 @@ This doc describes what IS, not what should be. Casing inconsistencies are docum
|
||||
| gsd-executor | Plan execution | `## PLAN COMPLETE`, `## CHECKPOINT REACHED` | `gsd-core/workflows/plan-phase.md`, `gsd-core/workflows/plan-phase/steps/chunked-planning-mode.md`, `agents/gsd-debug-session-manager.md`, `agents/gsd-debugger.md` | sentinel-match |
|
||||
| gsd-phase-researcher | Phase-scoped research | `## RESEARCH COMPLETE`, `## RESEARCH BLOCKED` | `gsd-core/workflows/plan-phase.md`, `gsd-core/workflows/quick/steps/research-phase.md`, `agents/gsd-project-researcher.md` | sentinel-match |
|
||||
| gsd-project-researcher | Project-wide research | `## RESEARCH COMPLETE`, `## RESEARCH BLOCKED` | `gsd-core/workflows/plan-phase.md`, `gsd-core/workflows/quick/steps/research-phase.md`, `agents/gsd-phase-researcher.md` | sentinel-match |
|
||||
| gsd-plan-checker | Plan validation | `## VERIFICATION PASSED`, `## ISSUES FOUND` | `gsd-core/workflows/plan-phase.md`, `gsd-core/workflows/plan-phase/steps/stall-detection-helpers.md`, `gsd-core/workflows/quick/steps/plan-checker-loop.md`, `gsd-core/workflows/ui-phase.md`, `gsd-core/workflows/verify-work.md`, `agents/gsd-ui-checker.md` | sentinel-match |
|
||||
| gsd-plan-checker | Plan validation | `## VERIFICATION PASSED`, `## ISSUES FOUND` | `gsd-core/workflows/plan-phase.md`, `gsd-core/workflows/plan-phase/steps/stall-detection-helpers.md`, `gsd-core/workflows/quick/steps/plan-checker-loop.md`, `gsd-core/workflows/import.md`, `gsd-core/workflows/ui-phase.md`, `gsd-core/workflows/verify-work.md`, `agents/gsd-ui-checker.md` | sentinel-match |
|
||||
| gsd-research-synthesizer | Multi-research synthesis | `## SYNTHESIS COMPLETE`, `## SYNTHESIS BLOCKED` (unconsumed: blocked-research return — spawners detect failure via the #222 SUMMARY.md-on-disk check, no dispatch branch keys on the marker) | `gsd-core/workflows/new-milestone.md`, `gsd-core/workflows/new-project.md` | sentinel-match |
|
||||
| gsd-debugger | Debug investigation | `## DEBUG COMPLETE`, `## ROOT CAUSE FOUND`, `## CHECKPOINT REACHED`, `## INVESTIGATION INCONCLUSIVE`, `## TDD CHECKPOINT`, `## FIX REJECTED BY GUARDRAIL` | `agents/gsd-debug-session-manager.md`, `gsd-core/workflows/diagnose-issues.md`, `gsd-core/workflows/plan-phase.md`, `agents/gsd-executor.md` | sentinel-match |
|
||||
| gsd-debug-session-manager | Debug checkpoint loop | `## DEBUG SESSION COMPLETE`, `## CONTINUE_REQUIRED` | `gsd-core/workflows/debug.md` | sentinel-match |
|
||||
|
||||
40
gsd-core/references/plan-checker-examples.md
Normal file
40
gsd-core/references/plan-checker-examples.md
Normal file
@@ -0,0 +1,40 @@
|
||||
# Plan-Checker Examples
|
||||
|
||||
> Progressive-disclosure reference for `agents/gsd-plan-checker.md`. The checker
|
||||
> inlines this file from its `<examples>` block via `@`; the calibrated few-shot
|
||||
> set lives separately in `gsd-core/references/few-shot-examples/plan-checker.md`.
|
||||
|
||||
## Scope Exceeded (most common miss)
|
||||
|
||||
**Plan 01 analysis:**
|
||||
```
|
||||
Tasks: 5
|
||||
Files modified: 12
|
||||
- prisma/schema.prisma
|
||||
- src/app/api/auth/login/route.ts
|
||||
- src/app/api/auth/logout/route.ts
|
||||
- src/app/api/auth/refresh/route.ts
|
||||
- src/middleware.ts
|
||||
- src/lib/auth.ts
|
||||
- src/lib/jwt.ts
|
||||
- src/components/LoginForm.tsx
|
||||
- src/components/LogoutButton.tsx
|
||||
- src/app/login/page.tsx
|
||||
- src/app/dashboard/page.tsx
|
||||
- src/types/auth.ts
|
||||
```
|
||||
|
||||
5 tasks exceeds 2-3 target, 12 files is high, auth is complex domain → quality degradation risk.
|
||||
|
||||
```yaml
|
||||
issue:
|
||||
dimension: scope_sanity
|
||||
severity: blocker
|
||||
description: "Plan 01 has 5 tasks with 12 files - exceeds context budget"
|
||||
plan: "01"
|
||||
metrics:
|
||||
tasks: 5
|
||||
files: 12
|
||||
estimated_context: "~80%"
|
||||
fix_hint: "Split into: 01 (schema + API), 02 (middleware + lib), 03 (UI components)"
|
||||
```
|
||||
42
gsd-core/references/planner-coupling.md
Normal file
42
gsd-core/references/planner-coupling.md
Normal file
@@ -0,0 +1,42 @@
|
||||
# Planner Coupling — Same-Wave Shared Mutable State
|
||||
|
||||
> Progressive-disclosure reference for `agents/gsd-planner.md`. The planner agent
|
||||
> reads this file when assigning waves (issue #3724). The slim pointer in
|
||||
> `agents/gsd-planner.md` → `assign_waves` routes here; the canonical schema row
|
||||
> for `coupling_justified` lives in `docs/reference/plan-md.md`. The verifying
|
||||
> side is `agents/gsd-plan-checker.md` Dimension 3b (#1954).
|
||||
|
||||
## The rule
|
||||
|
||||
`files_modified`/`files_deleted` overlap is not the only coupling between
|
||||
same-wave plans. If two plans in the same wave touch the same **mutable
|
||||
resource** through their task actions — a config key, DB table/row, migration,
|
||||
env var, singleton, cache — with at least one writer, or one plan produces a
|
||||
prerequisite the other consumes, the pair is coupled through shared state even
|
||||
though no file overlaps: under parallel execution the outcome depends on which
|
||||
executor gets there first.
|
||||
|
||||
Resolve it one of three ways, in order of preference:
|
||||
|
||||
1. **Declare the edge** — add the producing plan to the consumer's
|
||||
`depends_on`. Wave assignment then orders them automatically.
|
||||
2. **Re-wave** — move one plan to a later wave when the dependency direction
|
||||
is unclear but an ordering is still wanted.
|
||||
3. **Justify the pair** — when the coupling is deliberate and genuinely
|
||||
order-independent (both orders produce a correct result), record it in
|
||||
either plan's frontmatter, one `"plan-id: reason"` entry per coupled peer:
|
||||
|
||||
```yaml
|
||||
coupling_justified: ["03-02: both plans append independent keys to config; order irrelevant"]
|
||||
```
|
||||
|
||||
The plan-checker's Dimension 3b recognizes the declaration and does not
|
||||
flag the pair, so a deliberately coupled plan set passes verification
|
||||
without serializing waves it was designed to run in parallel.
|
||||
|
||||
## Why declare it up front
|
||||
|
||||
Dimension 3b flags same-wave plan pairs with an undeclared shared-mutable-state
|
||||
dependency (advisory severity — it never blocks). Declaring the edge, re-waving,
|
||||
or justifying the pair at plan time means the first checker pass comes back
|
||||
clean instead of surfacing an advisory the planner then has to interpret.
|
||||
@@ -22,6 +22,10 @@ files_modified: [] # Files this plan modifies.
|
||||
files_deleted: [] # OPTIONAL. Files this plan REMOVES. Declaring a path here is what
|
||||
# lets worktree cleanup-wave merge the branch that deletes it; an
|
||||
# undeclared deletion still blocks. Exact paths, not globs or dirs.
|
||||
coupling_justified: [] # OPTIONAL. Deliberate, order-independent same-wave couplings: one
|
||||
# "plan-id: reason" string per coupled peer, e.g.
|
||||
# ["03-02: both append independent config keys"]. Exempts the pair
|
||||
# from the plan-checker's Dimension 3b advisory (#3724).
|
||||
autonomous: true # false if plan has checkpoints requiring user interaction
|
||||
requirements: [] # REQUIRED — Requirement IDs from ROADMAP this plan addresses. MUST NOT be empty.
|
||||
user_setup: [] # Human-required setup Claude cannot automate (see below)
|
||||
|
||||
@@ -222,8 +222,8 @@ Agent({
|
||||
|
||||
> **ORCHESTRATOR RULE — CODEX RUNTIME**: After calling Agent() above, stop working on this task immediately. Do not read more files, edit code, or run tests related to this task while the subagent is active. Wait for the subagent to return its result. This prevents duplicate work, conflicting edits, and wasted context. Only resume when the subagent result is available.
|
||||
|
||||
If the checker returns errors:
|
||||
- Display the errors to the user
|
||||
Handle the checker return by severity, never by the sentinel alone: count BLOCKER + WARNING entries in the YAML issues block; an entry whose severity is missing or unrecognized counts as a BLOCKER (fail closed). If the return is `## VERIFICATION PASSED`, or the count is zero — every entry is explicitly INFO — display `ℹ advisory — {dimension}: {description}` per INFO entry and treat the plan as imported; INFO is advisory and never blocks an import (#3724). Otherwise:
|
||||
- Display the blocking issues to the user
|
||||
- Ask the user to resolve issues before the plan is considered imported
|
||||
- Do not delete the written file — the user can fix and re-validate manually
|
||||
|
||||
|
||||
@@ -1165,9 +1165,9 @@ Track `stall_reentry_count` (starts at 0; incremented each time "Adjust approach
|
||||
|
||||
**If iteration_count < 3:**
|
||||
|
||||
Parse issue count from checker return: count BLOCKER + WARNING entries in the YAML issues block (structured output from gsd-plan-checker). If the checker's return contains no YAML issues block (i.e., the plan was approved with no issues), treat `issue_count` as 0 and skip the stall check — the plan passed. Proceed to step 13.
|
||||
Parse issue count from checker return: count BLOCKER + WARNING entries in the YAML issues block (structured output from gsd-plan-checker); an entry whose severity is missing or unrecognized counts as a BLOCKER (fail closed). If the checker's return contains no YAML issues block (i.e., the plan was approved with no issues), treat `issue_count` as 0 and skip the stall check — the plan passed. Proceed to step 13 — likewise when every entry in the block is explicitly INFO (display them as advisories). Advisory format: `ℹ advisory — {dimension}: {description}` per INFO entry, listed once before the step-13 output.
|
||||
|
||||
Display: `Revision iteration {N}/3 -- {blocker_count} blockers, {warning_count} warnings`
|
||||
Display (only when entering the revision loop — skip if the paragraph above already proceeded to step 13): `Revision iteration {N}/3 -- {blocker_count} blockers, {warning_count} warnings`
|
||||
|
||||
**Stall detection:** If `issue_count >= prev_issue_count`:
|
||||
Display: `Revision loop stalled — issue count not decreasing ({issue_count} issues remain after {N} iterations)`
|
||||
@@ -1229,6 +1229,8 @@ After planner returns -> spawn checker again (step 10), increment iteration_coun
|
||||
|
||||
**If iteration_count >= 3:**
|
||||
|
||||
Recount BLOCKER + WARNING by the same rule — an entry whose severity is missing or unrecognized counts as a BLOCKER (fail closed). If `issue_count` is 0 — PASSED, or every entry in the block is explicitly INFO — display any advisories and proceed to step 13; the gate below fires on everything else (#3724).
|
||||
|
||||
Display: `Max iterations reached. {N} issues remain:` + issue list
|
||||
|
||||
Offer: 1) Force proceed, 2) Provide guidance and retry, 3) Abandon
|
||||
|
||||
@@ -56,7 +56,7 @@ Agent(
|
||||
**Handle checker return:**
|
||||
|
||||
- **`## VERIFICATION PASSED`:** Display confirmation, proceed to step 6.
|
||||
- **`## ISSUES FOUND`:** Display issues, check iteration count, enter revision loop.
|
||||
- **`## ISSUES FOUND`:** Count BLOCKER + WARNING entries in the YAML issues block; an entry whose severity is missing or unrecognized counts as a BLOCKER (fail closed). If zero — every entry is explicitly INFO — display `ℹ advisory — {dimension}: {description}` per entry and proceed to step 6; INFO is advisory and never enters the loop (#3724). Otherwise display issues, check iteration count, enter revision loop.
|
||||
|
||||
**Revision loop (max 2 iterations):**
|
||||
|
||||
|
||||
@@ -836,7 +836,7 @@ Return one of:
|
||||
|
||||
On return:
|
||||
- **VERIFICATION PASSED:** Proceed to `present_ready`
|
||||
- **ISSUES FOUND:** Proceed to `revision_loop`
|
||||
- **ISSUES FOUND:** Count BLOCKER + WARNING entries in the YAML issues block; an entry whose severity is missing or unrecognized counts as a BLOCKER (fail closed). If zero — every entry is explicitly INFO — display `ℹ advisory — {dimension}: {description}` per entry and proceed to `present_ready`; INFO is advisory and never enters the loop (#3724). Otherwise proceed to `revision_loop`
|
||||
</step>
|
||||
|
||||
<step name="revision_loop">
|
||||
|
||||
2
tests/fixtures/install-tree/antigravity.json
vendored
2
tests/fixtures/install-tree/antigravity.json
vendored
@@ -116,8 +116,10 @@
|
||||
"gsd-core/references/nyquist-compliance.md",
|
||||
"gsd-core/references/offer-next.md",
|
||||
"gsd-core/references/phase-argument-parsing.md",
|
||||
"gsd-core/references/plan-checker-examples.md",
|
||||
"gsd-core/references/planner-antipatterns.md",
|
||||
"gsd-core/references/planner-chunked.md",
|
||||
"gsd-core/references/planner-coupling.md",
|
||||
"gsd-core/references/planner-failing-direction.md",
|
||||
"gsd-core/references/planner-gap-closure.md",
|
||||
"gsd-core/references/planner-graphify-auto-update.md",
|
||||
|
||||
2
tests/fixtures/install-tree/augment.json
vendored
2
tests/fixtures/install-tree/augment.json
vendored
@@ -187,8 +187,10 @@
|
||||
"gsd-core/references/nyquist-compliance.md",
|
||||
"gsd-core/references/offer-next.md",
|
||||
"gsd-core/references/phase-argument-parsing.md",
|
||||
"gsd-core/references/plan-checker-examples.md",
|
||||
"gsd-core/references/planner-antipatterns.md",
|
||||
"gsd-core/references/planner-chunked.md",
|
||||
"gsd-core/references/planner-coupling.md",
|
||||
"gsd-core/references/planner-failing-direction.md",
|
||||
"gsd-core/references/planner-gap-closure.md",
|
||||
"gsd-core/references/planner-graphify-auto-update.md",
|
||||
|
||||
@@ -187,8 +187,10 @@
|
||||
"gsd-core/references/nyquist-compliance.md",
|
||||
"gsd-core/references/offer-next.md",
|
||||
"gsd-core/references/phase-argument-parsing.md",
|
||||
"gsd-core/references/plan-checker-examples.md",
|
||||
"gsd-core/references/planner-antipatterns.md",
|
||||
"gsd-core/references/planner-chunked.md",
|
||||
"gsd-core/references/planner-coupling.md",
|
||||
"gsd-core/references/planner-failing-direction.md",
|
||||
"gsd-core/references/planner-gap-closure.md",
|
||||
"gsd-core/references/planner-graphify-auto-update.md",
|
||||
|
||||
2
tests/fixtures/install-tree/claude.json
vendored
2
tests/fixtures/install-tree/claude.json
vendored
@@ -116,8 +116,10 @@
|
||||
"gsd-core/references/nyquist-compliance.md",
|
||||
"gsd-core/references/offer-next.md",
|
||||
"gsd-core/references/phase-argument-parsing.md",
|
||||
"gsd-core/references/plan-checker-examples.md",
|
||||
"gsd-core/references/planner-antipatterns.md",
|
||||
"gsd-core/references/planner-chunked.md",
|
||||
"gsd-core/references/planner-coupling.md",
|
||||
"gsd-core/references/planner-failing-direction.md",
|
||||
"gsd-core/references/planner-gap-closure.md",
|
||||
"gsd-core/references/planner-graphify-auto-update.md",
|
||||
|
||||
2
tests/fixtures/install-tree/cline.json
vendored
2
tests/fixtures/install-tree/cline.json
vendored
@@ -118,8 +118,10 @@
|
||||
"gsd-core/references/nyquist-compliance.md",
|
||||
"gsd-core/references/offer-next.md",
|
||||
"gsd-core/references/phase-argument-parsing.md",
|
||||
"gsd-core/references/plan-checker-examples.md",
|
||||
"gsd-core/references/planner-antipatterns.md",
|
||||
"gsd-core/references/planner-chunked.md",
|
||||
"gsd-core/references/planner-coupling.md",
|
||||
"gsd-core/references/planner-failing-direction.md",
|
||||
"gsd-core/references/planner-gap-closure.md",
|
||||
"gsd-core/references/planner-graphify-auto-update.md",
|
||||
|
||||
2
tests/fixtures/install-tree/codebuddy.json
vendored
2
tests/fixtures/install-tree/codebuddy.json
vendored
@@ -187,8 +187,10 @@
|
||||
"gsd-core/references/nyquist-compliance.md",
|
||||
"gsd-core/references/offer-next.md",
|
||||
"gsd-core/references/phase-argument-parsing.md",
|
||||
"gsd-core/references/plan-checker-examples.md",
|
||||
"gsd-core/references/planner-antipatterns.md",
|
||||
"gsd-core/references/planner-chunked.md",
|
||||
"gsd-core/references/planner-coupling.md",
|
||||
"gsd-core/references/planner-failing-direction.md",
|
||||
"gsd-core/references/planner-gap-closure.md",
|
||||
"gsd-core/references/planner-graphify-auto-update.md",
|
||||
|
||||
2
tests/fixtures/install-tree/codex.json
vendored
2
tests/fixtures/install-tree/codex.json
vendored
@@ -152,8 +152,10 @@
|
||||
"gsd-core/references/nyquist-compliance.md",
|
||||
"gsd-core/references/offer-next.md",
|
||||
"gsd-core/references/phase-argument-parsing.md",
|
||||
"gsd-core/references/plan-checker-examples.md",
|
||||
"gsd-core/references/planner-antipatterns.md",
|
||||
"gsd-core/references/planner-chunked.md",
|
||||
"gsd-core/references/planner-coupling.md",
|
||||
"gsd-core/references/planner-failing-direction.md",
|
||||
"gsd-core/references/planner-gap-closure.md",
|
||||
"gsd-core/references/planner-graphify-auto-update.md",
|
||||
|
||||
2
tests/fixtures/install-tree/copilot.json
vendored
2
tests/fixtures/install-tree/copilot.json
vendored
@@ -117,8 +117,10 @@
|
||||
"gsd-core/references/nyquist-compliance.md",
|
||||
"gsd-core/references/offer-next.md",
|
||||
"gsd-core/references/phase-argument-parsing.md",
|
||||
"gsd-core/references/plan-checker-examples.md",
|
||||
"gsd-core/references/planner-antipatterns.md",
|
||||
"gsd-core/references/planner-chunked.md",
|
||||
"gsd-core/references/planner-coupling.md",
|
||||
"gsd-core/references/planner-failing-direction.md",
|
||||
"gsd-core/references/planner-gap-closure.md",
|
||||
"gsd-core/references/planner-graphify-auto-update.md",
|
||||
|
||||
2
tests/fixtures/install-tree/cursor.json
vendored
2
tests/fixtures/install-tree/cursor.json
vendored
@@ -116,8 +116,10 @@
|
||||
"gsd-core/references/nyquist-compliance.md",
|
||||
"gsd-core/references/offer-next.md",
|
||||
"gsd-core/references/phase-argument-parsing.md",
|
||||
"gsd-core/references/plan-checker-examples.md",
|
||||
"gsd-core/references/planner-antipatterns.md",
|
||||
"gsd-core/references/planner-chunked.md",
|
||||
"gsd-core/references/planner-coupling.md",
|
||||
"gsd-core/references/planner-failing-direction.md",
|
||||
"gsd-core/references/planner-gap-closure.md",
|
||||
"gsd-core/references/planner-graphify-auto-update.md",
|
||||
|
||||
2
tests/fixtures/install-tree/hermes.json
vendored
2
tests/fixtures/install-tree/hermes.json
vendored
@@ -116,8 +116,10 @@
|
||||
"gsd-core/references/nyquist-compliance.md",
|
||||
"gsd-core/references/offer-next.md",
|
||||
"gsd-core/references/phase-argument-parsing.md",
|
||||
"gsd-core/references/plan-checker-examples.md",
|
||||
"gsd-core/references/planner-antipatterns.md",
|
||||
"gsd-core/references/planner-chunked.md",
|
||||
"gsd-core/references/planner-coupling.md",
|
||||
"gsd-core/references/planner-failing-direction.md",
|
||||
"gsd-core/references/planner-gap-closure.md",
|
||||
"gsd-core/references/planner-graphify-auto-update.md",
|
||||
|
||||
2
tests/fixtures/install-tree/kilo.json
vendored
2
tests/fixtures/install-tree/kilo.json
vendored
@@ -187,8 +187,10 @@
|
||||
"gsd-core/references/nyquist-compliance.md",
|
||||
"gsd-core/references/offer-next.md",
|
||||
"gsd-core/references/phase-argument-parsing.md",
|
||||
"gsd-core/references/plan-checker-examples.md",
|
||||
"gsd-core/references/planner-antipatterns.md",
|
||||
"gsd-core/references/planner-chunked.md",
|
||||
"gsd-core/references/planner-coupling.md",
|
||||
"gsd-core/references/planner-failing-direction.md",
|
||||
"gsd-core/references/planner-gap-closure.md",
|
||||
"gsd-core/references/planner-graphify-auto-update.md",
|
||||
|
||||
2
tests/fixtures/install-tree/kimi-code.json
vendored
2
tests/fixtures/install-tree/kimi-code.json
vendored
@@ -117,8 +117,10 @@
|
||||
"gsd-core/references/nyquist-compliance.md",
|
||||
"gsd-core/references/offer-next.md",
|
||||
"gsd-core/references/phase-argument-parsing.md",
|
||||
"gsd-core/references/plan-checker-examples.md",
|
||||
"gsd-core/references/planner-antipatterns.md",
|
||||
"gsd-core/references/planner-chunked.md",
|
||||
"gsd-core/references/planner-coupling.md",
|
||||
"gsd-core/references/planner-failing-direction.md",
|
||||
"gsd-core/references/planner-gap-closure.md",
|
||||
"gsd-core/references/planner-graphify-auto-update.md",
|
||||
|
||||
2
tests/fixtures/install-tree/kimi.json
vendored
2
tests/fixtures/install-tree/kimi.json
vendored
@@ -153,8 +153,10 @@
|
||||
"gsd-core/references/nyquist-compliance.md",
|
||||
"gsd-core/references/offer-next.md",
|
||||
"gsd-core/references/phase-argument-parsing.md",
|
||||
"gsd-core/references/plan-checker-examples.md",
|
||||
"gsd-core/references/planner-antipatterns.md",
|
||||
"gsd-core/references/planner-chunked.md",
|
||||
"gsd-core/references/planner-coupling.md",
|
||||
"gsd-core/references/planner-failing-direction.md",
|
||||
"gsd-core/references/planner-gap-closure.md",
|
||||
"gsd-core/references/planner-graphify-auto-update.md",
|
||||
|
||||
2
tests/fixtures/install-tree/opencode.json
vendored
2
tests/fixtures/install-tree/opencode.json
vendored
@@ -187,8 +187,10 @@
|
||||
"gsd-core/references/nyquist-compliance.md",
|
||||
"gsd-core/references/offer-next.md",
|
||||
"gsd-core/references/phase-argument-parsing.md",
|
||||
"gsd-core/references/plan-checker-examples.md",
|
||||
"gsd-core/references/planner-antipatterns.md",
|
||||
"gsd-core/references/planner-chunked.md",
|
||||
"gsd-core/references/planner-coupling.md",
|
||||
"gsd-core/references/planner-failing-direction.md",
|
||||
"gsd-core/references/planner-gap-closure.md",
|
||||
"gsd-core/references/planner-graphify-auto-update.md",
|
||||
|
||||
2
tests/fixtures/install-tree/pi.json
vendored
2
tests/fixtures/install-tree/pi.json
vendored
@@ -83,8 +83,10 @@
|
||||
"gsd-core/references/nyquist-compliance.md",
|
||||
"gsd-core/references/offer-next.md",
|
||||
"gsd-core/references/phase-argument-parsing.md",
|
||||
"gsd-core/references/plan-checker-examples.md",
|
||||
"gsd-core/references/planner-antipatterns.md",
|
||||
"gsd-core/references/planner-chunked.md",
|
||||
"gsd-core/references/planner-coupling.md",
|
||||
"gsd-core/references/planner-failing-direction.md",
|
||||
"gsd-core/references/planner-gap-closure.md",
|
||||
"gsd-core/references/planner-graphify-auto-update.md",
|
||||
|
||||
2
tests/fixtures/install-tree/qwen.json
vendored
2
tests/fixtures/install-tree/qwen.json
vendored
@@ -116,8 +116,10 @@
|
||||
"gsd-core/references/nyquist-compliance.md",
|
||||
"gsd-core/references/offer-next.md",
|
||||
"gsd-core/references/phase-argument-parsing.md",
|
||||
"gsd-core/references/plan-checker-examples.md",
|
||||
"gsd-core/references/planner-antipatterns.md",
|
||||
"gsd-core/references/planner-chunked.md",
|
||||
"gsd-core/references/planner-coupling.md",
|
||||
"gsd-core/references/planner-failing-direction.md",
|
||||
"gsd-core/references/planner-gap-closure.md",
|
||||
"gsd-core/references/planner-graphify-auto-update.md",
|
||||
|
||||
2
tests/fixtures/install-tree/trae.json
vendored
2
tests/fixtures/install-tree/trae.json
vendored
@@ -116,8 +116,10 @@
|
||||
"gsd-core/references/nyquist-compliance.md",
|
||||
"gsd-core/references/offer-next.md",
|
||||
"gsd-core/references/phase-argument-parsing.md",
|
||||
"gsd-core/references/plan-checker-examples.md",
|
||||
"gsd-core/references/planner-antipatterns.md",
|
||||
"gsd-core/references/planner-chunked.md",
|
||||
"gsd-core/references/planner-coupling.md",
|
||||
"gsd-core/references/planner-failing-direction.md",
|
||||
"gsd-core/references/planner-gap-closure.md",
|
||||
"gsd-core/references/planner-graphify-auto-update.md",
|
||||
|
||||
2
tests/fixtures/install-tree/windsurf.json
vendored
2
tests/fixtures/install-tree/windsurf.json
vendored
@@ -116,8 +116,10 @@
|
||||
"gsd-core/references/nyquist-compliance.md",
|
||||
"gsd-core/references/offer-next.md",
|
||||
"gsd-core/references/phase-argument-parsing.md",
|
||||
"gsd-core/references/plan-checker-examples.md",
|
||||
"gsd-core/references/planner-antipatterns.md",
|
||||
"gsd-core/references/planner-chunked.md",
|
||||
"gsd-core/references/planner-coupling.md",
|
||||
"gsd-core/references/planner-failing-direction.md",
|
||||
"gsd-core/references/planner-gap-closure.md",
|
||||
"gsd-core/references/planner-graphify-auto-update.md",
|
||||
|
||||
2
tests/fixtures/install-tree/zcode.json
vendored
2
tests/fixtures/install-tree/zcode.json
vendored
@@ -187,8 +187,10 @@
|
||||
"gsd-core/references/nyquist-compliance.md",
|
||||
"gsd-core/references/offer-next.md",
|
||||
"gsd-core/references/phase-argument-parsing.md",
|
||||
"gsd-core/references/plan-checker-examples.md",
|
||||
"gsd-core/references/planner-antipatterns.md",
|
||||
"gsd-core/references/planner-chunked.md",
|
||||
"gsd-core/references/planner-coupling.md",
|
||||
"gsd-core/references/planner-failing-direction.md",
|
||||
"gsd-core/references/planner-gap-closure.md",
|
||||
"gsd-core/references/planner-graphify-auto-update.md",
|
||||
|
||||
@@ -37,10 +37,36 @@ const { stripFencedCode } = require('../gsd-core/bin/lib/markdown-sectionizer.cj
|
||||
const ROOT = path.join(__dirname, '..');
|
||||
const AGENT_PATH = path.join(ROOT, 'agents', 'gsd-plan-checker.md');
|
||||
const DOCS_AGENTS_PATH = path.join(ROOT, 'docs', 'AGENTS.md');
|
||||
const REVISION_LOOP_PATH = path.join(ROOT, 'gsd-core', 'references', 'revision-loop.md');
|
||||
const PLAN_PHASE_PATH = path.join(ROOT, 'gsd-core', 'workflows', 'plan-phase.md');
|
||||
const PLANNER_PATH = path.join(ROOT, 'agents', 'gsd-planner.md');
|
||||
const PLANNER_COUPLING_REF_PATH = path.join(ROOT, 'gsd-core', 'references', 'planner-coupling.md');
|
||||
|
||||
const VERIFY_WORK_PATH = path.join(ROOT, 'gsd-core', 'workflows', 'verify-work.md');
|
||||
const QUICK_LOOP_PATH = path.join(ROOT, 'gsd-core', 'workflows', 'quick', 'steps', 'plan-checker-loop.md');
|
||||
const IMPORT_PATH = path.join(ROOT, 'gsd-core', 'workflows', 'import.md');
|
||||
const AGENT_CONTRACTS_PATH = path.join(ROOT, 'gsd-core', 'references', 'agent-contracts.md');
|
||||
const { splitLines } = require('../gsd-core/bin/lib/text-lines.cjs');
|
||||
|
||||
const agentDoc = fs.readFileSync(AGENT_PATH, 'utf-8');
|
||||
const docsAgents = fs.readFileSync(DOCS_AGENTS_PATH, 'utf-8');
|
||||
|
||||
// Severity tokens inside a span's fenced ```yaml examples only. Prose may
|
||||
// legitimately contrast another tier ("this shape would be a blocker — see
|
||||
// Dimension 9"); the yaml examples are the declaration the model copies, so
|
||||
// severity assertions scope here (round-5 Minor 3).
|
||||
function yamlSeverityTiers(span) {
|
||||
const tiers = [];
|
||||
let inYaml = false;
|
||||
for (const line of splitLines(span)) {
|
||||
if (line.trim() === '```yaml') { inYaml = true; continue; }
|
||||
if (inYaml && line.trim() === '```') { inYaml = false; continue; }
|
||||
const m = inYaml ? line.match(/severity:\s*(\w+)/) : null;
|
||||
if (m) tiers.push(m[1].toLowerCase());
|
||||
}
|
||||
return tiers;
|
||||
}
|
||||
|
||||
// ── Span helpers ───────────────────────────────────────────────────
|
||||
// Offsets of the headings that bound each region. `indexOfHeading` returns -1 when
|
||||
// absent so a missing heading fails as a named assertion rather than an off-by-one.
|
||||
@@ -147,13 +173,36 @@ describe('gsd-plan-checker Dimension 3b — undeclared/temporal coupling (#1954)
|
||||
});
|
||||
|
||||
describe('severity is advisory, and stays advisory', () => {
|
||||
test('the sub-check finding is a warning', () => {
|
||||
test('the sub-check severity is the tier the revision loop exempts (#3724 parity)', () => {
|
||||
// #3724's defect was exactly this coming apart: 3b spec'd "advisory" but
|
||||
// tagged `warning`, the tier the revision loop treats as must-fix. The
|
||||
// exempt tier is therefore DERIVED from revision-loop.md's flow, never
|
||||
// hardcoded — if the loop's exemption ever changes, this fails instead
|
||||
// of silently re-opening the guaranteed-replan defect.
|
||||
const loopDoc = fs.readFileSync(REVISION_LOOP_PATH, 'utf-8');
|
||||
const exempt = loopDoc.match(/If PASSED or only (\w+)-level issues/);
|
||||
assert.ok(exempt, 'revision-loop.md must state its exempt severity tier in the flow');
|
||||
const tier = exempt[1].toLowerCase();
|
||||
const span = sliceBetween(agentDoc, D3B_HEADING, D4_HEADING);
|
||||
assert.match(
|
||||
span,
|
||||
/severity:\s*warning/,
|
||||
'Dimension 3b\'s example issue must carry severity: warning'
|
||||
new RegExp(`severity:\\s*${tier}`),
|
||||
`Dimension 3b's example issue must carry severity: ${tier} — the tier revision-loop.md exempts`
|
||||
);
|
||||
// The negative is derived too: every severity token in the span's yaml
|
||||
// examples must BE the exempt tier. If revision-loop.md's exemption ever
|
||||
// moves, this fails naming the real conflict instead of blaming the agent
|
||||
// file with a stale hardcode.
|
||||
const tiersInSpan = yamlSeverityTiers(span);
|
||||
assert.ok(tiersInSpan.length > 0, 'Dimension 3b must carry at least one severity-tagged example');
|
||||
for (const found of tiersInSpan) {
|
||||
assert.strictEqual(
|
||||
found,
|
||||
tier,
|
||||
`Dimension 3b carries severity: ${found}, but the only tier revision-loop.md exempts is ` +
|
||||
`${tier} — a non-exempt tier re-arms the revision loop`
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
test('the sub-check forbids escalating to blocker', () => {
|
||||
@@ -166,9 +215,8 @@ describe('gsd-plan-checker Dimension 3b — undeclared/temporal coupling (#1954)
|
||||
/never\s+(a\s+)?blocker/i,
|
||||
'Dimension 3b must state that the finding is never a blocker'
|
||||
);
|
||||
assert.doesNotMatch(
|
||||
span,
|
||||
/severity:\s*blocker/,
|
||||
assert.ok(
|
||||
!yamlSeverityTiers(span).includes('blocker'),
|
||||
'Dimension 3b must not contain a blocker-severity example — it is advisory only'
|
||||
);
|
||||
});
|
||||
@@ -287,4 +335,244 @@ describe('gsd-plan-checker Dimension 3b — undeclared/temporal coupling (#1954)
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#3724 — the advisory contract holds across the wiring', () => {
|
||||
// The defect #3724 fixed lived in three places at once: the checker's tier,
|
||||
// the orchestrator's loop gate, and the planner's ignorance of the rule.
|
||||
// Each assertion here pins one side; reverting any one of them alone must
|
||||
// red this suite, because #3237 shipping 3b with no orchestration-side
|
||||
// assertion is exactly how the defect arrived.
|
||||
|
||||
test('plan-phase accepts an INFO-only issues block without entering the revision loop', () => {
|
||||
const planPhase = fs.readFileSync(PLAN_PHASE_PATH, 'utf-8');
|
||||
// Pins the full single-line paragraph: a reflow of that line in plan-phase.md
|
||||
// reds this find() — update the startsWith prefix and the regexes together.
|
||||
const paragraph = splitLines(planPhase).find((line) =>
|
||||
line.startsWith('Parse issue count from checker return:')
|
||||
);
|
||||
assert.ok(paragraph, 'plan-phase.md step 12 must carry the parse-issue-count paragraph');
|
||||
assert.match(
|
||||
paragraph,
|
||||
/likewise when every entry in the block is explicitly INFO \(display them as advisories\)/,
|
||||
'step 12 must accept only an explicitly-INFO issues block, surfacing the advisories (#3724 criterion 1)'
|
||||
);
|
||||
});
|
||||
|
||||
test('plan-phase still counts BLOCKER + WARNING for the revision gate', () => {
|
||||
// Criterion 2's orchestration half: severity-blindness must not invert.
|
||||
// INFO joining the count would re-arm the loop; BLOCKER or WARNING
|
||||
// leaving it would let real defects through.
|
||||
const planPhase = fs.readFileSync(PLAN_PHASE_PATH, 'utf-8');
|
||||
assert.match(
|
||||
planPhase,
|
||||
/count BLOCKER \+ WARNING entries in the YAML issues block/,
|
||||
'step 12 must keep gating the revision loop on BLOCKER + WARNING counts'
|
||||
);
|
||||
});
|
||||
|
||||
test('the checker recognizes coupling_justified as a Do-NOT-flag exemption', () => {
|
||||
const span = sliceBetween(agentDoc, D3B_HEADING, D4_HEADING);
|
||||
assert.match(
|
||||
span,
|
||||
/declared `coupling_justified` in either plan's frontmatter/,
|
||||
'Dimension 3b must exempt a coupling_justified pair so intentional coupling can converge (#3724 criterion 4)'
|
||||
);
|
||||
assert.match(
|
||||
span,
|
||||
/fix_hint:[^\n]*coupling_justified/,
|
||||
'the 3b fix_hint must name coupling_justified so the planner learns the escape hatch'
|
||||
);
|
||||
});
|
||||
|
||||
test('verify-work accepts an INFO-only issues block without entering its revision loop', () => {
|
||||
// Round-4 Blocker: verify_gap_plans is the SECOND multi-plan consumer of the
|
||||
// checker's sentinels (agent-contracts.md), spawning it over all phase plans with
|
||||
// no dimension override — so 3b is live there and its handler must be
|
||||
// severity-aware, or the guaranteed replan #3724 fixed survives on that surface.
|
||||
const verifyWork = fs.readFileSync(VERIFY_WORK_PATH, 'utf-8');
|
||||
// Pins the full single-line handler: a reflow of that line in verify-work.md
|
||||
// reds this find() — update the startsWith prefix and the regexes together.
|
||||
const handler = splitLines(verifyWork).find((line) =>
|
||||
line.startsWith('- **ISSUES FOUND:**')
|
||||
);
|
||||
assert.ok(handler, 'verify-work.md must carry the ISSUES FOUND handler line');
|
||||
assert.match(
|
||||
handler,
|
||||
/Count BLOCKER \+ WARNING/,
|
||||
'the verify_gap_plans handler must gate its revision loop on BLOCKER + WARNING counts'
|
||||
);
|
||||
assert.match(
|
||||
handler,
|
||||
/every entry is explicitly INFO/,
|
||||
'the verify_gap_plans handler must accept only an explicitly-INFO issues block (whitelist, not count-zero)'
|
||||
);
|
||||
});
|
||||
|
||||
test('plan-phase iteration cap recounts severities instead of gating on advisories', () => {
|
||||
// Round-4 Minor 1: the INFO-only accept must hold at iteration_count >= 3 too,
|
||||
// or an advisory-only third check halts the workflow on a "0 issues remain"
|
||||
// user gate. This is a prose pin, not an executed boundary check: the >= 3 arm's
|
||||
// text is what both the limit and limit+1 iterations land on, so one string
|
||||
// match covers both — nothing here runs a counter (round-5 Minor 4).
|
||||
const planPhase = fs.readFileSync(PLAN_PHASE_PATH, 'utf-8');
|
||||
const lines = splitLines(planPhase);
|
||||
const armIndex = lines.findIndex((line) => line.startsWith('**If iteration_count >= 3:**'));
|
||||
assert.ok(armIndex >= 0, 'plan-phase.md must carry the iteration_count >= 3 arm');
|
||||
const armWindow = lines.slice(armIndex, armIndex + 5).join(' ');
|
||||
assert.match(
|
||||
armWindow,
|
||||
/Recount BLOCKER \+ WARNING/,
|
||||
'the >= 3 arm must recount BLOCKER + WARNING before gating'
|
||||
);
|
||||
assert.match(
|
||||
armWindow,
|
||||
/explicitly INFO — display any advisories and proceed to step 13/,
|
||||
'an INFO-only result at the iteration cap must accept, not halt on the user gate'
|
||||
);
|
||||
});
|
||||
|
||||
test('the fail-closed severity rule is verbatim-identical on all three gate surfaces', () => {
|
||||
// Round-5 Blockers 2+3: a count-based accept ("BLOCKER + WARNING count is zero")
|
||||
// is also true for an entry whose severity is missing, misspelled, or
|
||||
// unrecognized — auto-accepting what base sent to the revision loop. All three
|
||||
// gates carry one canonical clause, asserted verbatim, so the predicates cannot
|
||||
// drift apart again (Generative Fix Divergence).
|
||||
const CLAUSE = 'an entry whose severity is missing or unrecognized counts as a BLOCKER (fail closed)';
|
||||
const planPhase = fs.readFileSync(PLAN_PHASE_PATH, 'utf-8');
|
||||
const verifyWork = fs.readFileSync(VERIFY_WORK_PATH, 'utf-8');
|
||||
const lines = splitLines(planPhase);
|
||||
const parseLine = lines.find((line) => line.startsWith('Parse issue count from checker return:'));
|
||||
assert.ok(
|
||||
parseLine && parseLine.includes(CLAUSE),
|
||||
'the plan-phase iteration_count < 3 arm must carry the fail-closed clause verbatim'
|
||||
);
|
||||
const armIndex = lines.findIndex((line) => line.startsWith('**If iteration_count >= 3:**'));
|
||||
assert.ok(armIndex >= 0, 'plan-phase.md must carry the iteration_count >= 3 arm');
|
||||
const armWindow = lines.slice(armIndex, armIndex + 5).join(' ');
|
||||
assert.ok(
|
||||
armWindow.includes(CLAUSE),
|
||||
'the plan-phase iteration_count >= 3 arm must carry the fail-closed clause verbatim'
|
||||
);
|
||||
const handler = splitLines(verifyWork).find((line) => line.startsWith('- **ISSUES FOUND:**'));
|
||||
assert.ok(
|
||||
handler && handler.includes(CLAUSE),
|
||||
'the verify-work verify_gap_plans handler must carry the fail-closed clause verbatim'
|
||||
);
|
||||
// Round-7 Blocker + Major: the checker's INFO-only ## ISSUES FOUND contract is
|
||||
// dimension-agnostic and reaches every consumer, so the two remaining
|
||||
// severity-blind handlers get the same clause — quick mode (issue-named in
|
||||
// #3724) and import's plan_validate (absent even from agent-contracts.md
|
||||
// until this round).
|
||||
const quickLoop = fs.readFileSync(QUICK_LOOP_PATH, 'utf-8');
|
||||
const quickHandler = splitLines(quickLoop).find((line) => line.startsWith('- **`## ISSUES FOUND`:**'));
|
||||
assert.ok(
|
||||
quickHandler && quickHandler.includes(CLAUSE),
|
||||
'the quick-mode plan-checker-loop handler must carry the fail-closed clause verbatim'
|
||||
);
|
||||
const importDoc = fs.readFileSync(IMPORT_PATH, 'utf-8');
|
||||
const importHandler = splitLines(importDoc).find((line) => line.startsWith('Handle the checker return by severity'));
|
||||
assert.ok(
|
||||
importHandler && importHandler.includes(CLAUSE),
|
||||
'the import plan_validate handler must carry the fail-closed clause verbatim'
|
||||
);
|
||||
});
|
||||
|
||||
test('quick mode and import accept an explicitly-INFO-only issues block', () => {
|
||||
const quickLoop = fs.readFileSync(QUICK_LOOP_PATH, 'utf-8');
|
||||
const quickHandler = splitLines(quickLoop).find((line) => line.startsWith('- **`## ISSUES FOUND`:**'));
|
||||
assert.ok(quickHandler, 'plan-checker-loop.md must carry the ISSUES FOUND handler line');
|
||||
assert.match(
|
||||
quickHandler,
|
||||
/every entry is explicitly INFO/,
|
||||
'quick mode must accept only an explicitly-INFO issues block (whitelist, not count-zero)'
|
||||
);
|
||||
assert.match(
|
||||
quickHandler,
|
||||
/proceed to step 6/,
|
||||
'an INFO-only result in quick mode must proceed, not enter the revision loop'
|
||||
);
|
||||
const importDoc = fs.readFileSync(IMPORT_PATH, 'utf-8');
|
||||
const importHandler = splitLines(importDoc).find((line) => line.startsWith('Handle the checker return by severity'));
|
||||
assert.ok(importHandler, 'import.md plan_validate must carry the severity-aware handler paragraph');
|
||||
assert.match(
|
||||
importHandler,
|
||||
/never blocks an import/,
|
||||
'an INFO-only checker return must not block an import'
|
||||
);
|
||||
const contracts = fs.readFileSync(AGENT_CONTRACTS_PATH, 'utf-8');
|
||||
const checkerRow = splitLines(contracts).find((line) => line.startsWith('| gsd-plan-checker |'));
|
||||
assert.ok(
|
||||
checkerRow && checkerRow.includes('gsd-core/workflows/import.md'),
|
||||
'agent-contracts.md must list import.md as a gsd-plan-checker sentinel consumer'
|
||||
);
|
||||
});
|
||||
|
||||
test('an applied coupling_justified exemption stays observable', () => {
|
||||
// Round-7 Minor: the exemption fires from a one-sided, schema-unvalidated
|
||||
// declaration; without a surfaced note, a stale or copy-pasted entry
|
||||
// suppresses the check silently and permanently.
|
||||
const span = sliceBetween(agentDoc, D3B_HEADING, D4_HEADING);
|
||||
assert.match(
|
||||
span,
|
||||
/note the applied\s+exemption as its own `info` advisory/,
|
||||
'Dimension 3b must surface an applied coupling_justified exemption as an info advisory'
|
||||
);
|
||||
});
|
||||
|
||||
test('the checker returns ## ISSUES FOUND for an INFO-only result, with an advisories section', () => {
|
||||
// Round-5 Blocker 1: with 3b at severity info, an INFO-only result satisfied
|
||||
// the old step-10 `passed` rule and routed to ## VERIFICATION PASSED — a
|
||||
// template with no issues block — so the advisory this fix exists to surface
|
||||
// was dropped, and both orchestrator display clauses were unreachable.
|
||||
assert.match(
|
||||
agentDoc,
|
||||
/An INFO-only result is NOT `passed`/,
|
||||
'step 10 must exclude an INFO-only result from `passed`'
|
||||
);
|
||||
assert.match(
|
||||
agentDoc,
|
||||
/Return `## ISSUES FOUND` even when every issue is INFO/,
|
||||
'step 10 must route an INFO-only result to ## ISSUES FOUND so the block reaches the orchestrator'
|
||||
);
|
||||
assert.match(
|
||||
agentDoc,
|
||||
/### Advisories \(info\)/,
|
||||
'the ISSUES FOUND template must carry an advisories section so INFO entries render'
|
||||
);
|
||||
assert.match(
|
||||
agentDoc,
|
||||
/Advisory only — no revision required/,
|
||||
'the recommendation must not claim a planner return for an INFO-only result'
|
||||
);
|
||||
});
|
||||
|
||||
test('the planner routes to the coupling reference, and the reference teaches the rule', () => {
|
||||
const planner = fs.readFileSync(PLANNER_PATH, 'utf-8');
|
||||
assert.match(
|
||||
planner,
|
||||
/@~\/\.claude\/gsd-core\/references\/planner-coupling\.md/,
|
||||
'gsd-planner.md must point at the planner-coupling reference (#3724 criterion 3)'
|
||||
);
|
||||
assert.ok(
|
||||
fs.existsSync(PLANNER_COUPLING_REF_PATH),
|
||||
'gsd-core/references/planner-coupling.md must exist — the planner pointer routes there'
|
||||
);
|
||||
const ref = fs.readFileSync(PLANNER_COUPLING_REF_PATH, 'utf-8');
|
||||
assert.match(
|
||||
ref,
|
||||
/mutable\s+resource/i,
|
||||
'planner-coupling.md must state the shared-mutable-resource rule'
|
||||
);
|
||||
assert.match(
|
||||
ref,
|
||||
/coupling_justified/,
|
||||
'planner-coupling.md must document the coupling_justified declaration'
|
||||
);
|
||||
assert.match(
|
||||
ref,
|
||||
/Dimension 3b/,
|
||||
'planner-coupling.md must name the verifying side (Dimension 3b)'
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user