Merge pull request #1849 from open-gsd/hotfix/1.6.1

chore: merge release v1.6.1 to main
This commit is contained in:
Tom Boucher
2026-06-30 21:35:15 -04:00
committed by GitHub
55 changed files with 661 additions and 133 deletions

View File

@@ -1,7 +1,7 @@
{
"name": "gsd-core",
"displayName": "GSD Core",
"version": "1.6.0",
"version": "1.6.1",
"description": "GSD Core is a meta-prompting, context engineering, and spec-driven development system for AI coding agents.",
"author": {
"name": "open-gsd",

View File

@@ -6,6 +6,19 @@ Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
## [Unreleased]
## [1.6.1] - 2026-07-01
### Added
- **Claude Sonnet 5 is now the `standard` (sonnet) tier model.** The model catalog and provider presets resolve the sonnet/standard tier to `claude-sonnet-5` (GA 2026-06-30) across the Anthropic-backed runtimes (`claude`, `copilot`, and the `anthropic`/`anthropic-fable` presets), plus the OpenRouter-style `anthropic/claude-sonnet-5` for `opencode`/`hermes`, replacing the superseded `claude-sonnet-4-6`. Opus and Haiku tiers are unchanged. (#1847) (#1848)
### Fixed
- `milestone complete` and `roadmap analyze` now exclude the Phase 0 / Phase 999 backlog sentinels. A milestone whose only directory-less ROADMAP heading is a backlog sentinel can be completed without `--force`, and `roadmap analyze` no longer counts the sentinel in `phase_count` or routes `next_phase` into it. Completes the `^999` exclusion #1445 added to the progress denominators. (#1691)
- **`phase.complete` no longer reports a false `is_last_phase` on a `<details>`-wrapped checkbox checklist (#1591, #1752)** — when the active milestone's phase checklist was written as `- [ ] Phase N:` checkbox items inside a `<details>` block and the next phase had no directory on disk yet (still in planning), `phase.complete`'s `isLastPhase` roadmap-enumeration fallback used a heading-only pattern (`/#{2,4}\s*Phase…/`) that never matched checkbox items. It returned `is_last_phase: true, next_phase: null` on a mid-milestone phase and — via the milestone-complete cascade — wrongly flipped STATE.md to `Milestone complete` and decremented `progress.total_phases` (e.g. 8 → 7). The pattern now matches both heading-style (`### Phase N:`) and checkbox-list phases (`- [ ] Phase N:` / `- [x] Phase N:`); `extractCurrentMilestone` already surfaces the `<details>`-wrapped checklist correctly, so no parser change was needed. Only the reproduced `phase.complete` fallback is changed; the heading-only sibling patterns elsewhere in `phase.cts` are untouched.
(#1819)
- Windows: stop double-quoting $CLAUDE_PROJECT_DIR-anchored managed node hook paths during the #2979 legacy rewrite, which produced "\"$CLAUDE_PROJECT_DIR\"/..." and broke every node managed hook with MODULE_NOT_FOUND (PreToolUse-guard deadlock). (#1746)
## [1.6.0] - 2026-06-24
### Added

View File

@@ -1,7 +1,7 @@
{
"id": "ai-integration",
"role": "feature",
"version": "1.6.0",
"version": "1.6.1",
"title": "AI design contract",
"description": "AI-SPEC design contract workflow for phases that build AI systems; owns the AI integration command, agents, and workflow.ai_integration_phase activation key.",
"tier": "full",

View File

@@ -1,7 +1,7 @@
{
"id": "antigravity",
"role": "runtime",
"version": "1.6.0",
"version": "1.6.1",
"title": "Antigravity",
"description": "Google Antigravity IDE — nested under ~/.gemini/antigravity; probed across 1.x and 2.x layouts; Gemini hook event dialect; flat skill layout; tier-1 support.",
"tier": "core",

View File

@@ -1,7 +1,7 @@
{
"id": "audit",
"role": "feature",
"version": "1.6.0",
"version": "1.6.1",
"title": "Audit",
"description": "Open-artifact audit and UAT-gap audit for milestone close gates; exposes `gsd-tools audit-uat` (cross-phase UAT outstanding items) and `gsd-tools audit-open` (structured open-artifact scan across debug, tasks, threads, todos, seeds, UAT, verification, context-questions).",
"tier": "full",

View File

@@ -1,7 +1,7 @@
{
"id": "augment",
"role": "runtime",
"version": "1.6.0",
"version": "1.6.1",
"title": "Augment Code",
"description": "Augment Code CLI — commands + nested-skill artifact layout; settings-json hook surface; Claude hook event dialect; tier-2 support.",
"tier": "core",

View File

@@ -1,7 +1,7 @@
{
"id": "claude",
"role": "runtime",
"version": "1.6.0",
"version": "1.6.1",
"title": "Claude Code",
"description": "Anthropic Claude Code — primary development runtime; tier-1 support with full hook surface and skills-based global install.",
"tier": "core",

View File

@@ -1,7 +1,7 @@
{
"id": "cline",
"role": "runtime",
"version": "1.6.0",
"version": "1.6.1",
"title": "Cline",
"description": "Cline (VS Code extension) — global-only nested-skill layout; cline-rules hook surface (.clinerules); no hook events emitted; tier-2 support.",
"tier": "core",

View File

@@ -1,7 +1,7 @@
{
"id": "code-review",
"role": "feature",
"version": "1.6.0",
"version": "1.6.1",
"title": "Code review",
"description": "Source-file code review and review-fix workflow support for completed execution work.",
"tier": "full",

View File

@@ -1,7 +1,7 @@
{
"id": "codebuddy",
"role": "runtime",
"version": "1.6.0",
"version": "1.6.1",
"title": "CodeBuddy",
"description": "CodeBuddy (Tencent) — converted commands + skills artifact layout; settings-json hook surface; Claude hook event dialect; tier-2 support.",
"tier": "core",

View File

@@ -1,7 +1,7 @@
{
"id": "codex",
"role": "runtime",
"version": "1.6.0",
"version": "1.6.1",
"title": "OpenAI Codex CLI",
"description": "OpenAI Codex CLI — shell-var command style; per-agent sandbox tiers; config.toml + hooks.json hook surface; tier-1 support.",
"tier": "core",

View File

@@ -1,7 +1,7 @@
{
"id": "copilot",
"role": "runtime",
"version": "1.6.0",
"version": "1.6.1",
"title": "GitHub Copilot",
"description": "GitHub Copilot (VS Code) — markdown config format; copilot-inline hook surface; no hook events emitted; flat skill nesting (unconfirmed recursive loader); tier-2 support.",
"tier": "core",

View File

@@ -1,7 +1,7 @@
{
"id": "cursor",
"role": "runtime",
"version": "1.6.0",
"version": "1.6.1",
"title": "Cursor",
"description": "Cursor IDE — skills + converted commands artifact layout; hooks.json surface; Claude hook event dialect; recursive skill loader (flat nesting); tier-2 support.",
"tier": "core",

View File

@@ -1,7 +1,7 @@
{
"id": "drift",
"role": "feature",
"version": "1.6.0",
"version": "1.6.1",
"title": "Drift detection gates",
"description": "Drift detection gates for the planning loop. At execute:wave:post: a blocking schema drift gate (detects schema files changed without a database push) and a non-blocking codebase drift gate (detects structural additions not reflected in STRUCTURE.md). At plan:pre: a non-blocking, warn-only codebase drift gate (gated on workflow.plan_drift_precheck) that flags a stale codebase map before planning, so plans are authored against a fresh STRUCTURE.md instead of discovering drift mid-execution.",
"tier": "full",

View File

@@ -1,7 +1,7 @@
{
"id": "gap-analysis",
"role": "feature",
"version": "1.6.0",
"version": "1.6.1",
"title": "Post-planning gap analysis",
"description": "Proactive, non-blocking post-planning coverage report. After all PLAN.md files are generated, cross-references every REQ-ID and D-ID from REQUIREMENTS.md and CONTEXT.md against plan bodies. Emits a Source | Item | Status table. Does not block phase advancement.",
"tier": "standard",

View File

@@ -1,7 +1,7 @@
{
"id": "gemini",
"role": "runtime",
"version": "1.6.0",
"version": "1.6.1",
"title": "Gemini CLI",
"description": "Google Gemini CLI — commands-only artifact layout (TOML); Gemini hook event dialect; settings-json hook surface; tier-2 support.",
"tier": "core",

View File

@@ -1,7 +1,7 @@
{
"id": "graphify",
"role": "feature",
"version": "1.6.0",
"version": "1.6.1",
"title": "Knowledge graph",
"description": "Build, query, and inspect the project knowledge graph in `.planning/graphs/`; exposes graphify CLI subcommands (build, query, status, diff) and the /gsd-graphify skill.",
"tier": "full",

View File

@@ -1,7 +1,7 @@
{
"id": "hermes",
"role": "runtime",
"version": "1.6.0",
"version": "1.6.1",
"title": "Hermes Agent",
"description": "Hermes Agent (NousResearch) — skills nest under skills/gsd/ category bucket; nested skill layout; settings-json hook surface; Claude hook event dialect; tier-2 support.",
"tier": "core",

View File

@@ -1,7 +1,7 @@
{
"id": "intel",
"role": "feature",
"version": "1.6.0",
"version": "1.6.1",
"title": "Codebase intelligence",
"description": "Code-intelligence store for codebase querying, diff, snapshot, and API-surface extraction; exposes `gsd-tools intel` subcommands (query, status, update, diff, snapshot, patch-meta, validate, extract-exports, api-surface) and backs `/gsd-map-codebase` and `gsd-intel-updater`.",
"tier": "full",

View File

@@ -1,7 +1,7 @@
{
"id": "kilo",
"role": "runtime",
"version": "1.6.0",
"version": "1.6.1",
"title": "Kilo Code",
"description": "Kilo Code — XDG-based config dir; global skills at ~/.kilo/skills (separate from XDG config); flat command/ + skills artifact layout; no lifecycle hook registration; tier-2 support.",
"tier": "core",

View File

@@ -1,7 +1,7 @@
{
"id": "kimi",
"role": "runtime",
"version": "1.6.0",
"version": "1.6.1",
"title": "Kimi CLI",
"description": "Kimi CLI (Moonshot AI) — generic agents root at ~/.config/agents; skills + kimi-agents artifact layout; no hook surface; no hook events; tier-2 support.",
"tier": "core",

View File

@@ -1,7 +1,7 @@
{
"id": "mempalace",
"role": "feature",
"version": "1.6.0",
"version": "1.6.1",
"title": "MemPalace memory",
"description": "Cross-session, cross-project memory: deliberate recall before discuss/plan and verbatim capture + temporal-KG sync at phase boundaries, via the MemPalace MCP server and CLI.",
"tier": "full",

View File

@@ -1,7 +1,7 @@
{
"id": "nyquist",
"role": "feature",
"version": "1.6.0",
"version": "1.6.1",
"title": "Nyquist validation",
"description": "Validation coverage audit that maps executed work back to tests and manual-only evidence.",
"tier": "full",

View File

@@ -1,7 +1,7 @@
{
"id": "opencode",
"role": "runtime",
"version": "1.6.0",
"version": "1.6.1",
"title": "OpenCode",
"description": "OpenCode — XDG-based config dir; flat command/ + skills artifact layout; settings-json config format; no lifecycle hook registration; tier-2 support.",
"tier": "core",

View File

@@ -1,7 +1,7 @@
{
"id": "pattern-mapper",
"role": "feature",
"version": "1.6.0",
"version": "1.6.1",
"title": "Pattern mapping",
"description": "Optional codebase-pattern mapping before planning; owns the pattern mapper agent and workflow.pattern_mapper activation key.",
"tier": "full",

View File

@@ -1,7 +1,7 @@
{
"id": "profile-pipeline",
"role": "feature",
"version": "1.6.0",
"version": "1.6.1",
"title": "Developer profiling pipeline",
"description": "Developer behavioral profiling from Claude Code session history; scans session JSONL files, extracts and samples user messages, and generates profile artifacts (USER-PROFILE.md, dev-preferences.md, CLAUDE.md sections). Exposes eight `gsd-tools` commands: scan-sessions, extract-messages, profile-sample (pipeline phase) and write-profile, profile-questionnaire, generate-dev-preferences, generate-claude-profile, generate-claude-md (output phase). Backs the /gsd-profile-user skill and gsd-user-profiler agent.",
"tier": "full",

View File

@@ -1,7 +1,7 @@
{
"id": "qwen",
"role": "runtime",
"version": "1.6.0",
"version": "1.6.1",
"title": "Qwen Code",
"description": "Qwen Code (Alibaba) — nested-skill artifact layout; settings-json hook surface; Claude hook event dialect; tier-2 support.",
"tier": "core",

View File

@@ -1,7 +1,7 @@
{
"id": "research",
"role": "feature",
"version": "1.6.0",
"version": "1.6.1",
"title": "Phase research",
"description": "Optional phase research before planning; owns the phase researcher agent and workflow.research activation key.",
"tier": "standard",

View File

@@ -1,7 +1,7 @@
{
"id": "schema-gate",
"role": "feature",
"version": "1.6.0",
"version": "1.6.1",
"title": "Schema push detection gate",
"description": "Detects ORM schema-relevant files in the phase scope during planning and injects a mandatory [BLOCKING] schema push task into the plan. Prevents false-positive verification where build/types pass because TypeScript types come from config, not the live database.",
"tier": "full",

View File

@@ -1,7 +1,7 @@
{
"id": "security",
"role": "feature",
"version": "1.6.0",
"version": "1.6.1",
"title": "Security enforcement",
"description": "Threat mitigation verification and ship-time security blocking for phases with security enforcement enabled.",
"tier": "full",

View File

@@ -1,7 +1,7 @@
{
"id": "tdd",
"role": "feature",
"version": "1.6.0",
"version": "1.6.1",
"title": "Test-driven development",
"description": "Injects TDD heuristics into the planner and enforces RED/GREEN gate compliance on type:tdd plans after execution. Owns workflow.tdd_mode; the --tdd CLI flag is the ephemeral override.",
"tier": "full",

View File

@@ -1,7 +1,7 @@
{
"id": "trae",
"role": "runtime",
"version": "1.6.0",
"version": "1.6.1",
"title": "Trae IDE",
"description": "Trae IDE — nested-skill artifact layout; no hook surface (profile-marker-only config); tier-2 support.",
"tier": "core",

View File

@@ -1,7 +1,7 @@
{
"id": "ui",
"role": "feature",
"version": "1.6.0",
"version": "1.6.1",
"title": "UI design contracts",
"description": "UI-SPEC design contract + retrospective UI audit for frontend phases.",
"tier": "full",

View File

@@ -1,7 +1,7 @@
{
"id": "windsurf",
"role": "runtime",
"version": "1.6.0",
"version": "1.6.1",
"title": "Windsurf",
"description": "Windsurf (Codeium) — workspace workflow artifact layout for slash commands; no hook surface; no hook events; tier-2 support.",
"tier": "core",

View File

@@ -1355,13 +1355,13 @@ When `runtime` is set, profile tiers (`opus`/`sonnet`/`haiku`) resolve to runtim
| Runtime | `opus` | `sonnet` | `haiku` | reasoning_effort |
|---------|--------|----------|---------|------------------|
| `claude` | `claude-opus-4-8` | `claude-sonnet-4-6` | `claude-haiku-4-5` | (not used) |
| `claude` | `claude-opus-4-8` | `claude-sonnet-5` | `claude-haiku-4-5` | (not used) |
| `codex` | `gpt-5.5` | `gpt-5.4` | `gpt-5.4-mini` | `xhigh` / `medium` / `medium` |
| `gemini` | `gemini-3.1-pro-preview` | `gemini-3-flash` | `gemini-2.5-flash-lite` | (not used) |
| `qwen` | `qwen3-max-2026-01-23` | `qwen3-coder-plus` | `qwen3-coder-next` | (not used) |
| `opencode` | `anthropic/claude-opus-4-8` | `anthropic/claude-sonnet-4-6` | `anthropic/claude-haiku-4-5` | (not used) |
| `copilot` | `claude-opus-4-8` | `claude-sonnet-4-6` | `claude-haiku-4-5` | (not used) |
| `hermes` | `anthropic/claude-opus-4-8` | `anthropic/claude-sonnet-4-6` | `anthropic/claude-haiku-4-5` | (not used) |
| `opencode` | `anthropic/claude-opus-4-8` | `anthropic/claude-sonnet-5` | `anthropic/claude-haiku-4-5` | (not used) |
| `copilot` | `claude-opus-4-8` | `claude-sonnet-5` | `claude-haiku-4-5` | (not used) |
| `hermes` | `anthropic/claude-opus-4-8` | `anthropic/claude-sonnet-5` | `anthropic/claude-haiku-4-5` | (not used) |
| Group B (`kilo`, `cline`, `cursor`, `windsurf` (alias: `devin-desktop`), `augment`, `trae`, `codebuddy`, `antigravity`) | (no built-in default — your runtime handles model selection) | | | |
> **How these model IDs are sourced.** The catalog (`bin/shared/model-catalog.json`) pins each runtime's tier defaults to that provider's current frontier IDs, and may intentionally carry forward-dated IDs ahead of a provider's public docs. To verify an ID is live before changing it, check the provider's own source/API — e.g. Gemini: gemini-cli `packages/core/src/config/models.ts` or `gemini --model <id> --prompt ping`; Codex: `codex debug models` or the OpenAI Codex models page; Qwen: Alibaba Model Studio model list. Only change an ID that the provider actually rejects — absence from documentation alone is not proof of invalidity.

View File

@@ -1139,13 +1139,13 @@ A saída JSON de `resolve-model` inclui `reasoning_effort` quando o nível de ru
| Runtime | `opus` | `sonnet` | `haiku` | reasoning_effort |
|---------|--------|----------|---------|------------------|
| `claude` | `claude-opus-4-8` | `claude-sonnet-4-6` | `claude-haiku-4-5` | (não usado) |
| `claude` | `claude-opus-4-8` | `claude-sonnet-5` | `claude-haiku-4-5` | (não usado) |
| `codex` | `gpt-5.5` | `gpt-5.3-codex` | `gpt-5.4-mini` | `xhigh` / `medium` / `medium` |
| `gemini` | `gemini-3-pro` | `gemini-3-flash` | `gemini-2.5-flash-lite` | (não usado) |
| `qwen` | `qwen3-max-2026-01-23` | `qwen3-coder-plus` | `qwen3-coder-next` | (não usado) |
| `opencode` | `anthropic/claude-opus-4-8` | `anthropic/claude-sonnet-4-6` | `anthropic/claude-haiku-4-5` | (não usado) |
| `copilot` | `claude-opus-4-8` | `claude-sonnet-4-6` | `claude-haiku-4-5` | (não usado) |
| `hermes` | `anthropic/claude-opus-4-8` | `anthropic/claude-sonnet-4-6` | `anthropic/claude-haiku-4-5` | (não usado) |
| `opencode` | `anthropic/claude-opus-4-8` | `anthropic/claude-sonnet-5` | `anthropic/claude-haiku-4-5` | (não usado) |
| `copilot` | `claude-opus-4-8` | `claude-sonnet-5` | `claude-haiku-4-5` | (não usado) |
| `hermes` | `anthropic/claude-opus-4-8` | `anthropic/claude-sonnet-5` | `anthropic/claude-haiku-4-5` | (não usado) |
| Grupo B (`kilo`, `cline`, `cursor`, `windsurf`, `augment`, `trae`, `codebuddy`, `antigravity`) | (sem padrão integrado — seu runtime trata da seleção de modelo) | | | |
**Exemplo Codex** — uma configuração, modelos em nível, sem bloco grande de `model_overrides`:

View File

@@ -1108,13 +1108,13 @@ minimal < low < medium < high < xhigh < max
| 运行时 | `opus` | `sonnet` | `haiku` | reasoning_effort |
|---------|--------|----------|---------|------------------|
| `claude` | `claude-opus-4-8` | `claude-sonnet-4-6` | `claude-haiku-4-5` | (不使用) |
| `claude` | `claude-opus-4-8` | `claude-sonnet-5` | `claude-haiku-4-5` | (不使用) |
| `codex` | `gpt-5.5` | `gpt-5.3-codex` | `gpt-5.4-mini` | `xhigh` / `medium` / `medium` |
| `gemini` | `gemini-3-pro` | `gemini-3-flash` | `gemini-2.5-flash-lite` | (不使用) |
| `qwen` | `qwen3-max-2026-01-23` | `qwen3-coder-plus` | `qwen3-coder-next` | (不使用) |
| `opencode` | `anthropic/claude-opus-4-8` | `anthropic/claude-sonnet-4-6` | `anthropic/claude-haiku-4-5` | (不使用) |
| `copilot` | `claude-opus-4-8` | `claude-sonnet-4-6` | `claude-haiku-4-5` | (不使用) |
| `hermes` | `anthropic/claude-opus-4-8` | `anthropic/claude-sonnet-4-6` | `anthropic/claude-haiku-4-5` | (不使用) |
| `opencode` | `anthropic/claude-opus-4-8` | `anthropic/claude-sonnet-5` | `anthropic/claude-haiku-4-5` | (不使用) |
| `copilot` | `claude-opus-4-8` | `claude-sonnet-5` | `claude-haiku-4-5` | (不使用) |
| `hermes` | `anthropic/claude-opus-4-8` | `anthropic/claude-sonnet-5` | `anthropic/claude-haiku-4-5` | (不使用) |
| B 组(`kilo`、`cline`、`cursor`、`windsurf`、`augment`、`trae`、`codebuddy`、`antigravity`) | (无内置默认——您的运行时处理模型选择) | | | |
**Codex 示例** — 单个配置,分层模型,无大型 `model_overrides` 块:

View File

@@ -1,6 +1,6 @@
{
"name": "gsd-core",
"version": "1.6.0",
"version": "1.6.1",
"description": "GSD Core — a meta-prompting, context engineering, and spec-driven development system for AI coding agents. Loads gsd's operating context into every Gemini CLI session.",
"contextFileName": "GEMINI.md"
}

View File

@@ -10,7 +10,7 @@ const capabilities = {
"ai-integration": {
"id": "ai-integration",
"role": "feature",
"version": "1.6.0",
"version": "1.6.1",
"title": "AI design contract",
"description": "AI-SPEC design contract workflow for phases that build AI systems; owns the AI integration command, agents, and workflow.ai_integration_phase activation key.",
"tier": "full",
@@ -63,7 +63,7 @@ const capabilities = {
"antigravity": {
"id": "antigravity",
"role": "runtime",
"version": "1.6.0",
"version": "1.6.1",
"title": "Antigravity",
"description": "Google Antigravity IDE — nested under ~/.gemini/antigravity; probed across 1.x and 2.x layouts; Gemini hook event dialect; flat skill layout; tier-1 support.",
"tier": "core",
@@ -123,7 +123,7 @@ const capabilities = {
"audit": {
"id": "audit",
"role": "feature",
"version": "1.6.0",
"version": "1.6.1",
"title": "Audit",
"description": "Open-artifact audit and UAT-gap audit for milestone close gates; exposes `gsd-tools audit-uat` (cross-phase UAT outstanding items) and `gsd-tools audit-open` (structured open-artifact scan across debug, tasks, threads, todos, seeds, UAT, verification, context-questions).",
"tier": "full",
@@ -160,7 +160,7 @@ const capabilities = {
"augment": {
"id": "augment",
"role": "runtime",
"version": "1.6.0",
"version": "1.6.1",
"title": "Augment Code",
"description": "Augment Code CLI — commands + nested-skill artifact layout; settings-json hook surface; Claude hook event dialect; tier-2 support.",
"tier": "core",
@@ -229,7 +229,7 @@ const capabilities = {
"claude": {
"id": "claude",
"role": "runtime",
"version": "1.6.0",
"version": "1.6.1",
"title": "Claude Code",
"description": "Anthropic Claude Code — primary development runtime; tier-1 support with full hook surface and skills-based global install.",
"tier": "core",
@@ -295,7 +295,7 @@ const capabilities = {
"cline": {
"id": "cline",
"role": "runtime",
"version": "1.6.0",
"version": "1.6.1",
"title": "Cline",
"description": "Cline (VS Code extension) — global-only nested-skill layout; cline-rules hook surface (.clinerules); no hook events emitted; tier-2 support.",
"tier": "core",
@@ -338,7 +338,7 @@ const capabilities = {
"code-review": {
"id": "code-review",
"role": "feature",
"version": "1.6.0",
"version": "1.6.1",
"title": "Code review",
"description": "Source-file code review and review-fix workflow support for completed execution work.",
"tier": "full",
@@ -399,7 +399,7 @@ const capabilities = {
"codebuddy": {
"id": "codebuddy",
"role": "runtime",
"version": "1.6.0",
"version": "1.6.1",
"title": "CodeBuddy",
"description": "CodeBuddy (Tencent) — converted commands + skills artifact layout; settings-json hook surface; Claude hook event dialect; tier-2 support.",
"tier": "core",
@@ -468,7 +468,7 @@ const capabilities = {
"codex": {
"id": "codex",
"role": "runtime",
"version": "1.6.0",
"version": "1.6.1",
"title": "OpenAI Codex CLI",
"description": "OpenAI Codex CLI — shell-var command style; per-agent sandbox tiers; config.toml + hooks.json hook surface; tier-1 support.",
"tier": "core",
@@ -521,7 +521,7 @@ const capabilities = {
"copilot": {
"id": "copilot",
"role": "runtime",
"version": "1.6.0",
"version": "1.6.1",
"title": "GitHub Copilot",
"description": "GitHub Copilot (VS Code) — markdown config format; copilot-inline hook surface; no hook events emitted; flat skill nesting (unconfirmed recursive loader); tier-2 support.",
"tier": "core",
@@ -574,7 +574,7 @@ const capabilities = {
"cursor": {
"id": "cursor",
"role": "runtime",
"version": "1.6.0",
"version": "1.6.1",
"title": "Cursor",
"description": "Cursor IDE — skills + converted commands artifact layout; hooks.json surface; Claude hook event dialect; recursive skill loader (flat nesting); tier-2 support.",
"tier": "core",
@@ -643,7 +643,7 @@ const capabilities = {
"drift": {
"id": "drift",
"role": "feature",
"version": "1.6.0",
"version": "1.6.1",
"title": "Drift detection gates",
"description": "Drift detection gates for the planning loop. At execute:wave:post: a blocking schema drift gate (detects schema files changed without a database push) and a non-blocking codebase drift gate (detects structural additions not reflected in STRUCTURE.md). At plan:pre: a non-blocking, warn-only codebase drift gate (gated on workflow.plan_drift_precheck) that flags a stale codebase map before planning, so plans are authored against a fresh STRUCTURE.md instead of discovering drift mid-execution.",
"tier": "full",
@@ -721,7 +721,7 @@ const capabilities = {
"gap-analysis": {
"id": "gap-analysis",
"role": "feature",
"version": "1.6.0",
"version": "1.6.1",
"title": "Post-planning gap analysis",
"description": "Proactive, non-blocking post-planning coverage report. After all PLAN.md files are generated, cross-references every REQ-ID and D-ID from REQUIREMENTS.md and CONTEXT.md against plan bodies. Emits a Source | Item | Status table. Does not block phase advancement.",
"tier": "standard",
@@ -762,7 +762,7 @@ const capabilities = {
"gemini": {
"id": "gemini",
"role": "runtime",
"version": "1.6.0",
"version": "1.6.1",
"title": "Gemini CLI",
"description": "Google Gemini CLI — commands-only artifact layout (TOML); Gemini hook event dialect; settings-json hook surface; tier-2 support.",
"tier": "core",
@@ -819,7 +819,7 @@ const capabilities = {
"graphify": {
"id": "graphify",
"role": "feature",
"version": "1.6.0",
"version": "1.6.1",
"title": "Knowledge graph",
"description": "Build, query, and inspect the project knowledge graph in `.planning/graphs/`; exposes graphify CLI subcommands (build, query, status, diff) and the /gsd-graphify skill.",
"tier": "full",
@@ -860,7 +860,7 @@ const capabilities = {
"hermes": {
"id": "hermes",
"role": "runtime",
"version": "1.6.0",
"version": "1.6.1",
"title": "Hermes Agent",
"description": "Hermes Agent (NousResearch) — skills nest under skills/gsd/ category bucket; nested skill layout; settings-json hook surface; Claude hook event dialect; tier-2 support.",
"tier": "core",
@@ -913,7 +913,7 @@ const capabilities = {
"intel": {
"id": "intel",
"role": "feature",
"version": "1.6.0",
"version": "1.6.1",
"title": "Codebase intelligence",
"description": "Code-intelligence store for codebase querying, diff, snapshot, and API-surface extraction; exposes `gsd-tools intel` subcommands (query, status, update, diff, snapshot, patch-meta, validate, extract-exports, api-surface) and backs `/gsd-map-codebase` and `gsd-intel-updater`.",
"tier": "full",
@@ -965,7 +965,7 @@ const capabilities = {
"kilo": {
"id": "kilo",
"role": "runtime",
"version": "1.6.0",
"version": "1.6.1",
"title": "Kilo Code",
"description": "Kilo Code — XDG-based config dir; global skills at ~/.kilo/skills (separate from XDG config); flat command/ + skills artifact layout; no lifecycle hook registration; tier-2 support.",
"tier": "core",
@@ -1040,7 +1040,7 @@ const capabilities = {
"kimi": {
"id": "kimi",
"role": "runtime",
"version": "1.6.0",
"version": "1.6.1",
"title": "Kimi CLI",
"description": "Kimi CLI (Moonshot AI) — generic agents root at ~/.config/agents; skills + kimi-agents artifact layout; no hook surface; no hook events; tier-2 support.",
"tier": "core",
@@ -1096,7 +1096,7 @@ const capabilities = {
"mempalace": {
"id": "mempalace",
"role": "feature",
"version": "1.6.0",
"version": "1.6.1",
"title": "MemPalace memory",
"description": "Cross-session, cross-project memory: deliberate recall before discuss/plan and verbatim capture + temporal-KG sync at phase boundaries, via the MemPalace MCP server and CLI.",
"tier": "full",
@@ -1270,7 +1270,7 @@ const capabilities = {
"nyquist": {
"id": "nyquist",
"role": "feature",
"version": "1.6.0",
"version": "1.6.1",
"title": "Nyquist validation",
"description": "Validation coverage audit that maps executed work back to tests and manual-only evidence.",
"tier": "full",
@@ -1320,7 +1320,7 @@ const capabilities = {
"opencode": {
"id": "opencode",
"role": "runtime",
"version": "1.6.0",
"version": "1.6.1",
"title": "OpenCode",
"description": "OpenCode — XDG-based config dir; flat command/ + skills artifact layout; settings-json config format; no lifecycle hook registration; tier-2 support.",
"tier": "core",
@@ -1390,7 +1390,7 @@ const capabilities = {
"pattern-mapper": {
"id": "pattern-mapper",
"role": "feature",
"version": "1.6.0",
"version": "1.6.1",
"title": "Pattern mapping",
"description": "Optional codebase-pattern mapping before planning; owns the pattern mapper agent and workflow.pattern_mapper activation key.",
"tier": "full",
@@ -1444,7 +1444,7 @@ const capabilities = {
"profile-pipeline": {
"id": "profile-pipeline",
"role": "feature",
"version": "1.6.0",
"version": "1.6.1",
"title": "Developer profiling pipeline",
"description": "Developer behavioral profiling from Claude Code session history; scans session JSONL files, extracts and samples user messages, and generates profile artifacts (USER-PROFILE.md, dev-preferences.md, CLAUDE.md sections). Exposes eight `gsd-tools` commands: scan-sessions, extract-messages, profile-sample (pipeline phase) and write-profile, profile-questionnaire, generate-dev-preferences, generate-claude-profile, generate-claude-md (output phase). Backs the /gsd-profile-user skill and gsd-user-profiler agent.",
"tier": "full",
@@ -1521,7 +1521,7 @@ const capabilities = {
"qwen": {
"id": "qwen",
"role": "runtime",
"version": "1.6.0",
"version": "1.6.1",
"title": "Qwen Code",
"description": "Qwen Code (Alibaba) — nested-skill artifact layout; settings-json hook surface; Claude hook event dialect; tier-2 support.",
"tier": "core",
@@ -1578,7 +1578,7 @@ const capabilities = {
"research": {
"id": "research",
"role": "feature",
"version": "1.6.0",
"version": "1.6.1",
"title": "Phase research",
"description": "Optional phase research before planning; owns the phase researcher agent and workflow.research activation key.",
"tier": "standard",
@@ -1630,7 +1630,7 @@ const capabilities = {
"schema-gate": {
"id": "schema-gate",
"role": "feature",
"version": "1.6.0",
"version": "1.6.1",
"title": "Schema push detection gate",
"description": "Detects ORM schema-relevant files in the phase scope during planning and injects a mandatory [BLOCKING] schema push task into the plan. Prevents false-positive verification where build/types pass because TypeScript types come from config, not the live database.",
"tier": "full",
@@ -1676,7 +1676,7 @@ const capabilities = {
"security": {
"id": "security",
"role": "feature",
"version": "1.6.0",
"version": "1.6.1",
"title": "Security enforcement",
"description": "Threat mitigation verification and ship-time security blocking for phases with security enforcement enabled.",
"tier": "full",
@@ -1775,7 +1775,7 @@ const capabilities = {
"tdd": {
"id": "tdd",
"role": "feature",
"version": "1.6.0",
"version": "1.6.1",
"title": "Test-driven development",
"description": "Injects TDD heuristics into the planner and enforces RED/GREEN gate compliance on type:tdd plans after execution. Owns workflow.tdd_mode; the --tdd CLI flag is the ephemeral override.",
"tier": "full",
@@ -1828,7 +1828,7 @@ const capabilities = {
"trae": {
"id": "trae",
"role": "runtime",
"version": "1.6.0",
"version": "1.6.1",
"title": "Trae IDE",
"description": "Trae IDE — nested-skill artifact layout; no hook surface (profile-marker-only config); tier-2 support.",
"tier": "core",
@@ -1880,7 +1880,7 @@ const capabilities = {
"ui": {
"id": "ui",
"role": "feature",
"version": "1.6.0",
"version": "1.6.1",
"title": "UI design contracts",
"description": "UI-SPEC design contract + retrospective UI audit for frontend phases.",
"tier": "full",
@@ -1975,7 +1975,7 @@ const capabilities = {
"windsurf": {
"id": "windsurf",
"role": "runtime",
"version": "1.6.0",
"version": "1.6.1",
"title": "Windsurf",
"description": "Windsurf (Codeium) — workspace workflow artifact layout for slash commands; no hook surface; no hook events; tier-2 support.",
"tier": "core",
@@ -2743,7 +2743,7 @@ const runtimes = {
"antigravity": {
"id": "antigravity",
"role": "runtime",
"version": "1.6.0",
"version": "1.6.1",
"title": "Antigravity",
"description": "Google Antigravity IDE — nested under ~/.gemini/antigravity; probed across 1.x and 2.x layouts; Gemini hook event dialect; flat skill layout; tier-1 support.",
"tier": "core",
@@ -2803,7 +2803,7 @@ const runtimes = {
"augment": {
"id": "augment",
"role": "runtime",
"version": "1.6.0",
"version": "1.6.1",
"title": "Augment Code",
"description": "Augment Code CLI — commands + nested-skill artifact layout; settings-json hook surface; Claude hook event dialect; tier-2 support.",
"tier": "core",
@@ -2872,7 +2872,7 @@ const runtimes = {
"claude": {
"id": "claude",
"role": "runtime",
"version": "1.6.0",
"version": "1.6.1",
"title": "Claude Code",
"description": "Anthropic Claude Code — primary development runtime; tier-1 support with full hook surface and skills-based global install.",
"tier": "core",
@@ -2938,7 +2938,7 @@ const runtimes = {
"cline": {
"id": "cline",
"role": "runtime",
"version": "1.6.0",
"version": "1.6.1",
"title": "Cline",
"description": "Cline (VS Code extension) — global-only nested-skill layout; cline-rules hook surface (.clinerules); no hook events emitted; tier-2 support.",
"tier": "core",
@@ -2981,7 +2981,7 @@ const runtimes = {
"codebuddy": {
"id": "codebuddy",
"role": "runtime",
"version": "1.6.0",
"version": "1.6.1",
"title": "CodeBuddy",
"description": "CodeBuddy (Tencent) — converted commands + skills artifact layout; settings-json hook surface; Claude hook event dialect; tier-2 support.",
"tier": "core",
@@ -3050,7 +3050,7 @@ const runtimes = {
"codex": {
"id": "codex",
"role": "runtime",
"version": "1.6.0",
"version": "1.6.1",
"title": "OpenAI Codex CLI",
"description": "OpenAI Codex CLI — shell-var command style; per-agent sandbox tiers; config.toml + hooks.json hook surface; tier-1 support.",
"tier": "core",
@@ -3103,7 +3103,7 @@ const runtimes = {
"copilot": {
"id": "copilot",
"role": "runtime",
"version": "1.6.0",
"version": "1.6.1",
"title": "GitHub Copilot",
"description": "GitHub Copilot (VS Code) — markdown config format; copilot-inline hook surface; no hook events emitted; flat skill nesting (unconfirmed recursive loader); tier-2 support.",
"tier": "core",
@@ -3156,7 +3156,7 @@ const runtimes = {
"cursor": {
"id": "cursor",
"role": "runtime",
"version": "1.6.0",
"version": "1.6.1",
"title": "Cursor",
"description": "Cursor IDE — skills + converted commands artifact layout; hooks.json surface; Claude hook event dialect; recursive skill loader (flat nesting); tier-2 support.",
"tier": "core",
@@ -3225,7 +3225,7 @@ const runtimes = {
"gemini": {
"id": "gemini",
"role": "runtime",
"version": "1.6.0",
"version": "1.6.1",
"title": "Gemini CLI",
"description": "Google Gemini CLI — commands-only artifact layout (TOML); Gemini hook event dialect; settings-json hook surface; tier-2 support.",
"tier": "core",
@@ -3282,7 +3282,7 @@ const runtimes = {
"hermes": {
"id": "hermes",
"role": "runtime",
"version": "1.6.0",
"version": "1.6.1",
"title": "Hermes Agent",
"description": "Hermes Agent (NousResearch) — skills nest under skills/gsd/ category bucket; nested skill layout; settings-json hook surface; Claude hook event dialect; tier-2 support.",
"tier": "core",
@@ -3335,7 +3335,7 @@ const runtimes = {
"kilo": {
"id": "kilo",
"role": "runtime",
"version": "1.6.0",
"version": "1.6.1",
"title": "Kilo Code",
"description": "Kilo Code — XDG-based config dir; global skills at ~/.kilo/skills (separate from XDG config); flat command/ + skills artifact layout; no lifecycle hook registration; tier-2 support.",
"tier": "core",
@@ -3410,7 +3410,7 @@ const runtimes = {
"kimi": {
"id": "kimi",
"role": "runtime",
"version": "1.6.0",
"version": "1.6.1",
"title": "Kimi CLI",
"description": "Kimi CLI (Moonshot AI) — generic agents root at ~/.config/agents; skills + kimi-agents artifact layout; no hook surface; no hook events; tier-2 support.",
"tier": "core",
@@ -3466,7 +3466,7 @@ const runtimes = {
"opencode": {
"id": "opencode",
"role": "runtime",
"version": "1.6.0",
"version": "1.6.1",
"title": "OpenCode",
"description": "OpenCode — XDG-based config dir; flat command/ + skills artifact layout; settings-json config format; no lifecycle hook registration; tier-2 support.",
"tier": "core",
@@ -3536,7 +3536,7 @@ const runtimes = {
"qwen": {
"id": "qwen",
"role": "runtime",
"version": "1.6.0",
"version": "1.6.1",
"title": "Qwen Code",
"description": "Qwen Code (Alibaba) — nested-skill artifact layout; settings-json hook surface; Claude hook event dialect; tier-2 support.",
"tier": "core",
@@ -3593,7 +3593,7 @@ const runtimes = {
"trae": {
"id": "trae",
"role": "runtime",
"version": "1.6.0",
"version": "1.6.1",
"title": "Trae IDE",
"description": "Trae IDE — nested-skill artifact layout; no hook surface (profile-marker-only config); tier-2 support.",
"tier": "core",
@@ -3645,7 +3645,7 @@ const runtimes = {
"windsurf": {
"id": "windsurf",
"role": "runtime",
"version": "1.6.0",
"version": "1.6.1",
"title": "Windsurf",
"description": "Windsurf (Codeium) — workspace workflow artifact layout for slash commands; no hook surface; no hook events; tier-2 support.",
"tier": "core",

View File

@@ -9,7 +9,7 @@
"runtimeTierDefaults": {
"claude": {
"opus": { "model": "claude-opus-4-8" },
"sonnet": { "model": "claude-sonnet-4-6" },
"sonnet": { "model": "claude-sonnet-5" },
"haiku": { "model": "claude-haiku-4-5" }
},
"codex": {
@@ -29,17 +29,17 @@
},
"opencode": {
"opus": { "model": "anthropic/claude-opus-4-8" },
"sonnet": { "model": "anthropic/claude-sonnet-4-6" },
"sonnet": { "model": "anthropic/claude-sonnet-5" },
"haiku": { "model": "anthropic/claude-haiku-4-5" }
},
"copilot": {
"opus": { "model": "claude-opus-4-8" },
"sonnet": { "model": "claude-sonnet-4-6" },
"sonnet": { "model": "claude-sonnet-5" },
"haiku": { "model": "claude-haiku-4-5" }
},
"hermes": {
"opus": { "model": "anthropic/claude-opus-4-8" },
"sonnet": { "model": "anthropic/claude-sonnet-4-6" },
"sonnet": { "model": "anthropic/claude-sonnet-5" },
"haiku": { "model": "anthropic/claude-haiku-4-5" }
},
"kilo": {
@@ -91,13 +91,13 @@
"providerPresets": {
"anthropic": {
"opus": { "low": { "model": "claude-opus-4-5" }, "medium": { "model": "claude-opus-4-8" }, "high": { "model": "claude-opus-4-8" } },
"sonnet": { "low": { "model": "claude-haiku-4-5" }, "medium": { "model": "claude-sonnet-4-6" }, "high": { "model": "claude-opus-4-8" } },
"haiku": { "low": { "model": "claude-haiku-4-5" }, "medium": { "model": "claude-haiku-4-5" }, "high": { "model": "claude-sonnet-4-6" } }
"sonnet": { "low": { "model": "claude-haiku-4-5" }, "medium": { "model": "claude-sonnet-5" }, "high": { "model": "claude-opus-4-8" } },
"haiku": { "low": { "model": "claude-haiku-4-5" }, "medium": { "model": "claude-haiku-4-5" }, "high": { "model": "claude-sonnet-5" } }
},
"anthropic-fable": {
"opus": { "low": { "model": "claude-opus-4-5" }, "medium": { "model": "claude-opus-4-8" }, "high": { "model": "claude-fable-5" } },
"sonnet": { "low": { "model": "claude-haiku-4-5" }, "medium": { "model": "claude-sonnet-4-6" }, "high": { "model": "claude-fable-5" } },
"haiku": { "low": { "model": "claude-haiku-4-5" }, "medium": { "model": "claude-haiku-4-5" }, "high": { "model": "claude-sonnet-4-6" } }
"sonnet": { "low": { "model": "claude-haiku-4-5" }, "medium": { "model": "claude-sonnet-5" }, "high": { "model": "claude-fable-5" } },
"haiku": { "low": { "model": "claude-haiku-4-5" }, "medium": { "model": "claude-haiku-4-5" }, "high": { "model": "claude-sonnet-5" } }
},
"openai": {
"opus": { "low": { "model": "gpt-5.4", "reasoning_effort": "medium" }, "medium": { "model": "gpt-5.5", "reasoning_effort": "high" }, "high": { "model": "gpt-5.5", "reasoning_effort": "xhigh" } },

View File

@@ -353,13 +353,13 @@ Built-in tier defaults by runtime:
| Runtime | `opus` | `sonnet` | `haiku` |
|------------|-------------------------------|---------------------------------|-------------------------------|
| `claude` | `claude-opus-4-8` | `claude-sonnet-4-6` | `claude-haiku-4-5` |
| `claude` | `claude-opus-4-8` | `claude-sonnet-5` | `claude-haiku-4-5` |
| `codex` | `gpt-5.5` | `gpt-5.4` | `gpt-5.4-mini` |
| `gemini` | `gemini-3.1-pro-preview` | `gemini-3-flash` | `gemini-2.5-flash-lite` |
| `qwen` | `qwen3-max-2026-01-23` | `qwen3-coder-plus` | `qwen3-coder-next` |
| `opencode` | `anthropic/claude-opus-4-8` | `anthropic/claude-sonnet-4-6` | `anthropic/claude-haiku-4-5` |
| `copilot` | `claude-opus-4-8` | `claude-sonnet-4-6` | `claude-haiku-4-5` |
| `hermes` | `anthropic/claude-opus-4-8` | `anthropic/claude-sonnet-4-6` | `anthropic/claude-haiku-4-5` |
| `opencode` | `anthropic/claude-opus-4-8` | `anthropic/claude-sonnet-5` | `anthropic/claude-haiku-4-5` |
| `copilot` | `claude-opus-4-8` | `claude-sonnet-5` | `claude-haiku-4-5` |
| `hermes` | `anthropic/claude-opus-4-8` | `anthropic/claude-sonnet-5` | `anthropic/claude-haiku-4-5` |
| Group B (`kilo`, `cline`, `cursor`, `windsurf`, `augment`, `trae`, `codebuddy`, `antigravity`) | (no built-in default — your runtime handles model selection) | | |
Display a table to the user showing the effective configuration:
@@ -623,8 +623,8 @@ AskUserQuestion([
header: "Provider",
multiSelect: false,
options: [
{ label: "anthropic", description: "claude-opus-4-8 / claude-sonnet-4-6 / claude-haiku-4-5 (Anthropic / Claude)" },
{ label: "anthropic-fable", description: "claude-fable-5 / claude-sonnet-4-6 / claude-haiku-4-5 (Anthropic / Claude Fable opt-in)" },
{ label: "anthropic", description: "claude-opus-4-8 / claude-sonnet-5 / claude-haiku-4-5 (Anthropic / Claude)" },
{ label: "anthropic-fable", description: "claude-fable-5 / claude-sonnet-5 / claude-haiku-4-5 (Anthropic / Claude Fable opt-in)" },
{ label: "openai", description: "gpt-5.5 / gpt-5.4 / gpt-5.4-mini (OpenAI / Codex)" },
{ label: "Other known provider", description: "Type google or qwen; both still use the canonical tier mapping." }
]
@@ -654,11 +654,11 @@ Canonical tier mappings by provider and budget:
| Provider | Budget | high | medium | low |
|-----------|--------|----------------------------|----------------------------|----------------------------|
| anthropic | high | claude-opus-4-8 | claude-opus-4-8 | claude-sonnet-4-6 |
| anthropic | medium | claude-opus-4-8 | claude-sonnet-4-6 | claude-haiku-4-5 |
| anthropic | high | claude-opus-4-8 | claude-opus-4-8 | claude-sonnet-5 |
| anthropic | medium | claude-opus-4-8 | claude-sonnet-5 | claude-haiku-4-5 |
| anthropic | low | claude-haiku-4-5 | claude-haiku-4-5 | claude-haiku-4-5 |
| anthropic-fable | high | claude-fable-5 | claude-fable-5 | claude-sonnet-4-6 |
| anthropic-fable | medium | claude-opus-4-8 | claude-sonnet-4-6 | claude-haiku-4-5 |
| anthropic-fable | high | claude-fable-5 | claude-fable-5 | claude-sonnet-5 |
| anthropic-fable | medium | claude-opus-4-8 | claude-sonnet-5 | claude-haiku-4-5 |
| anthropic-fable | low | claude-haiku-4-5 | claude-haiku-4-5 | claude-haiku-4-5 |
| openai | high | gpt-5.5 | gpt-5.5 | gpt-5.5 |
| openai | medium | gpt-5.5 | gpt-5.4 | gpt-5.4-mini |

4
package-lock.json generated
View File

@@ -1,12 +1,12 @@
{
"name": "@opengsd/gsd-core",
"version": "1.6.0",
"version": "1.6.1",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@opengsd/gsd-core",
"version": "1.6.0",
"version": "1.6.1",
"license": "MIT",
"dependencies": {
"@anthropic-ai/claude-agent-sdk": "^0.2.84",

View File

@@ -1,6 +1,6 @@
{
"name": "@opengsd/gsd-core",
"version": "1.6.0",
"version": "1.6.1",
"description": "GSD Core is a meta-prompting, context engineering, and spec-driven development system for AI coding agents.",
"bin": {
"gsd-core": "bin/install.js",

View File

@@ -184,6 +184,13 @@ function cmdMilestoneComplete(cwd: string, version: string, options: MilestoneCo
})();
while ((pm = phasePattern.exec(scopedContent)) !== null) {
const phaseNum = pm[1];
// Phase 0 (pre-milestone) and Phase 999 (backlog) are sentinels, not
// real phases — they legitimately have no directory and must not block
// milestone completion. Mirrors the engine-wide sentinel convention
// (phase-id getMilestoneFromPhaseId, roadmap-command-router SENTINELS,
// the #1445 /^999/ progress filters). (#1580)
const major = parseInt(phaseNum, 10);
if (major === 0 || major === 999) continue;
const normalized = normalizePhaseName(phaseNum);
// A phase has disk_status: 'no_directory' when no phase directory
// with a matching token exists on disk. Use the same phaseTokenMatches

View File

@@ -1649,7 +1649,19 @@ function cmdPhaseComplete(cwd: string, phaseNum: string, raw: boolean): void {
if (isLastPhase && roadmapContent !== null) {
try {
const roadmapForPhases = extractCurrentMilestone(roadmapContent, cwd);
const phasePattern = /#{2,4}\s*Phase\s+(\d+[A-Z]?(?:\.\d+)*)\s*:\s*([^\n]+)/gi;
// #1591: match BOTH heading-style phases (`### Phase N:`) AND
// checkbox-list items, INCLUDING the canonical bold form the roadmap
// template emits (`- [ ] **Phase N: Name**`). When the active
// milestone's checklist is `- [ ]` items inside a <details> block
// (and the next phase has no directory yet, so the disk-based
// resolver finds nothing), this roadmap-enumeration fallback is the
// only path that can find the next phase. The prior heading-only
// pattern missed checkbox items, and a checkbox-only broadening still
// missed the bold template rows → is_last_phase=true on a mid-milestone
// phase. Allow optional `**`/`__` emphasis after the marker and stop
// the name capture at emphasis so bold names slug cleanly; the number
// capture is unchanged.
const phasePattern = /(?:#{2,4}|-\s*\[[ xX]\])\s*(?:\*\*|__)?\s*Phase\s+(\d+[A-Z]?(?:\.\d+)*)\s*:\s*([^\n*]+)/gi;
let pm: RegExpExecArray | null;
while ((pm = phasePattern.exec(roadmapForPhases)) !== null) {
if (comparePhaseNum(pm[1], phaseNum) > 0) {

View File

@@ -318,6 +318,16 @@ function cmdRoadmapAnalyze(cwd: string, raw: boolean): void {
}> = [];
let match: RegExpExecArray | null;
// Phase 0 (pre-milestone) and Phase 999 (backlog) are sentinels, not real
// phases. They legitimately have no directory and must never be surfaced as
// current/next phase or counted in phase_count. Mirrors the engine-wide
// sentinel convention (phase-id getMilestoneFromPhaseId, roadmap-command-router
// SENTINELS, the #1445 /^999/ progress filters). (#1580)
const isSentinelPhase = (num: string): boolean => {
const major = parseInt(num, 10);
return major === 0 || major === 999;
};
// Build phase directory lookup once (O(1) readdir instead of O(N) per phase)
const _phaseDirNames = (() => {
try {
@@ -329,6 +339,7 @@ function cmdRoadmapAnalyze(cwd: string, raw: boolean): void {
while ((match = phasePattern.exec(content)) !== null) {
const phaseNum = match[1];
if (isSentinelPhase(phaseNum)) continue;
const phaseName = match[2].replace(/\(INSERTED\)/i, '').trim();
// Extract goal from the section
@@ -437,7 +448,7 @@ function cmdRoadmapAnalyze(cwd: string, raw: boolean): void {
checklistPhases.add(checklistMatch[1]);
}
const detailPhases = new Set(phases.map(p => p.number));
const missingDetails = [...checklistPhases].filter(p => !detailPhases.has(p));
const missingDetails = [...checklistPhases].filter(p => !detailPhases.has(p) && !isSentinelPhase(p));
const result = {
milestones,

View File

@@ -253,6 +253,15 @@ export function isManagedHookCommand(commandText: unknown, opts: { surface?: str
return false;
}
/**
* Detect a `"$VAR"/rest` anchored hook-script token — a path whose leading
* shell variable is already double-quoted with the remainder left bare (the
* shape `projectLocalHookPrefix` emits for local installs, e.g.
* `"$CLAUDE_PROJECT_DIR"/.claude/hooks/gsd-x.js`). Such a token is ALREADY a
* valid, correctly-quoted shell argument and must never be re-quoted.
*/
const ANCHORED_HOOK_SCRIPT_TOKEN = /^"\$[A-Za-z_][A-Za-z0-9_]*"\//;
/**
* Projection helper for legacy settings.json hook rewrites.
*
@@ -275,8 +284,20 @@ export function projectLegacySettingsHookCommand({
}): string | null {
if (!absoluteRunner || !scriptPath) return null;
const normalizedScriptPath = platform === 'win32' ? scriptPath.replace(/\\/g, '/') : scriptPath;
// #1693: a script path already carrying a `"$CLAUDE_PROJECT_DIR"`-anchored
// quoted prefix (local installs) is already a valid shell token — only the
// variable is quoted, the rest is bare. JSON.stringify-ing it on Windows
// yields `"\"$CLAUDE_PROJECT_DIR\"/..."` (escaped quotes inside an outer
// quote); node then receives an argument that *starts* with a `"`, treats it
// as relative, and dies with MODULE_NOT_FOUND. Emit anchored tokens verbatim;
// only bare absolute paths (which may contain spaces, e.g. "Program Files")
// need the JSON.stringify quoting. Scoped to win32: the non-Windows branch
// already preserves the caller's `scriptToken` (which is the bare anchored
// token for these inputs), so it never had the double-quote bug.
const commandScriptToken = platform === 'win32'
? JSON.stringify(normalizedScriptPath)
? (ANCHORED_HOOK_SCRIPT_TOKEN.test(normalizedScriptPath)
? normalizedScriptPath
: JSON.stringify(normalizedScriptPath))
: (scriptToken || JSON.stringify(normalizedScriptPath));
return projectShellCommandText({
runnerToken: absoluteRunner,

View File

@@ -187,3 +187,92 @@ describe('bug #3439: shell projection module owns managed-hook policy and legacy
);
});
});
describe('#1693 regression: Windows legacy-node rewrite must not double-quote a "$CLAUDE_PROJECT_DIR"-anchored local hook path', () => {
const winRunner = '"C:/Program Files/nodejs/node.exe"';
// WHY: a local-install hook path already carries a `"$CLAUDE_PROJECT_DIR"`
// anchored prefix (only the variable quoted, rest bare). On Windows the legacy
// rewrite previously JSON.stringify'd the whole token, yielding
// `"\"$CLAUDE_PROJECT_DIR\"/..."`. node then received an argument starting with
// a literal `"`, treated it as relative, and died with MODULE_NOT_FOUND —
// breaking every node managed hook at once (self-locking deadlock).
test('projectLegacySettingsHookCommand emits the anchored path verbatim, not re-quoted', () => {
const anchored = '"$CLAUDE_PROJECT_DIR"/.claude/hooks/gsd-context-monitor.js';
const command = projectLegacySettingsHookCommand({
absoluteRunner: winRunner,
scriptPath: anchored,
scriptToken: anchored,
platform: 'win32',
runtime: 'claude',
});
assert.equal(
command,
'"C:/Program Files/nodejs/node.exe" "$CLAUDE_PROJECT_DIR"/.claude/hooks/gsd-context-monitor.js',
);
assert.ok(!command.includes('\\"'), 'must not contain escaped double-quotes');
});
// WHY: the fix must be surgical — a BARE absolute Windows path (no anchored
// prefix) can contain spaces ("Program Files") and still REQUIRES quoting.
test('projectLegacySettingsHookCommand still quotes a bare absolute Windows path', () => {
const abs = 'C:/Program Files App/.claude/hooks/gsd-context-monitor.js';
const command = projectLegacySettingsHookCommand({
absoluteRunner: winRunner,
scriptPath: abs,
scriptToken: JSON.stringify(abs),
platform: 'win32',
runtime: 'claude',
});
assert.equal(
command,
'"C:/Program Files/nodejs/node.exe" "C:/Program Files App/.claude/hooks/gsd-context-monitor.js"',
);
});
// WHY: the anchored short-circuit is scoped to win32. On POSIX the rewrite
// already preserved the caller's original `scriptToken` and never had the
// double-quote bug, so that behavior must be left byte-identical. scriptPath
// is anchored but scriptToken is a DISTINCT single-quoted value: if the
// win32 gate were removed, the anchored short-circuit would emit scriptPath
// and this assertion would fail — that is what pins the gate.
test('projectLegacySettingsHookCommand preserves the original scriptToken for anchored paths on POSIX', () => {
const command = projectLegacySettingsHookCommand({
absoluteRunner: '"/usr/local/bin/node"',
scriptPath: '"$CLAUDE_PROJECT_DIR"/.claude/hooks/gsd-statusline.js',
scriptToken: "'/x/hooks/gsd-statusline.js'",
platform: 'linux',
runtime: 'claude',
});
assert.equal(command, `"/usr/local/bin/node" '/x/hooks/gsd-statusline.js'`);
});
// WHY: end-to-end through the installer rewrite — the actual #2979 path that
// ran during the user's 1.5.0 -> 1.6.0 local update. Managed node hooks get
// the absolute runner + clean anchored path; a non-node-prefixed managed .sh
// hook (already correct) is left untouched.
test('rewriteLegacyManagedNodeHookCommands produces clean anchored node commands on Windows', () => {
const settings = {
hooks: {
PostToolUse: [
{
hooks: [
{ command: 'node "$CLAUDE_PROJECT_DIR"/.claude/hooks/gsd-context-monitor.js' },
],
},
],
},
};
const changed = rewriteLegacyManagedNodeHookCommands(settings, winRunner, {
platform: 'win32',
runtime: 'claude',
});
assert.equal(changed, true);
const rewritten = settings.hooks.PostToolUse[0].hooks[0].command;
assert.equal(
rewritten,
'"C:/Program Files/nodejs/node.exe" "$CLAUDE_PROJECT_DIR"/.claude/hooks/gsd-context-monitor.js',
);
assert.ok(!rewritten.includes('\\"'), 'rewritten command must not contain escaped double-quotes');
});
});

View File

@@ -19,10 +19,13 @@ const { runGsdTools, createTempProject, cleanup } = require('./helpers.cjs');
* Build a fixture where the guard will fire:
* - STATE.md has `milestone: <version>` so the guard's version-match check is
* satisfied.
* - ROADMAP.md lists a `### Phase 999.1: Backlog Work` heading for that
* milestone, but there is NO on-disk phase directory for it.
* - ROADMAP.md lists a `### Phase 2: Real Work` heading for that milestone, but
* there is NO on-disk phase directory for it.
*
* This guarantees "unstarted phase" detection without touching any real phases.
* NOTE: the unstarted phase must be a REAL phase number — Phase 0 and Phase 999
* are backlog/pre-milestone sentinels that are intentionally excluded from this
* guard (#1580), so they would not fire it.
*/
function makeGuardFixture(tmpDir, version) {
// STATE.md with frontmatter milestone field matching the version
@@ -32,10 +35,10 @@ function makeGuardFixture(tmpDir, version) {
);
// ROADMAP.md — the heading must include the version so getMilestonePhaseFilter
// does not return missingExplicitVersion. Phase 999.1 has no on-disk dir.
// does not return missingExplicitVersion. Phase 2 has no on-disk dir.
fs.writeFileSync(
path.join(tmpDir, '.planning', 'ROADMAP.md'),
`# Roadmap ${version}\n\n### Phase 999.1: Backlog Work\n**Goal:** Not started\n`,
`# Roadmap ${version}\n\n### Phase 2: Real Work\n**Goal:** Not started\n`,
);
}
@@ -80,7 +83,7 @@ describe('bug-978: milestone complete --force overrides unstarted-phase guard',
const output = JSON.parse(result.output);
assert.strictEqual(output.version, 'v1.0');
// Milestone entry should have been created even though phase 999.1 has no dir
// Milestone entry should have been created even though phase 2 has no dir
assert.ok(
fs.existsSync(path.join(tmpDir, '.planning', 'MILESTONES.md')),
'MILESTONES.md should have been created',

View File

@@ -142,8 +142,8 @@ describe('#49 resolveModelPolicy Sub-path B: provider presets', () => {
test('known provider "anthropic-fable" + tier "haiku" + budget "high" keeps low tier on Sonnet', () => {
const policy = { provider: 'anthropic-fable', budget: 'high' };
const result = resolveModelPolicy(policy, 'haiku');
assert.strictEqual(result, 'claude-sonnet-4-6',
`expected anthropic-fable haiku/high to resolve to claude-sonnet-4-6, got: ${result}`);
assert.strictEqual(result, 'claude-sonnet-5',
`expected anthropic-fable haiku/high to resolve to claude-sonnet-5, got: ${result}`);
});
test('known provider "openai" + tier "sonnet" + budget "low" returns model with reasoning_effort from preset', () => {

View File

@@ -16,6 +16,14 @@
* Scenarios:
* A. deriveProgressFromRoadmap with a progress table containing a 999.x row.
* B. state json total_phases via extractCurrentMilestone / roadmapPhaseCount.
*
* Follow-up #1580: the same `^999` (and Phase 0) sentinel exclusion was missing
* in two more code paths — `milestone complete`'s unstarted-phase guard
* (src/milestone.cts) and `roadmap analyze`'s next_phase routing + phase_count
* (src/roadmap.cts). Scenarios C and D below cover those.
*
* C. milestone complete is NOT blocked by a Phase 999 backlog heading.
* D. roadmap analyze never routes next_phase to 999 / never counts it.
*/
const { describe, test, beforeEach, afterEach } = require('node:test');
@@ -172,3 +180,116 @@ describe('bug #1445 — state json excludes 999.x phase headings from total_phas
);
});
});
// ─── Scenario C: milestone complete not blocked by a 999 backlog heading ─────
describe('fix #1580 — milestone complete ignores the 999 backlog sentinel', () => {
let tmpDir;
beforeEach(() => {
tmpDir = createTempProject('fix-1580-mc-');
const planning = path.join(tmpDir, '.planning');
// One real, on-disk phase + a directory-less Phase 999 backlog heading.
fs.writeFileSync(
path.join(planning, 'ROADMAP.md'),
[
'# Roadmap v1.0',
'## v1.0 Milestone',
'## Phases',
'- [x] **Phase 1: Foundation**',
'## Phase Details',
'### Phase 1: Foundation',
'**Goal:** build it',
'### Phase 999: Backlog / Someday',
'**Goal:** deferred, never executed',
].join('\n'),
'utf-8',
);
fs.writeFileSync(
path.join(planning, 'STATE.md'),
`---\nmilestone: v1.0\n---\n# State\n\n**Status:** In progress\n**Last Activity:** 2025-01-01\n**Last Activity Description:** Working\n`,
'utf-8',
);
const dir = path.join(planning, 'phases', '01-foundation');
fs.mkdirSync(dir, { recursive: true });
fs.writeFileSync(path.join(dir, 'PLAN.md'), '# Plan\n', 'utf-8');
});
afterEach(() => {
cleanup(tmpDir);
});
test('completes WITHOUT --force despite a Phase 999 backlog heading', () => {
const result = runGsdTools(
['milestone', 'complete', 'v1.0', '--name', 'Regression'],
tmpDir,
);
assert.ok(
result.success,
`milestone complete must not be blocked by the 999 sentinel; got error: ${result.error}`,
);
assert.ok(
!/Cannot mark milestone complete/.test(result.error || ''),
`the unstarted-phase guard must not fire on Phase 999. Got: ${result.error}`,
);
});
});
// ─── Scenario D: roadmap analyze never routes/ counts the 999 sentinel ────────
describe('fix #1580 — roadmap analyze excludes the 999 backlog sentinel', () => {
let tmpDir;
beforeEach(() => {
tmpDir = createTempProject('fix-1580-ra-');
const planning = path.join(tmpDir, '.planning');
fs.writeFileSync(
path.join(planning, 'ROADMAP.md'),
[
'# Roadmap v1.0',
'## v1.0 Milestone',
'## Phases',
'- [x] **Phase 1: Foundation**',
'## Phase Details',
'### Phase 1: Foundation',
'**Goal:** build it',
'### Phase 999: Backlog / Someday',
'**Goal:** deferred, never executed',
].join('\n'),
'utf-8',
);
fs.writeFileSync(
path.join(planning, 'STATE.md'),
`---\nmilestone: v1.0\n---\n# State\n`,
'utf-8',
);
const dir = path.join(planning, 'phases', '01-foundation');
fs.mkdirSync(dir, { recursive: true });
fs.writeFileSync(path.join(dir, 'PLAN.md'), '# Plan\n', 'utf-8');
fs.writeFileSync(path.join(dir, 'SUMMARY.md'), '# Summary\n', 'utf-8');
});
afterEach(() => {
cleanup(tmpDir);
});
test('next_phase is never 999 and phase_count excludes the sentinel', () => {
const result = runGsdTools(['roadmap', 'analyze', '--raw'], tmpDir);
assert.ok(result.success, `roadmap analyze failed: ${result.error}`);
const analysis = JSON.parse(result.output);
assert.notEqual(
String(analysis.next_phase),
'999',
`next_phase must never route to the 999 backlog sentinel. Got ${analysis.next_phase}`,
);
assert.equal(
analysis.phase_count,
1,
`phase_count must exclude the 999 sentinel (expected 1). Got ${analysis.phase_count}`,
);
assert.ok(
!(analysis.phases || []).some(p => String(p.number) === '999'),
'the phases array must not include the 999 backlog sentinel',
);
});
});

View File

@@ -705,9 +705,9 @@ describe('issue #2612: runtime "opencode" — OpenCode tier resolution', () => {
assert.strictEqual(resolveModelInternal(tmpDir, 'gsd-planner'), 'anthropic/claude-opus-4-8');
});
test('sonnet tier -> anthropic/claude-sonnet-4-6', () => {
test('sonnet tier -> anthropic/claude-sonnet-5', () => {
writeConfig(tmpDir, { runtime: 'opencode', model_profile: 'balanced' });
assert.strictEqual(resolveModelInternal(tmpDir, 'gsd-roadmapper'), 'anthropic/claude-sonnet-4-6');
assert.strictEqual(resolveModelInternal(tmpDir, 'gsd-roadmapper'), 'anthropic/claude-sonnet-5');
});
test('haiku tier -> anthropic/claude-haiku-4-5', () => {
@@ -733,9 +733,9 @@ describe('issue #2612: runtime "copilot" — Copilot tier resolution', () => {
assert.strictEqual(resolveModelInternal(tmpDir, 'gsd-planner'), 'claude-opus-4-8');
});
test('sonnet tier -> claude-sonnet-4-6', () => {
test('sonnet tier -> claude-sonnet-5', () => {
writeConfig(tmpDir, { runtime: 'copilot', model_profile: 'balanced' });
assert.strictEqual(resolveModelInternal(tmpDir, 'gsd-roadmapper'), 'claude-sonnet-4-6');
assert.strictEqual(resolveModelInternal(tmpDir, 'gsd-roadmapper'), 'claude-sonnet-5');
});
test('haiku tier -> claude-haiku-4-5', () => {
@@ -862,6 +862,6 @@ describe('issue #2612: partial override merge for new Group A runtimes', () => {
// gsd-codebase-mapper budget -> haiku -> overridden
assert.strictEqual(resolveModelInternal(tmpDir, 'gsd-codebase-mapper'), 'claude-haiku-4-6');
// gsd-planner budget -> sonnet -> built-in default
assert.strictEqual(resolveModelInternal(tmpDir, 'gsd-planner'), 'claude-sonnet-4-6');
assert.strictEqual(resolveModelInternal(tmpDir, 'gsd-planner'), 'claude-sonnet-5');
});
});

View File

@@ -17,8 +17,8 @@ describe('MODEL_ALIAS_MAP (#1690 regression)', () => {
assert.equal(MODEL_ALIAS_MAP.opus, 'claude-opus-4-8');
});
test('sonnet maps to claude-sonnet-4-6', () => {
assert.equal(MODEL_ALIAS_MAP.sonnet, 'claude-sonnet-4-6');
test('sonnet maps to claude-sonnet-5', () => {
assert.equal(MODEL_ALIAS_MAP.sonnet, 'claude-sonnet-5');
});
test('haiku maps to claude-haiku-4-5', () => {

View File

@@ -2362,6 +2362,257 @@ describe('phase complete command', () => {
assert.ok(state.includes('Milestone complete'), 'status should be milestone complete');
});
// #1591: when the active milestone's phase checklist is wrapped in a
// <details> block AND phases are written as `- [ ] Phase N:` checkbox list
// items (not `### Phase N:` headings), phase.complete's next-phase enumerator
// saw no further phases → is_last_phase=true, next_phase=null on a mid-
// milestone phase, and STATE.md was wrongly marked "Milestone complete" with
// total_phases decremented. extractCurrentMilestone correctly surfaces the
// <details>-wrapped checklist; the defect was the heading-only phasePattern
// at the isLastPhase enumerator not recognizing checkbox-list phase items.
test('#1591: <details>-wrapped checkbox checklist — mid-milestone phase is NOT last', () => {
fs.writeFileSync(
path.join(tmpDir, '.planning', 'ROADMAP.md'),
[
'# ROADMAP',
'',
'## Phases',
'',
'<details>',
'<summary>✅ v1.0 First (Phases 1–3) — SHIPPED</summary>',
'',
'- [x] Phase 1: a',
'- [x] Phase 2: b',
'- [x] Phase 3: c',
'',
'</details>',
'',
'<details>',
'<summary>🚀 v2.0 Second (Phases 36–38) — IN PLANNING</summary>',
'',
'- [x] Phase 36: first (completed)',
'- [ ] Phase 37: second',
'- [ ] Phase 38: third',
'',
'</details>',
'',
'## Backlog',
'',
'### Phase 999.1: future (BACKLOG)',
'',
].join('\n')
);
fs.writeFileSync(
path.join(tmpDir, '.planning', 'STATE.md'),
[
'---',
'gsd_state_version: 1.0',
'milestone: v2.0',
'milestone_name: Second',
'current_phase: "36"',
'status: executing',
'---',
'',
'# GSD State',
'',
'**Current Phase:** 36',
'**Status:** Executing Phase 36',
'',
].join('\n')
);
// Only the COMPLETING phase (36) has a directory. Phases 37/38 exist only
// as `- [ ]` checklist items in the ROADMAP — they are not yet started, so
// they have no phase dirs. This is the @Azd325 scenario: the disk-based
// next-phase resolver finds nothing, and the roadmap-enumeration fallback
// (the heading-only phasePattern) is the only path that can find Phase 37.
const d36 = path.join(tmpDir, '.planning', 'phases', '36-first');
fs.mkdirSync(d36, { recursive: true });
fs.writeFileSync(path.join(d36, '36-PLAN.md'), '# Plan\n');
fs.writeFileSync(path.join(d36, '36-SUMMARY.md'), '# Summary\n');
const result = runVerifiedPhaseComplete('phase complete 36', tmpDir);
assert.ok(result.success, `phase complete failed: ${result.error}`);
const output = JSON.parse(result.output);
assert.strictEqual(
output.is_last_phase,
false,
'Phase 36 of 36–38 must NOT be last — Phases 37/38 are still open `- [ ]` (#1591)',
);
assert.strictEqual(
output.next_phase,
'37',
'next_phase must resolve to 37 from the <details>-wrapped checkbox checklist (#1591)',
);
// Cascade check: a wrong is_last_phase=true previously wrote "Milestone
// complete" + decremented total_phases. With the fix, the milestone is
// still in progress.
const state = fs.readFileSync(path.join(tmpDir, '.planning', 'STATE.md'), 'utf-8');
assert.ok(
!/Milestone complete/i.test(state),
'a mid-milestone phase must not flip STATE.md to "Milestone complete" (#1591)',
);
});
// #1591 (bold-checklist follow-up): the roadmap TEMPLATE emits checklist rows
// in the canonical BOLD form `- [ ] **Phase N: Name**`. The initial checkbox
// broadening only matched the un-bolded `- [ ] Phase N:` shape, so the exact
// <details>-wrapped bold template still fell through to is_last_phase=true.
// Guard the canonical bold form explicitly.
test('#1591: <details>-wrapped BOLD checkbox checklist — mid-milestone phase is NOT last', () => {
fs.writeFileSync(
path.join(tmpDir, '.planning', 'ROADMAP.md'),
[
'# ROADMAP',
'',
'## Phases',
'',
'<details>',
'<summary>🚀 v2.0 Second (Phases 36–38) — IN PLANNING</summary>',
'',
'- [x] **Phase 36: first (completed)** - done',
'- [ ] **Phase 37: second** - one-line description',
'- [ ] **Phase 38: third** - one-line description',
'',
'</details>',
'',
].join('\n')
);
fs.writeFileSync(
path.join(tmpDir, '.planning', 'STATE.md'),
[
'---',
'gsd_state_version: 1.0',
'milestone: v2.0',
'milestone_name: Second',
'current_phase: "36"',
'status: executing',
'---',
'',
'# GSD State',
'',
'**Current Phase:** 36',
'**Status:** Executing Phase 36',
'',
].join('\n')
);
// Only Phase 36 has a directory; 37/38 are bold `- [ ]` checklist rows only.
const d36 = path.join(tmpDir, '.planning', 'phases', '36-first');
fs.mkdirSync(d36, { recursive: true });
fs.writeFileSync(path.join(d36, '36-PLAN.md'), '# Plan\n');
fs.writeFileSync(path.join(d36, '36-SUMMARY.md'), '# Summary\n');
const result = runVerifiedPhaseComplete('phase complete 36', tmpDir);
assert.ok(result.success, `phase complete failed: ${result.error}`);
const output = JSON.parse(result.output);
assert.strictEqual(
output.is_last_phase,
false,
'Phase 36 of 36–38 must NOT be last with the canonical BOLD checklist (#1591)',
);
assert.strictEqual(
output.next_phase,
'37',
'next_phase must resolve to 37 from the BOLD `- [ ] **Phase N: ...**` checklist (#1591)',
);
const state = fs.readFileSync(path.join(tmpDir, '.planning', 'STATE.md'), 'utf-8');
assert.ok(
!/Milestone complete/i.test(state),
'a mid-milestone phase must not flip STATE.md to "Milestone complete" — bold checklist (#1591)',
);
});
// #1752: the #1591 follow-up — when phase.complete wrongly returned
// is_last_phase=true on a <details>-wrapped mid-milestone checklist, the
// milestone-complete cascade also DECREMENTED progress.total_phases (e.g.
// 8 -> 7) and flipped status. Same root cause, distinct symptom. With the
// #1591 fix (is_last_phase=false), the decrement must not occur: with all 8
// phase dirs on disk, total_phases stays 8 and status does not flip.
test('#1752: <details>-wrapped checklist — total_phases is NOT decremented on a mid-milestone phase', () => {
fs.writeFileSync(
path.join(tmpDir, '.planning', 'ROADMAP.md'),
[
'# ROADMAP',
'',
'## Phases',
'',
'<details>',
'<summary>✅ v1.0 First (Phases 1–3) — SHIPPED</summary>',
'',
'- [x] Phase 1: a',
'- [x] Phase 2: b',
'- [x] Phase 3: c',
'',
'</details>',
'',
'<details>',
'<summary>🚀 v2.0 Second (Phases 36–43) — IN PLANNING</summary>',
'',
'- [x] Phase 36: first (completed)',
'- [ ] Phase 37: second',
'- [ ] Phase 38: third',
'- [ ] Phase 39: fourth',
'',
'</details>',
'',
].join('\n')
);
fs.writeFileSync(
path.join(tmpDir, '.planning', 'STATE.md'),
[
'---',
'gsd_state_version: 1.0',
'milestone: v2.0',
'milestone_name: Second',
'current_phase: "36"',
'status: executing',
'progress:',
' total_phases: 8',
' completed_phases: 5',
' percent: 62',
'---',
'',
'# GSD State',
'',
'**Current Phase:** 36',
'**Status:** Executing Phase 36',
'',
].join('\n')
);
// All 8 phase dirs on disk (Phases 36–43) so the disk count is 8 — the
// reporter's real state. Before the #1591 fix, phase.complete 36 returned
// is_last_phase=true (no Phase 37+ heading match) and the milestone-complete
// path DECREMENTED total_phases 8 -> 7.
const names = ['first', 'second', 'third', 'fourth', 'fifth', 'sixth', 'seventh', 'eighth'];
for (let i = 0; i < 8; i++) {
const num = String(36 + i);
const d = path.join(tmpDir, '.planning', 'phases', `${num}-${names[i]}`);
fs.mkdirSync(d, { recursive: true });
fs.writeFileSync(path.join(d, `${num}-PLAN.md`), '# Plan\n');
}
const result = runVerifiedPhaseComplete('phase complete 36', tmpDir);
assert.ok(result.success, `phase complete failed: ${result.error}`);
const output = JSON.parse(result.output);
assert.strictEqual(output.is_last_phase, false, 'is_last_phase must be false (#1752 cascade root)');
const state = fs.readFileSync(path.join(tmpDir, '.planning', 'STATE.md'), 'utf-8');
assert.ok(
!/Milestone complete/i.test(state),
'a mid-milestone phase must not flip STATE.md to "Milestone complete" (#1752)',
);
const tpMatch = state.match(/total_phases:\s*(\d+)/);
assert.ok(tpMatch, 'STATE.md must carry a total_phases value after phase.complete');
assert.notStrictEqual(
parseInt(tpMatch[1], 10),
7,
'total_phases must NOT be decremented to 7 — the #1752 cascade of the false is_last_phase',
);
});
test('updates REQUIREMENTS.md traceability when phase completes', () => {
fs.writeFileSync(
path.join(tmpDir, '.planning', 'ROADMAP.md'),

View File

@@ -67,7 +67,7 @@
"scan.md": 7688,
"secure-phase.md": 13476,
"session-report.md": 4044,
"settings-advanced.md": 39666,
"settings-advanced.md": 39646,
"settings-integrations.md": 15848,
"settings.md": 33413,
"ship.md": 24647,