diff --git a/.changeset/quick-pumas-fly.md b/.changeset/quick-pumas-fly.md new file mode 100644 index 000000000..8793f7bd2 --- /dev/null +++ b/.changeset/quick-pumas-fly.md @@ -0,0 +1,5 @@ +--- +type: Changed +pr: 591 +--- +**`gsd-roadmapper` granularity defaults tightened to reduce thin-phase fragmentation.** Coarse 3-5 -> 2-4, Standard 4-6 (was 5-8), Fine 6-10 (was 8-12). New inline guidance below the Granularity Calibration table names the thin-phase failure pattern (single requirement, internal-quality goal, task-shaped success criteria) and instructs the agent to fold into the most-related neighbor rather than create a standalone phase. Affects `/gsd-new-project`, `/gsd-new-milestone`, and `/gsd-plan-milestone-gaps`. diff --git a/agents/gsd-roadmapper.md b/agents/gsd-roadmapper.md index aef008d50..5a65447ee 100644 --- a/agents/gsd-roadmapper.md +++ b/agents/gsd-roadmapper.md @@ -215,11 +215,11 @@ Read granularity from config.json. Granularity controls compression tolerance. | Granularity | Typical Phases | What It Means | |-------------|----------------|---------------| -| Coarse | 3-5 | Combine aggressively, critical path only | -| Standard | 5-8 | Balanced grouping | -| Fine | 8-12 | Let natural boundaries stand | +| Coarse | 2-4 | Combine aggressively, critical path only | +| Standard | 4-6 | Balanced grouping (tightened from 5-8 in 2026-05; downstream observation that the prior baseline encouraged ~15-20% over-fragmentation, often manifesting as thin "maintenance" phases that would have been better folded into a neighbor) | +| Fine | 6-10 | Let natural boundaries stand | -**Key:** Derive phases from work, then apply granularity as compression guidance. Don't pad small projects or compress complex ones. +**Key:** Derive phases from work, then apply granularity as compression guidance. Don't pad small projects or compress complex ones. When a phase you are about to write would have a single requirement, an internal-quality goal ("improve X", "refactor Y", "add tests for Z"), or success criteria that read as tasks rather than user-observable outcomes, prefer to fold it into the most-related neighbor instead of creating a standalone phase. ## Good Phase Patterns diff --git a/docs/CONFIGURATION.md b/docs/CONFIGURATION.md index d6c2ac67e..9c9f4515c 100644 --- a/docs/CONFIGURATION.md +++ b/docs/CONFIGURATION.md @@ -135,7 +135,7 @@ GSD stores project settings in `.planning/config.json`. Created during `/gsd-new | Setting | Type | Options | Default | Description | |---------|------|---------|---------|-------------| | `mode` | enum | `interactive`, `yolo` | `interactive` | `yolo` auto-approves decisions; `interactive` confirms at each step | -| `granularity` | enum | `coarse`, `standard`, `fine` | `standard` | Controls phase count: `coarse` (3-5), `standard` (5-8), `fine` (8-12) | +| `granularity` | enum | `coarse`, `standard`, `fine` | `standard` | Controls phase count: `coarse` (2-4), `standard` (4-6), `fine` (6-10) | | `model_profile` | enum | `quality`, `balanced`, `budget`, `adaptive`, `inherit` | `balanced` | Model tier for each agent (see [Model Profiles](#model-profiles)). `adaptive` was added per [#1713](https://github.com/open-gsd/gsd-core/issues/1713) / [#1806](https://github.com/open-gsd/gsd-core/issues/1806) and resolves the same way as the other tiers under runtime-aware profiles. | | `runtime` | string | `claude`, `codex`, or any string | (none) | Active runtime for [runtime-aware profile resolution](#runtime-aware-profiles-2517). When set, profile tiers (opus/sonnet/haiku) resolve to runtime-native model IDs. Today only the Codex install path emits per-agent model IDs from this resolver; other runtimes (`opencode`, `gemini`, `qwen`, `copilot`, …) consume the resolver at spawn time and gain dedicated install-path support in [#2612](https://github.com/open-gsd/gsd-core/issues/2612). When unset (default), behavior is unchanged from prior versions. Added in v1.39 | | `model_profile_overrides..` | string \| object | per-runtime tier override | (none) | Override the runtime-aware tier mapping for a specific `(runtime, tier)`. Tier is one of `opus`, `sonnet`, `haiku`. Value is either a model ID string (e.g. `"gpt-5-pro"`) or `{ model, reasoning_effort }`. See [Runtime-Aware Profiles](#runtime-aware-profiles-2517). Added in v1.39 | diff --git a/docs/FEATURES.md b/docs/FEATURES.md index 9351ae989..63a400d37 100644 --- a/docs/FEATURES.md +++ b/docs/FEATURES.md @@ -208,7 +208,7 @@ **Functional Requirements:** - Questions adapt based on detected project type (web app, CLI, mobile, API, etc.) - Research agents have web search capability for current ecosystem information -- Granularity setting controls phase count: `coarse` (3-5), `standard` (5-8), `fine` (8-12) +- Granularity setting controls phase count: `coarse` (2-4), `standard` (4-6), `fine` (6-10) - `--auto` mode extracts all information from the provided document without interactive questioning - Existing codebase context (from `/gsd-map-codebase`) is loaded if present diff --git a/tests/roadmapper-granularity.test.cjs b/tests/roadmapper-granularity.test.cjs new file mode 100644 index 000000000..1b8380040 --- /dev/null +++ b/tests/roadmapper-granularity.test.cjs @@ -0,0 +1,58 @@ +// allow-test-rule: source-text-is-the-product +// agents/gsd-roadmapper.md is the installed agent — the Granularity Calibration +// table IS the deployed instruction. Asserting on its text asserts what runs in +// production. Locks the tightened phase-count buckets from #163. +'use strict'; + +const { describe, test } = require('node:test'); +const assert = require('node:assert/strict'); +const fs = require('fs'); +const path = require('path'); + +const AGENTS_DIR = path.join(__dirname, '..', 'agents'); + +function readAgent(name) { + return fs.readFileSync(path.join(AGENTS_DIR, `${name}.md`), 'utf8'); +} + +// Extract the "## Granularity Calibration" section (up to the next "## " heading) +// so number-range assertions are scoped and cannot be satisfied by unrelated text +// elsewhere in the agent file. +function granularitySection(content) { + const start = content.indexOf('## Granularity Calibration'); + assert.ok(start !== -1, 'Granularity Calibration section must exist'); + const rest = content.slice(start + '## Granularity Calibration'.length); + const nextHeading = rest.indexOf('\n## '); + return nextHeading === -1 ? rest : rest.slice(0, nextHeading); +} + +describe('gsd-roadmapper granularity calibration (#163)', () => { + const section = granularitySection(readAgent('gsd-roadmapper')); + + test('Coarse bucket is tightened to 2-4', () => { + assert.ok(/\|\s*Coarse\s*\|\s*2-4\s*\|/.test(section), 'Coarse must be 2-4'); + }); + + test('Standard bucket is tightened to 4-6', () => { + assert.ok(/\|\s*Standard\s*\|\s*4-6\b/.test(section), 'Standard must be 4-6'); + }); + + test('Fine bucket is tightened to 6-10', () => { + assert.ok(/\|\s*Fine\s*\|\s*6-10\s*\|/.test(section), 'Fine must be 6-10'); + }); + + test('no granularity row maps to an old bucket (3-5 / 5-8 / 8-12)', () => { + // Scope to the second ("Typical Phases") column of each row so the approved + // explanatory footnote mentioning "5-8" in the third column does not false-fail. + assert.ok(!/\|\s*Coarse\s*\|\s*3-5\b/.test(section), 'Coarse must not map to 3-5'); + assert.ok(!/\|\s*Standard\s*\|\s*5-8\b/.test(section), 'Standard must not map to 5-8'); + assert.ok(!/\|\s*Fine\s*\|\s*8-12\b/.test(section), 'Fine must not map to 8-12'); + }); + + test('Key paragraph names the thin-phase pattern and prefers folding into a neighbor', () => { + assert.ok( + section.includes('fold it into the most-related neighbor'), + 'Key guidance must instruct folding thin phases into the most-related neighbor' + ); + }); +});