Merge branch 'main' into feat/cursor-cli-support

This commit is contained in:
adam.mitchell
2026-03-18 18:34:04 +00:00
48 changed files with 2241 additions and 235 deletions

51
.release-monitor.sh Executable file
View File

@@ -0,0 +1,51 @@
#!/usr/bin/env bash
# Release monitor for gsd-build/get-shit-done
# Checks every 15 minutes, writes new release info to a signal file
REPO="gsd-build/get-shit-done"
SIGNAL_FILE="/tmp/gsd-new-release.json"
STATE_FILE="/tmp/gsd-monitor-last-tag"
LOG_FILE="/tmp/gsd-monitor.log"
# Initialize with current latest
echo "v1.25.1" > "$STATE_FILE"
rm -f "$SIGNAL_FILE"
log() {
echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1" >> "$LOG_FILE"
echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1"
}
log "Monitor started. Watching $REPO for releases newer than v1.25.1"
log "Checking every 15 minutes..."
while true; do
sleep 900 # 15 minutes
LAST_KNOWN=$(cat "$STATE_FILE" 2>/dev/null)
# Get latest release tag
LATEST=$(gh release list -R "$REPO" --limit 1 2>/dev/null | awk '{print $1}')
if [ -z "$LATEST" ]; then
log "WARNING: Failed to fetch releases (network issue?)"
continue
fi
if [ "$LATEST" != "$LAST_KNOWN" ]; then
log "NEW RELEASE DETECTED: $LATEST (was: $LAST_KNOWN)"
# Fetch release notes
RELEASE_BODY=$(gh release view "$LATEST" -R "$REPO" --json tagName,name,body 2>/dev/null)
# Write signal file for the agent to pick up
echo "$RELEASE_BODY" > "$SIGNAL_FILE"
echo "$LATEST" > "$STATE_FILE"
log "Signal file written to $SIGNAL_FILE"
# Exit so the agent can process it, then restart
exit 0
else
log "No new release. Latest is still $LATEST"
fi
done

View File

@@ -6,15 +6,44 @@ Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
## [Unreleased]
## [1.26.0] - 2026-03-18
### Added
- **`/gsd:profile-user` command** — Developer behavioral profiling from session analysis across 8 dimensions (communication, decisions, debugging, UX, vendor choices, frustrations, learning style, explanation depth). Generates `USER-PROFILE.md`, `/gsd:dev-preferences`, and `CLAUDE.md` profile section for personalized responses. Includes `--questionnaire` fallback and `--refresh` for re-analysis
- **Execution hardening** — Three quality improvements to the execution pipeline:
- Pre-wave dependency check in `execute-phase`: verifies key-links from prior wave artifacts before spawning next wave
- Cross-Plan Data Contracts (Dimension 9) in plan-checker: detects incompatible transformations between plans sharing data pipelines
- Export-level spot check in `verify-phase`: catches dead stores that exist in wired files but are never called
- **Developer profiling pipeline** — `/gsd:profile-user` analyzes Claude Code session history to build behavioral profiles across 8 dimensions (communication, decisions, debugging, UX, vendor choices, frustrations, learning style, explanation depth). Generates `USER-PROFILE.md`, `/gsd:dev-preferences`, and `CLAUDE.md` profile section. Includes `--questionnaire` fallback and `--refresh` for re-analysis (#1084)
- **`/gsd:ship` command** — PR creation from verified phase work. Auto-generates rich PR body from planning artifacts, pushes branch, creates PR via `gh`, and updates STATE.md (#829)
- **`/gsd:next` command** — Automatic workflow advancement to the next logical step (#927)
- **Cross-phase regression gate** — Execute-phase runs prior phases' test suites after execution, catching regressions before they compound (#945)
- **Requirements coverage gate** — Plan-phase verifies all phase requirements are covered by at least one plan before proceeding (#984)
- **Structured session handoff artifact** — `/gsd:pause-work` writes `.planning/HANDOFF.json` for machine-readable cross-session continuity (#940)
- **WAITING.json signal file** — Machine-readable signal for decision points requiring user input (#1034)
- **Interactive executor mode** — Pair-programming style execution with step-by-step user involvement (#963)
- **MCP tool awareness** — GSD subagents can discover and use MCP server tools (#973)
- **Codex hooks support** — SessionStart hook support for Codex runtime (#1020)
- **Model alias-to-full-ID resolution** — Task API compatibility for model alias strings (#991)
- **Execution hardening** — Pre-wave dependency checks, cross-plan data contracts, and export-level spot checks (#1082)
- **Markdown normalization** — Generated markdown conforms to markdownlint standards (#1112)
### Changed
- Test suite consolidated: runtime converters deduplicated, helpers standardized (#1169)
- Added test coverage for model-profiles, templates, profile-pipeline, profile-output (#1170)
- Documented `inherit` profile for non-Anthropic providers (#1036)
### Fixed
- **Requirements `mark-complete` is now idempotent** — Re-marking already-completed requirements returns `already_complete` instead of `not_found` (#948)
- Agent suggests non-existent `/gsd:transition` — replaced with real commands (#1081, #1100)
- PROJECT.md drift and phase completion counter accuracy (#956)
- Copilot executor stuck issue — runtime compatibility fallback added (#1128)
- Explicit agent type listings prevent fallback after `/clear` (#949)
- Nested Skill calls breaking AskUserQuestion (#1009)
- Negative-heuristic `stripShippedMilestones` replaced with positive milestone lookup (#1145)
- Hook version tracking, stale hook detection, stdin timeout, session-report command (#1153, #1157, #1161, #1162)
- Hook build script syntax validation (#1165)
- Verification examples use `fetch()` instead of `curl` for Windows compatibility (#899)
- Sequential fallback for `map-codebase` on runtimes without Task tool (#1174)
- Zsh word-splitting fix for RUNTIME_DIRS arrays (#1173)
- CRLF frontmatter parsing, duplicate cwd crash, STATE.md phase transitions (#1105)
- Requirements `mark-complete` made idempotent (#948)
- Profile template paths, field names, and evidence key corrections (#1095)
- Duplicate variable declaration removed (#1101)
## [1.25.0] - 2026-03-16
@@ -1536,7 +1565,8 @@ Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
- YOLO mode for autonomous execution
- Interactive mode with checkpoints
[Unreleased]: https://github.com/glittercowboy/get-shit-done/compare/v1.25.0...HEAD
[Unreleased]: https://github.com/glittercowboy/get-shit-done/compare/v1.26.0...HEAD
[1.26.0]: https://github.com/glittercowboy/get-shit-done/releases/tag/v1.26.0
[1.25.0]: https://github.com/glittercowboy/get-shit-done/releases/tag/v1.25.0
[1.24.0]: https://github.com/glittercowboy/get-shit-done/releases/tag/v1.24.0
[1.23.0]: https://github.com/glittercowboy/get-shit-done/releases/tag/v1.23.0

View File

@@ -2850,10 +2850,14 @@ function install(isGlobal, runtime = 'claude') {
if (fs.statSync(srcFile).isFile()) {
const destFile = path.join(hooksDest, entry);
// Template .js files to replace '.claude' with runtime-specific config dir
// and stamp the current GSD version into the hook version header
if (entry.endsWith('.js')) {
let content = fs.readFileSync(srcFile, 'utf8');
content = content.replace(/'\.claude'/g, configDirReplacement);
content = content.replace(/\{\{GSD_VERSION\}\}/g, pkg.version);
fs.writeFileSync(destFile, content);
// Ensure hook files are executable (fixes #1162 — missing +x permission)
try { fs.chmodSync(destFile, 0o755); } catch (e) { /* Windows doesn't support chmod */ }
} else {
fs.copyFileSync(srcFile, destFile);
}
@@ -2933,6 +2937,36 @@ function install(isGlobal, runtime = 'claude') {
const agentCount = installCodexConfig(targetDir, agentsSrc);
console.log(` ${green}✓${reset} Generated config.toml with ${agentCount} agent roles`);
console.log(` ${green}✓${reset} Generated ${agentCount} agent .toml config files`);
// Add Codex hooks (SessionStart for update checking) — requires codex_hooks feature flag
const configPath = path.join(targetDir, 'config.toml');
try {
let configContent = fs.existsSync(configPath) ? fs.readFileSync(configPath, 'utf-8') : '';
// Enable hooks feature flag if not present
if (!configContent.includes('codex_hooks')) {
const featuresSection = '[features]\ncodex_hooks = true\n';
if (configContent.includes('[features]')) {
configContent = configContent.replace(/\[features\]\n/, featuresSection);
} else {
configContent = featuresSection + '\n' + configContent;
}
}
// Add SessionStart hook for update checking
const updateCheckScript = path.resolve(targetDir, 'get-shit-done', 'hooks', 'gsd-update-check.js').replace(/\\/g, '/');
const hookBlock = `\n# GSD Hooks\n[[hooks]]\nevent = "SessionStart"\ncommand = "node ${updateCheckScript}"\n`;
if (!configContent.includes('gsd-update-check')) {
configContent += hookBlock;
}
fs.writeFileSync(configPath, configContent, 'utf-8');
console.log(` ${green}✓${reset} Configured Codex hooks (SessionStart)`);
} catch (e) {
console.warn(` ${yellow}⚠${reset} Could not configure Codex hooks: ${e.message}`);
}
return { settingsPath: null, settings: null, statuslineCommand: null, runtime };
}

View File

@@ -1,7 +1,7 @@
---
name: gsd:execute-phase
description: Execute all plans in a phase with wave-based parallelization
argument-hint: "<phase-number> [--gaps-only]"
argument-hint: "<phase-number> [--gaps-only] [--interactive]"
allowed-tools:
- Read
- Write
@@ -31,6 +31,7 @@ Phase: $ARGUMENTS
**Flags:**
- `--gaps-only` — Execute only gap closure plans (plans with `gap_closure: true` in frontmatter). Use after verify-work creates fix plans.
- `--interactive` — Execute plans sequentially inline (no subagents) with user checkpoints between tasks. Lower token usage, pair-programming style. Best for small phases, bug fixes, and verification gaps.
Context files are resolved inside the workflow via `gsd-tools init execute-phase` and per-subagent `<files_to_read>` blocks.
</context>

24
commands/gsd/next.md Normal file
View File

@@ -0,0 +1,24 @@
---
name: gsd:next
description: Automatically advance to the next logical step in the GSD workflow
allowed-tools:
- Read
- Bash
- Grep
- Glob
- SlashCommand
---
<objective>
Detect the current project state and automatically invoke the next logical GSD workflow step.
No arguments needed — reads STATE.md, ROADMAP.md, and phase directories to determine what comes next.
Designed for rapid multi-project workflows where remembering which phase/step you're on is overhead.
</objective>
<execution_context>
@~/.claude/get-shit-done/workflows/next.md
</execution_context>
<process>
Execute the next workflow from @~/.claude/get-shit-done/workflows/next.md end-to-end.
</process>

View File

@@ -0,0 +1,19 @@
---
name: gsd:session-report
description: Generate a session report with token usage estimates, work summary, and outcomes
allowed-tools:
- Read
- Bash
- Write
---
<objective>
Generate a structured SESSION_REPORT.md document capturing session outcomes, work performed, and estimated resource usage. Provides a shareable artifact for post-session review.
</objective>
<execution_context>
@~/.claude/get-shit-done/workflows/session-report.md
</execution_context>
<process>
Execute the session-report workflow from @~/.claude/get-shit-done/workflows/session-report.md end-to-end.
</process>

23
commands/gsd/ship.md Normal file
View File

@@ -0,0 +1,23 @@
---
name: gsd:ship
description: Create PR, run review, and prepare for merge after verification passes
argument-hint: "[phase number or milestone, e.g., '4' or 'v1.0']"
allowed-tools:
- Read
- Bash
- Grep
- Glob
- Write
- AskUserQuestion
---
<objective>
Bridge local completion → merged PR. After /gsd:verify-work passes, ship the work: push branch, create PR with auto-generated body, optionally trigger review, and track the merge.
Closes the plan → execute → verify → ship loop.
</objective>
<execution_context>
@~/.claude/get-shit-done/workflows/ship.md
</execution_context>
Execute the ship workflow from @~/.claude/get-shit-done/workflows/ship.md end-to-end.

View File

@@ -132,6 +132,32 @@ User acceptance testing with auto-diagnosis.
---
### `/gsd:ship`
Create PR from completed phase work with auto-generated body.
| Argument | Required | Description |
|----------|----------|-------------|
| `N` | No | Phase number or milestone version (e.g., `4` or `v1.0`) |
| `--draft` | No | Create as draft PR |
**Prerequisites:** Phase verified (`/gsd:verify-work` passed), `gh` CLI installed and authenticated
**Produces:** GitHub PR with rich body from planning artifacts, STATE.md updated
```bash
/gsd:ship 4 # Ship phase 4
/gsd:ship 4 --draft # Ship as draft PR
```
**PR body includes:**
- Phase goal from ROADMAP.md
- Changes summary from SUMMARY.md files
- Requirements addressed (REQ-IDs)
- Verification status
- Key decisions
---
### `/gsd:ui-review`
Retroactive 6-pillar visual audit of implemented frontend.

View File

@@ -171,6 +171,7 @@
- REQ-PLAN-06: System MUST support `--skip-research` flag to bypass research phase
- REQ-PLAN-07: System MUST prompt user to run `/gsd:ui-phase` if frontend phase detected and no UI-SPEC.md exists (UI safety gate)
- REQ-PLAN-08: System MUST include Nyquist validation mapping when `workflow.nyquist_validation` is enabled
- REQ-PLAN-09: System MUST verify all phase requirements are covered by at least one plan before planning completes (requirements coverage gate)
**Produces:**
| Artifact | Description |
@@ -220,6 +221,7 @@
- REQ-EXEC-06: System MUST run post-execution verifier to check phase goals were met
- REQ-EXEC-07: System MUST support git branching strategies (`none`, `phase`, `milestone`)
- REQ-EXEC-08: System MUST invoke node repair operator on task verification failure (when enabled)
- REQ-EXEC-09: System MUST run prior phases' test suites before verification to catch cross-phase regressions
**Produces:**
| Artifact | Description |
@@ -261,6 +263,25 @@
---
### 6.5. Ship
**Command:** `/gsd:ship [N] [--draft]`
**Purpose:** Bridge local completion → merged PR. After verification passes, push branch, create PR with auto-generated body from planning artifacts, optionally trigger review, and track in STATE.md.
**Requirements:**
- REQ-SHIP-01: System MUST verify phase has passed verification before shipping
- REQ-SHIP-02: System MUST push branch and create PR via `gh` CLI
- REQ-SHIP-03: System MUST auto-generate PR body from SUMMARY.md, VERIFICATION.md, and REQUIREMENTS.md
- REQ-SHIP-04: System MUST update STATE.md with shipping status and PR number
- REQ-SHIP-05: System MUST support `--draft` flag for draft PRs
**Prerequisites:** Phase verified, `gh` CLI installed and authenticated, work on feature branch
**Produces:** GitHub PR with rich body, STATE.md updated
---
### 7. UI Review
**Command:** `/gsd:ui-review [N]`
@@ -494,11 +515,13 @@
**Purpose:** Maintain project continuity across context resets and sessions.
**Requirements:**
- REQ-SESSION-01: Pause MUST save current position and next steps to `continue-here.md`
- REQ-SESSION-02: Resume MUST restore full project context from state files
- REQ-SESSION-01: Pause MUST save current position and next steps to `continue-here.md` and structured `HANDOFF.json`
- REQ-SESSION-02: Resume MUST restore full project context from HANDOFF.json (preferred) or state files (fallback)
- REQ-SESSION-03: Progress MUST show current position, next action, and overall completion
- REQ-SESSION-04: Progress MUST read all state files (STATE.md, ROADMAP.md, phase directories)
- REQ-SESSION-05: All session operations MUST work after `/clear` (context reset)
- REQ-SESSION-06: HANDOFF.json MUST include blockers, human actions pending, and in-progress task state
- REQ-SESSION-07: Resume MUST surface human actions and blockers immediately on session start
---

View File

@@ -284,6 +284,7 @@ Controlled by `workflow.ui_safety_gate` config toggle.
| `/gsd:plan-phase [N]` | Research + plan + verify | Before executing a phase |
| `/gsd:execute-phase <N>` | Execute all plans in parallel waves | After planning is complete |
| `/gsd:verify-work [N]` | Manual UAT with auto-diagnosis | After execution completes |
| `/gsd:ship [N]` | Create PR from verified work | After verification passes |
| `/gsd:ui-review [N]` | Retroactive 6-pillar visual audit | After execution or verify-work (frontend projects) |
| `/gsd:audit-milestone` | Verify milestone met its definition of done | Before completing milestone |
| `/gsd:complete-milestone` | Archive milestone, tag release | All phases verified |
@@ -295,7 +296,7 @@ Controlled by `workflow.ui_safety_gate` config toggle.
|---------|---------|-------------|
| `/gsd:progress` | Show status and next steps | Anytime -- "where am I?" |
| `/gsd:resume-work` | Restore full context from last session | Starting a new session |
| `/gsd:pause-work` | Save context handoff | Stopping mid-phase |
| `/gsd:pause-work` | Save structured handoff (HANDOFF.json + continue-here.md) | Stopping mid-phase |
| `/gsd:help` | Show all commands | Quick reference |
| `/gsd:update` | Update GSD with changelog preview | Check for new versions |
| `/gsd:join-discord` | Open Discord community invite | Questions or community |
@@ -363,7 +364,7 @@ GSD stores project settings in `.planning/config.json`. Configure during `/gsd:n
|---------|---------|---------|------------------|
| `mode` | `interactive`, `yolo` | `interactive` | `yolo` auto-approves decisions; `interactive` confirms at each step |
| `granularity` | `coarse`, `standard`, `fine` | `standard` | Phase granularity: how finely scope is sliced (3-5, 5-8, or 8-12 phases) |
| `model_profile` | `quality`, `balanced`, `budget`, `inherit` | `balanced` | Model tier for each agent (see table below) |
| `model_profile` | `quality`, `balanced`, `budget`, `inherit` | `balanced` | Model tier for each agent (see table below) |
### Planning Settings
@@ -407,25 +408,25 @@ Disable these to speed up phases in familiar domains or when conserving tokens.
### Model Profiles (Per-Agent Breakdown)
| Agent | `quality` | `balanced` | `budget` | `inherit` |
|-------|-----------|------------|----------|-----------|
| gsd-planner | Opus | Opus | Sonnet | Inherit |
| gsd-roadmapper | Opus | Sonnet | Sonnet | Inherit |
| gsd-executor | Opus | Sonnet | Sonnet | Inherit |
| gsd-phase-researcher | Opus | Sonnet | Haiku | Inherit |
| gsd-project-researcher | Opus | Sonnet | Haiku | Inherit |
| gsd-research-synthesizer | Sonnet | Sonnet | Haiku | Inherit |
| gsd-debugger | Opus | Sonnet | Sonnet | Inherit |
| gsd-codebase-mapper | Sonnet | Haiku | Haiku | Inherit |
| gsd-verifier | Sonnet | Sonnet | Haiku | Inherit |
| gsd-plan-checker | Sonnet | Sonnet | Haiku | Inherit |
| gsd-integration-checker | Sonnet | Sonnet | Haiku | Inherit |
| Agent | `quality` | `balanced` | `budget` | `inherit` |
|-------|-----------|------------|----------|-----------|
| gsd-planner | Opus | Opus | Sonnet | Inherit |
| gsd-roadmapper | Opus | Sonnet | Sonnet | Inherit |
| gsd-executor | Opus | Sonnet | Sonnet | Inherit |
| gsd-phase-researcher | Opus | Sonnet | Haiku | Inherit |
| gsd-project-researcher | Opus | Sonnet | Haiku | Inherit |
| gsd-research-synthesizer | Sonnet | Sonnet | Haiku | Inherit |
| gsd-debugger | Opus | Sonnet | Sonnet | Inherit |
| gsd-codebase-mapper | Sonnet | Haiku | Haiku | Inherit |
| gsd-verifier | Sonnet | Sonnet | Haiku | Inherit |
| gsd-plan-checker | Sonnet | Sonnet | Haiku | Inherit |
| gsd-integration-checker | Sonnet | Sonnet | Haiku | Inherit |
**Profile philosophy:**
- **quality** -- Opus for all decision-making agents, Sonnet for read-only verification. Use when quota is available and the work is critical.
- **balanced** -- Opus only for planning (where architecture decisions happen), Sonnet for everything else. The default for good reason.
- **budget** -- Sonnet for anything that writes code, Haiku for research and verification. Use for high-volume work or less critical phases.
- **inherit** -- All agents use the current session model. Best when switching models dynamically (for example OpenCode `/model`).
- **quality** -- Opus for all decision-making agents, Sonnet for read-only verification. Use when quota is available and the work is critical.
- **balanced** -- Opus only for planning (where architecture decisions happen), Sonnet for everything else. The default for good reason.
- **budget** -- Sonnet for anything that writes code, Haiku for research and verification. Use for high-volume work or less critical phases.
- **inherit** -- All agents use the current session model. Best when switching models dynamically (for example OpenCode `/model`).
---

View File

@@ -15,6 +15,8 @@
* state get [section] Get STATE.md content or section
* state patch --field val ... Batch update STATE.md fields
* state begin-phase --phase N --name S --plans C Update STATE.md for new phase start
* state signal-waiting --type T --question Q --options "A|B" --phase P Write WAITING.json signal
* state signal-resume Remove WAITING.json signal
* resolve-model <agent-type> Get model for agent based on profile
* find-phase <phase> Find phase directory by number
* commit <message> [--files f1 f2] Commit planning docs
@@ -255,6 +257,21 @@ async function main() {
plansIdx !== -1 ? parseInt(args[plansIdx + 1], 10) : null,
raw
);
} else if (subcommand === 'signal-waiting') {
const typeIdx = args.indexOf('--type');
const qIdx = args.indexOf('--question');
const optIdx = args.indexOf('--options');
const phaseIdx = args.indexOf('--phase');
state.cmdSignalWaiting(
cwd,
typeIdx !== -1 ? args[typeIdx + 1] : null,
qIdx !== -1 ? args[qIdx + 1] : null,
optIdx !== -1 ? args[optIdx + 1] : null,
phaseIdx !== -1 ? args[phaseIdx + 1] : null,
raw
);
} else if (subcommand === 'signal-resume') {
state.cmdSignalResume(cwd, raw);
} else {
state.cmdStateLoad(cwd, raw);
}

View File

@@ -4,7 +4,7 @@
const fs = require('fs');
const path = require('path');
const { execSync } = require('child_process');
const { safeReadFile, loadConfig, isGitIgnored, execGit, normalizePhaseName, comparePhaseNum, getArchivedPhaseDirs, generateSlugInternal, getMilestoneInfo, getMilestonePhaseFilter, resolveModelInternal, stripShippedMilestones, toPosixPath, output, error, findPhaseInternal } = require('./core.cjs');
const { safeReadFile, loadConfig, isGitIgnored, execGit, normalizePhaseName, comparePhaseNum, getArchivedPhaseDirs, generateSlugInternal, getMilestoneInfo, getMilestonePhaseFilter, resolveModelInternal, stripShippedMilestones, extractCurrentMilestone, toPosixPath, output, error, findPhaseInternal } = require('./core.cjs');
const { extractFrontmatter } = require('./frontmatter.cjs');
const { MODEL_PROFILES } = require('./model-profiles.cjs');
@@ -547,7 +547,7 @@ function cmdStats(cwd, format, raw) {
let totalSummaries = 0;
try {
const roadmapContent = stripShippedMilestones(fs.readFileSync(roadmapPath, 'utf-8'));
const roadmapContent = extractCurrentMilestone(fs.readFileSync(roadmapPath, 'utf-8'), cwd);
const headingPattern = /#{2,4}\s*Phase\s+(\d+[A-Z]?(?:\.\d+)*)\s*:\s*([^\n]+)/gi;
let match;
while ((match = headingPattern.exec(roadmapContent)) !== null) {

View File

@@ -64,6 +64,7 @@ function loadConfig(cwd) {
nyquist_validation: true,
parallelization: true,
brave_search: false,
resolve_model_ids: false, // when true, resolve aliases (opus/sonnet/haiku) to full model IDs
};
try {
@@ -106,6 +107,7 @@ function loadConfig(cwd) {
nyquist_validation: get('nyquist_validation', { section: 'workflow', field: 'nyquist_validation' }) ?? defaults.nyquist_validation,
parallelization,
brave_search: get('brave_search') ?? defaults.brave_search,
resolve_model_ids: get('resolve_model_ids') ?? defaults.resolve_model_ids,
model_overrides: parsed.model_overrides || null,
};
} catch {
@@ -420,6 +422,91 @@ function stripShippedMilestones(content) {
return content.replace(/<details>[\s\S]*?<\/details>/gi, '');
}
/**
* Extract the current milestone section from ROADMAP.md by positive lookup.
*
* Instead of stripping <details> blocks (negative heuristic that breaks if
* agents wrap the current milestone in <details>), this finds the section
* matching the current milestone version and returns only that content.
*
* Falls back to stripShippedMilestones() if:
* - cwd is not provided
* - STATE.md doesn't exist or has no milestone field
* - Version can't be found in ROADMAP.md
*
* @param {string} content - Full ROADMAP.md content
* @param {string} [cwd] - Working directory for reading STATE.md
* @returns {string} Content scoped to current milestone
*/
function extractCurrentMilestone(content, cwd) {
if (!cwd) return stripShippedMilestones(content);
// 1. Get current milestone version from STATE.md frontmatter
let version = null;
try {
const statePath = path.join(cwd, '.planning', 'STATE.md');
if (fs.existsSync(statePath)) {
const stateRaw = fs.readFileSync(statePath, 'utf-8');
const milestoneMatch = stateRaw.match(/^milestone:\s*(.+)/m);
if (milestoneMatch) {
version = milestoneMatch[1].trim();
}
}
} catch {}
// 2. Fallback: derive version from getMilestoneInfo pattern in ROADMAP.md itself
if (!version) {
// Check for 🚧 in-progress marker
const inProgressMatch = content.match(/🚧\s*\*\*v(\d+\.\d+)\s/);
if (inProgressMatch) {
version = 'v' + inProgressMatch[1];
}
}
if (!version) return stripShippedMilestones(content);
// 3. Find the section matching this version
// Match headings like: ## Roadmap v3.0: Name, ## v3.0 Name, etc.
const escapedVersion = escapeRegex(version);
const sectionPattern = new RegExp(
`(^#{1,3}\\s+.*${escapedVersion}[^\\n]*)`,
'mi'
);
const sectionMatch = content.match(sectionPattern);
if (!sectionMatch) return stripShippedMilestones(content);
const sectionStart = sectionMatch.index;
// Find the end: next milestone heading at same or higher level, or EOF
// Milestone headings look like: ## v2.0, ## Roadmap v2.0, ## ✅ v1.0, etc.
const headingLevel = sectionMatch[1].match(/^(#{1,3})\s/)[1].length;
const restContent = content.slice(sectionStart + sectionMatch[0].length);
const nextMilestonePattern = new RegExp(
`^#{1,${headingLevel}}\\s+(?:.*v\\d+\\.\\d+|✅|📋|🚧)`,
'mi'
);
const nextMatch = restContent.match(nextMilestonePattern);
let sectionEnd;
if (nextMatch) {
sectionEnd = sectionStart + sectionMatch[0].length + nextMatch.index;
} else {
sectionEnd = content.length;
}
// Return everything before the current milestone section (non-milestone content
// like title, overview) plus the current milestone section
const beforeMilestones = content.slice(0, sectionStart);
const currentSection = content.slice(sectionStart, sectionEnd);
// Also include any content before the first milestone heading (title, overview, etc.)
// but strip any <details> blocks in it (these are definitely shipped)
const preamble = beforeMilestones.replace(/<details>[\s\S]*?<\/details>/gi, '');
return preamble + currentSection;
}
/**
* Replace a pattern only in the current milestone section of ROADMAP.md
* (everything after the last </details> close tag). Used for write operations
@@ -444,7 +531,7 @@ function getRoadmapPhaseInternal(cwd, phaseNum) {
if (!fs.existsSync(roadmapPath)) return null;
try {
const content = stripShippedMilestones(fs.readFileSync(roadmapPath, 'utf-8'));
const content = extractCurrentMilestone(fs.readFileSync(roadmapPath, 'utf-8'), cwd);
const escapedPhase = escapeRegex(phaseNum.toString());
const phasePattern = new RegExp(`#{2,4}\\s*Phase\\s+${escapedPhase}:\\s*([^\\n]+)`, 'i');
const headerMatch = content.match(phasePattern);
@@ -472,6 +559,19 @@ function getRoadmapPhaseInternal(cwd, phaseNum) {
}
}
// ─── Model alias resolution ───────────────────────────────────────────────────
/**
* Map short model aliases to full model IDs.
* Updated each release to match current model versions.
* Users can override with model_overrides in config.json for custom/latest models.
*/
const MODEL_ALIAS_MAP = {
'opus': 'claude-opus-4-0',
'sonnet': 'claude-sonnet-4-5',
'haiku': 'claude-haiku-3-5',
};
function resolveModelInternal(cwd, agentType) {
const config = loadConfig(cwd);
@@ -486,7 +586,15 @@ function resolveModelInternal(cwd, agentType) {
const agentModels = MODEL_PROFILES[agentType];
if (!agentModels) return 'sonnet';
if (profile === 'inherit') return 'inherit';
return agentModels[profile] || agentModels['balanced'] || 'sonnet';
const alias = agentModels[profile] || agentModels['balanced'] || 'sonnet';
// If resolve_model_ids is true, map alias to full model ID
// This prevents 404s when the Task tool passes aliases directly to the API
if (config.resolve_model_ids) {
return MODEL_ALIAS_MAP[alias] || alias;
}
return alias;
}
// ─── Misc utilities ───────────────────────────────────────────────────────────
@@ -549,7 +657,7 @@ function getMilestoneInfo(cwd) {
function getMilestonePhaseFilter(cwd) {
const milestonePhaseNums = new Set();
try {
const roadmap = stripShippedMilestones(fs.readFileSync(path.join(cwd, '.planning', 'ROADMAP.md'), 'utf-8'));
const roadmap = extractCurrentMilestone(fs.readFileSync(path.join(cwd, '.planning', 'ROADMAP.md'), 'utf-8'), cwd);
const phasePattern = /#{2,4}\s*Phase\s+(\d+[A-Z]?(?:\.\d+)*)\s*:/gi;
let m;
while ((m = phasePattern.exec(roadmap)) !== null) {
@@ -597,6 +705,8 @@ module.exports = {
getMilestoneInfo,
getMilestonePhaseFilter,
stripShippedMilestones,
extractCurrentMilestone,
replaceInCurrentMilestone,
toPosixPath,
MODEL_ALIAS_MAP,
};

View File

@@ -5,7 +5,7 @@
const fs = require('fs');
const path = require('path');
const { execSync } = require('child_process');
const { loadConfig, resolveModelInternal, findPhaseInternal, getRoadmapPhaseInternal, pathExistsInternal, generateSlugInternal, getMilestoneInfo, getMilestonePhaseFilter, stripShippedMilestones, normalizePhaseName, toPosixPath, output, error } = require('./core.cjs');
const { loadConfig, resolveModelInternal, findPhaseInternal, getRoadmapPhaseInternal, pathExistsInternal, generateSlugInternal, getMilestoneInfo, getMilestonePhaseFilter, stripShippedMilestones, extractCurrentMilestone, normalizePhaseName, toPosixPath, output, error } = require('./core.cjs');
function cmdInitExecutePhase(cwd, phase, raw) {
if (!phase) {
@@ -633,8 +633,8 @@ function cmdInitProgress(cwd, raw) {
const roadmapPhaseNums = new Set();
const roadmapPhaseNames = new Map();
try {
const roadmapContent = stripShippedMilestones(
fs.readFileSync(path.join(cwd, '.planning', 'ROADMAP.md'), 'utf-8')
const roadmapContent = extractCurrentMilestone(
fs.readFileSync(path.join(cwd, '.planning', 'ROADMAP.md'), 'utf-8'), cwd
);
const headingPattern = /#{2,4}\s*Phase\s+(\d+[A-Z]?(?:\.\d+)*)\s*:\s*([^\n]+)/gi;
let hm;

View File

@@ -4,7 +4,7 @@
const fs = require('fs');
const path = require('path');
const { escapeRegex, normalizePhaseName, comparePhaseNum, findPhaseInternal, getArchivedPhaseDirs, generateSlugInternal, getMilestonePhaseFilter, stripShippedMilestones, replaceInCurrentMilestone, toPosixPath, output, error } = require('./core.cjs');
const { escapeRegex, normalizePhaseName, comparePhaseNum, findPhaseInternal, getArchivedPhaseDirs, generateSlugInternal, getMilestonePhaseFilter, stripShippedMilestones, extractCurrentMilestone, replaceInCurrentMilestone, toPosixPath, output, error } = require('./core.cjs');
const { extractFrontmatter } = require('./frontmatter.cjs');
const { writeStateMd } = require('./state.cjs');
@@ -319,7 +319,7 @@ function cmdPhaseAdd(cwd, description, raw) {
}
const rawContent = fs.readFileSync(roadmapPath, 'utf-8');
const content = stripShippedMilestones(rawContent);
const content = extractCurrentMilestone(rawContent, cwd);
const slug = generateSlugInternal(description);
// Find highest integer phase number (in current milestone only)
@@ -376,7 +376,7 @@ function cmdPhaseInsert(cwd, afterPhase, description, raw) {
}
const rawContent = fs.readFileSync(roadmapPath, 'utf-8');
const content = stripShippedMilestones(rawContent);
const content = extractCurrentMilestone(rawContent, cwd);
const slug = generateSlugInternal(description);
// Normalize input then strip leading zeros for flexible matching
@@ -760,7 +760,7 @@ function cmdPhaseComplete(cwd, phaseNum, raw) {
if (fs.existsSync(reqPath)) {
// Extract the current phase section from roadmap (scoped to avoid cross-phase matching)
const phaseEsc = escapeRegex(phaseNum);
const currentMilestoneRoadmap = stripShippedMilestones(roadmapContent);
const currentMilestoneRoadmap = extractCurrentMilestone(roadmapContent, cwd);
const phaseSectionMatch = currentMilestoneRoadmap.match(
new RegExp(`(#{2,4}\\s*Phase\\s+${phaseEsc}[:\\s][\\s\\S]*?)(?=#{2,4}\\s*Phase\\s+|$)`, 'i')
);
@@ -824,7 +824,7 @@ function cmdPhaseComplete(cwd, phaseNum, raw) {
// for phases that are defined but not yet planned (no directory on disk)
if (isLastPhase && fs.existsSync(roadmapPath)) {
try {
const roadmapForPhases = stripShippedMilestones(fs.readFileSync(roadmapPath, 'utf-8'));
const roadmapForPhases = extractCurrentMilestone(fs.readFileSync(roadmapPath, 'utf-8'), cwd);
const phasePattern = /#{2,4}\s*Phase\s+(\d+[A-Z]?(?:\.\d+)*)\s*:\s*([^\n]+)/gi;
let pm;
while ((pm = phasePattern.exec(roadmapForPhases)) !== null) {
@@ -880,6 +880,34 @@ function cmdPhaseComplete(cwd, phaseNum, raw) {
`$1Phase ${phaseNum} complete${nextPhaseNum ? `, transitioned to Phase ${nextPhaseNum}` : ''}`
);
// Increment Completed Phases counter (#956)
const completedMatch = stateContent.match(/\*\*Completed Phases:\*\*\s*(\d+)/);
if (completedMatch) {
const newCompleted = parseInt(completedMatch[1], 10) + 1;
stateContent = stateContent.replace(
/(\*\*Completed Phases:\*\*\s*)\d+/,
`$1${newCompleted}`
);
// Recalculate percent based on completed / total (#956)
const totalMatch = stateContent.match(/\*\*Total Phases:\*\*\s*(\d+)/);
if (totalMatch) {
const totalPhases = parseInt(totalMatch[1], 10);
if (totalPhases > 0) {
const newPercent = Math.round((newCompleted / totalPhases) * 100);
stateContent = stateContent.replace(
/(\*\*Progress:\*\*\s*)\d+%/,
`$1${newPercent}%`
);
// Also update percent field if it exists separately
stateContent = stateContent.replace(
/(percent:\s*)\d+/,
`$1${newPercent}`
);
}
}
}
writeStateMd(statePath, stateContent, cwd);
}

View File

@@ -4,7 +4,7 @@
const fs = require('fs');
const path = require('path');
const { escapeRegex, normalizePhaseName, output, error, findPhaseInternal, stripShippedMilestones, replaceInCurrentMilestone } = require('./core.cjs');
const { escapeRegex, normalizePhaseName, output, error, findPhaseInternal, stripShippedMilestones, extractCurrentMilestone, replaceInCurrentMilestone } = require('./core.cjs');
function cmdRoadmapGetPhase(cwd, phaseNum, raw) {
const roadmapPath = path.join(cwd, '.planning', 'ROADMAP.md');
@@ -15,7 +15,7 @@ function cmdRoadmapGetPhase(cwd, phaseNum, raw) {
}
try {
const content = stripShippedMilestones(fs.readFileSync(roadmapPath, 'utf-8'));
const content = extractCurrentMilestone(fs.readFileSync(roadmapPath, 'utf-8'), cwd);
// Escape special regex chars in phase number, handle decimal
const escapedPhase = escapeRegex(phaseNum);
@@ -99,7 +99,7 @@ function cmdRoadmapAnalyze(cwd, raw) {
}
const rawContent = fs.readFileSync(roadmapPath, 'utf-8');
const content = stripShippedMilestones(rawContent);
const content = extractCurrentMilestone(rawContent, cwd);
const phasesDir = path.join(cwd, '.planning', 'phases');
// Extract all phase headings: ## Phase N: Name or ### Phase N: Name

View File

@@ -778,6 +778,53 @@ function cmdStateBeginPhase(cwd, phaseNumber, phaseName, planCount, raw) {
output({ updated, phase: phaseNumber, phase_name: phaseName || null, plan_count: planCount || null }, raw, updated.length > 0 ? 'true' : 'false');
}
/**
* Write a WAITING.json signal file when GSD hits a decision point.
* External watchers (fswatch, polling, orchestrators) can detect this.
* File is written to .planning/WAITING.json (or .gsd/WAITING.json if .gsd exists).
* Fixes #1034.
*/
function cmdSignalWaiting(cwd, type, question, options, phase, raw) {
const gsdDir = fs.existsSync(path.join(cwd, '.gsd')) ? path.join(cwd, '.gsd') : path.join(cwd, '.planning');
const waitingPath = path.join(gsdDir, 'WAITING.json');
const signal = {
status: 'waiting',
type: type || 'decision_point',
question: question || null,
options: options ? options.split('|').map(o => o.trim()) : [],
since: new Date().toISOString(),
phase: phase || null,
};
try {
fs.mkdirSync(gsdDir, { recursive: true });
fs.writeFileSync(waitingPath, JSON.stringify(signal, null, 2), 'utf-8');
output({ signaled: true, path: waitingPath }, raw, 'true');
} catch (e) {
output({ signaled: false, error: e.message }, raw, 'false');
}
}
/**
* Remove the WAITING.json signal file when user answers and agent resumes.
*/
function cmdSignalResume(cwd, raw) {
const paths = [
path.join(cwd, '.gsd', 'WAITING.json'),
path.join(cwd, '.planning', 'WAITING.json'),
];
let removed = false;
for (const p of paths) {
if (fs.existsSync(p)) {
try { fs.unlinkSync(p); removed = true; } catch {}
}
}
output({ resumed: true, removed }, raw, removed ? 'true' : 'false');
}
module.exports = {
stateExtractField,
stateReplaceField,
@@ -796,4 +843,6 @@ module.exports = {
cmdStateSnapshot,
cmdStateJson,
cmdStateBeginPhase,
cmdSignalWaiting,
cmdSignalResume,
};

View File

@@ -5,7 +5,7 @@
const fs = require('fs');
const path = require('path');
const os = require('os');
const { safeReadFile, normalizePhaseName, execGit, findPhaseInternal, getMilestoneInfo, stripShippedMilestones, output, error } = require('./core.cjs');
const { safeReadFile, normalizePhaseName, execGit, findPhaseInternal, getMilestoneInfo, stripShippedMilestones, extractCurrentMilestone, output, error } = require('./core.cjs');
const { extractFrontmatter, parseMustHavesBlock } = require('./frontmatter.cjs');
const { writeStateMd } = require('./state.cjs');
@@ -409,7 +409,7 @@ function cmdValidateConsistency(cwd, raw) {
}
const roadmapContentRaw = fs.readFileSync(roadmapPath, 'utf-8');
const roadmapContent = stripShippedMilestones(roadmapContentRaw);
const roadmapContent = extractCurrentMilestone(roadmapContentRaw, cwd);
// Extract phases from ROADMAP (archived milestones already stripped)
const roadmapPhases = new Set();
@@ -695,7 +695,7 @@ function cmdValidateHealth(cwd, options, raw) {
// Inline subset of cmdValidateConsistency
if (fs.existsSync(roadmapPath)) {
const roadmapContentRaw = fs.readFileSync(roadmapPath, 'utf-8');
const roadmapContent = stripShippedMilestones(roadmapContentRaw);
const roadmapContent = extractCurrentMilestone(roadmapContentRaw, cwd);
const roadmapPhases = new Set();
const phasePattern = /#{2,4}\s*Phase\s+(\d+[A-Z]?(?:\.\d+)*)\s*:/gi;
let m;

View File

@@ -50,7 +50,7 @@ Plans execute autonomously. Checkpoints formalize interaction points where human
<task type="auto">
<name>Start dev server for verification</name>
<action>Run `npm run dev` in background, wait for "ready" message, capture port</action>
<verify>curl http://localhost:3000 returns 200</verify>
<verify>fetch http://localhost:3000 returns 200</verify>
<done>Dev server running at http://localhost:3000</done>
</task>
@@ -240,7 +240,7 @@ Plans execute autonomously. Checkpoints formalize interaction points where human
<name>Deploy to Vercel</name>
<files>.vercel/, vercel.json</files>
<action>Run `vercel --yes` to deploy</action>
<verify>vercel ls shows deployment, curl returns 200</verify>
<verify>vercel ls shows deployment, fetch returns 200</verify>
</task>
<!-- If vercel returns "Error: Not authenticated", Claude creates checkpoint on the fly -->
@@ -261,7 +261,7 @@ Plans execute autonomously. Checkpoints formalize interaction points where human
<task type="auto">
<name>Retry Vercel deployment</name>
<action>Run `vercel --yes` (now authenticated)</action>
<verify>vercel ls shows deployment, curl returns 200</verify>
<verify>vercel ls shows deployment, fetch returns 200</verify>
</task>
```
@@ -455,8 +455,8 @@ I'll verify: vercel whoami returns your account
npm run dev &
DEV_SERVER_PID=$!
# Wait for ready (max 30s)
timeout 30 bash -c 'until curl -s localhost:3000 > /dev/null 2>&1; do sleep 1; done'
# Wait for ready (max 30s) — uses fetch() for cross-platform compatibility
timeout 30 bash -c 'until node -e "fetch(\"http://localhost:3000\").then(r=>{process.exit(r.ok?0:1)}).catch(()=>process.exit(1))" 2>/dev/null; do sleep 1; done'
```
**Port conflicts:** Kill stale process (`lsof -ti:3000 | xargs kill`) or use alternate port (`--port 3001`).
@@ -489,7 +489,9 @@ timeout 30 bash -c 'until curl -s localhost:3000 > /dev/null 2>&1; do sleep 1; d
| Auth error | Create auth gate checkpoint |
| Network timeout | Retry with backoff, then checkpoint if persistent |
**Never present a checkpoint with broken verification environment.** If `curl localhost:3000` fails, don't ask user to "visit localhost:3000".
**Never present a checkpoint with broken verification environment.** If the local server isn't responding, don't ask user to "visit localhost:3000".
> **Cross-platform note:** Use `node -e "fetch('http://localhost:3000').then(r=>console.log(r.status))"` instead of `curl` for health checks. `curl` is broken on Windows MSYS/Git Bash due to SSL/path mangling issues.
```xml
<!-- WRONG: Checkpoint with broken environment -->
@@ -502,7 +504,7 @@ timeout 30 bash -c 'until curl -s localhost:3000 > /dev/null 2>&1; do sleep 1; d
<task type="auto">
<name>Fix server startup issue</name>
<action>Investigate error, fix root cause, restart server</action>
<verify>curl http://localhost:3000 returns 200</verify>
<verify>fetch http://localhost:3000 returns 200</verify>
</task>
<task type="checkpoint:human-verify">
@@ -608,7 +610,7 @@ timeout 30 bash -c 'until curl -s localhost:3000 > /dev/null 2>&1; do sleep 1; d
<task type="auto">
<name>Start dev server for auth testing</name>
<action>Run `npm run dev` in background, wait for ready signal</action>
<verify>curl http://localhost:3000 returns 200</verify>
<verify>fetch http://localhost:3000 returns 200</verify>
<done>Dev server running at http://localhost:3000</done>
</task>
@@ -651,7 +653,7 @@ timeout 30 bash -c 'until curl -s localhost:3000 > /dev/null 2>&1; do sleep 1; d
<task type="auto">
<name>Start dev server</name>
<action>Run `npm run dev` in background</action>
<verify>curl localhost:3000 returns 200</verify>
<verify>fetch http://localhost:3000 returns 200</verify>
</task>
<task type="checkpoint:human-verify" gate="blocking">
@@ -677,7 +679,7 @@ timeout 30 bash -c 'until curl -s localhost:3000 > /dev/null 2>&1; do sleep 1; d
<task type="auto">
<name>Deploy to Vercel</name>
<action>Run `vercel --yes`. Capture URL.</action>
<verify>vercel ls shows deployment, curl returns 200</verify>
<verify>vercel ls shows deployment, fetch returns 200</verify>
</task>
<task type="checkpoint:human-verify">

View File

@@ -40,8 +40,26 @@ Model profiles control which Claude model each GSD agent uses. This allows balan
**inherit** - Follow the current session model
- All agents resolve to `inherit`
- Best when you switch models interactively (for example OpenCode `/model`)
- **Required when using non-Anthropic providers** (OpenRouter, local models, etc.) — otherwise GSD may call Anthropic models directly, incurring unexpected costs
- Use when: you want GSD to follow your currently selected runtime model
## Using Non-Anthropic Models (OpenRouter, Local, etc.)
If you're using Claude Code with OpenRouter, a local model, or any non-Anthropic provider, set the `inherit` profile to prevent GSD from calling Anthropic models for subagents:
```bash
# Via settings command
/gsd:settings
# → Select "Inherit" for model profile
# Or manually in .planning/config.json
{
"model_profile": "inherit"
}
```
Without `inherit`, GSD's default `balanced` profile spawns specific Anthropic models (`opus`, `sonnet`, `haiku`) for each agent type, which can result in additional API costs through your non-Anthropic provider.
## Resolution Logic
Orchestrators resolve model before spawning:

View File

@@ -341,7 +341,7 @@ Output: User model, API endpoints, and UI components.
<name>Task 2: Create User API endpoints</name>
<files>src/features/user/api.ts</files>
<action>GET /users (list), GET /users/:id (single), POST /users (create). Use User type from model.</action>
<verify>curl tests pass for all endpoints</verify>
<verify>fetch tests pass for all endpoints</verify>
<done>All CRUD operations work</done>
</task>
</tasks>
@@ -407,7 +407,7 @@ Output: Working dashboard component.
<task type="auto">
<name>Start dev server</name>
<action>Run `npm run dev` in background, wait for ready</action>
<verify>curl localhost:3000 returns 200</verify>
<verify>fetch http://localhost:3000 returns 200</verify>
</task>
<task type="checkpoint:human-verify" gate="blocking">

View File

@@ -6,10 +6,42 @@ Execute all plans in a phase using wave-based parallel execution. Orchestrator s
Orchestrator coordinates, not executes. Each subagent loads the full execute-plan context. Orchestrator: discover plans → analyze deps → group waves → spawn agents → handle checkpoints → collect results.
</core_principle>
<runtime_compatibility>
**Subagent spawning is runtime-specific:**
- **Claude Code:** Uses `Task(subagent_type="gsd-executor", ...)` — blocks until complete, returns result
- **Copilot:** Uses `@gsd-executor` agent reference — if subagent spawning hangs or fails to return,
fall back to **sequential inline execution**: read and follow execute-plan.md directly for each plan
instead of spawning parallel agents. This is slower but reliable.
- **Other runtimes (Gemini, Codex, OpenCode):** If Task/subagent API is unavailable, use sequential
inline execution as the fallback.
**Fallback rule:** If a spawned agent completes its work (commits visible, SUMMARY.md exists) but
the orchestrator never receives the completion signal, treat it as successful based on spot-checks
and continue to the next wave/plan.
</runtime_compatibility>
<required_reading>
Read STATE.md before any operation to load project context.
</required_reading>
<available_agent_types>
These are the valid GSD subagent types registered in .claude/agents/ (or equivalent for your runtime).
Always use the exact name from this list — do not fall back to 'general-purpose' or other built-in types:
- gsd-executor — Executes plan tasks, commits, creates SUMMARY.md
- gsd-verifier — Verifies phase completion, checks quality gates
- gsd-planner — Creates detailed plans from phase scope
- gsd-phase-researcher — Researches technical approaches for a phase
- gsd-plan-checker — Reviews plan quality before execution
- gsd-debugger — Diagnoses and fixes issues
- gsd-codebase-mapper — Maps project structure and dependencies
- gsd-integration-checker — Checks cross-phase integration
- gsd-nyquist-auditor — Validates verification coverage
- gsd-ui-researcher — Researches UI/UX approaches
- gsd-ui-checker — Reviews UI implementation quality
- gsd-ui-auditor — Audits UI against design requirements
</available_agent_types>
<process>
<step name="initialize" priority="first">
@@ -37,6 +69,54 @@ fi
```
</step>
<step name="check_interactive_mode">
**Parse `--interactive` flag from $ARGUMENTS.**
**If `--interactive` flag present:** Switch to interactive execution mode.
Interactive mode executes plans sequentially **inline** (no subagent spawning) with user
checkpoints between tasks. The user can review, modify, or redirect work at any point.
**Interactive execution flow:**
1. Load plan inventory as normal (discover_and_group_plans)
2. For each plan (sequentially, ignoring wave grouping):
a. **Present the plan to the user:**
```
## Plan {plan_id}: {plan_name}
Objective: {from plan file}
Tasks: {task_count}
Options:
- Execute (proceed with all tasks)
- Review first (show task breakdown before starting)
- Skip (move to next plan)
- Stop (end execution, save progress)
```
b. **If "Review first":** Read and display the full plan file. Ask again: Execute, Modify, Skip.
c. **If "Execute":** Read and follow `~/.claude/get-shit-done/workflows/execute-plan.md` **inline**
(do NOT spawn a subagent). Execute tasks one at a time.
d. **After each task:** Pause briefly. If the user intervenes (types anything), stop and address
their feedback before continuing. Otherwise proceed to next task.
e. **After plan complete:** Show results, commit, create SUMMARY.md, then present next plan.
3. After all plans: proceed to verification (same as normal mode).
**Benefits of interactive mode:**
- No subagent overhead — dramatically lower token usage
- User catches mistakes early — saves costly verification cycles
- Maintains GSD's planning/tracking structure
- Best for: small phases, bug fixes, verification gaps, learning GSD
**Skip to handle_branching step** (interactive plans execute inline after grouping).
</step>
<step name="handle_branching">
Check `branching_strategy` from init:
@@ -140,6 +220,13 @@ Execute each wave in sequence. Within a wave: parallel if `PARALLELIZATION=true`
- .claude/skills/ or .agents/skills/ (Project skills, if either exists — list skills, read SKILL.md for each, follow relevant rules during implementation)
</files_to_read>
<mcp_tools>
If CLAUDE.md or project instructions reference MCP tools (e.g. jCodeMunch, context7,
or other MCP servers), prefer those tools over Grep/Glob for code navigation when available.
MCP tools often save significant tokens by providing structured code indexes.
Check tool availability first — if MCP tools are not accessible, fall back to Grep/Glob.
</mcp_tools>
<success_criteria>
- [ ] All tasks executed
- [ ] Each task committed individually
@@ -328,6 +415,67 @@ node "$HOME/.claude/get-shit-done/bin/gsd-tools.cjs" commit "docs(phase-${PARENT
```
</step>
<step name="regression_gate">
Run prior phases' test suites to catch cross-phase regressions BEFORE verification.
**Skip if:** This is the first phase (no prior phases), or no prior VERIFICATION.md files exist.
**Step 1: Discover prior phases' test files**
```bash
# Find all VERIFICATION.md files from prior phases in current milestone
PRIOR_VERIFICATIONS=$(find .planning/phases/ -name "*-VERIFICATION.md" ! -path "*${PHASE_NUMBER}*" 2>/dev/null)
```
**Step 2: Extract test file lists from prior verifications**
For each VERIFICATION.md found, look for test file references:
- Lines containing `test`, `spec`, or `__tests__` paths
- The "Test Suite" or "Automated Checks" section
- File patterns from `key-files.created` in corresponding SUMMARY.md files that match `*.test.*` or `*.spec.*`
Collect all unique test file paths into `REGRESSION_FILES`.
**Step 3: Run regression tests (if any found)**
```bash
# Detect test runner and run prior phase tests
if [ -f "package.json" ]; then
# Node.js — use project's test runner
npx jest ${REGRESSION_FILES} --passWithNoTests --no-coverage -q 2>&1 || npx vitest run ${REGRESSION_FILES} 2>&1
elif [ -f "Cargo.toml" ]; then
cargo test 2>&1
elif [ -f "requirements.txt" ] || [ -f "pyproject.toml" ]; then
python -m pytest ${REGRESSION_FILES} -q --tb=short 2>&1
fi
```
**Step 4: Report results**
If all tests pass:
```
✓ Regression gate: {N} prior-phase test files passed — no regressions detected
```
→ Proceed to verify_phase_goal
If any tests fail:
```
## ⚠ Cross-Phase Regression Detected
Phase {X} execution may have broken functionality from prior phases.
| Test File | Phase | Status | Detail |
|-----------|-------|--------|--------|
| {file} | {origin_phase} | FAILED | {first_failure_line} |
Options:
1. Fix regressions before verification (recommended)
2. Continue to verification anyway (regressions will compound)
3. Abort phase — roll back and re-plan
```
Use AskUserQuestion to present the options.
</step>
<step name="verify_phase_goal">
Verify phase achieved its GOAL, not just completed tasks.
@@ -412,6 +560,28 @@ node "$HOME/.claude/get-shit-done/bin/gsd-tools.cjs" commit "docs(phase-{X}): co
```
</step>
<step name="update_project_md">
**Evolve PROJECT.md to reflect phase completion (prevents planning document drift — #956):**
PROJECT.md tracks validated requirements, decisions, and current state. Without this step,
PROJECT.md falls behind silently over multiple phases.
1. Read `.planning/PROJECT.md`
2. If the file exists and has a `## Validated Requirements` or `## Requirements` section:
- Move any requirements validated by this phase from Active → Validated
- Add a brief note: `Validated in Phase {X}: {Name}`
3. If the file has a `## Current State` or similar section:
- Update it to reflect this phase's completion (e.g., "Phase {X} complete — {one-liner}")
4. Update the `Last updated:` footer to today's date
5. Commit the change:
```bash
node "$HOME/.claude/get-shit-done/bin/gsd-tools.cjs" commit "docs(phase-{X}): evolve PROJECT.md after phase completion" --files .planning/PROJECT.md
```
**Skip this step if** `.planning/PROJECT.md` does not exist.
</step>
<step name="offer_next">
**Exception:** If `gaps_found`, the `verify_phase_goal` step already presents the gap-closure path (`/gsd:plan-phase {X} --gaps`). No additional routing needed — skip auto-advance.
@@ -465,6 +635,8 @@ Read and follow `~/.claude/get-shit-done/workflows/transition.md`, passing throu
**STOP. Do not auto-advance. Do not execute transition. Do not plan next phase. Present options to the user and wait.**
**IMPORTANT: There is NO `/gsd:transition` command. Never suggest it. The transition workflow is internal only.**
```
## ✓ Phase {X}: {Name} Complete
@@ -473,6 +645,8 @@ Read and follow `~/.claude/get-shit-done/workflows/transition.md`, passing throu
/gsd:plan-phase {next} — plan next phase
/gsd:execute-phase {next} — execute next phase
```
Only suggest the commands listed above. Do not invent or hallucinate command names.
</step>
</process>

View File

@@ -135,7 +135,8 @@ If previous SUMMARY has unresolved "Issues Encountered" or "Next Phase Readiness
Deviations are normal — handle via rules below.
1. Read @context files from prompt
2. Per task:
2. **MCP tools:** If CLAUDE.md or project instructions reference MCP tools (e.g. jCodeMunch for code navigation), prefer them over Grep/Glob when available. Fall back to Grep/Glob if MCP tools are not accessible.
3. Per task:
- **MANDATORY read_first gate:** If the task has a `<read_first>` field, you MUST read every listed file BEFORE making any edits. This is not optional. Do not skip files because you "already know" what's in them — read them. The read_first files establish ground truth for the task.
- `type="auto"`: if `tdd="true"` → TDD execution. Implement with deviation rules + auth gates. Verify done criteria. Commit (see task_commit). Track hash for Summary.
- `type="checkpoint:*"`: STOP → checkpoint_protocol → wait for user → continue only after confirmation.
@@ -370,7 +371,7 @@ One-liner SUBSTANTIVE: "JWT auth with refresh rotation using jose library" not "
Include: duration, start/end times, task count, file count.
Next: more plans → "Ready for {next-plan}" | last → "Phase complete, ready for transition".
Next: more plans → "Ready for {next-plan}" | last → "Phase complete, ready for next step".
</step>
<step name="update_current_position">

View File

@@ -308,6 +308,20 @@ Validate built features through conversational UAT.
Usage: `/gsd:verify-work 3`
### Ship Work
**`/gsd:ship [phase]`**
Create a PR from completed phase work with an auto-generated body.
- Pushes branch to remote
- Creates PR with summary from SUMMARY.md, VERIFICATION.md, REQUIREMENTS.md
- Optionally requests code review
- Updates STATE.md with shipping status
Prerequisites: Phase verified, `gh` CLI installed and authenticated.
Usage: `/gsd:ship 4` or `/gsd:ship 4 --draft`
### Milestone Auditing
**`/gsd:audit-milestone [version]`**

View File

@@ -82,12 +82,25 @@ mkdir -p .planning/codebase
Continue to spawn_agents.
</step>
<step name="spawn_agents">
<step name="detect_runtime_capabilities">
Before spawning agents, detect whether the current runtime supports the `Task` tool for subagent delegation.
**Runtimes with Task tool:** Claude Code, Cursor (native subagent support)
**Runtimes WITHOUT Task tool:** Antigravity, Gemini CLI, OpenCode, Codex, and others
**How to detect:** Check if you have access to a `Task` tool. If you do NOT have a `Task` tool (or only have tools like `browser_subagent` which is for web browsing, NOT code analysis):
→ **Skip `spawn_agents` and `collect_confirmations`** — go directly to `sequential_mapping` instead.
**CRITICAL:** Never use `browser_subagent` or `Explore` as a substitute for `Task`. The `browser_subagent` tool is exclusively for web page interaction and will fail for codebase analysis. If `Task` is unavailable, perform the mapping sequentially in-context.
</step>
<step name="spawn_agents" condition="Task tool is available">
Spawn 4 parallel gsd-codebase-mapper agents.
Use Task tool with `subagent_type="gsd-codebase-mapper"`, `model="{mapper_model}"`, and `run_in_background=true` for parallel execution.
**CRITICAL:** Use the dedicated `gsd-codebase-mapper` agent, NOT `Explore`. The mapper agent writes documents directly.
**CRITICAL:** Use the dedicated `gsd-codebase-mapper` agent, NOT `Explore` or `browser_subagent`. The mapper agent writes documents directly.
**Agent 1: Tech Focus**
@@ -195,6 +208,37 @@ If any agent failed, note the failure and continue with successful documents.
Continue to verify_output.
</step>
<step name="sequential_mapping" condition="Task tool is NOT available (e.g. Antigravity, Gemini CLI, Codex)">
When the `Task` tool is unavailable, perform codebase mapping sequentially in the current context. This replaces `spawn_agents` and `collect_confirmations`.
**IMPORTANT:** Do NOT use `browser_subagent`, `Explore`, or any browser-based tool. Use only file system tools (Read, Bash, Write, Grep, Glob, list_dir, view_file, grep_search, or equivalent tools available in your runtime).
Perform all 4 mapping passes sequentially:
**Pass 1: Tech Focus**
- Explore package.json/Cargo.toml/go.mod/requirements.txt, config files, dependency trees
- Write `.planning/codebase/STACK.md` — Languages, runtime, frameworks, dependencies, configuration
- Write `.planning/codebase/INTEGRATIONS.md` — External APIs, databases, auth providers, webhooks
**Pass 2: Architecture Focus**
- Explore directory structure, entry points, module boundaries, data flow
- Write `.planning/codebase/ARCHITECTURE.md` — Pattern, layers, data flow, abstractions, entry points
- Write `.planning/codebase/STRUCTURE.md` — Directory layout, key locations, naming conventions
**Pass 3: Quality Focus**
- Explore code style, error handling patterns, test files, CI config
- Write `.planning/codebase/CONVENTIONS.md` — Code style, naming, patterns, error handling
- Write `.planning/codebase/TESTING.md` — Framework, structure, mocking, coverage
**Pass 4: Concerns Focus**
- Explore TODOs, known issues, fragile areas, security patterns
- Write `.planning/codebase/CONCERNS.md` — Tech debt, bugs, security, performance, fragile areas
Use the same document templates as the `gsd-codebase-mapper` agent. Include actual file paths formatted with backticks.
Continue to verify_output.
</step>
<step name="verify_output">
Verify all documents created successfully:
@@ -307,10 +351,10 @@ End workflow.
<success_criteria>
- .planning/codebase/ directory created
- 4 parallel gsd-codebase-mapper agents spawned with run_in_background=true
- Agents write documents directly (orchestrator doesn't receive document contents)
- Read agent output files to collect confirmations
- If Task tool available: 4 parallel gsd-codebase-mapper agents spawned with run_in_background=true
- If Task tool NOT available: 4 sequential mapping passes performed inline (never using browser_subagent)
- All 7 codebase documents exist
- No empty documents (each should have >20 lines)
- Clear completion summary with line counts
- User offered clear next steps in GSD style
</success_criteria>

View File

@@ -0,0 +1,97 @@
<purpose>
Detect current project state and automatically advance to the next logical GSD workflow step.
Reads project state to determine: discuss → plan → execute → verify → complete progression.
</purpose>
<required_reading>
Read all files referenced by the invoking prompt's execution_context before starting.
</required_reading>
<process>
<step name="detect_state">
Read project state to determine current position:
```bash
# Get state snapshot
node "$HOME/.claude/get-shit-done/bin/gsd-tools.cjs" state json 2>/dev/null || echo "{}"
```
Also read:
- `.planning/STATE.md` — current phase, progress, plan counts
- `.planning/ROADMAP.md` — milestone structure and phase list
Extract:
- `current_phase` — which phase is active
- `plan_of` / `plans_total` — plan execution progress
- `progress` — overall percentage
- `status` — active, paused, etc.
If no `.planning/` directory exists:
```
No GSD project detected. Run `/gsd:new-project` to get started.
```
Exit.
</step>
<step name="determine_next_action">
Apply routing rules based on state:
**Route 1: No phases exist yet → discuss**
If ROADMAP has phases but no phase directories exist on disk:
→ Next action: `/gsd:discuss-phase <first-phase>`
**Route 2: Phase exists but has no CONTEXT.md or RESEARCH.md → discuss**
If the current phase directory exists but has neither CONTEXT.md nor RESEARCH.md:
→ Next action: `/gsd:discuss-phase <current-phase>`
**Route 3: Phase has context but no plans → plan**
If the current phase has CONTEXT.md (or RESEARCH.md) but no PLAN.md files:
→ Next action: `/gsd:plan-phase <current-phase>`
**Route 4: Phase has plans but incomplete summaries → execute**
If plans exist but not all have matching summaries:
→ Next action: `/gsd:execute-phase <current-phase>`
**Route 5: All plans have summaries → verify and complete**
If all plans in the current phase have summaries:
→ Next action: `/gsd:verify-work` then `/gsd:complete-phase`
**Route 6: Phase complete, next phase exists → advance**
If the current phase is complete and the next phase exists in ROADMAP:
→ Next action: `/gsd:discuss-phase <next-phase>`
**Route 7: All phases complete → complete milestone**
If all phases are complete:
→ Next action: `/gsd:complete-milestone`
**Route 8: Paused → resume**
If STATE.md shows paused_at:
→ Next action: `/gsd:resume-work`
</step>
<step name="show_and_execute">
Display the determination:
```
## GSD Next
**Current:** Phase [N] — [name] | [progress]%
**Status:** [status description]
▶ **Next step:** `/gsd:[command] [args]`
[One-line explanation of why this is the next step]
```
Then immediately invoke the determined command via SlashCommand.
Do not ask for confirmation — the whole point of `/gsd:next` is zero-friction advancement.
</step>
</process>
<success_criteria>
- [ ] Project state correctly detected
- [ ] Next action correctly determined from routing rules
- [ ] Command invoked immediately without user confirmation
- [ ] Clear status shown before invoking
</success_criteria>

View File

@@ -1,5 +1,5 @@
<purpose>
Create `.continue-here.md` handoff file to preserve complete work state across sessions. Enables seamless resumption with full context restoration.
Create structured `.planning/HANDOFF.json` and `.continue-here.md` handoff files to preserve complete work state across sessions. The JSON provides machine-readable state for `/gsd:resume-work`; the markdown provides human-readable context.
</purpose>
<required_reading>
@@ -27,10 +27,61 @@ If no active phase detected, ask user which phase they're pausing work on.
3. **Work remaining**: What's left in current plan/phase
4. **Decisions made**: Key decisions and rationale
5. **Blockers/issues**: Anything stuck
6. **Mental context**: The approach, next steps, "vibe"
7. **Files modified**: What's changed but not committed
6. **Human actions pending**: Things that need manual intervention (MCP setup, API keys, approvals, manual testing)
7. **Background processes**: Any running servers/watchers that were part of the workflow
8. **Files modified**: What's changed but not committed
Ask user for clarifications if needed via conversational questions.
**Also inspect SUMMARY.md files for false completions:**
```bash
# Check for placeholder content in existing summaries
grep -l "To be filled\|placeholder\|TBD" .planning/phases/*/*.md 2>/dev/null
```
Report any summaries with placeholder content as incomplete items.
</step>
<step name="write_structured">
**Write structured handoff to `.planning/HANDOFF.json`:**
```bash
timestamp=$(node "$HOME/.claude/get-shit-done/bin/gsd-tools.cjs" current-timestamp full --raw)
```
```json
{
"version": "1.0",
"timestamp": "{timestamp}",
"phase": "{phase_number}",
"phase_name": "{phase_name}",
"phase_dir": "{phase_dir}",
"plan": {current_plan_number},
"task": {current_task_number},
"total_tasks": {total_task_count},
"status": "paused",
"completed_tasks": [
{"id": 1, "name": "{task_name}", "status": "done", "commit": "{short_hash}"},
{"id": 2, "name": "{task_name}", "status": "done", "commit": "{short_hash}"},
{"id": 3, "name": "{task_name}", "status": "in_progress", "progress": "{what_done}"}
],
"remaining_tasks": [
{"id": 4, "name": "{task_name}", "status": "not_started"},
{"id": 5, "name": "{task_name}", "status": "not_started"}
],
"blockers": [
{"description": "{blocker}", "type": "technical|human_action|external", "workaround": "{if any}"}
],
"human_actions_pending": [
{"action": "{what needs to be done}", "context": "{why}", "blocking": true}
],
"decisions": [
{"decision": "{what}", "rationale": "{why}", "phase": "{phase_number}"}
],
"uncommitted_files": [],
"next_action": "{specific first action when resuming}",
"context_notes": "{mental state, approach, what you were thinking}"
}
```
</step>
<step name="write">
@@ -92,19 +143,22 @@ timestamp=$(node "$HOME/.claude/get-shit-done/bin/gsd-tools.cjs" current-timesta
<step name="commit">
```bash
node "$HOME/.claude/get-shit-done/bin/gsd-tools.cjs" commit "wip: [phase-name] paused at task [X]/[Y]" --files .planning/phases/*/.continue-here.md
node "$HOME/.claude/get-shit-done/bin/gsd-tools.cjs" commit "wip: [phase-name] paused at task [X]/[Y]" --files .planning/phases/*/.continue-here.md .planning/HANDOFF.json
```
</step>
<step name="confirm">
```
✓ Handoff created: .planning/phases/[XX-name]/.continue-here.md
✓ Handoff created:
- .planning/HANDOFF.json (structured, machine-readable)
- .planning/phases/[XX-name]/.continue-here.md (human-readable)
Current state:
- Phase: [XX-name]
- Task: [X] of [Y]
- Status: [in_progress/blocked]
- Blockers: [count] ({human_actions_pending count} need human action)
- Committed as WIP
To resume: /gsd:resume-work

View File

@@ -8,6 +8,13 @@ Read all files referenced by the invoking prompt's execution_context before star
@~/.claude/get-shit-done/references/ui-brand.md
</required_reading>
<available_agent_types>
Valid GSD subagent types (use exact names — do not fall back to 'general-purpose'):
- gsd-phase-researcher — Researches technical approaches for a phase
- gsd-planner — Creates detailed plans from phase scope
- gsd-plan-checker — Reviews plan quality before execution
</available_agent_types>
<process>
## 1. Initialize
@@ -170,7 +177,16 @@ Use AskUserQuestion:
- "Run discuss-phase first" — Capture design decisions before planning
If "Continue without context": Proceed to step 5.
If "Run discuss-phase first": Display `/gsd:discuss-phase {X}` and exit workflow.
If "Run discuss-phase first":
**IMPORTANT:** Do NOT invoke discuss-phase as a nested Skill/Task call — AskUserQuestion
does not work correctly in nested subcontexts (#1009). Instead, display the command
and exit so the user runs it as a top-level command:
```
Run this command first, then re-run /gsd:plan-phase {X}:
/gsd:discuss-phase {X}
```
**Exit the plan-phase workflow. Do not continue.**
## 5. Handle Research
@@ -571,11 +587,62 @@ Display: `Max iterations reached. {N} issues remain:` + issue list
Offer: 1) Force proceed, 2) Provide guidance and retry, 3) Abandon
## 13. Present Final Status
## 13. Requirements Coverage Gate
After plans pass the checker (or checker is skipped), verify that all phase requirements are covered by at least one plan.
**Skip if:** `phase_req_ids` is null or TBD (no requirements mapped to this phase).
**Step 1: Extract requirement IDs claimed by plans**
```bash
# Collect all requirement IDs from plan frontmatter
PLAN_REQS=$(grep -h "requirements_addressed\|requirements:" ${PHASE_DIR}/*-PLAN.md 2>/dev/null | tr -d '[]' | tr ',' '\n' | sed 's/^[[:space:]]*//' | sort -u)
```
**Step 2: Compare against phase requirements from ROADMAP**
For each REQ-ID in `phase_req_ids`:
- If REQ-ID appears in `PLAN_REQS` → covered ✓
- If REQ-ID does NOT appear in any plan → uncovered ✗
**Step 3: Check CONTEXT.md features against plan objectives**
Read CONTEXT.md `<decisions>` section. Extract feature/capability names. Check each against plan `<objective>` blocks. Features not mentioned in any plan objective → potentially dropped.
**Step 4: Report**
If all requirements covered and no dropped features:
```
✓ Requirements coverage: {N}/{N} REQ-IDs covered by plans
```
→ Proceed to step 14.
If gaps found:
```
## ⚠ Requirements Coverage Gap
{M} of {N} phase requirements are not assigned to any plan:
| REQ-ID | Description | Plans |
|--------|-------------|-------|
| {id} | {from REQUIREMENTS.md} | None |
{K} CONTEXT.md features not found in plan objectives:
- {feature_name} — described in CONTEXT.md but no plan covers it
Options:
1. Re-plan to include missing requirements (recommended)
2. Move uncovered requirements to next phase
3. Proceed anyway — accept coverage gaps
```
Use AskUserQuestion to present the options.
## 14. Present Final Status
Route to `<offer_next>` OR `auto_advance` depending on flags/config.
## 14. Auto-Advance Check
## 15. Auto-Advance Check
Check for auto-advance trigger:

View File

@@ -63,6 +63,9 @@ cat .planning/PROJECT.md
Look for incomplete work that needs attention:
```bash
# Check for structured handoff (preferred — machine-readable)
cat .planning/HANDOFF.json 2>/dev/null
# Check for continue-here files (mid-plan resumption)
ls .planning/phases/*/.continue-here*.md 2>/dev/null
@@ -78,7 +81,18 @@ if [ "$has_interrupted_agent" = "true" ]; then
fi
```
**If .continue-here file exists:**
**If HANDOFF.json exists:**
- This is the primary resumption source — structured data from `/gsd:pause-work`
- Parse `status`, `phase`, `plan`, `task`, `total_tasks`, `next_action`
- Check `blockers` and `human_actions_pending` — surface these immediately
- Check `completed_tasks` for `in_progress` items — these need attention first
- Validate `uncommitted_files` against `git status` — flag divergence
- Use `context_notes` to restore mental model
- Flag: "Found structured handoff — resuming from task {task}/{total_tasks}"
- **After successful resumption, delete HANDOFF.json** (it's a one-shot artifact)
**If .continue-here file exists (fallback):**
- This is a mid-plan resumption point
- Read the file for specific resumption context
@@ -145,8 +159,12 @@ Based on project state, determine the most logical next action:
→ Primary: Resume interrupted agent (Task tool with resume parameter)
→ Option: Start fresh (abandon agent work)
**If HANDOFF.json exists:**
→ Primary: Resume from structured handoff (highest priority — specific task/blocker context)
→ Option: Discard handoff and reassess from files
**If .continue-here file exists:**
→ Primary: Resume from checkpoint
→ Fallback: Resume from checkpoint
→ Option: Start fresh on current plan
**If incomplete plan (PLAN without SUMMARY):**
@@ -154,7 +172,7 @@ Based on project state, determine the most logical next action:
→ Option: Abandon and move on
**If phase in progress, all plans complete:**
→ Primary: Transition to next phase
→ Primary: Advance to next phase (via internal transition workflow)
→ Option: Review completed work
**If phase ready to plan:**
@@ -242,7 +260,7 @@ Based on user selection, route to appropriate workflow:
---
```
- **Transition** → ./transition.md
- **Advance to next phase** → ./transition.md (internal workflow, invoked inline — NOT a user command)
- **Check todos** → Read .planning/todos/pending/, present summary
- **Review alignment** → Read PROJECT.md, compare to current state
- **Something else** → Ask what they need

View File

@@ -0,0 +1,146 @@
<purpose>
Generate a post-session summary document capturing work performed, outcomes achieved, and estimated resource usage. Writes SESSION_REPORT.md to .planning/reports/ for human review and stakeholder sharing.
</purpose>
<required_reading>
Read all files referenced by the invoking prompt's execution_context before starting.
</required_reading>
<process>
<step name="gather_session_data">
Collect session data from available sources:
1. **STATE.md** — current phase, milestone, progress, blockers, decisions
2. **Git log** — commits made during this session (last 24h or since last report)
3. **Plan/Summary files** — plans executed, summaries written
4. **ROADMAP.md** — milestone context and phase goals
```bash
# Get recent commits (last 24 hours)
git log --oneline --since="24 hours ago" --no-merges 2>/dev/null || echo "No recent commits"
# Count files changed
git diff --stat HEAD~10 HEAD 2>/dev/null | tail -1 || echo "No diff available"
```
Read `.planning/STATE.md` to get:
- Current milestone and phase
- Progress percentage
- Active blockers
- Recent decisions
Read `.planning/ROADMAP.md` to get milestone name and goals.
Check for existing reports:
```bash
ls -la .planning/reports/SESSION_REPORT*.md 2>/dev/null || echo "No previous reports"
```
</step>
<step name="estimate_usage">
Estimate token usage from observable signals:
- Count of tool calls is not directly available, so estimate from git activity and file operations
- Note: This is an **estimate** — exact token counts require API-level instrumentation not available to hooks
Estimation heuristics:
- Each commit ≈ 1 plan cycle (research + plan + execute + verify)
- Each plan file ≈ 2,000-5,000 tokens of agent context
- Each summary file ≈ 1,000-2,000 tokens generated
- Subagent spawns multiply by ~1.5x per agent type used
</step>
<step name="generate_report">
Create the report directory and file:
```bash
mkdir -p .planning/reports
```
Write `.planning/reports/SESSION_REPORT.md` (or `.planning/reports/YYYYMMDD-session-report.md` if previous reports exist):
```markdown
# GSD Session Report
**Generated:** [timestamp]
**Project:** [from PROJECT.md title or directory name]
**Milestone:** [N] — [milestone name from ROADMAP.md]
---
## Session Summary
**Duration:** [estimated from first to last commit timestamp, or "Single session"]
**Phase Progress:** [from STATE.md]
**Plans Executed:** [count of summaries written this session]
**Commits Made:** [count from git log]
## Work Performed
### Phases Touched
[List phases worked on with brief description of what was done]
### Key Outcomes
[Bullet list of concrete deliverables: files created, features implemented, bugs fixed]
### Decisions Made
[From STATE.md decisions table, if any were added this session]
## Files Changed
[Summary of files modified, created, deleted — from git diff stat]
## Blockers & Open Items
[Active blockers from STATE.md]
[Any TODO items created during session]
## Estimated Resource Usage
| Metric | Estimate |
|--------|----------|
| Commits | [N] |
| Files changed | [N] |
| Plans executed | [N] |
| Subagents spawned | [estimated] |
> **Note:** Token and cost estimates require API-level instrumentation.
> These metrics reflect observable session activity only.
---
*Generated by `/gsd:session-report`*
```
</step>
<step name="display_result">
Show the user:
```
## Session Report Generated
📄 `.planning/reports/[filename].md`
### Highlights
- **Commits:** [N]
- **Files changed:** [N]
- **Phase progress:** [X]%
- **Plans executed:** [N]
```
If this is the first report, mention:
```
💡 Run `/gsd:session-report` at the end of each session to build a history of project activity.
```
</step>
</process>
<success_criteria>
- [ ] Session data gathered from STATE.md, git log, and plan files
- [ ] Report written to .planning/reports/
- [ ] Report includes work summary, outcomes, and file changes
- [ ] Filename includes date to prevent overwrites
- [ ] Result summary displayed to user
</success_criteria>

View File

@@ -49,7 +49,7 @@ AskUserQuestion([
{ label: "Quality", description: "Opus everywhere except verification (highest cost)" },
{ label: "Balanced (Recommended)", description: "Opus for planning, Sonnet for research/execution/verification" },
{ label: "Budget", description: "Sonnet for writing, Haiku for research/verification (lowest cost)" },
{ label: "Inherit", description: "Use current session model for all agents (best for OpenCode /model)" }
{ label: "Inherit", description: "Use current session model for all agents (best for OpenRouter, local models, or runtime model switching)" }
]
},
{

View File

@@ -0,0 +1,228 @@
<purpose>
Create a pull request from completed phase/milestone work, generate a rich PR body from planning artifacts, optionally run code review, and prepare for merge. Closes the plan → execute → verify → ship loop.
</purpose>
<required_reading>
Read all files referenced by the invoking prompt's execution_context before starting.
</required_reading>
<process>
<step name="initialize">
Parse arguments and load project state:
```bash
INIT=$(node "$HOME/.claude/get-shit-done/bin/gsd-tools.cjs" init phase-op "${PHASE_ARG}")
if [[ "$INIT" == @file:* ]]; then INIT=$(cat "${INIT#@file:}"); fi
```
Parse from init JSON: `phase_found`, `phase_dir`, `phase_number`, `phase_name`, `padded_phase`, `commit_docs`.
Also load config for branching strategy:
```bash
CONFIG=$(node "$HOME/.claude/get-shit-done/bin/gsd-tools.cjs" state load)
```
Extract: `branching_strategy`, `branch_name`.
</step>
<step name="preflight_checks">
Verify the work is ready to ship:
1. **Verification passed?**
```bash
VERIFICATION=$(cat ${PHASE_DIR}/*-VERIFICATION.md 2>/dev/null)
```
Check for `status: passed` or `status: human_needed` (with human approval).
If no VERIFICATION.md or status is `gaps_found`: warn and ask user to confirm.
2. **Clean working tree?**
```bash
git status --short
```
If uncommitted changes exist: ask user to commit or stash first.
3. **On correct branch?**
```bash
CURRENT_BRANCH=$(git branch --show-current)
```
If on `main`/`master`: warn — should be on a feature branch.
If branching_strategy is `none`: offer to create a branch now.
4. **Remote configured?**
```bash
git remote -v | head -2
```
Detect `origin` remote. If no remote: error — can't create PR.
5. **`gh` CLI available?**
```bash
which gh && gh auth status 2>&1
```
If `gh` not found or not authenticated: provide setup instructions and exit.
</step>
<step name="push_branch">
Push the current branch to remote:
```bash
git push origin ${CURRENT_BRANCH} 2>&1
```
If push fails (e.g., no upstream): set upstream:
```bash
git push --set-upstream origin ${CURRENT_BRANCH} 2>&1
```
Report: "Pushed `{branch}` to origin ({commit_count} commits ahead of main)"
</step>
<step name="generate_pr_body">
Auto-generate a rich PR body from planning artifacts:
**1. Title:**
```
Phase {phase_number}: {phase_name}
```
Or for milestone: `Milestone {version}: {name}`
**2. Summary section:**
Read ROADMAP.md for phase goal. Read VERIFICATION.md for verification status.
```markdown
## Summary
**Phase {N}: {Name}**
**Goal:** {goal from ROADMAP.md}
**Status:** Verified ✓
{One paragraph synthesized from SUMMARY.md files — what was built}
```
**3. Changes section:**
For each SUMMARY.md in the phase directory:
```markdown
## Changes
### Plan {plan_id}: {plan_name}
{one_liner from SUMMARY.md frontmatter}
**Key files:**
{key-files.created and key-files.modified from SUMMARY.md frontmatter}
```
**4. Requirements section:**
```markdown
## Requirements Addressed
{REQ-IDs from plan frontmatter, linked to REQUIREMENTS.md descriptions}
```
**5. Testing section:**
```markdown
## Verification
- [x] Automated verification: {pass/fail from VERIFICATION.md}
- {human verification items from VERIFICATION.md, if any}
```
**6. Decisions section:**
```markdown
## Key Decisions
{Decisions from STATE.md accumulated context relevant to this phase}
```
</step>
<step name="create_pr">
Create the PR using the generated body:
```bash
gh pr create \
--title "Phase ${PHASE_NUMBER}: ${PHASE_NAME}" \
--body "${PR_BODY}" \
--base main
```
If `--draft` flag was passed: add `--draft`.
Report: "PR #{number} created: {url}"
</step>
<step name="optional_review">
Ask if user wants to trigger a code review:
```
AskUserQuestion:
question: "PR created. Run a code review before merge?"
options:
- label: "Skip review"
description: "PR is ready — merge when CI passes"
- label: "Self-review"
description: "I'll review the diff in the PR myself"
- label: "Request review"
description: "Request review from a teammate"
```
**If "Request review":**
```bash
gh pr edit ${PR_NUMBER} --add-reviewer "${REVIEWER}"
```
**If "Self-review":**
Report the PR URL and suggest: "Review the diff at {url}/files"
</step>
<step name="track_shipping">
Update STATE.md to reflect the shipping action:
```bash
node "$HOME/.claude/get-shit-done/bin/gsd-tools.cjs" state update "Last Activity" "$(date +%Y-%m-%d)"
node "$HOME/.claude/get-shit-done/bin/gsd-tools.cjs" state update "Status" "Phase ${PHASE_NUMBER} shipped — PR #${PR_NUMBER}"
```
If `commit_docs` is true:
```bash
node "$HOME/.claude/get-shit-done/bin/gsd-tools.cjs" commit "docs(${padded_phase}): ship phase ${PHASE_NUMBER} — PR #${PR_NUMBER}" --files .planning/STATE.md
```
</step>
<step name="report">
```
───────────────────────────────────────────────────────────────
## ✓ Phase {X}: {Name} — Shipped
PR: #{number} ({url})
Branch: {branch} → main
Commits: {count}
Verification: ✓ Passed
Requirements: {N} REQ-IDs addressed
Next steps:
- Review/approve PR
- Merge when CI passes
- /gsd:complete-milestone (if last phase in milestone)
- /gsd:progress (to see what's next)
───────────────────────────────────────────────────────────────
```
</step>
</process>
<offer_next>
After shipping:
- /gsd:complete-milestone — if all phases in milestone are done
- /gsd:progress — see overall project state
- /gsd:execute-phase {next} — continue to next phase
</offer_next>
<success_criteria>
- [ ] Preflight checks passed (verification, clean tree, branch, remote, gh)
- [ ] Branch pushed to remote
- [ ] PR created with rich auto-generated body
- [ ] STATE.md updated with shipping status
- [ ] User knows PR number and next steps
</success_criteria>

View File

@@ -1,3 +1,19 @@
<internal_workflow>
**This is an INTERNAL workflow — NOT a user-facing command.**
There is no `/gsd:transition` command. This workflow is invoked automatically by
`execute-phase` during auto-advance, or inline by the orchestrator after phase
verification. Users should never be told to run `/gsd:transition`.
**Valid user commands for phase progression:**
- `/gsd:discuss-phase {N}` — discuss a phase before planning
- `/gsd:plan-phase {N}` — plan a phase
- `/gsd:execute-phase {N}` — execute a phase
- `/gsd:progress` — see roadmap progress
</internal_workflow>
<required_reading>
**Read these files NOW:**

View File

@@ -20,8 +20,11 @@ First, derive `PREFERRED_RUNTIME` from the invoking prompt's `execution_context`
Use `PREFERRED_RUNTIME` as the first runtime checked so `/gsd:update` targets the runtime that invoked it.
```bash
# Runtime candidates: "<runtime>:<config-dir>"
RUNTIME_DIRS="claude:.claude opencode:.config/opencode opencode:.opencode gemini:.gemini codex:.codex"
# Runtime candidates: "<runtime>:<config-dir>" stored as an array.
# Using an array instead of a space-separated string ensures correct
# iteration in both bash and zsh (zsh does not word-split unquoted
# variables by default). Fixes #1173.
RUNTIME_DIRS=( "claude:.claude" "opencode:.config/opencode" "opencode:.opencode" "gemini:.gemini" "codex:.codex" )
# PREFERRED_RUNTIME should be set from execution_context before running this block.
# If not set, infer from runtime env vars; fallback to claude.
@@ -40,23 +43,23 @@ if [ -z "$PREFERRED_RUNTIME" ]; then
fi
# Reorder entries so preferred runtime is checked first.
ORDERED_RUNTIME_DIRS=""
for entry in $RUNTIME_DIRS; do
ORDERED_RUNTIME_DIRS=()
for entry in "${RUNTIME_DIRS[@]}"; do
runtime="${entry%%:*}"
if [ "$runtime" = "$PREFERRED_RUNTIME" ]; then
ORDERED_RUNTIME_DIRS="$ORDERED_RUNTIME_DIRS $entry"
ORDERED_RUNTIME_DIRS+=( "$entry" )
fi
done
for entry in $RUNTIME_DIRS; do
for entry in "${RUNTIME_DIRS[@]}"; do
runtime="${entry%%:*}"
if [ "$runtime" != "$PREFERRED_RUNTIME" ]; then
ORDERED_RUNTIME_DIRS="$ORDERED_RUNTIME_DIRS $entry"
ORDERED_RUNTIME_DIRS+=( "$entry" )
fi
done
# Check local first (takes priority only if valid and distinct from global)
LOCAL_VERSION_FILE="" LOCAL_MARKER_FILE="" LOCAL_DIR="" LOCAL_RUNTIME=""
for entry in $ORDERED_RUNTIME_DIRS; do
for entry in "${ORDERED_RUNTIME_DIRS[@]}"; do
runtime="${entry%%:*}"
dir="${entry#*:}"
if [ -f "./$dir/get-shit-done/VERSION" ] || [ -f "./$dir/get-shit-done/workflows/update.md" ]; then
@@ -69,7 +72,7 @@ for entry in $ORDERED_RUNTIME_DIRS; do
done
GLOBAL_VERSION_FILE="" GLOBAL_MARKER_FILE="" GLOBAL_DIR="" GLOBAL_RUNTIME=""
for entry in $ORDERED_RUNTIME_DIRS; do
for entry in "${ORDERED_RUNTIME_DIRS[@]}"; do
runtime="${entry%%:*}"
dir="${entry#*:}"
if [ -f "$HOME/$dir/get-shit-done/VERSION" ] || [ -f "$HOME/$dir/get-shit-done/workflows/update.md" ]; then

View File

@@ -1,4 +1,5 @@
#!/usr/bin/env node
// gsd-hook-version: {{GSD_VERSION}}
// Check for GSD updates in background, write result to cache
// Called by SessionStart hook - runs once per session
@@ -43,6 +44,7 @@ if (!fs.existsSync(cacheDir)) {
// Run check in background (spawn background process, windowsHide prevents console flash)
const child = spawn(process.execPath, ['-e', `
const fs = require('fs');
const path = require('path');
const { execSync } = require('child_process');
const cacheFile = ${JSON.stringify(cacheFile)};
@@ -51,14 +53,43 @@ const child = spawn(process.execPath, ['-e', `
// Check project directory first (local install), then global
let installed = '0.0.0';
let configDir = '';
try {
if (fs.existsSync(projectVersionFile)) {
installed = fs.readFileSync(projectVersionFile, 'utf8').trim();
configDir = path.dirname(path.dirname(projectVersionFile));
} else if (fs.existsSync(globalVersionFile)) {
installed = fs.readFileSync(globalVersionFile, 'utf8').trim();
configDir = path.dirname(path.dirname(globalVersionFile));
}
} catch (e) {}
// Check for stale hooks — compare hook version headers against installed VERSION
let staleHooks = [];
if (configDir) {
const hooksDir = path.join(configDir, 'hooks');
try {
if (fs.existsSync(hooksDir)) {
const hookFiles = fs.readdirSync(hooksDir).filter(f => f.endsWith('.js'));
for (const hookFile of hookFiles) {
try {
const content = fs.readFileSync(path.join(hooksDir, hookFile), 'utf8');
const versionMatch = content.match(/\\/\\/ gsd-hook-version:\\s*(.+)/);
if (versionMatch) {
const hookVersion = versionMatch[1].trim();
if (hookVersion !== installed && !hookVersion.includes('{{')) {
staleHooks.push({ file: hookFile, hookVersion, installedVersion: installed });
}
} else {
// No version header at all — definitely stale (pre-version-tracking)
staleHooks.push({ file: hookFile, hookVersion: 'unknown', installedVersion: installed });
}
} catch (e) {}
}
}
} catch (e) {}
}
let latest = null;
try {
latest = execSync('npm view get-shit-done-cc version', { encoding: 'utf8', timeout: 10000, windowsHide: true }).trim();
@@ -68,7 +99,8 @@ const child = spawn(process.execPath, ['-e', `
update_available: latest && installed !== latest,
installed,
latest: latest || 'unknown',
checked: Math.floor(Date.now() / 1000)
checked: Math.floor(Date.now() / 1000),
stale_hooks: staleHooks.length > 0 ? staleHooks : undefined
};
fs.writeFileSync(cacheFile, JSON.stringify(result));

View File

@@ -1,4 +1,5 @@
#!/usr/bin/env node
// gsd-hook-version: {{GSD_VERSION}}
// Context Monitor - PostToolUse/AfterTool hook (Gemini uses AfterTool)
// Reads context metrics from the statusline bridge file and injects
// warnings when context usage is high. This makes the AGENT aware of
@@ -27,10 +28,11 @@ const STALE_SECONDS = 60; // ignore metrics older than 60s
const DEBOUNCE_CALLS = 5; // min tool uses between warnings
let input = '';
// Timeout guard: if stdin doesn't close within 3s (e.g. pipe issues on
// Windows/Git Bash), exit silently instead of hanging until Claude Code
// kills the process and reports "hook error". See #775.
const stdinTimeout = setTimeout(() => process.exit(0), 3000);
// Timeout guard: if stdin doesn't close within 10s (e.g. pipe issues on
// Windows/Git Bash, or slow Claude Code piping during large outputs),
// exit silently instead of hanging until Claude Code kills the process
// and reports "hook error". See #775, #1162.
const stdinTimeout = setTimeout(() => process.exit(0), 10000);
process.stdin.setEncoding('utf8');
process.stdin.on('data', chunk => input += chunk);
process.stdin.on('end', () => {

View File

@@ -1,4 +1,5 @@
#!/usr/bin/env node
// gsd-hook-version: {{GSD_VERSION}}
// Claude Code Statusline - GSD Edition
// Shows: model | current task | directory | context usage
@@ -99,6 +100,9 @@ process.stdin.on('end', () => {
if (cache.update_available) {
gsdUpdate = '\x1b[33m⬆ /gsd:update\x1b[0m │ ';
}
if (cache.stale_hooks && cache.stale_hooks.length > 0) {
gsdUpdate += '\x1b[31m⚠ stale hooks — run /gsd:update\x1b[0m │ ';
}
} catch (e) {}
}

4
package-lock.json generated
View File

@@ -1,12 +1,12 @@
{
"name": "get-shit-done-cc",
"version": "1.25.1",
"version": "1.26.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "get-shit-done-cc",
"version": "1.25.1",
"version": "1.26.0",
"license": "MIT",
"bin": {
"get-shit-done-cc": "bin/install.js"

View File

@@ -1,6 +1,6 @@
{
"name": "get-shit-done-cc",
"version": "1.25.1",
"version": "1.26.0",
"description": "A meta-prompting, context engineering and spec-driven development system for Claude Code, OpenCode, Gemini and Codex by TÂCHES.",
"bin": {
"get-shit-done-cc": "bin/install.js"

View File

@@ -1,10 +1,14 @@
#!/usr/bin/env node
/**
* Copy GSD hooks to dist for installation.
* Validates JavaScript syntax before copying to prevent shipping broken hooks.
* See #1107, #1109, #1125, #1161 — a duplicate const declaration shipped
* in dist and caused PostToolUse hook errors for all users.
*/
const fs = require('fs');
const path = require('path');
const vm = require('vm');
const HOOKS_DIR = path.join(__dirname, '..', 'hooks');
const DIST_DIR = path.join(HOOKS_DIR, 'dist');
@@ -16,13 +20,34 @@ const HOOKS_TO_COPY = [
'gsd-statusline.js'
];
/**
* Validate JavaScript syntax without executing the file.
* Catches SyntaxError (duplicate const, missing brackets, etc.)
* before the hook gets shipped to users.
*/
function validateSyntax(filePath) {
const content = fs.readFileSync(filePath, 'utf8');
try {
// Use vm.compileFunction to check syntax without executing
new vm.Script(content, { filename: path.basename(filePath) });
return null; // No error
} catch (e) {
if (e instanceof SyntaxError) {
return e.message;
}
throw e;
}
}
function build() {
// Ensure dist directory exists
if (!fs.existsSync(DIST_DIR)) {
fs.mkdirSync(DIST_DIR, { recursive: true });
}
// Copy hooks to dist
let hasErrors = false;
// Copy hooks to dist with syntax validation
for (const hook of HOOKS_TO_COPY) {
const src = path.join(HOOKS_DIR, hook);
const dest = path.join(DIST_DIR, hook);
@@ -32,9 +57,21 @@ function build() {
continue;
}
console.log(`Copying ${hook}...`);
// Validate syntax before copying
const syntaxError = validateSyntax(src);
if (syntaxError) {
console.error(`\x1b[31m✗ ${hook}: SyntaxError — ${syntaxError}\x1b[0m`);
hasErrors = true;
continue;
}
console.log(`\x1b[32m✓\x1b[0m Copying ${hook}...`);
fs.copyFileSync(src, dest);
console.log(` → ${dest}`);
}
if (hasErrors) {
console.error('\n\x1b[31mBuild failed: fix syntax errors above before publishing.\x1b[0m');
process.exit(1);
}
console.log('\nBuild complete.');

View File

@@ -625,7 +625,7 @@ describe('copyCommandsAsCopilotSkills', () => {
// Count gsd-* directories — should be 31
const dirs = fs.readdirSync(tempDir, { withFileTypes: true })
.filter(e => e.isDirectory() && e.name.startsWith('gsd-'));
assert.strictEqual(dirs.length, 39, `expected 39 skill folders, got ${dirs.length}`);
assert.strictEqual(dirs.length, 42, `expected 42 skill folders, got ${dirs.length}`);
} finally {
fs.rmSync(tempDir, { recursive: true });
}
@@ -1119,7 +1119,7 @@ const { execFileSync } = require('child_process');
const crypto = require('crypto');
const INSTALL_PATH = path.join(__dirname, '..', 'bin', 'install.js');
const EXPECTED_SKILLS = 39;
const EXPECTED_SKILLS = 42;
const EXPECTED_AGENTS = 16;
function runCopilotInstall(cwd) {

View File

@@ -10,6 +10,7 @@ const assert = require('node:assert');
const fs = require('fs');
const path = require('path');
const os = require('os');
const { createTempProject, cleanup } = require('./helpers.cjs');
const {
loadConfig,
@@ -35,14 +36,13 @@ describe('loadConfig', () => {
let originalCwd;
beforeEach(() => {
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-core-test-'));
fs.mkdirSync(path.join(tmpDir, '.planning'), { recursive: true });
tmpDir = createTempProject();
originalCwd = process.cwd();
});
afterEach(() => {
process.chdir(originalCwd);
fs.rmSync(tmpDir, { recursive: true, force: true });
cleanup(tmpDir);
});
function writeConfig(obj) {
@@ -129,12 +129,11 @@ describe('resolveModelInternal', () => {
let tmpDir;
beforeEach(() => {
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-core-test-'));
fs.mkdirSync(path.join(tmpDir, '.planning'), { recursive: true });
tmpDir = createTempProject();
});
afterEach(() => {
fs.rmSync(tmpDir, { recursive: true, force: true });
cleanup(tmpDir);
});
function writeConfig(obj) {
@@ -276,62 +275,11 @@ describe('generateSlugInternal', () => {
});
});
// ─── normalizePhaseName ────────────────────────────────────────────────────────
describe('normalizePhaseName', () => {
test('pads single digit', () => {
assert.strictEqual(normalizePhaseName('1'), '01');
});
test('preserves double digit', () => {
assert.strictEqual(normalizePhaseName('12'), '12');
});
test('handles letter suffix', () => {
assert.strictEqual(normalizePhaseName('1A'), '01A');
});
test('handles decimal phases', () => {
assert.strictEqual(normalizePhaseName('2.1'), '02.1');
});
test('handles multi-level decimals', () => {
assert.strictEqual(normalizePhaseName('1.2.3'), '01.2.3');
});
test('returns non-matching input unchanged', () => {
assert.strictEqual(normalizePhaseName('abc'), 'abc');
});
});
// ─── comparePhaseNum ───────────────────────────────────────────────────────────
describe('comparePhaseNum', () => {
test('sorts integer phases numerically', () => {
assert.ok(comparePhaseNum('1', '2') < 0);
assert.ok(comparePhaseNum('10', '2') > 0);
});
test('sorts letter suffixes', () => {
assert.ok(comparePhaseNum('12', '12A') < 0);
assert.ok(comparePhaseNum('12A', '12B') < 0);
});
test('sorts decimal phases', () => {
assert.ok(comparePhaseNum('2', '2.1') < 0);
assert.ok(comparePhaseNum('2.1', '2.2') < 0);
});
test('handles multi-level decimals', () => {
assert.ok(comparePhaseNum('1.1', '1.1.2') < 0);
assert.ok(comparePhaseNum('1.1.2', '1.2') < 0);
});
test('returns 0 for equal phases', () => {
assert.strictEqual(comparePhaseNum('1', '1'), 0);
assert.strictEqual(comparePhaseNum('2.1', '2.1'), 0);
});
});
// ─── normalizePhaseName / comparePhaseNum ──────────────────────────────────────
// NOTE: Comprehensive tests for normalizePhaseName and comparePhaseNum are in
// phase.test.cjs (which covers all edge cases: hybrid, letter-suffix,
// multi-level decimal, case-insensitive, directory-slug, and full sort order).
// Removed duplicates here to keep a single authoritative test location.
// ─── safeReadFile ──────────────────────────────────────────────────────────────
@@ -343,7 +291,7 @@ describe('safeReadFile', () => {
});
afterEach(() => {
fs.rmSync(tmpDir, { recursive: true, force: true });
cleanup(tmpDir);
});
test('reads existing file', () => {
@@ -363,12 +311,11 @@ describe('pathExistsInternal', () => {
let tmpDir;
beforeEach(() => {
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-core-test-'));
fs.mkdirSync(path.join(tmpDir, '.planning'), { recursive: true });
tmpDir = createTempProject();
});
afterEach(() => {
fs.rmSync(tmpDir, { recursive: true, force: true });
cleanup(tmpDir);
});
test('returns true for existing path', () => {
@@ -390,12 +337,11 @@ describe('getMilestoneInfo', () => {
let tmpDir;
beforeEach(() => {
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-core-test-'));
fs.mkdirSync(path.join(tmpDir, '.planning'), { recursive: true });
tmpDir = createTempProject();
});
afterEach(() => {
fs.rmSync(tmpDir, { recursive: true, force: true });
cleanup(tmpDir);
});
test('extracts version and name from roadmap', () => {
@@ -502,7 +448,7 @@ describe('searchPhaseInDir', () => {
});
afterEach(() => {
fs.rmSync(tmpDir, { recursive: true, force: true });
cleanup(tmpDir);
});
test('finds phase directory by normalized prefix', () => {
@@ -564,12 +510,11 @@ describe('findPhaseInternal', () => {
let tmpDir;
beforeEach(() => {
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-core-test-'));
fs.mkdirSync(path.join(tmpDir, '.planning', 'phases'), { recursive: true });
tmpDir = createTempProject();
});
afterEach(() => {
fs.rmSync(tmpDir, { recursive: true, force: true });
cleanup(tmpDir);
});
test('finds phase in current phases directory', () => {
@@ -605,12 +550,11 @@ describe('getRoadmapPhaseInternal', () => {
let tmpDir;
beforeEach(() => {
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-core-test-'));
fs.mkdirSync(path.join(tmpDir, '.planning'), { recursive: true });
tmpDir = createTempProject();
});
afterEach(() => {
fs.rmSync(tmpDir, { recursive: true, force: true });
cleanup(tmpDir);
});
// Bug: getRoadmapPhaseInternal was missing from module.exports
@@ -687,12 +631,11 @@ describe('getMilestonePhaseFilter', () => {
let tmpDir;
beforeEach(() => {
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-core-test-'));
fs.mkdirSync(path.join(tmpDir, '.planning', 'phases'), { recursive: true });
tmpDir = createTempProject();
});
afterEach(() => {
fs.rmSync(tmpDir, { recursive: true, force: true });
cleanup(tmpDir);
});
test('filters directories to only current milestone phases', () => {

View File

@@ -1,47 +0,0 @@
/**
* GSD Tools Tests - Gemini agent conversion
*
* Verifies Gemini-specific agent frontmatter conversion removes
* unsupported fields while preserving converted tools and body text.
*/
process.env.GSD_TEST_MODE = '1';
const { test, describe } = require('node:test');
const assert = require('node:assert');
const { convertClaudeToGeminiAgent } = require('../bin/install.js');
describe('convertClaudeToGeminiAgent', () => {
test('drops unsupported skills frontmatter while keeping converted tools', () => {
const input = `---
name: gsd-codebase-mapper
description: Explores codebase and writes structured analysis documents.
tools: Read, Bash, Grep, Glob, Write
color: cyan
skills:
- gsd-mapper-workflow
---
<role>
Use \${PHASE} in shell examples.
</role>`;
const result = convertClaudeToGeminiAgent(input);
const frontmatter = result.split('---')[1] || '';
assert.ok(frontmatter.includes('name: gsd-codebase-mapper'), 'keeps name');
assert.ok(frontmatter.includes('description: Explores codebase and writes structured analysis documents.'), 'keeps description');
assert.ok(frontmatter.includes('tools:'), 'adds Gemini tools array');
assert.ok(frontmatter.includes(' - read_file'), 'maps Read -> read_file');
assert.ok(frontmatter.includes(' - run_shell_command'), 'maps Bash -> run_shell_command');
assert.ok(frontmatter.includes(' - search_file_content'), 'maps Grep -> search_file_content');
assert.ok(frontmatter.includes(' - glob'), 'maps Glob -> glob');
assert.ok(frontmatter.includes(' - write_file'), 'maps Write -> write_file');
assert.ok(!frontmatter.includes('color:'), 'drops unsupported color field');
assert.ok(!frontmatter.includes('skills:'), 'drops unsupported skills field');
assert.ok(!frontmatter.includes('gsd-mapper-workflow'), 'drops skills list items');
assert.ok(result.includes('$PHASE'), 'escapes ${PHASE} shell variable for Gemini');
assert.ok(!result.includes('${PHASE}'), 'removes Gemini template-string pattern');
});
});

View File

@@ -0,0 +1,134 @@
/**
* Model Profiles Tests
*
* Tests for MODEL_PROFILES data structure, VALID_PROFILES list,
* formatAgentToModelMapAsTable, and getAgentToModelMapForProfile.
*/
const { test, describe } = require('node:test');
const assert = require('node:assert');
const {
MODEL_PROFILES,
VALID_PROFILES,
formatAgentToModelMapAsTable,
getAgentToModelMapForProfile,
} = require('../get-shit-done/bin/lib/model-profiles.cjs');
// ─── MODEL_PROFILES data integrity ────────────────────────────────────────────
describe('MODEL_PROFILES', () => {
test('contains all expected GSD agents', () => {
const expectedAgents = [
'gsd-planner', 'gsd-roadmapper', 'gsd-executor',
'gsd-phase-researcher', 'gsd-project-researcher', 'gsd-research-synthesizer',
'gsd-debugger', 'gsd-codebase-mapper', 'gsd-verifier',
'gsd-plan-checker', 'gsd-integration-checker', 'gsd-nyquist-auditor',
'gsd-ui-researcher', 'gsd-ui-checker', 'gsd-ui-auditor',
];
for (const agent of expectedAgents) {
assert.ok(MODEL_PROFILES[agent], `Missing agent: ${agent}`);
}
});
test('every agent has quality, balanced, and budget profiles', () => {
for (const [agent, profiles] of Object.entries(MODEL_PROFILES)) {
assert.ok(profiles.quality, `${agent} missing quality profile`);
assert.ok(profiles.balanced, `${agent} missing balanced profile`);
assert.ok(profiles.budget, `${agent} missing budget profile`);
}
});
test('all profile values are valid model aliases', () => {
const validModels = ['opus', 'sonnet', 'haiku'];
for (const [agent, profiles] of Object.entries(MODEL_PROFILES)) {
for (const [profile, model] of Object.entries(profiles)) {
assert.ok(
validModels.includes(model),
`${agent}.${profile} has invalid model "${model}" — expected one of ${validModels.join(', ')}`
);
}
}
});
test('quality profile never uses haiku', () => {
for (const [agent, profiles] of Object.entries(MODEL_PROFILES)) {
assert.notStrictEqual(
profiles.quality, 'haiku',
`${agent} quality profile should not use haiku`
);
}
});
});
// ─── VALID_PROFILES ───────────────────────────────────────────────────────────
describe('VALID_PROFILES', () => {
test('contains quality, balanced, and budget', () => {
assert.deepStrictEqual(VALID_PROFILES.sort(), ['balanced', 'budget', 'quality']);
});
test('is derived from MODEL_PROFILES keys', () => {
const fromData = Object.keys(MODEL_PROFILES['gsd-planner']);
assert.deepStrictEqual(VALID_PROFILES.sort(), fromData.sort());
});
});
// ─── getAgentToModelMapForProfile ─────────────────────────────────────────────
describe('getAgentToModelMapForProfile', () => {
test('returns correct models for balanced profile', () => {
const map = getAgentToModelMapForProfile('balanced');
assert.strictEqual(map['gsd-planner'], 'opus');
assert.strictEqual(map['gsd-codebase-mapper'], 'haiku');
assert.strictEqual(map['gsd-verifier'], 'sonnet');
});
test('returns correct models for budget profile', () => {
const map = getAgentToModelMapForProfile('budget');
assert.strictEqual(map['gsd-planner'], 'sonnet');
assert.strictEqual(map['gsd-phase-researcher'], 'haiku');
});
test('returns correct models for quality profile', () => {
const map = getAgentToModelMapForProfile('quality');
assert.strictEqual(map['gsd-planner'], 'opus');
assert.strictEqual(map['gsd-executor'], 'opus');
});
test('returns all agents in the map', () => {
const map = getAgentToModelMapForProfile('balanced');
const agentCount = Object.keys(MODEL_PROFILES).length;
assert.strictEqual(Object.keys(map).length, agentCount);
});
});
// ─── formatAgentToModelMapAsTable ─────────────────────────────────────────────
describe('formatAgentToModelMapAsTable', () => {
test('produces a table with header and separator', () => {
const map = { 'gsd-planner': 'opus', 'gsd-executor': 'sonnet' };
const table = formatAgentToModelMapAsTable(map);
assert.ok(table.includes('Agent'), 'should have Agent header');
assert.ok(table.includes('Model'), 'should have Model header');
assert.ok(table.includes('─'), 'should have separator line');
assert.ok(table.includes('gsd-planner'), 'should list agent');
assert.ok(table.includes('opus'), 'should list model');
});
test('pads columns correctly', () => {
const map = { 'a': 'opus', 'very-long-agent-name': 'haiku' };
const table = formatAgentToModelMapAsTable(map);
const lines = table.split('\n').filter(l => l.trim());
// Separator line uses ┼, data/header lines use │
const dataLines = lines.filter(l => l.includes('│'));
const pipePositions = dataLines.map(l => l.indexOf('│'));
const unique = [...new Set(pipePositions)];
assert.strictEqual(unique.length, 1, 'all data lines should align on │');
});
test('handles empty map', () => {
const table = formatAgentToModelMapAsTable({});
assert.ok(table.includes('Agent'), 'should still have header');
});
});

View File

@@ -0,0 +1,197 @@
/**
* Profile Output Tests
*
* Tests for profile rendering commands and PROFILING_QUESTIONS data.
*/
const { test, describe, beforeEach, afterEach } = require('node:test');
const assert = require('node:assert');
const fs = require('fs');
const path = require('path');
const { runGsdTools, createTempProject, createTempGitProject, cleanup } = require('./helpers.cjs');
const {
PROFILING_QUESTIONS,
CLAUDE_INSTRUCTIONS,
} = require('../get-shit-done/bin/lib/profile-output.cjs');
// ─── PROFILING_QUESTIONS data ─────────────────────────────────────────────────
describe('PROFILING_QUESTIONS', () => {
test('is a non-empty array', () => {
assert.ok(Array.isArray(PROFILING_QUESTIONS));
assert.ok(PROFILING_QUESTIONS.length > 0);
});
test('each question has required fields', () => {
for (const q of PROFILING_QUESTIONS) {
assert.ok(q.dimension, `question missing dimension`);
assert.ok(q.header, `${q.dimension} missing header`);
assert.ok(q.question, `${q.dimension} missing question`);
assert.ok(Array.isArray(q.options), `${q.dimension} options should be array`);
assert.ok(q.options.length >= 2, `${q.dimension} should have at least 2 options`);
}
});
test('each option has label, value, and rating', () => {
for (const q of PROFILING_QUESTIONS) {
for (const opt of q.options) {
assert.ok(opt.label, `${q.dimension} option missing label`);
assert.ok(opt.value, `${q.dimension} option missing value`);
assert.ok(opt.rating, `${q.dimension} option missing rating`);
}
}
});
test('all dimension keys are unique', () => {
const dims = PROFILING_QUESTIONS.map(q => q.dimension);
const unique = [...new Set(dims)];
assert.strictEqual(dims.length, unique.length);
});
});
// ─── CLAUDE_INSTRUCTIONS ──────────────────────────────────────────────────────
describe('CLAUDE_INSTRUCTIONS', () => {
test('is a non-empty object', () => {
assert.ok(typeof CLAUDE_INSTRUCTIONS === 'object');
assert.ok(Object.keys(CLAUDE_INSTRUCTIONS).length > 0);
});
test('each dimension has at least one instruction', () => {
for (const [dim, instructions] of Object.entries(CLAUDE_INSTRUCTIONS)) {
assert.ok(typeof instructions === 'object', `${dim} should be an object`);
assert.ok(Object.keys(instructions).length > 0, `${dim} should have instructions`);
}
});
test('every PROFILING_QUESTIONS dimension has CLAUDE_INSTRUCTIONS', () => {
for (const q of PROFILING_QUESTIONS) {
assert.ok(
CLAUDE_INSTRUCTIONS[q.dimension],
`${q.dimension} has questions but no CLAUDE_INSTRUCTIONS`
);
}
});
});
// ─── write-profile command ────────────────────────────────────────────────────
describe('write-profile command', () => {
let tmpDir;
beforeEach(() => {
tmpDir = createTempProject();
});
afterEach(() => {
cleanup(tmpDir);
});
test('writes USER-PROFILE.md from analysis JSON', () => {
const analysis = {
profile_version: '1.0',
dimensions: {
communication_style: { rating: 'terse-direct', confidence: 'HIGH' },
decision_speed: { rating: 'fast-intuitive', confidence: 'MEDIUM' },
explanation_depth: { rating: 'concise', confidence: 'HIGH' },
debugging_approach: { rating: 'fix-first', confidence: 'LOW' },
ux_philosophy: { rating: 'function-first', confidence: 'MEDIUM' },
vendor_philosophy: { rating: 'pragmatic', confidence: 'HIGH' },
frustration_triggers: { rating: 'over-explanation', confidence: 'LOW' },
learning_style: { rating: 'hands-on', confidence: 'MEDIUM' },
},
};
const analysisPath = path.join(tmpDir, 'analysis.json');
fs.writeFileSync(analysisPath, JSON.stringify(analysis));
const result = runGsdTools(['write-profile', '--input', analysisPath, '--raw'], tmpDir);
assert.ok(result.success, `Failed: ${result.error}`);
const out = JSON.parse(result.output);
assert.ok(out.profile_path, 'should return profile_path');
assert.ok(out.dimensions_scored > 0, 'should have scored dimensions');
});
test('errors when --input is missing', () => {
const result = runGsdTools('write-profile --raw', tmpDir);
assert.ok(!result.success, 'should fail without --input');
assert.ok(result.error.includes('--input'), 'should mention --input');
});
});
// ─── generate-claude-md command ───────────────────────────────────────────────
describe('generate-claude-md command', () => {
let tmpDir;
beforeEach(() => {
tmpDir = createTempGitProject();
fs.writeFileSync(
path.join(tmpDir, '.planning', 'PROJECT.md'),
'# My Project\n\nA test project.\n\n## Tech Stack\n\n- Node.js\n- TypeScript\n'
);
});
afterEach(() => {
cleanup(tmpDir);
});
test('generates CLAUDE.md with --auto flag', () => {
const outputPath = path.join(tmpDir, 'CLAUDE.md');
const result = runGsdTools(['generate-claude-md', '--output', outputPath, '--auto', '--raw'], tmpDir);
assert.ok(result.success, `Failed: ${result.error}`);
if (fs.existsSync(outputPath)) {
const content = fs.readFileSync(outputPath, 'utf-8');
assert.ok(content.length > 0, 'should have content');
}
});
test('does not overwrite existing CLAUDE.md without --force', () => {
const outputPath = path.join(tmpDir, 'CLAUDE.md');
fs.writeFileSync(outputPath, '# Custom CLAUDE.md\n\nUser content.\n');
const result = runGsdTools(['generate-claude-md', '--output', outputPath, '--auto', '--raw'], tmpDir);
// Should merge, not overwrite
const content = fs.readFileSync(outputPath, 'utf-8');
assert.ok(content.length > 0, 'should still have content');
});
});
// ─── generate-dev-preferences ─────────────────────────────────────────────────
describe('generate-dev-preferences command', () => {
let tmpDir;
beforeEach(() => {
tmpDir = createTempProject();
});
afterEach(() => {
cleanup(tmpDir);
});
test('errors when --analysis is missing', () => {
const result = runGsdTools('generate-dev-preferences --raw', tmpDir);
assert.ok(!result.success, 'should fail without --analysis');
assert.ok(result.error.includes('--analysis'), 'should mention --analysis');
});
test('generates preferences from analysis file', () => {
const analysis = {
profile_version: '1.0',
dimensions: {
communication_style: { rating: 'terse-direct', confidence: 'HIGH' },
decision_speed: { rating: 'fast-intuitive', confidence: 'MEDIUM' },
},
};
const analysisPath = path.join(tmpDir, 'analysis.json');
fs.writeFileSync(analysisPath, JSON.stringify(analysis));
const result = runGsdTools(['generate-dev-preferences', '--analysis', analysisPath, '--raw'], tmpDir);
assert.ok(result.success, `Failed: ${result.error}`);
const out = JSON.parse(result.output);
assert.ok(out.command_path || out.command_name, 'should return command output');
});
});

View File

@@ -0,0 +1,160 @@
/**
* Profile Pipeline Tests
*
* Tests for session scanning, message extraction, and profile sampling.
* Uses synthetic session data in temp directories via --path override.
*/
const { test, describe, beforeEach, afterEach } = require('node:test');
const assert = require('node:assert');
const fs = require('fs');
const path = require('path');
const os = require('os');
const { runGsdTools, createTempProject, cleanup } = require('./helpers.cjs');
// ─── scan-sessions ────────────────────────────────────────────────────────────
describe('scan-sessions command', () => {
let tmpDir;
beforeEach(() => {
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-profile-test-'));
});
afterEach(() => {
cleanup(tmpDir);
});
test('returns empty array for empty sessions directory', () => {
const sessionsDir = path.join(tmpDir, 'projects');
fs.mkdirSync(sessionsDir, { recursive: true });
const result = runGsdTools(`scan-sessions --path ${sessionsDir} --raw`, tmpDir);
assert.ok(result.success, `Failed: ${result.error}`);
const out = JSON.parse(result.output);
assert.ok(Array.isArray(out), 'should return an array');
assert.strictEqual(out.length, 0, 'should be empty');
});
test('scans synthetic project directory', () => {
const sessionsDir = path.join(tmpDir, 'projects');
const projectDir = path.join(sessionsDir, 'test-project-abc123');
fs.mkdirSync(projectDir, { recursive: true });
// Create a synthetic session file
const sessionData = [
JSON.stringify({ type: 'user', userType: 'external', message: { content: 'hello' }, timestamp: Date.now() }),
JSON.stringify({ type: 'assistant', message: { content: 'hi' }, timestamp: Date.now() }),
].join('\n');
fs.writeFileSync(path.join(projectDir, 'session-001.jsonl'), sessionData);
const result = runGsdTools(`scan-sessions --path ${sessionsDir} --raw`, tmpDir);
assert.ok(result.success, `Failed: ${result.error}`);
const out = JSON.parse(result.output);
assert.ok(Array.isArray(out), 'should return array');
assert.strictEqual(out.length, 1, 'should find 1 project');
assert.strictEqual(out[0].sessionCount, 1, 'should have 1 session');
});
test('reports multiple sessions and sizes', () => {
const sessionsDir = path.join(tmpDir, 'projects');
const projectDir = path.join(sessionsDir, 'multi-session-project');
fs.mkdirSync(projectDir, { recursive: true });
for (let i = 1; i <= 3; i++) {
const data = JSON.stringify({ type: 'user', userType: 'external', message: { content: `msg ${i}` }, timestamp: Date.now() });
fs.writeFileSync(path.join(projectDir, `session-${i}.jsonl`), data + '\n');
}
const result = runGsdTools(`scan-sessions --path ${sessionsDir} --raw`, tmpDir);
assert.ok(result.success, `Failed: ${result.error}`);
const out = JSON.parse(result.output);
assert.strictEqual(out[0].sessionCount, 3);
assert.ok(out[0].totalSize > 0, 'should have non-zero size');
});
});
// ─── extract-messages ─────────────────────────────────────────────────────────
describe('extract-messages command', () => {
let tmpDir;
beforeEach(() => {
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-profile-test-'));
});
afterEach(() => {
cleanup(tmpDir);
});
test('extracts user messages from synthetic session', () => {
const sessionsDir = path.join(tmpDir, 'projects');
const projectDir = path.join(sessionsDir, 'my-project');
fs.mkdirSync(projectDir, { recursive: true });
const messages = [
{ type: 'user', userType: 'external', message: { content: 'fix the login bug' }, timestamp: Date.now() },
{ type: 'assistant', message: { content: 'I will fix it.' }, timestamp: Date.now() },
{ type: 'user', userType: 'external', message: { content: 'add dark mode' }, timestamp: Date.now() },
{ type: 'user', userType: 'internal', isMeta: true, message: { content: '<local-command' }, timestamp: Date.now() },
];
fs.writeFileSync(
path.join(projectDir, 'session-001.jsonl'),
messages.map(m => JSON.stringify(m)).join('\n')
);
const result = runGsdTools(`extract-messages my-project --path ${sessionsDir} --raw`, tmpDir);
assert.ok(result.success, `Failed: ${result.error}`);
const out = JSON.parse(result.output);
assert.strictEqual(out.messages_extracted, 2, 'should extract 2 genuine user messages');
assert.strictEqual(out.project, 'my-project');
assert.ok(out.output_file, 'should have output file path');
});
test('filters out meta and internal messages', () => {
const sessionsDir = path.join(tmpDir, 'projects');
const projectDir = path.join(sessionsDir, 'filter-test');
fs.mkdirSync(projectDir, { recursive: true });
const messages = [
{ type: 'user', userType: 'external', message: { content: 'real message' }, timestamp: Date.now() },
{ type: 'user', userType: 'internal', message: { content: 'internal msg' }, timestamp: Date.now() },
{ type: 'user', userType: 'external', isMeta: true, message: { content: 'meta msg' }, timestamp: Date.now() },
{ type: 'user', userType: 'external', message: { content: '<local-command test' }, timestamp: Date.now() },
{ type: 'user', userType: 'external', message: { content: '' }, timestamp: Date.now() },
{ type: 'user', userType: 'external', message: { content: 'second real' }, timestamp: Date.now() },
];
fs.writeFileSync(
path.join(projectDir, 'session-001.jsonl'),
messages.map(m => JSON.stringify(m)).join('\n')
);
const result = runGsdTools(`extract-messages filter-test --path ${sessionsDir} --raw`, tmpDir);
assert.ok(result.success, `Failed: ${result.error}`);
const out = JSON.parse(result.output);
assert.strictEqual(out.messages_extracted, 2, 'should only extract 2 genuine external messages');
});
});
// ─── profile-questionnaire ────────────────────────────────────────────────────
describe('profile-questionnaire command', () => {
let tmpDir;
beforeEach(() => {
tmpDir = createTempProject();
});
afterEach(() => {
cleanup(tmpDir);
});
test('returns questionnaire structure', () => {
const result = runGsdTools('profile-questionnaire --raw', tmpDir);
assert.ok(result.success, `Failed: ${result.error}`);
const out = JSON.parse(result.output);
assert.ok(out.questions, 'should have questions array');
assert.ok(out.questions.length > 0, 'should have at least one question');
assert.ok(out.questions[0].dimension, 'each question should have a dimension');
assert.ok(out.questions[0].options, 'each question should have options');
});
});

View File

@@ -1,19 +1,21 @@
/**
* OpenCode Agent Frontmatter Conversion Tests
* Runtime Converter Tests — OpenCode + Gemini
*
* Validates that convertClaudeToOpencodeFrontmatter correctly converts
* agent frontmatter for OpenCode compatibility when isAgent: true.
* Tests for small runtime-specific conversion functions from install.js.
* Larger runtime test suites (Copilot, Codex, Antigravity) have their own files.
*
* Bug: Without isAgent flag, the function strips name: (agents need it),
* keeps color:/skills:/tools: record (should strip), and doesn't add
* model: inherit / mode: subagent (required by OpenCode agents).
* OpenCode: convertClaudeToOpencodeFrontmatter (agent + command modes)
* Gemini: convertClaudeToGeminiAgent (frontmatter + tool mapping + body escaping)
*/
const { test, describe } = require('node:test');
const assert = require('node:assert');
process.env.GSD_TEST_MODE = '1';
const { convertClaudeToOpencodeFrontmatter } = require('../bin/install.js');
const {
convertClaudeToOpencodeFrontmatter,
convertClaudeToGeminiAgent,
} = require('../bin/install.js');
// Sample Claude agent frontmatter (matches actual GSD agent format)
const SAMPLE_AGENT = `---
@@ -141,3 +143,41 @@ describe('OpenCode command conversion (isAgent: false, default)', () => {
assert.ok(frontmatter.includes('description:'), 'description should be kept');
});
});
// ─────────────────────────────────────────────────────────────────────────────
// Gemini CLI agent conversion (merged from gemini-config.test.cjs)
// ─────────────────────────────────────────────────────────────────────────────
describe('convertClaudeToGeminiAgent', () => {
test('drops unsupported skills frontmatter while keeping converted tools', () => {
const input = `---
name: gsd-codebase-mapper
description: Explores codebase and writes structured analysis documents.
tools: Read, Bash, Grep, Glob, Write
color: cyan
skills:
- gsd-mapper-workflow
---
<role>
Use \${PHASE} in shell examples.
</role>`;
const result = convertClaudeToGeminiAgent(input);
const frontmatter = result.split('---')[1] || '';
assert.ok(frontmatter.includes('name: gsd-codebase-mapper'), 'keeps name');
assert.ok(frontmatter.includes('description: Explores codebase and writes structured analysis documents.'), 'keeps description');
assert.ok(frontmatter.includes('tools:'), 'adds Gemini tools array');
assert.ok(frontmatter.includes(' - read_file'), 'maps Read -> read_file');
assert.ok(frontmatter.includes(' - run_shell_command'), 'maps Bash -> run_shell_command');
assert.ok(frontmatter.includes(' - search_file_content'), 'maps Grep -> search_file_content');
assert.ok(frontmatter.includes(' - glob'), 'maps Glob -> glob');
assert.ok(frontmatter.includes(' - write_file'), 'maps Write -> write_file');
assert.ok(!frontmatter.includes('color:'), 'drops unsupported color field');
assert.ok(!frontmatter.includes('skills:'), 'drops unsupported skills field');
assert.ok(!frontmatter.includes('gsd-mapper-workflow'), 'drops skills list items');
assert.ok(result.includes('$PHASE'), 'escapes ${PHASE} shell variable for Gemini');
assert.ok(!result.includes('${PHASE}'), 'removes Gemini template-string pattern');
});
});

186
tests/template.test.cjs Normal file
View File

@@ -0,0 +1,186 @@
/**
* Template Tests
*
* Tests for cmdTemplateSelect (heuristic template selection) and
* cmdTemplateFill (summary, plan, verification template generation).
*/
const { test, describe, beforeEach, afterEach } = require('node:test');
const assert = require('node:assert');
const fs = require('fs');
const path = require('path');
const { runGsdTools, createTempProject, cleanup } = require('./helpers.cjs');
// ─── template select ──────────────────────────────────────────────────────────
describe('template select command', () => {
let tmpDir;
beforeEach(() => {
tmpDir = createTempProject();
// Create a phase directory with a plan
const phaseDir = path.join(tmpDir, '.planning', 'phases', '01-setup');
fs.mkdirSync(phaseDir, { recursive: true });
});
afterEach(() => {
cleanup(tmpDir);
});
test('selects minimal template for simple plan', () => {
const planPath = path.join(tmpDir, '.planning', 'phases', '01-setup', '01-01-PLAN.md');
fs.writeFileSync(planPath, [
'# Plan',
'',
'### Task 1',
'Do the thing.',
'',
'File: `src/index.ts`',
].join('\n'));
const result = runGsdTools(`template select .planning/phases/01-setup/01-01-PLAN.md`, tmpDir);
assert.ok(result.success, `Failed: ${result.error}`);
const out = JSON.parse(result.output);
assert.strictEqual(out.type, 'minimal');
assert.ok(out.template.includes('summary-minimal'));
});
test('selects standard template for moderate plan', () => {
const planPath = path.join(tmpDir, '.planning', 'phases', '01-setup', '01-01-PLAN.md');
fs.writeFileSync(planPath, [
'# Plan',
'',
'### Task 1',
'Create `src/auth/login.ts`',
'',
'### Task 2',
'Create `src/auth/register.ts`',
'',
'### Task 3',
'Update `src/routes/index.ts`',
'',
'Files: `src/auth/login.ts`, `src/auth/register.ts`, `src/routes/index.ts`, `src/middleware/auth.ts`',
].join('\n'));
const result = runGsdTools(`template select .planning/phases/01-setup/01-01-PLAN.md`, tmpDir);
assert.ok(result.success, `Failed: ${result.error}`);
const out = JSON.parse(result.output);
assert.strictEqual(out.type, 'standard');
});
test('selects complex template for plan with decisions and many files', () => {
const planPath = path.join(tmpDir, '.planning', 'phases', '01-setup', '01-01-PLAN.md');
const lines = ['# Plan', ''];
for (let i = 1; i <= 6; i++) {
lines.push(`### Task ${i}`, `Do task ${i}.`, '');
}
lines.push('Made a decision about architecture.', 'Another decision here.');
for (let i = 1; i <= 8; i++) {
lines.push(`File: \`src/module${i}/index.ts\``);
}
fs.writeFileSync(planPath, lines.join('\n'));
const result = runGsdTools(`template select .planning/phases/01-setup/01-01-PLAN.md`, tmpDir);
assert.ok(result.success, `Failed: ${result.error}`);
const out = JSON.parse(result.output);
assert.strictEqual(out.type, 'complex');
});
test('returns standard as fallback for nonexistent file', () => {
const result = runGsdTools(`template select .planning/phases/01-setup/nonexistent.md`, tmpDir);
assert.ok(result.success, `Failed: ${result.error}`);
const out = JSON.parse(result.output);
assert.strictEqual(out.type, 'standard');
assert.ok(out.error, 'should include error message');
});
});
// ─── template fill ────────────────────────────────────────────────────────────
describe('template fill command', () => {
let tmpDir;
beforeEach(() => {
tmpDir = createTempProject();
const phaseDir = path.join(tmpDir, '.planning', 'phases', '01-setup');
fs.mkdirSync(phaseDir, { recursive: true });
fs.writeFileSync(
path.join(tmpDir, '.planning', 'ROADMAP.md'),
'## Roadmap\n\n### Phase 1: Setup\n**Goal:** Initial setup\n'
);
});
afterEach(() => {
cleanup(tmpDir);
});
test('fills summary template', () => {
const result = runGsdTools('template fill summary --phase 1', tmpDir);
assert.ok(result.success, `Failed: ${result.error}`);
const out = JSON.parse(result.output);
assert.strictEqual(out.created, true);
assert.ok(out.path.includes('01-01-SUMMARY.md'));
const content = fs.readFileSync(path.join(tmpDir, out.path), 'utf-8');
assert.ok(content.includes('---'), 'should have frontmatter');
assert.ok(content.includes('Phase 1'), 'should reference phase');
assert.ok(content.includes('Accomplishments'), 'should have accomplishments section');
});
test('fills plan template', () => {
const result = runGsdTools('template fill plan --phase 1', tmpDir);
assert.ok(result.success, `Failed: ${result.error}`);
const out = JSON.parse(result.output);
assert.strictEqual(out.created, true);
assert.ok(out.path.includes('01-01-PLAN.md'));
const content = fs.readFileSync(path.join(tmpDir, out.path), 'utf-8');
assert.ok(content.includes('---'), 'should have frontmatter');
assert.ok(content.includes('Objective'), 'should have objective section');
assert.ok(content.includes('<task type="code">'), 'should have task XML');
});
test('fills verification template', () => {
const result = runGsdTools('template fill verification --phase 1', tmpDir);
assert.ok(result.success, `Failed: ${result.error}`);
const out = JSON.parse(result.output);
assert.strictEqual(out.created, true);
assert.ok(out.path.includes('01-VERIFICATION.md'));
const content = fs.readFileSync(path.join(tmpDir, out.path), 'utf-8');
assert.ok(content.includes('Observable Truths'), 'should have truths section');
assert.ok(content.includes('Required Artifacts'), 'should have artifacts section');
});
test('rejects existing file', () => {
// Create the file first
const phaseDir = path.join(tmpDir, '.planning', 'phases', '01-setup');
fs.writeFileSync(path.join(phaseDir, '01-01-SUMMARY.md'), '# Existing');
const result = runGsdTools('template fill summary --phase 1', tmpDir);
assert.ok(result.success); // outputs JSON, doesn't crash
const out = JSON.parse(result.output);
assert.ok(out.error, 'should report error for existing file');
assert.ok(out.error.includes('already exists'));
});
test('errors on unknown template type', () => {
const result = runGsdTools('template fill bogus --phase 1', tmpDir);
assert.ok(!result.success, 'should fail for unknown type');
assert.ok(result.error.includes('Unknown template type'));
});
test('errors when phase not found', () => {
const result = runGsdTools('template fill summary --phase 99', tmpDir);
assert.ok(result.success);
const out = JSON.parse(result.output);
assert.ok(out.error, 'should report phase not found');
});
test('respects --plan option for plan number', () => {
const result = runGsdTools('template fill plan --phase 1 --plan 03', tmpDir);
assert.ok(result.success, `Failed: ${result.error}`);
const out = JSON.parse(result.output);
assert.ok(out.path.includes('01-03-PLAN.md'), `Expected plan 03 in path, got ${out.path}`);
});
});