Merge branch 'main' into feat/materialize-new-project-config
This commit is contained in:
51
.release-monitor.sh
Executable file
51
.release-monitor.sh
Executable 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
|
||||
44
CHANGELOG.md
44
CHANGELOG.md
@@ -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
|
||||
|
||||
19
README.md
19
README.md
@@ -342,19 +342,26 @@ If everything passes, you move on. If something's broken, you don't manually deb
|
||||
|
||||
---
|
||||
|
||||
### 6. Repeat → Complete → Next Milestone
|
||||
### 6. Repeat → Ship → Complete → Next Milestone
|
||||
|
||||
```
|
||||
/gsd:discuss-phase 2
|
||||
/gsd:plan-phase 2
|
||||
/gsd:execute-phase 2
|
||||
/gsd:verify-work 2
|
||||
/gsd:ship 2 # Create PR from verified work
|
||||
...
|
||||
/gsd:complete-milestone
|
||||
/gsd:new-milestone
|
||||
```
|
||||
|
||||
Loop **discuss → plan → execute → verify** until milestone complete.
|
||||
Or let GSD figure out the next step automatically:
|
||||
|
||||
```
|
||||
/gsd:next # Auto-detect and run next step
|
||||
```
|
||||
|
||||
Loop **discuss → plan → execute → verify → ship** until milestone complete.
|
||||
|
||||
If you want faster intake during discussion, use `/gsd:discuss-phase <n> --batch` to answer a small grouped set of questions at once instead of one-by-one.
|
||||
|
||||
@@ -491,6 +498,8 @@ You're never locked in. The system adapts.
|
||||
| `/gsd:plan-phase [N] [--auto]` | Research + plan + verify for a phase |
|
||||
| `/gsd:execute-phase <N>` | Execute all plans in parallel waves, verify when complete |
|
||||
| `/gsd:verify-work [N]` | Manual user acceptance testing ¹ |
|
||||
| `/gsd:ship [N] [--draft]` | Create PR from verified phase work with auto-generated body |
|
||||
| `/gsd:next` | Automatically advance to the next logical workflow step |
|
||||
| `/gsd:audit-milestone` | Verify milestone achieved its definition of done |
|
||||
| `/gsd:complete-milestone` | Archive milestone, tag release |
|
||||
| `/gsd:new-milestone [name]` | Start next version: questions → research → requirements → roadmap |
|
||||
@@ -507,6 +516,7 @@ You're never locked in. The system adapts.
|
||||
| Command | What it does |
|
||||
|---------|--------------|
|
||||
| `/gsd:progress` | Where am I? What's next? |
|
||||
| `/gsd:next` | Auto-detect state and run the next step |
|
||||
| `/gsd:help` | Show all commands and usage guide |
|
||||
| `/gsd:update` | Update GSD with changelog preview |
|
||||
| `/gsd:join-discord` | Join the GSD Discord community |
|
||||
@@ -531,8 +541,9 @@ You're never locked in. The system adapts.
|
||||
|
||||
| Command | What it does |
|
||||
|---------|--------------|
|
||||
| `/gsd:pause-work` | Create handoff when stopping mid-phase |
|
||||
| `/gsd:pause-work` | Create handoff when stopping mid-phase (writes HANDOFF.json) |
|
||||
| `/gsd:resume-work` | Restore from last session |
|
||||
| `/gsd:session-report` | Generate session summary with work performed and outcomes |
|
||||
|
||||
### Utilities
|
||||
|
||||
@@ -581,7 +592,7 @@ Switch profiles:
|
||||
/gsd:set-profile budget
|
||||
```
|
||||
|
||||
Use `inherit` to follow the current runtime model selection (for example OpenCode `/model`).
|
||||
Use `inherit` when using non-Anthropic providers (OpenRouter, local models) or to follow the current runtime model selection (e.g. OpenCode `/model`).
|
||||
|
||||
Or configure via `/gsd:settings`.
|
||||
|
||||
|
||||
333
bin/install.js
333
bin/install.js
@@ -64,6 +64,7 @@ const hasGemini = args.includes('--gemini');
|
||||
const hasCodex = args.includes('--codex');
|
||||
const hasCopilot = args.includes('--copilot');
|
||||
const hasAntigravity = args.includes('--antigravity');
|
||||
const hasCursor = args.includes('--cursor');
|
||||
const hasBoth = args.includes('--both'); // Legacy flag, keeps working
|
||||
const hasAll = args.includes('--all');
|
||||
const hasUninstall = args.includes('--uninstall') || args.includes('-u');
|
||||
@@ -71,7 +72,7 @@ const hasUninstall = args.includes('--uninstall') || args.includes('-u');
|
||||
// Runtime selection - can be set by flags or interactive prompt
|
||||
let selectedRuntimes = [];
|
||||
if (hasAll) {
|
||||
selectedRuntimes = ['claude', 'opencode', 'gemini', 'codex', 'copilot', 'antigravity'];
|
||||
selectedRuntimes = ['claude', 'opencode', 'gemini', 'codex', 'copilot', 'antigravity', 'cursor'];
|
||||
} else if (hasBoth) {
|
||||
selectedRuntimes = ['claude', 'opencode'];
|
||||
} else {
|
||||
@@ -81,6 +82,7 @@ if (hasAll) {
|
||||
if (hasCodex) selectedRuntimes.push('codex');
|
||||
if (hasCopilot) selectedRuntimes.push('copilot');
|
||||
if (hasAntigravity) selectedRuntimes.push('antigravity');
|
||||
if (hasCursor) selectedRuntimes.push('cursor');
|
||||
}
|
||||
|
||||
// WSL + Windows Node.js detection
|
||||
@@ -124,6 +126,7 @@ function getDirName(runtime) {
|
||||
if (runtime === 'gemini') return '.gemini';
|
||||
if (runtime === 'codex') return '.codex';
|
||||
if (runtime === 'antigravity') return '.agent';
|
||||
if (runtime === 'cursor') return '.cursor';
|
||||
return '.claude';
|
||||
}
|
||||
|
||||
@@ -151,6 +154,7 @@ function getConfigDirFromHome(runtime, isGlobal) {
|
||||
if (!isGlobal) return "'.agent'";
|
||||
return "'.gemini', 'antigravity'";
|
||||
}
|
||||
if (runtime === 'cursor') return "'.cursor'";
|
||||
return "'.claude'";
|
||||
}
|
||||
|
||||
@@ -237,6 +241,18 @@ function getGlobalDir(runtime, explicitDir = null) {
|
||||
return path.join(os.homedir(), '.gemini', 'antigravity');
|
||||
}
|
||||
|
||||
if (runtime === 'cursor') {
|
||||
// Cursor: --config-dir > CURSOR_CONFIG_DIR > ~/.cursor
|
||||
if (explicitDir) {
|
||||
return expandTilde(explicitDir);
|
||||
}
|
||||
if (process.env.CURSOR_CONFIG_DIR) {
|
||||
return expandTilde(process.env.CURSOR_CONFIG_DIR);
|
||||
}
|
||||
return path.join(os.homedir(), '.cursor');
|
||||
}
|
||||
|
||||
|
||||
// Claude Code: --config-dir > CLAUDE_CONFIG_DIR > ~/.claude
|
||||
if (explicitDir) {
|
||||
return expandTilde(explicitDir);
|
||||
@@ -257,7 +273,7 @@ const banner = '\n' +
|
||||
'\n' +
|
||||
' Get Shit Done ' + dim + 'v' + pkg.version + reset + '\n' +
|
||||
' A meta-prompting, context engineering and spec-driven\n' +
|
||||
' development system for Claude Code, OpenCode, Gemini, Codex, Copilot, and Antigravity by TÂCHES.\n';
|
||||
' development system for Claude Code, OpenCode, Gemini, Codex, Copilot, Antigravity, and Cursor by TÂCHES.\n';
|
||||
|
||||
// Parse --config-dir argument
|
||||
function parseConfigDirArg() {
|
||||
@@ -295,7 +311,7 @@ if (hasUninstall) {
|
||||
|
||||
// Show help if requested
|
||||
if (hasHelp) {
|
||||
console.log(` ${yellow}Usage:${reset} npx get-shit-done-cc [options]\n\n ${yellow}Options:${reset}\n ${cyan}-g, --global${reset} Install globally (to config directory)\n ${cyan}-l, --local${reset} Install locally (to current directory)\n ${cyan}--claude${reset} Install for Claude Code only\n ${cyan}--opencode${reset} Install for OpenCode only\n ${cyan}--gemini${reset} Install for Gemini only\n ${cyan}--codex${reset} Install for Codex only\n ${cyan}--copilot${reset} Install for Copilot only\n ${cyan}--antigravity${reset} Install for Antigravity only\n ${cyan}--all${reset} Install for all runtimes\n ${cyan}-u, --uninstall${reset} Uninstall GSD (remove all GSD files)\n ${cyan}-c, --config-dir <path>${reset} Specify custom config directory\n ${cyan}-h, --help${reset} Show this help message\n ${cyan}--force-statusline${reset} Replace existing statusline config\n\n ${yellow}Examples:${reset}\n ${dim}# Interactive install (prompts for runtime and location)${reset}\n npx get-shit-done-cc\n\n ${dim}# Install for Claude Code globally${reset}\n npx get-shit-done-cc --claude --global\n\n ${dim}# Install for Gemini globally${reset}\n npx get-shit-done-cc --gemini --global\n\n ${dim}# Install for Codex globally${reset}\n npx get-shit-done-cc --codex --global\n\n ${dim}# Install for Copilot globally${reset}\n npx get-shit-done-cc --copilot --global\n\n ${dim}# Install for Copilot locally${reset}\n npx get-shit-done-cc --copilot --local\n\n ${dim}# Install for Antigravity globally${reset}\n npx get-shit-done-cc --antigravity --global\n\n ${dim}# Install for Antigravity locally${reset}\n npx get-shit-done-cc --antigravity --local\n\n ${dim}# Install for all runtimes globally${reset}\n npx get-shit-done-cc --all --global\n\n ${dim}# Install to custom config directory${reset}\n npx get-shit-done-cc --codex --global --config-dir ~/.codex-work\n\n ${dim}# Install to current project only${reset}\n npx get-shit-done-cc --claude --local\n\n ${dim}# Uninstall GSD from Codex globally${reset}\n npx get-shit-done-cc --codex --global --uninstall\n\n ${yellow}Notes:${reset}\n The --config-dir option is useful when you have multiple configurations.\n It takes priority over CLAUDE_CONFIG_DIR / GEMINI_CONFIG_DIR / CODEX_HOME / COPILOT_CONFIG_DIR / ANTIGRAVITY_CONFIG_DIR environment variables.\n`);
|
||||
console.log(` ${yellow}Usage:${reset} npx get-shit-done-cc [options]\n\n ${yellow}Options:${reset}\n ${cyan}-g, --global${reset} Install globally (to config directory)\n ${cyan}-l, --local${reset} Install locally (to current directory)\n ${cyan}--claude${reset} Install for Claude Code only\n ${cyan}--opencode${reset} Install for OpenCode only\n ${cyan}--gemini${reset} Install for Gemini only\n ${cyan}--codex${reset} Install for Codex only\n ${cyan}--copilot${reset} Install for Copilot only\n ${cyan}--antigravity${reset} Install for Antigravity only\n ${cyan}--cursor${reset} Install for Cursor only\n ${cyan}--all${reset} Install for all runtimes\n ${cyan}-u, --uninstall${reset} Uninstall GSD (remove all GSD files)\n ${cyan}-c, --config-dir <path>${reset} Specify custom config directory\n ${cyan}-h, --help${reset} Show this help message\n ${cyan}--force-statusline${reset} Replace existing statusline config\n\n ${yellow}Examples:${reset}\n ${dim}# Interactive install (prompts for runtime and location)${reset}\n npx get-shit-done-cc\n\n ${dim}# Install for Claude Code globally${reset}\n npx get-shit-done-cc --claude --global\n\n ${dim}# Install for Gemini globally${reset}\n npx get-shit-done-cc --gemini --global\n\n ${dim}# Install for Codex globally${reset}\n npx get-shit-done-cc --codex --global\n\n ${dim}# Install for Copilot globally${reset}\n npx get-shit-done-cc --copilot --global\n\n ${dim}# Install for Copilot locally${reset}\n npx get-shit-done-cc --copilot --local\n\n ${dim}# Install for Antigravity globally${reset}\n npx get-shit-done-cc --antigravity --global\n\n ${dim}# Install for Antigravity locally${reset}\n npx get-shit-done-cc --antigravity --local\n\n ${dim}# Install for Cursor globally${reset}\n npx get-shit-done-cc --cursor --global\n\n ${dim}# Install for Cursor locally${reset}\n npx get-shit-done-cc --cursor --local\n\n ${dim}# Install for all runtimes globally${reset}\n npx get-shit-done-cc --all --global\n\n ${dim}# Install to custom config directory${reset}\n npx get-shit-done-cc --codex --global --config-dir ~/.codex-work\n\n ${dim}# Install to current project only${reset}\n npx get-shit-done-cc --claude --local\n\n ${dim}# Uninstall GSD from Cursor globally${reset}\n npx get-shit-done-cc --cursor --global --uninstall\n\n ${yellow}Notes:${reset}\n The --config-dir option is useful when you have multiple configurations.\n It takes priority over CLAUDE_CONFIG_DIR / GEMINI_CONFIG_DIR / CODEX_HOME / COPILOT_CONFIG_DIR / ANTIGRAVITY_CONFIG_DIR / CURSOR_CONFIG_DIR environment variables.\n`);
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
@@ -694,6 +710,14 @@ function yamlQuote(value) {
|
||||
return JSON.stringify(value);
|
||||
}
|
||||
|
||||
function yamlIdentifier(value) {
|
||||
const text = String(value).trim();
|
||||
if (/^[A-Za-z0-9][A-Za-z0-9-]*$/.test(text)) {
|
||||
return text;
|
||||
}
|
||||
return yamlQuote(text);
|
||||
}
|
||||
|
||||
function extractFrontmatterAndBody(content) {
|
||||
if (!content.startsWith('---')) {
|
||||
return { frontmatter: null, body: content };
|
||||
@@ -717,6 +741,121 @@ function extractFrontmatterField(frontmatter, fieldName) {
|
||||
return match[1].trim().replace(/^['"]|['"]$/g, '');
|
||||
}
|
||||
|
||||
// Tool name mapping from Claude Code to Cursor CLI
|
||||
const claudeToCursorTools = {
|
||||
Bash: 'Shell',
|
||||
Edit: 'StrReplace',
|
||||
AskUserQuestion: null, // No direct equivalent — use conversational prompting
|
||||
SlashCommand: null, // No equivalent — skills are auto-discovered
|
||||
};
|
||||
|
||||
/**
|
||||
* Convert a Claude Code tool name to Cursor CLI format
|
||||
* @returns {string|null} Cursor tool name, or null if tool should be excluded
|
||||
*/
|
||||
function convertCursorToolName(claudeTool) {
|
||||
if (claudeTool in claudeToCursorTools) {
|
||||
return claudeToCursorTools[claudeTool];
|
||||
}
|
||||
// MCP tools keep their format (Cursor supports MCP)
|
||||
if (claudeTool.startsWith('mcp__')) {
|
||||
return claudeTool;
|
||||
}
|
||||
// Most tools share the same name (Read, Write, Glob, Grep, Task, WebSearch, WebFetch, TodoWrite)
|
||||
return claudeTool;
|
||||
}
|
||||
|
||||
function convertSlashCommandsToCursorSkillMentions(content) {
|
||||
// Keep leading "/" for slash commands; only normalize gsd: -> gsd-.
|
||||
// This preserves rendered "next step" commands like "/gsd-execute-phase 17".
|
||||
return content.replace(/gsd:/gi, 'gsd-');
|
||||
}
|
||||
|
||||
function convertClaudeToCursorMarkdown(content) {
|
||||
let converted = convertSlashCommandsToCursorSkillMentions(content);
|
||||
// Replace tool name references in body text
|
||||
converted = converted.replace(/\bBash\(/g, 'Shell(');
|
||||
converted = converted.replace(/\bEdit\(/g, 'StrReplace(');
|
||||
converted = converted.replace(/\bAskUserQuestion\b/g, 'conversational prompting');
|
||||
// Replace subagent_type from Claude to Cursor format
|
||||
converted = converted.replace(/subagent_type="general-purpose"/g, 'subagent_type="generalPurpose"');
|
||||
converted = converted.replace(/\$ARGUMENTS\b/g, '{{GSD_ARGS}}');
|
||||
// Replace project-level Claude conventions with Cursor equivalents
|
||||
converted = converted.replace(/`\.\/CLAUDE\.md`/g, '`.cursor/rules/`');
|
||||
converted = converted.replace(/\.\/CLAUDE\.md/g, '.cursor/rules/');
|
||||
converted = converted.replace(/`CLAUDE\.md`/g, '`.cursor/rules/`');
|
||||
converted = converted.replace(/\bCLAUDE\.md\b/g, '.cursor/rules/');
|
||||
converted = converted.replace(/\.claude\/skills\//g, '.cursor/skills/');
|
||||
// Remove Claude Code-specific bug workarounds before brand replacement
|
||||
converted = converted.replace(/\*\*Known Claude Code bug \(classifyHandoffIfNeeded\):\*\*[^\n]*\n/g, '');
|
||||
converted = converted.replace(/- \*\*classifyHandoffIfNeeded false failure:\*\*[^\n]*\n/g, '');
|
||||
// Replace "Claude Code" brand references with "Cursor"
|
||||
converted = converted.replace(/\bClaude Code\b/g, 'Cursor');
|
||||
return converted;
|
||||
}
|
||||
|
||||
function getCursorSkillAdapterHeader(skillName) {
|
||||
return `<cursor_skill_adapter>
|
||||
## A. Skill Invocation
|
||||
- This skill is invoked when the user mentions \`${skillName}\` or describes a task matching this skill.
|
||||
- Treat all user text after the skill mention as \`{{GSD_ARGS}}\`.
|
||||
- If no arguments are present, treat \`{{GSD_ARGS}}\` as empty.
|
||||
|
||||
## B. User Prompting
|
||||
When the workflow needs user input, prompt the user conversationally:
|
||||
- Present options as a numbered list in your response text
|
||||
- Ask the user to reply with their choice
|
||||
- For multi-select, ask for comma-separated numbers
|
||||
|
||||
## C. Tool Usage
|
||||
Use these Cursor tools when executing GSD workflows:
|
||||
- \`Shell\` for running commands (terminal operations)
|
||||
- \`StrReplace\` for editing existing files
|
||||
- \`Read\`, \`Write\`, \`Glob\`, \`Grep\`, \`Task\`, \`WebSearch\`, \`WebFetch\`, \`TodoWrite\` as needed
|
||||
|
||||
## D. Subagent Spawning
|
||||
When the workflow needs to spawn a subagent:
|
||||
- Use \`Task(subagent_type="generalPurpose", ...)\`
|
||||
- The \`model\` parameter maps to Cursor's model options (e.g., "fast")
|
||||
</cursor_skill_adapter>`;
|
||||
}
|
||||
|
||||
function convertClaudeCommandToCursorSkill(content, skillName) {
|
||||
const converted = convertClaudeToCursorMarkdown(content);
|
||||
const { frontmatter, body } = extractFrontmatterAndBody(converted);
|
||||
let description = `Run GSD workflow ${skillName}.`;
|
||||
if (frontmatter) {
|
||||
const maybeDescription = extractFrontmatterField(frontmatter, 'description');
|
||||
if (maybeDescription) {
|
||||
description = maybeDescription;
|
||||
}
|
||||
}
|
||||
description = toSingleLine(description);
|
||||
const shortDescription = description.length > 180 ? `${description.slice(0, 177)}...` : description;
|
||||
const adapter = getCursorSkillAdapterHeader(skillName);
|
||||
|
||||
return `---\nname: ${yamlIdentifier(skillName)}\ndescription: ${yamlQuote(shortDescription)}\n---\n\n${adapter}\n\n${body.trimStart()}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert Claude Code agent markdown to Cursor agent format.
|
||||
* Strips frontmatter fields Cursor doesn't support (color, skills),
|
||||
* converts tool references, and adds a role context header.
|
||||
*/
|
||||
function convertClaudeAgentToCursorAgent(content) {
|
||||
let converted = convertClaudeToCursorMarkdown(content);
|
||||
|
||||
const { frontmatter, body } = extractFrontmatterAndBody(converted);
|
||||
if (!frontmatter) return converted;
|
||||
|
||||
const name = extractFrontmatterField(frontmatter, 'name') || 'unknown';
|
||||
const description = extractFrontmatterField(frontmatter, 'description') || '';
|
||||
|
||||
const cleanFrontmatter = `---\nname: ${yamlIdentifier(name)}\ndescription: ${yamlQuote(toSingleLine(description))}\n---`;
|
||||
|
||||
return `${cleanFrontmatter}\n${body}`;
|
||||
}
|
||||
|
||||
function convertSlashCommandsToCodexSkillMentions(content) {
|
||||
let converted = content.replace(/\/gsd:([a-z0-9-]+)/gi, (_, commandName) => {
|
||||
return `$gsd-${String(commandName).toLowerCase()}`;
|
||||
@@ -1228,6 +1367,13 @@ function convertClaudeToOpencodeFrontmatter(content, { isAgent = false } = {}) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Strip model: field — OpenCode doesn't support Claude Code model aliases
|
||||
// like 'haiku', 'sonnet', 'opus', or 'inherit'. Omitting lets OpenCode use
|
||||
// its configured default model. See #1156.
|
||||
if (trimmed.startsWith('model:')) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Convert color names to hex for opencode (commands only; agents strip color above)
|
||||
if (trimmed.startsWith('color:')) {
|
||||
const colorValue = trimmed.substring(6).trim().toLowerCase();
|
||||
@@ -1264,8 +1410,10 @@ function convertClaudeToOpencodeFrontmatter(content, { isAgent = false } = {}) {
|
||||
}
|
||||
|
||||
// For agents: add required OpenCode agent fields
|
||||
// Note: Do NOT add 'model: inherit' — OpenCode does not recognize the 'inherit'
|
||||
// keyword and throws ProviderModelNotFoundError. Omitting model: lets OpenCode
|
||||
// use its default model for subagents. See #1156.
|
||||
if (isAgent) {
|
||||
newLines.push('model: inherit');
|
||||
newLines.push('mode: subagent');
|
||||
}
|
||||
|
||||
@@ -1445,6 +1593,59 @@ function copyCommandsAsCodexSkills(srcDir, skillsDir, prefix, pathPrefix, runtim
|
||||
recurse(srcDir, prefix);
|
||||
}
|
||||
|
||||
function copyCommandsAsCursorSkills(srcDir, skillsDir, prefix, pathPrefix, runtime) {
|
||||
if (!fs.existsSync(srcDir)) {
|
||||
return;
|
||||
}
|
||||
|
||||
fs.mkdirSync(skillsDir, { recursive: true });
|
||||
|
||||
// Remove previous GSD Cursor skills to avoid stale command skills
|
||||
const existing = fs.readdirSync(skillsDir, { withFileTypes: true });
|
||||
for (const entry of existing) {
|
||||
if (entry.isDirectory() && entry.name.startsWith(`${prefix}-`)) {
|
||||
fs.rmSync(path.join(skillsDir, entry.name), { recursive: true });
|
||||
}
|
||||
}
|
||||
|
||||
function recurse(currentSrcDir, currentPrefix) {
|
||||
const entries = fs.readdirSync(currentSrcDir, { withFileTypes: true });
|
||||
|
||||
for (const entry of entries) {
|
||||
const srcPath = path.join(currentSrcDir, entry.name);
|
||||
if (entry.isDirectory()) {
|
||||
recurse(srcPath, `${currentPrefix}-${entry.name}`);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!entry.name.endsWith('.md')) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const baseName = entry.name.replace('.md', '');
|
||||
const skillName = `${currentPrefix}-${baseName}`;
|
||||
const skillDir = path.join(skillsDir, skillName);
|
||||
fs.mkdirSync(skillDir, { recursive: true });
|
||||
|
||||
let content = fs.readFileSync(srcPath, 'utf8');
|
||||
const globalClaudeRegex = /~\/\.claude\//g;
|
||||
const globalClaudeHomeRegex = /\$HOME\/\.claude\//g;
|
||||
const localClaudeRegex = /\.\/\.claude\//g;
|
||||
const cursorDirRegex = /~\/\.cursor\//g;
|
||||
content = content.replace(globalClaudeRegex, pathPrefix);
|
||||
content = content.replace(globalClaudeHomeRegex, pathPrefix);
|
||||
content = content.replace(localClaudeRegex, `./${getDirName(runtime)}/`);
|
||||
content = content.replace(cursorDirRegex, pathPrefix);
|
||||
content = processAttribution(content, getCommitAttribution(runtime));
|
||||
content = convertClaudeCommandToCursorSkill(content, skillName);
|
||||
|
||||
fs.writeFileSync(path.join(skillDir, 'SKILL.md'), content);
|
||||
}
|
||||
}
|
||||
|
||||
recurse(srcDir, prefix);
|
||||
}
|
||||
|
||||
/**
|
||||
* Copy Claude commands as Copilot skills — one folder per skill with SKILL.md.
|
||||
* Applies CONV-01 (structure), CONV-02 (allowed-tools), CONV-06 (paths), CONV-07 (command names).
|
||||
@@ -1561,6 +1762,7 @@ function copyWithPathReplacement(srcDir, destDir, pathPrefix, runtime, isCommand
|
||||
const isCodex = runtime === 'codex';
|
||||
const isCopilot = runtime === 'copilot';
|
||||
const isAntigravity = runtime === 'antigravity';
|
||||
const isCursor = runtime === 'cursor';
|
||||
const dirName = getDirName(runtime);
|
||||
|
||||
// Clean install: remove existing destination to prevent orphaned files
|
||||
@@ -1617,6 +1819,9 @@ function copyWithPathReplacement(srcDir, destDir, pathPrefix, runtime, isCommand
|
||||
content = convertClaudeToAntigravityContent(content, isGlobal);
|
||||
content = processAttribution(content, getCommitAttribution(runtime));
|
||||
fs.writeFileSync(destPath, content);
|
||||
} else if (isCursor) {
|
||||
content = convertClaudeToCursorMarkdown(content);
|
||||
fs.writeFileSync(destPath, content);
|
||||
} else {
|
||||
fs.writeFileSync(destPath, content);
|
||||
}
|
||||
@@ -1630,6 +1835,14 @@ function copyWithPathReplacement(srcDir, destDir, pathPrefix, runtime, isCommand
|
||||
let content = fs.readFileSync(srcPath, 'utf8');
|
||||
content = convertClaudeToAntigravityContent(content, isGlobal);
|
||||
fs.writeFileSync(destPath, content);
|
||||
} else if (isCursor && (entry.name.endsWith('.cjs') || entry.name.endsWith('.js'))) {
|
||||
// For Cursor, also convert Claude references in JS/CJS utility scripts
|
||||
let jsContent = fs.readFileSync(srcPath, 'utf8');
|
||||
jsContent = jsContent.replace(/gsd:/gi, 'gsd-');
|
||||
jsContent = jsContent.replace(/\.claude\/skills\//g, '.cursor/skills/');
|
||||
jsContent = jsContent.replace(/CLAUDE\.md/g, '.cursor/rules/');
|
||||
jsContent = jsContent.replace(/\bClaude Code\b/g, 'Cursor');
|
||||
fs.writeFileSync(destPath, jsContent);
|
||||
} else {
|
||||
fs.copyFileSync(srcPath, destPath);
|
||||
}
|
||||
@@ -1722,6 +1935,7 @@ function uninstall(isGlobal, runtime = 'claude') {
|
||||
const isCodex = runtime === 'codex';
|
||||
const isCopilot = runtime === 'copilot';
|
||||
const isAntigravity = runtime === 'antigravity';
|
||||
const isCursor = runtime === 'cursor';
|
||||
const dirName = getDirName(runtime);
|
||||
|
||||
// Get the target directory based on runtime and install type
|
||||
@@ -1739,6 +1953,7 @@ function uninstall(isGlobal, runtime = 'claude') {
|
||||
if (runtime === 'codex') runtimeLabel = 'Codex';
|
||||
if (runtime === 'copilot') runtimeLabel = 'Copilot';
|
||||
if (runtime === 'antigravity') runtimeLabel = 'Antigravity';
|
||||
if (runtime === 'cursor') runtimeLabel = 'Cursor';
|
||||
|
||||
console.log(` Uninstalling GSD from ${cyan}${runtimeLabel}${reset} at ${cyan}${locationLabel}${reset}\n`);
|
||||
|
||||
@@ -1765,8 +1980,8 @@ function uninstall(isGlobal, runtime = 'claude') {
|
||||
}
|
||||
console.log(` ${green}✓${reset} Removed GSD commands from command/`);
|
||||
}
|
||||
} else if (isCodex) {
|
||||
// Codex: remove skills/gsd-*/SKILL.md skill directories
|
||||
} else if (isCodex || isCursor) {
|
||||
// Codex/Cursor: remove skills/gsd-*/SKILL.md skill directories
|
||||
const skillsDir = path.join(targetDir, 'skills');
|
||||
if (fs.existsSync(skillsDir)) {
|
||||
let skillCount = 0;
|
||||
@@ -1779,11 +1994,12 @@ function uninstall(isGlobal, runtime = 'claude') {
|
||||
}
|
||||
if (skillCount > 0) {
|
||||
removedCount++;
|
||||
console.log(` ${green}✓${reset} Removed ${skillCount} Codex skills`);
|
||||
console.log(` ${green}✓${reset} Removed ${skillCount} ${runtimeLabel} skills`);
|
||||
}
|
||||
}
|
||||
|
||||
// Codex: remove GSD agent .toml config files
|
||||
// Codex-only: remove GSD agent .toml config files and config.toml sections
|
||||
if (isCodex) {
|
||||
const codexAgentsDir = path.join(targetDir, 'agents');
|
||||
if (fs.existsSync(codexAgentsDir)) {
|
||||
const tomlFiles = fs.readdirSync(codexAgentsDir);
|
||||
@@ -1816,6 +2032,7 @@ function uninstall(isGlobal, runtime = 'claude') {
|
||||
console.log(` ${green}✓${reset} Cleaned GSD sections from config.toml`);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (isCopilot) {
|
||||
// Copilot: remove skills/gsd-*/ directories (same layout as Codex skills)
|
||||
const skillsDir = path.join(targetDir, 'skills');
|
||||
@@ -1866,6 +2083,23 @@ function uninstall(isGlobal, runtime = 'claude') {
|
||||
console.log(` ${green}✓${reset} Removed ${skillCount} Antigravity skills`);
|
||||
}
|
||||
}
|
||||
} else if (isCursor) {
|
||||
// Cursor: remove skills/gsd-*/ directories (same layout as Codex skills)
|
||||
const skillsDir = path.join(targetDir, 'skills');
|
||||
if (fs.existsSync(skillsDir)) {
|
||||
let skillCount = 0;
|
||||
const entries = fs.readdirSync(skillsDir, { withFileTypes: true });
|
||||
for (const entry of entries) {
|
||||
if (entry.isDirectory() && entry.name.startsWith('gsd-')) {
|
||||
fs.rmSync(path.join(skillsDir, entry.name), { recursive: true });
|
||||
skillCount++;
|
||||
}
|
||||
}
|
||||
if (skillCount > 0) {
|
||||
removedCount++;
|
||||
console.log(` ${green}✓${reset} Removed ${skillCount} Cursor skills`);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
const gsdCommandsDir = path.join(targetDir, 'commands', 'gsd');
|
||||
if (fs.existsSync(gsdCommandsDir)) {
|
||||
@@ -2274,6 +2508,7 @@ function writeManifest(configDir, runtime = 'claude') {
|
||||
const isCodex = runtime === 'codex';
|
||||
const isCopilot = runtime === 'copilot';
|
||||
const isAntigravity = runtime === 'antigravity';
|
||||
const isCursor = runtime === 'cursor';
|
||||
const gsdDir = path.join(configDir, 'get-shit-done');
|
||||
const commandsDir = path.join(configDir, 'commands', 'gsd');
|
||||
const opencodeCommandDir = path.join(configDir, 'command');
|
||||
@@ -2285,7 +2520,7 @@ function writeManifest(configDir, runtime = 'claude') {
|
||||
for (const [rel, hash] of Object.entries(gsdHashes)) {
|
||||
manifest.files['get-shit-done/' + rel] = hash;
|
||||
}
|
||||
if (!isOpencode && !isCodex && !isCopilot && !isAntigravity && fs.existsSync(commandsDir)) {
|
||||
if (!isOpencode && !isCodex && !isCopilot && !isAntigravity && !isCursor && fs.existsSync(commandsDir)) {
|
||||
const cmdHashes = generateManifest(commandsDir);
|
||||
for (const [rel, hash] of Object.entries(cmdHashes)) {
|
||||
manifest.files['commands/gsd/' + rel] = hash;
|
||||
@@ -2298,7 +2533,7 @@ function writeManifest(configDir, runtime = 'claude') {
|
||||
}
|
||||
}
|
||||
}
|
||||
if ((isCodex || isCopilot || isAntigravity) && fs.existsSync(codexSkillsDir)) {
|
||||
if ((isCodex || isCopilot || isAntigravity || isCursor) && fs.existsSync(codexSkillsDir)) {
|
||||
for (const skillName of listCodexSkillNames(codexSkillsDir)) {
|
||||
const skillRoot = path.join(codexSkillsDir, skillName);
|
||||
const skillHashes = generateManifest(skillRoot);
|
||||
@@ -2376,7 +2611,9 @@ function reportLocalPatches(configDir, runtime = 'claude') {
|
||||
? '/gsd-reapply-patches'
|
||||
: runtime === 'codex'
|
||||
? '$gsd-reapply-patches'
|
||||
: '/gsd:reapply-patches';
|
||||
: runtime === 'cursor'
|
||||
? 'gsd-reapply-patches (mention the skill name)'
|
||||
: '/gsd:reapply-patches';
|
||||
console.log('');
|
||||
console.log(' ' + yellow + 'Local patches detected' + reset + ' (from v' + meta.from_version + '):');
|
||||
for (const f of meta.files) {
|
||||
@@ -2397,6 +2634,7 @@ function install(isGlobal, runtime = 'claude') {
|
||||
const isCodex = runtime === 'codex';
|
||||
const isCopilot = runtime === 'copilot';
|
||||
const isAntigravity = runtime === 'antigravity';
|
||||
const isCursor = runtime === 'cursor';
|
||||
const dirName = getDirName(runtime);
|
||||
const src = path.join(__dirname, '..');
|
||||
|
||||
@@ -2421,6 +2659,7 @@ function install(isGlobal, runtime = 'claude') {
|
||||
if (isCodex) runtimeLabel = 'Codex';
|
||||
if (isCopilot) runtimeLabel = 'Copilot';
|
||||
if (isAntigravity) runtimeLabel = 'Antigravity';
|
||||
if (isCursor) runtimeLabel = 'Cursor';
|
||||
|
||||
console.log(` Installing for ${cyan}${runtimeLabel}${reset} to ${cyan}${locationLabel}${reset}\n`);
|
||||
|
||||
@@ -2488,6 +2727,16 @@ function install(isGlobal, runtime = 'claude') {
|
||||
} else {
|
||||
failures.push('skills/gsd-*');
|
||||
}
|
||||
} else if (isCursor) {
|
||||
const skillsDir = path.join(targetDir, 'skills');
|
||||
const gsdSrc = path.join(src, 'commands', 'gsd');
|
||||
copyCommandsAsCursorSkills(gsdSrc, skillsDir, 'gsd', pathPrefix, runtime);
|
||||
const installedSkillNames = listCodexSkillNames(skillsDir); // reuse — same dir structure
|
||||
if (installedSkillNames.length > 0) {
|
||||
console.log(` ${green}✓${reset} Installed ${installedSkillNames.length} skills to skills/`);
|
||||
} else {
|
||||
failures.push('skills/gsd-*');
|
||||
}
|
||||
} else {
|
||||
// Claude Code & Gemini: nested structure in commands/ directory
|
||||
const commandsDir = path.join(targetDir, 'commands');
|
||||
@@ -2552,6 +2801,8 @@ function install(isGlobal, runtime = 'claude') {
|
||||
content = convertClaudeAgentToCopilotAgent(content, isGlobal);
|
||||
} else if (isAntigravity) {
|
||||
content = convertClaudeAgentToAntigravityAgent(content, isGlobal);
|
||||
} else if (isCursor) {
|
||||
content = convertClaudeAgentToCursorAgent(content);
|
||||
}
|
||||
const destName = isCopilot ? entry.name.replace('.md', '.agent.md') : entry.name;
|
||||
fs.writeFileSync(path.join(agentsDest, destName), content);
|
||||
@@ -2585,7 +2836,7 @@ function install(isGlobal, runtime = 'claude') {
|
||||
failures.push('VERSION');
|
||||
}
|
||||
|
||||
if (!isCodex && !isCopilot) {
|
||||
if (!isCodex && !isCopilot && !isCursor) {
|
||||
// Write package.json to force CommonJS mode for GSD scripts
|
||||
// Prevents "require is not defined" errors when project has "type": "module"
|
||||
// Node.js walks up looking for package.json - this stops inheritance from project
|
||||
@@ -2606,10 +2857,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);
|
||||
}
|
||||
@@ -2689,6 +2944,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 };
|
||||
}
|
||||
|
||||
@@ -2705,6 +2990,11 @@ function install(isGlobal, runtime = 'claude') {
|
||||
return { settingsPath: null, settings: null, statuslineCommand: null, runtime };
|
||||
}
|
||||
|
||||
if (isCursor) {
|
||||
// Cursor uses skills — no config.toml, no settings.json hooks needed
|
||||
return { settingsPath: null, settings: null, statuslineCommand: null, runtime };
|
||||
}
|
||||
|
||||
// Configure statusline and hooks in settings.json
|
||||
// Gemini and Antigravity use AfterTool instead of PostToolUse for post-tool hooks
|
||||
const postToolEvent = (runtime === 'gemini' || runtime === 'antigravity') ? 'AfterTool' : 'PostToolUse';
|
||||
@@ -2788,8 +3078,9 @@ function finishInstall(settingsPath, settings, statuslineCommand, shouldInstallS
|
||||
const isOpencode = runtime === 'opencode';
|
||||
const isCodex = runtime === 'codex';
|
||||
const isCopilot = runtime === 'copilot';
|
||||
const isCursor = runtime === 'cursor';
|
||||
|
||||
if (shouldInstallStatusline && !isOpencode && !isCodex && !isCopilot) {
|
||||
if (shouldInstallStatusline && !isOpencode && !isCodex && !isCopilot && !isCursor) {
|
||||
settings.statusLine = {
|
||||
type: 'command',
|
||||
command: statuslineCommand
|
||||
@@ -2798,7 +3089,7 @@ function finishInstall(settingsPath, settings, statuslineCommand, shouldInstallS
|
||||
}
|
||||
|
||||
// Write settings when runtime supports settings.json
|
||||
if (!isCodex && !isCopilot) {
|
||||
if (!isCodex && !isCopilot && !isCursor) {
|
||||
writeSettings(settingsPath, settings);
|
||||
}
|
||||
|
||||
@@ -2813,12 +3104,14 @@ function finishInstall(settingsPath, settings, statuslineCommand, shouldInstallS
|
||||
if (runtime === 'codex') program = 'Codex';
|
||||
if (runtime === 'copilot') program = 'Copilot';
|
||||
if (runtime === 'antigravity') program = 'Antigravity';
|
||||
if (runtime === 'cursor') program = 'Cursor';
|
||||
|
||||
let command = '/gsd:new-project';
|
||||
if (runtime === 'opencode') command = '/gsd-new-project';
|
||||
if (runtime === 'codex') command = '$gsd-new-project';
|
||||
if (runtime === 'copilot') command = '/gsd-new-project';
|
||||
if (runtime === 'antigravity') command = '/gsd-new-project';
|
||||
if (runtime === 'cursor') command = 'gsd-new-project (mention the skill name)';
|
||||
console.log(`
|
||||
${green}Done!${reset} Open a blank directory in ${program} and run ${cyan}${command}${reset}.
|
||||
|
||||
@@ -2902,15 +3195,18 @@ function promptRuntime(callback) {
|
||||
${cyan}4${reset}) Codex ${dim}(~/.codex)${reset}
|
||||
${cyan}5${reset}) Copilot ${dim}(~/.copilot)${reset}
|
||||
${cyan}6${reset}) Antigravity ${dim}(~/.gemini/antigravity)${reset}
|
||||
${cyan}7${reset}) All
|
||||
${cyan}7${reset}) Cursor ${dim}(~/.cursor)${reset}
|
||||
${cyan}8${reset}) All
|
||||
`);
|
||||
|
||||
rl.question(` Choice ${dim}[1]${reset}: `, (answer) => {
|
||||
answered = true;
|
||||
rl.close();
|
||||
const choice = answer.trim() || '1';
|
||||
if (choice === '7') {
|
||||
callback(['claude', 'opencode', 'gemini', 'codex', 'copilot', 'antigravity']);
|
||||
if (choice === '8') {
|
||||
callback(['claude', 'opencode', 'gemini', 'codex', 'copilot', 'antigravity', 'cursor']);
|
||||
} else if (choice === '7') {
|
||||
callback(['cursor']);
|
||||
} else if (choice === '6') {
|
||||
callback(['antigravity']);
|
||||
} else if (choice === '5') {
|
||||
@@ -3010,7 +3306,10 @@ function installAllRuntimes(runtimes, isGlobal, isInteractive) {
|
||||
// Test-only exports — skip main logic when loaded as a module for testing
|
||||
if (process.env.GSD_TEST_MODE) {
|
||||
module.exports = {
|
||||
yamlIdentifier,
|
||||
getCodexSkillAdapterHeader,
|
||||
convertClaudeCommandToCursorSkill,
|
||||
convertClaudeAgentToCursorAgent,
|
||||
convertClaudeToGeminiAgent,
|
||||
convertClaudeAgentToCodexAgent,
|
||||
generateCodexAgentToml,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
---
|
||||
name: gsd:discuss-phase
|
||||
description: Gather phase context through adaptive questioning before planning. Use --auto to skip interactive questions (Claude picks recommended defaults).
|
||||
argument-hint: "<phase> [--auto]"
|
||||
argument-hint: "<phase> [--auto] [--batch] [--analyze]"
|
||||
allowed-tools:
|
||||
- Read
|
||||
- Write
|
||||
|
||||
@@ -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>
|
||||
|
||||
30
commands/gsd/fast.md
Normal file
30
commands/gsd/fast.md
Normal file
@@ -0,0 +1,30 @@
|
||||
---
|
||||
name: gsd:fast
|
||||
description: Execute a trivial task inline — no subagents, no planning overhead
|
||||
argument-hint: "[task description]"
|
||||
allowed-tools:
|
||||
- Read
|
||||
- Write
|
||||
- Edit
|
||||
- Bash
|
||||
- Grep
|
||||
- Glob
|
||||
---
|
||||
|
||||
<objective>
|
||||
Execute a trivial task directly in the current context without spawning subagents
|
||||
or generating PLAN.md files. For tasks too small to justify planning overhead:
|
||||
typo fixes, config changes, small refactors, forgotten commits, simple additions.
|
||||
|
||||
This is NOT a replacement for /gsd:quick — use /gsd:quick for anything that
|
||||
needs research, multi-step planning, or verification. /gsd:fast is for tasks
|
||||
you could describe in one sentence and execute in under 2 minutes.
|
||||
</objective>
|
||||
|
||||
<execution_context>
|
||||
@~/.claude/get-shit-done/workflows/fast.md
|
||||
</execution_context>
|
||||
|
||||
<process>
|
||||
Execute the fast workflow from @~/.claude/get-shit-done/workflows/fast.md end-to-end.
|
||||
</process>
|
||||
24
commands/gsd/next.md
Normal file
24
commands/gsd/next.md
Normal 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>
|
||||
19
commands/gsd/session-report.md
Normal file
19
commands/gsd/session-report.md
Normal 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
23
commands/gsd/ship.md
Normal 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.
|
||||
@@ -247,6 +247,14 @@ Each executor gets:
|
||||
- Project context (PROJECT.md, STATE.md)
|
||||
- Phase context (CONTEXT.md, RESEARCH.md if available)
|
||||
|
||||
#### Parallel Commit Safety
|
||||
|
||||
When multiple executors run within the same wave, two mechanisms prevent conflicts:
|
||||
|
||||
1. **`--no-verify` commits** — Parallel agents skip pre-commit hooks (which can cause build lock contention, e.g., cargo lock fights in Rust projects). The orchestrator runs `git hook run pre-commit` once after each wave completes.
|
||||
|
||||
2. **STATE.md file locking** — All `writeStateMd()` calls use lockfile-based mutual exclusion (`STATE.md.lock` with `O_EXCL` atomic creation). This prevents the read-modify-write race condition where two agents read STATE.md, modify different fields, and the last writer overwrites the other's changes. Includes stale lock detection (10s timeout) and spin-wait with jitter.
|
||||
|
||||
---
|
||||
|
||||
## Data Flow
|
||||
@@ -358,7 +366,7 @@ Equivalent paths for other runtimes:
|
||||
|
||||
```
|
||||
.planning/
|
||||
├── PROJECT.md # Project vision, constraints, decisions
|
||||
├── PROJECT.md # Project vision, constraints, decisions, evolution rules
|
||||
├── REQUIREMENTS.md # Scoped requirements (v1/v2/out-of-scope)
|
||||
├── ROADMAP.md # Phase breakdown with status tracking
|
||||
├── STATE.md # Living memory: position, decisions, blockers, metrics
|
||||
|
||||
@@ -331,7 +331,10 @@ node gsd-tools.cjs progress [json|table|bar]
|
||||
node gsd-tools.cjs todo complete <filename>
|
||||
|
||||
# Git commit with config checks
|
||||
node gsd-tools.cjs commit <message> [--files f1 f2] [--amend]
|
||||
node gsd-tools.cjs commit <message> [--files f1 f2] [--amend] [--no-verify]
|
||||
```
|
||||
|
||||
> **`--no-verify`**: Skips pre-commit hooks. Used by parallel executor agents during wave-based execution to avoid build lock contention (e.g., cargo lock fights in Rust projects). The orchestrator runs hooks once after each wave completes. Do not use `--no-verify` during sequential execution — let hooks run normally.
|
||||
|
||||
# Web search (requires Brave API key)
|
||||
node gsd-tools.cjs websearch <query> [--limit N] [--freshness day|week|month]
|
||||
|
||||
@@ -132,6 +132,71 @@ User acceptance testing with auto-diagnosis.
|
||||
|
||||
---
|
||||
|
||||
### `/gsd:next`
|
||||
|
||||
Automatically advance to the next logical workflow step. Reads project state and runs the appropriate command.
|
||||
|
||||
**Prerequisites:** `.planning/` directory exists
|
||||
**Behavior:**
|
||||
- No project → suggests `/gsd:new-project`
|
||||
- Phase needs discussion → runs `/gsd:discuss-phase`
|
||||
- Phase needs planning → runs `/gsd:plan-phase`
|
||||
- Phase needs execution → runs `/gsd:execute-phase`
|
||||
- Phase needs verification → runs `/gsd:verify-work`
|
||||
- All phases complete → suggests `/gsd:complete-milestone`
|
||||
|
||||
```bash
|
||||
/gsd:next # Auto-detect and run next step
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### `/gsd:session-report`
|
||||
|
||||
Generate a session report with work summary, outcomes, and estimated resource usage.
|
||||
|
||||
**Prerequisites:** Active project with recent work
|
||||
**Produces:** `.planning/reports/SESSION_REPORT.md`
|
||||
|
||||
```bash
|
||||
/gsd:session-report # Generate post-session summary
|
||||
```
|
||||
|
||||
**Report includes:**
|
||||
- Work performed (commits, plans executed, phases progressed)
|
||||
- Outcomes and deliverables
|
||||
- Blockers and decisions made
|
||||
- Estimated token/cost usage
|
||||
- Next steps recommendation
|
||||
|
||||
---
|
||||
|
||||
### `/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.
|
||||
|
||||
@@ -133,6 +133,8 @@ To keep planning artifacts out of git:
|
||||
| `parallelization.max_concurrent_agents` | number | `3` | Maximum simultaneous agents |
|
||||
| `parallelization.min_plans_for_parallel` | number | `2` | Minimum plans to trigger parallel execution |
|
||||
|
||||
> **Pre-commit hooks and parallel execution**: When parallelization is enabled, executor agents commit with `--no-verify` to avoid build lock contention (e.g., cargo lock fights in Rust projects). The orchestrator validates hooks once after each wave completes. STATE.md writes are protected by file-level locking to prevent concurrent write corruption. If you need hooks to run per-commit, set `parallelization.enabled: false`.
|
||||
|
||||
---
|
||||
|
||||
## Git Branching
|
||||
@@ -246,7 +248,7 @@ Valid override values: `opus`, `sonnet`, `haiku`, `inherit`
|
||||
| `quality` | Opus for all decision-making, Sonnet for verification | Quota available, critical architecture work |
|
||||
| `balanced` | Opus for planning only, Sonnet for everything else | Normal development (default) |
|
||||
| `budget` | Sonnet for code-writing, Haiku for research/verification | High-volume work, less critical phases |
|
||||
| `inherit` | All agents use current session model | Dynamic model switching (OpenCode `/model`) |
|
||||
| `inherit` | All agents use current session model | Dynamic model switching, **non-Anthropic providers** (OpenRouter, local models) |
|
||||
|
||||
---
|
||||
|
||||
|
||||
206
docs/FEATURES.md
206
docs/FEATURES.md
@@ -20,33 +20,38 @@
|
||||
- [Quick Mode](#10-quick-mode)
|
||||
- [Autonomous Mode](#11-autonomous-mode)
|
||||
- [Freeform Routing](#12-freeform-routing)
|
||||
- [Note Capture](#13-note-capture)
|
||||
- [Auto-Advance (Next)](#14-auto-advance-next)
|
||||
- [Quality Assurance Features](#quality-assurance-features)
|
||||
- [Nyquist Validation](#13-nyquist-validation)
|
||||
- [Plan Checking](#14-plan-checking)
|
||||
- [Post-Execution Verification](#15-post-execution-verification)
|
||||
- [Node Repair](#16-node-repair)
|
||||
- [Health Validation](#17-health-validation)
|
||||
- [Nyquist Validation](#15-nyquist-validation)
|
||||
- [Plan Checking](#16-plan-checking)
|
||||
- [Post-Execution Verification](#17-post-execution-verification)
|
||||
- [Node Repair](#18-node-repair)
|
||||
- [Health Validation](#19-health-validation)
|
||||
- [Cross-Phase Regression Gate](#20-cross-phase-regression-gate)
|
||||
- [Requirements Coverage Gate](#21-requirements-coverage-gate)
|
||||
- [Context Engineering Features](#context-engineering-features)
|
||||
- [Context Window Monitoring](#18-context-window-monitoring)
|
||||
- [Session Management](#19-session-management)
|
||||
- [Multi-Agent Orchestration](#20-multi-agent-orchestration)
|
||||
- [Model Profiles](#21-model-profiles)
|
||||
- [Context Window Monitoring](#22-context-window-monitoring)
|
||||
- [Session Management](#23-session-management)
|
||||
- [Session Reporting](#24-session-reporting)
|
||||
- [Multi-Agent Orchestration](#25-multi-agent-orchestration)
|
||||
- [Model Profiles](#26-model-profiles)
|
||||
- [Brownfield Features](#brownfield-features)
|
||||
- [Codebase Mapping](#22-codebase-mapping)
|
||||
- [Codebase Mapping](#27-codebase-mapping)
|
||||
- [Utility Features](#utility-features)
|
||||
- [Debug System](#23-debug-system)
|
||||
- [Todo Management](#24-todo-management)
|
||||
- [Statistics Dashboard](#25-statistics-dashboard)
|
||||
- [Update System](#26-update-system)
|
||||
- [Settings Management](#27-settings-management)
|
||||
- [Test Generation](#28-test-generation)
|
||||
- [Debug System](#28-debug-system)
|
||||
- [Todo Management](#29-todo-management)
|
||||
- [Statistics Dashboard](#30-statistics-dashboard)
|
||||
- [Update System](#31-update-system)
|
||||
- [Settings Management](#32-settings-management)
|
||||
- [Test Generation](#33-test-generation)
|
||||
- [Infrastructure Features](#infrastructure-features)
|
||||
- [Git Integration](#29-git-integration)
|
||||
- [CLI Tools](#30-cli-tools)
|
||||
- [Multi-Runtime Support](#31-multi-runtime-support)
|
||||
- [Hook System](#32-hook-system)
|
||||
- [Developer Profiling](#33-developer-profiling)
|
||||
- [Execution Hardening](#34-execution-hardening)
|
||||
- [Git Integration](#34-git-integration)
|
||||
- [CLI Tools](#35-cli-tools)
|
||||
- [Multi-Runtime Support](#36-multi-runtime-support)
|
||||
- [Hook System](#37-hook-system)
|
||||
- [Developer Profiling](#38-developer-profiling)
|
||||
- [Execution Hardening](#39-execution-hardening)
|
||||
|
||||
---
|
||||
|
||||
@@ -70,7 +75,7 @@
|
||||
**Produces:**
|
||||
| Artifact | Description |
|
||||
|----------|-------------|
|
||||
| `PROJECT.md` | Project vision, constraints, technical decisions |
|
||||
| `PROJECT.md` | Project vision, constraints, technical decisions, evolution rules |
|
||||
| `REQUIREMENTS.md` | Scoped requirements with unique IDs (REQ-XX) |
|
||||
| `ROADMAP.md` | Phase breakdown with status tracking and requirement mapping |
|
||||
| `STATE.md` | Initial project state with position, decisions, metrics |
|
||||
@@ -171,6 +176,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 +226,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 |
|
||||
@@ -238,9 +245,14 @@
|
||||
- Reads PLAN.md with full task instructions
|
||||
- Has access to PROJECT.md, STATE.md, CONTEXT.md, RESEARCH.md
|
||||
- Commits each task atomically with structured commit messages
|
||||
- Uses `--no-verify` on commits during parallel execution to avoid build lock contention
|
||||
- Handles checkpoint types: `auto`, `checkpoint:human-verify`, `checkpoint:decision`, `checkpoint:human-action`
|
||||
- Reports deviations from plan in SUMMARY.md
|
||||
|
||||
**Parallel Safety:**
|
||||
- **Pre-commit hooks**: Skipped by parallel agents (`--no-verify`), run once by orchestrator after each wave
|
||||
- **STATE.md locking**: File-level lockfile prevents concurrent write corruption across agents
|
||||
|
||||
---
|
||||
|
||||
### 6. Work Verification
|
||||
@@ -261,6 +273,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]`
|
||||
@@ -387,9 +418,34 @@
|
||||
|
||||
---
|
||||
|
||||
### 14. Auto-Advance (Next)
|
||||
|
||||
**Command:** `/gsd:next`
|
||||
|
||||
**Purpose:** Automatically detect current project state and advance to the next logical workflow step, eliminating the need to remember which phase/step you're on.
|
||||
|
||||
**Requirements:**
|
||||
- REQ-NEXT-01: System MUST read STATE.md, ROADMAP.md, and phase directories to determine current position
|
||||
- REQ-NEXT-02: System MUST detect whether discuss, plan, execute, or verify is needed
|
||||
- REQ-NEXT-03: System MUST invoke the correct command automatically
|
||||
- REQ-NEXT-04: System MUST suggest `/gsd:new-project` if no project exists
|
||||
- REQ-NEXT-05: System MUST suggest `/gsd:complete-milestone` when all phases are complete
|
||||
|
||||
**State Detection Logic:**
|
||||
| State | Action |
|
||||
|-------|--------|
|
||||
| No `.planning/` directory | Suggest `/gsd:new-project` |
|
||||
| Phase has no CONTEXT.md | Run `/gsd:discuss-phase` |
|
||||
| Phase has no PLAN.md files | Run `/gsd:plan-phase` |
|
||||
| Phase has plans but no SUMMARY.md | Run `/gsd:execute-phase` |
|
||||
| Phase executed but no VERIFICATION.md | Run `/gsd:verify-work` |
|
||||
| All phases complete | Suggest `/gsd:complete-milestone` |
|
||||
|
||||
---
|
||||
|
||||
## Quality Assurance Features
|
||||
|
||||
### 14. Nyquist Validation
|
||||
### 15. Nyquist Validation
|
||||
|
||||
**Purpose:** Map automated test coverage to phase requirements before any code is written. Named after the Nyquist sampling theorem — ensures a feedback signal exists for every requirement.
|
||||
|
||||
@@ -412,7 +468,7 @@
|
||||
|
||||
---
|
||||
|
||||
### 15. Plan Checking
|
||||
### 16. Plan Checking
|
||||
|
||||
**Purpose:** Goal-backward verification that plans will achieve phase objectives before execution.
|
||||
|
||||
@@ -424,7 +480,7 @@
|
||||
|
||||
---
|
||||
|
||||
### 16. Post-Execution Verification
|
||||
### 17. Post-Execution Verification
|
||||
|
||||
**Purpose:** Automated check that the codebase delivers what the phase promised.
|
||||
|
||||
@@ -436,7 +492,7 @@
|
||||
|
||||
---
|
||||
|
||||
### 17. Node Repair
|
||||
### 18. Node Repair
|
||||
|
||||
**Purpose:** Autonomous recovery when task verification fails during execution.
|
||||
|
||||
@@ -450,7 +506,7 @@
|
||||
|
||||
---
|
||||
|
||||
### 18. Health Validation
|
||||
### 19. Health Validation
|
||||
|
||||
**Command:** `/gsd:health [--repair]`
|
||||
|
||||
@@ -465,9 +521,37 @@
|
||||
|
||||
---
|
||||
|
||||
### 20. Cross-Phase Regression Gate
|
||||
|
||||
**Purpose:** Prevent regressions from compounding across phases by running prior phases' test suites after execution.
|
||||
|
||||
**Requirements:**
|
||||
- REQ-REGR-01: System MUST run test suites from all completed prior phases after phase execution
|
||||
- REQ-REGR-02: System MUST report any test failures as cross-phase regressions
|
||||
- REQ-REGR-03: Regressions MUST be surfaced before post-execution verification
|
||||
- REQ-REGR-04: System MUST identify which prior phase's tests were broken
|
||||
|
||||
**When:** Runs automatically during `/gsd:execute-phase` before the verifier step.
|
||||
|
||||
---
|
||||
|
||||
### 21. Requirements Coverage Gate
|
||||
|
||||
**Purpose:** Ensure all phase requirements are covered by at least one plan before planning completes.
|
||||
|
||||
**Requirements:**
|
||||
- REQ-COVGATE-01: System MUST extract all requirement IDs assigned to the phase from ROADMAP.md
|
||||
- REQ-COVGATE-02: System MUST verify each requirement appears in at least one PLAN.md
|
||||
- REQ-COVGATE-03: Uncovered requirements MUST block planning completion
|
||||
- REQ-COVGATE-04: System MUST report which specific requirements lack plan coverage
|
||||
|
||||
**When:** Runs automatically at the end of `/gsd:plan-phase` after the plan checker loop.
|
||||
|
||||
---
|
||||
|
||||
## Context Engineering Features
|
||||
|
||||
### 19. Context Window Monitoring
|
||||
### 22. Context Window Monitoring
|
||||
|
||||
**Purpose:** Prevent context rot by alerting both user and agent when context is running low.
|
||||
|
||||
@@ -487,22 +571,49 @@
|
||||
|
||||
---
|
||||
|
||||
### 20. Session Management
|
||||
### 23. Session Management
|
||||
|
||||
**Commands:** `/gsd:pause-work`, `/gsd:resume-work`, `/gsd:progress`
|
||||
|
||||
**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
|
||||
|
||||
---
|
||||
|
||||
### 21. Multi-Agent Orchestration
|
||||
### 24. Session Reporting
|
||||
|
||||
**Command:** `/gsd:session-report`
|
||||
|
||||
**Purpose:** Generate a structured post-session summary document capturing work performed, outcomes achieved, and estimated resource usage.
|
||||
|
||||
**Requirements:**
|
||||
- REQ-REPORT-01: System MUST gather data from STATE.md, git log, and plan/summary files
|
||||
- REQ-REPORT-02: System MUST include commits made, plans executed, and phases progressed
|
||||
- REQ-REPORT-03: System MUST estimate token usage and cost based on session activity
|
||||
- REQ-REPORT-04: System MUST include active blockers and decisions made
|
||||
- REQ-REPORT-05: System MUST recommend next steps
|
||||
|
||||
**Produces:** `.planning/reports/SESSION_REPORT.md`
|
||||
|
||||
**Report Sections:**
|
||||
- Session overview (duration, milestone, phase)
|
||||
- Work performed (commits, plans, phases)
|
||||
- Outcomes and deliverables
|
||||
- Blockers and decisions
|
||||
- Resource estimates (tokens, cost)
|
||||
- Next steps recommendation
|
||||
|
||||
---
|
||||
|
||||
### 25. Multi-Agent Orchestration
|
||||
|
||||
**Purpose:** Coordinate specialized agents with fresh context windows for each task.
|
||||
|
||||
@@ -516,7 +627,7 @@
|
||||
|
||||
---
|
||||
|
||||
### 22. Model Profiles
|
||||
### 26. Model Profiles
|
||||
|
||||
**Command:** `/gsd:set-profile <quality|balanced|budget|inherit>`
|
||||
|
||||
@@ -527,6 +638,7 @@
|
||||
- REQ-MODEL-02: Each profile MUST define model tier per agent (see profile table)
|
||||
- REQ-MODEL-03: Per-agent overrides MUST take precedence over profile
|
||||
- REQ-MODEL-04: `inherit` profile MUST defer to runtime's current model selection
|
||||
- REQ-MODEL-04a: `inherit` profile MUST be used when running non-Anthropic providers (OpenRouter, local models) to avoid unexpected API costs
|
||||
- REQ-MODEL-05: Profile switch MUST be programmatic (script, not LLM-driven)
|
||||
- REQ-MODEL-06: Model resolution MUST happen once per orchestration, not per spawn
|
||||
|
||||
@@ -551,7 +663,7 @@
|
||||
|
||||
## Brownfield Features
|
||||
|
||||
### 23. Codebase Mapping
|
||||
### 27. Codebase Mapping
|
||||
|
||||
**Command:** `/gsd:map-codebase [area]`
|
||||
|
||||
@@ -579,7 +691,7 @@
|
||||
|
||||
## Utility Features
|
||||
|
||||
### 24. Debug System
|
||||
### 28. Debug System
|
||||
|
||||
**Command:** `/gsd:debug [description]`
|
||||
|
||||
@@ -597,7 +709,7 @@
|
||||
|
||||
---
|
||||
|
||||
### 25. Todo Management
|
||||
### 29. Todo Management
|
||||
|
||||
**Commands:** `/gsd:add-todo [desc]`, `/gsd:check-todos`
|
||||
|
||||
@@ -611,7 +723,7 @@
|
||||
|
||||
---
|
||||
|
||||
### 26. Statistics Dashboard
|
||||
### 30. Statistics Dashboard
|
||||
|
||||
**Command:** `/gsd:stats`
|
||||
|
||||
@@ -625,7 +737,7 @@
|
||||
|
||||
---
|
||||
|
||||
### 27. Update System
|
||||
### 31. Update System
|
||||
|
||||
**Command:** `/gsd:update`
|
||||
|
||||
@@ -640,7 +752,7 @@
|
||||
|
||||
---
|
||||
|
||||
### 28. Settings Management
|
||||
### 32. Settings Management
|
||||
|
||||
**Command:** `/gsd:settings`
|
||||
|
||||
@@ -673,7 +785,7 @@
|
||||
|
||||
---
|
||||
|
||||
### 29. Test Generation
|
||||
### 33. Test Generation
|
||||
|
||||
**Command:** `/gsd:add-tests [N]`
|
||||
|
||||
@@ -688,7 +800,7 @@
|
||||
|
||||
## Infrastructure Features
|
||||
|
||||
### 30. Git Integration
|
||||
### 34. Git Integration
|
||||
|
||||
**Purpose:** Atomic commits, branching strategies, and clean history management.
|
||||
|
||||
@@ -714,7 +826,7 @@ fix(03-01): correct auth token expiry
|
||||
|
||||
---
|
||||
|
||||
### 31. CLI Tools
|
||||
### 35. CLI Tools
|
||||
|
||||
**Purpose:** Programmatic utilities for workflows and agents, replacing repetitive inline bash patterns.
|
||||
|
||||
@@ -729,7 +841,7 @@ fix(03-01): correct auth token expiry
|
||||
|
||||
---
|
||||
|
||||
### 32. Multi-Runtime Support
|
||||
### 36. Multi-Runtime Support
|
||||
|
||||
**Purpose:** Run GSD across 6 different AI coding agent runtimes.
|
||||
|
||||
@@ -752,7 +864,7 @@ fix(03-01): correct auth token expiry
|
||||
|
||||
---
|
||||
|
||||
### 33. Hook System
|
||||
### 37. Hook System
|
||||
|
||||
**Purpose:** Runtime event hooks for context monitoring, status display, and update checking.
|
||||
|
||||
@@ -772,7 +884,7 @@ fix(03-01): correct auth token expiry
|
||||
|
||||
Color coding: <50% green, <65% yellow, <80% orange, ≥80% red with skull emoji
|
||||
|
||||
### 33. Developer Profiling
|
||||
### 38. Developer Profiling
|
||||
|
||||
**Command:** `/gsd:profile-user [--questionnaire] [--refresh]`
|
||||
|
||||
@@ -808,7 +920,7 @@ Color coding: <50% green, <65% yellow, <80% orange, ≥80% red with skull emoji
|
||||
- REQ-PROF-03: Questionnaire MUST be available as fallback when no session history exists
|
||||
- REQ-PROF-04: Generated artifacts MUST be discoverable by Claude Code (CLAUDE.md integration)
|
||||
|
||||
### 34. Execution Hardening
|
||||
### 39. Execution Hardening
|
||||
|
||||
**Purpose:** Three additive quality improvements to the execution pipeline that catch cross-plan failures before they cascade.
|
||||
|
||||
|
||||
@@ -50,6 +50,10 @@ A detailed reference for workflows, troubleshooting, and configuration. For quic
|
||||
│ │ /gsd:verify-work │ │ <- Manual UAT
|
||||
│ └──────────┬─────────┘ │
|
||||
│ │ │
|
||||
│ ┌──────────▼─────────┐ │
|
||||
│ │ /gsd:ship │ │ <- Create PR (optional)
|
||||
│ └──────────┬─────────┘ │
|
||||
│ │ │
|
||||
│ Next Phase?────────────┘
|
||||
│ │ No
|
||||
└─────────────┼──────────────┘
|
||||
@@ -284,6 +288,8 @@ 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:next` | Auto-detect state and run next step | Anytime — "what should I do next?" |
|
||||
| `/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 +301,8 @@ 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:session-report` | Generate session summary with work and outcomes | End of session, stakeholder sharing |
|
||||
| `/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 +370,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 +414,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 (e.g. OpenCode `/model`), or **required** when using non-Anthropic providers (OpenRouter, local models) to avoid unexpected API costs.
|
||||
|
||||
---
|
||||
|
||||
@@ -442,12 +449,14 @@ claude --dangerously-skip-permissions
|
||||
/gsd:plan-phase 1 # Research + plan + verify
|
||||
/gsd:execute-phase 1 # Parallel execution
|
||||
/gsd:verify-work 1 # Manual UAT
|
||||
/gsd:ship 1 # Create PR from verified work
|
||||
/gsd:ui-review 1 # Visual audit (frontend phases)
|
||||
/clear
|
||||
/gsd:discuss-phase 2 # Repeat for each phase
|
||||
/gsd:next # Auto-detect and run next step
|
||||
...
|
||||
/gsd:audit-milestone # Check everything shipped
|
||||
/gsd:complete-milestone # Archive, tag, done
|
||||
/gsd:session-report # Generate session summary
|
||||
```
|
||||
|
||||
### New Project from Existing Document
|
||||
@@ -539,6 +548,10 @@ Do not re-run `/gsd:execute-phase`. Use `/gsd:quick` for targeted fixes, or `/gs
|
||||
|
||||
Switch to budget profile: `/gsd:set-profile budget`. Disable research and plan-check agents via `/gsd:settings` if the domain is familiar to you (or to Claude).
|
||||
|
||||
### Using Non-Anthropic Models (OpenRouter, Local)
|
||||
|
||||
If GSD subagents call Anthropic models and you're paying through OpenRouter or a local provider, switch to the `inherit` profile: `/gsd:set-profile inherit`. This makes all agents use your current session model instead of specific Anthropic models. See also `/gsd:settings` → Model Profile → Inherit.
|
||||
|
||||
### Working on a Sensitive/Private Project
|
||||
|
||||
Set `commit_docs: false` during `/gsd:new-project` or via `/gsd:settings`. Add `.planning/` to your `.gitignore`. Planning artifacts stay local and never touch git.
|
||||
@@ -551,6 +564,21 @@ Since v1.17, the installer backs up locally modified files to `gsd-local-patches
|
||||
|
||||
A known workaround exists for a Claude Code classification bug. GSD's orchestrators (execute-phase, quick) spot-check actual output before reporting failure. If you see a failure message but commits were made, check `git log` -- the work may have succeeded.
|
||||
|
||||
### Parallel Execution Causes Build Lock Errors
|
||||
|
||||
If you see pre-commit hook failures, cargo lock contention, or 30+ minute execution times during parallel wave execution, this is caused by multiple agents triggering build tools simultaneously. GSD handles this automatically since v1.26 — parallel agents use `--no-verify` on commits and the orchestrator runs hooks once after each wave. If you're on an older version, add this to your project's `CLAUDE.md`:
|
||||
|
||||
```markdown
|
||||
## Git Commit Rules for Agents
|
||||
All subagent/executor commits MUST use `--no-verify`.
|
||||
```
|
||||
|
||||
To disable parallel execution entirely: `/gsd:settings` → set `parallelization.enabled` to `false`.
|
||||
|
||||
### Windows: Installation Crashes on Protected Directories
|
||||
|
||||
If the installer crashes with `EPERM: operation not permitted, scandir` on Windows, this is caused by OS-protected directories (e.g., Chromium browser profiles). Fixed since v1.24 — update to the latest version. As a workaround, temporarily rename the problematic directory before running the installer.
|
||||
|
||||
---
|
||||
|
||||
## Recovery Quick Reference
|
||||
@@ -566,6 +594,9 @@ A known workaround exists for a Claude Code classification bug. GSD's orchestrat
|
||||
| Plan doesn't match your vision | `/gsd:discuss-phase [N]` then re-plan |
|
||||
| Costs running high | `/gsd:set-profile budget` and `/gsd:settings` to toggle agents off |
|
||||
| Update broke local changes | `/gsd:reapply-patches` |
|
||||
| Want session summary for stakeholder | `/gsd:session-report` |
|
||||
| Don't know what step is next | `/gsd:next` |
|
||||
| Parallel execution build errors | Update GSD or set `parallelization.enabled: false` |
|
||||
|
||||
---
|
||||
|
||||
@@ -581,7 +612,9 @@ For reference, here is what GSD creates in your project:
|
||||
STATE.md # Decisions, blockers, session memory
|
||||
config.json # Workflow configuration
|
||||
MILESTONES.md # Completed milestone archive
|
||||
HANDOFF.json # Structured session handoff (from /gsd:pause-work)
|
||||
research/ # Domain research from /gsd:new-project
|
||||
reports/ # Session reports (from /gsd:session-report)
|
||||
todos/
|
||||
pending/ # Captured ideas awaiting work
|
||||
done/ # Completed todos
|
||||
|
||||
@@ -15,9 +15,11 @@
|
||||
* 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
|
||||
* commit <message> [--files f1 f2] [--no-verify] Commit planning docs
|
||||
* verify-summary <path> Verify a SUMMARY.md file
|
||||
* generate-slug <text> Convert text to URL-safe slug
|
||||
* current-timestamp [format] Get timestamp (full|date|filename)
|
||||
@@ -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);
|
||||
}
|
||||
@@ -273,6 +290,7 @@ async function main() {
|
||||
|
||||
case 'commit': {
|
||||
const amend = args.includes('--amend');
|
||||
const noVerify = args.includes('--no-verify');
|
||||
const filesIndex = args.indexOf('--files');
|
||||
// Collect all positional args between command name and first flag,
|
||||
// then join them — handles both quoted ("multi word msg") and
|
||||
@@ -281,7 +299,7 @@ async function main() {
|
||||
const messageArgs = args.slice(1, endIndex).filter(a => !a.startsWith('--'));
|
||||
const message = messageArgs.join(' ') || undefined;
|
||||
const files = filesIndex !== -1 ? args.slice(filesIndex + 1).filter(a => !a.startsWith('--')) : [];
|
||||
commands.cmdCommit(cwd, message, files, raw, amend);
|
||||
commands.cmdCommit(cwd, message, files, raw, amend, noVerify);
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -522,8 +540,10 @@ async function main() {
|
||||
const subcommand = args[1];
|
||||
if (subcommand === 'complete') {
|
||||
commands.cmdTodoComplete(cwd, args[2], raw);
|
||||
} else if (subcommand === 'match-phase') {
|
||||
commands.cmdTodoMatchPhase(cwd, args[2], raw);
|
||||
} else {
|
||||
error('Unknown todo subcommand. Available: complete');
|
||||
error('Unknown todo subcommand. Available: complete, match-phase');
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -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, getRoadmapPhaseInternal } = require('./core.cjs');
|
||||
const { extractFrontmatter } = require('./frontmatter.cjs');
|
||||
const { MODEL_PROFILES } = require('./model-profiles.cjs');
|
||||
|
||||
@@ -71,9 +71,9 @@ function cmdListTodos(cwd, area, raw) {
|
||||
area: todoArea,
|
||||
path: toPosixPath(path.join('.planning', 'todos', 'pending', file)),
|
||||
});
|
||||
} catch {}
|
||||
} catch { /* intentionally empty */ }
|
||||
}
|
||||
} catch {}
|
||||
} catch { /* intentionally empty */ }
|
||||
|
||||
const result = { count, todos };
|
||||
output(result, raw, count.toString());
|
||||
@@ -120,7 +120,7 @@ function cmdHistoryDigest(cwd, raw) {
|
||||
for (const dir of currentDirs) {
|
||||
allPhaseDirs.push({ name: dir, fullPath: path.join(phasesDir, dir), milestone: null });
|
||||
}
|
||||
} catch {}
|
||||
} catch { /* intentionally empty */ }
|
||||
}
|
||||
|
||||
if (allPhaseDirs.length === 0) {
|
||||
@@ -214,7 +214,7 @@ function cmdResolveModel(cwd, agentType, raw) {
|
||||
output(result, raw, model);
|
||||
}
|
||||
|
||||
function cmdCommit(cwd, message, files, raw, amend) {
|
||||
function cmdCommit(cwd, message, files, raw, amend, noVerify) {
|
||||
if (!message && !amend) {
|
||||
error('commit message required');
|
||||
}
|
||||
@@ -241,8 +241,9 @@ function cmdCommit(cwd, message, files, raw, amend) {
|
||||
execGit(cwd, ['add', file]);
|
||||
}
|
||||
|
||||
// Commit
|
||||
// Commit (--no-verify skips pre-commit hooks, used by parallel executor agents)
|
||||
const commitArgs = amend ? ['commit', '--amend', '--no-edit'] : ['commit', '-m', message];
|
||||
if (noVerify) commitArgs.push('--no-verify');
|
||||
const commitResult = execGit(cwd, commitArgs);
|
||||
if (commitResult.exitCode !== 0) {
|
||||
if (commitResult.stdout.includes('nothing to commit') || commitResult.stderr.includes('nothing to commit')) {
|
||||
@@ -412,7 +413,7 @@ function cmdProgressRender(cwd, format, raw) {
|
||||
|
||||
phases.push({ number: phaseNum, name: phaseName, plans, summaries, status });
|
||||
}
|
||||
} catch {}
|
||||
} catch { /* intentionally empty */ }
|
||||
|
||||
const percent = totalPlans > 0 ? Math.min(100, Math.round((totalSummaries / totalPlans) * 100)) : 0;
|
||||
|
||||
@@ -448,6 +449,130 @@ function cmdProgressRender(cwd, format, raw) {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Match pending todos against a phase's goal/name/requirements.
|
||||
* Returns todos with relevance scores based on keyword, area, and file overlap.
|
||||
* Used by discuss-phase to surface relevant todos before scope-setting.
|
||||
*/
|
||||
function cmdTodoMatchPhase(cwd, phase, raw) {
|
||||
if (!phase) { error('phase required for todo match-phase'); }
|
||||
|
||||
const pendingDir = path.join(cwd, '.planning', 'todos', 'pending');
|
||||
const todos = [];
|
||||
|
||||
// Load pending todos
|
||||
try {
|
||||
const files = fs.readdirSync(pendingDir).filter(f => f.endsWith('.md'));
|
||||
for (const file of files) {
|
||||
try {
|
||||
const content = fs.readFileSync(path.join(pendingDir, file), 'utf-8');
|
||||
const titleMatch = content.match(/^title:\s*(.+)$/m);
|
||||
const areaMatch = content.match(/^area:\s*(.+)$/m);
|
||||
const filesMatch = content.match(/^files:\s*(.+)$/m);
|
||||
const body = content.replace(/^(title|area|files|created|priority):.*$/gm, '').trim();
|
||||
|
||||
todos.push({
|
||||
file,
|
||||
title: titleMatch ? titleMatch[1].trim() : 'Untitled',
|
||||
area: areaMatch ? areaMatch[1].trim() : 'general',
|
||||
files: filesMatch ? filesMatch[1].trim().split(/[,\s]+/).filter(Boolean) : [],
|
||||
body: body.slice(0, 200), // first 200 chars for context
|
||||
});
|
||||
} catch {}
|
||||
}
|
||||
} catch {}
|
||||
|
||||
if (todos.length === 0) {
|
||||
output({ phase, matches: [], todo_count: 0 }, raw);
|
||||
return;
|
||||
}
|
||||
|
||||
// Load phase goal/name from ROADMAP
|
||||
const phaseInfo = getRoadmapPhaseInternal(cwd, phase);
|
||||
const phaseName = phaseInfo ? (phaseInfo.phase_name || '') : '';
|
||||
const phaseGoal = phaseInfo ? (phaseInfo.goal || '') : '';
|
||||
const phaseSection = phaseInfo ? (phaseInfo.section || '') : '';
|
||||
|
||||
// Build keyword set from phase name + goal + section text
|
||||
const phaseText = `${phaseName} ${phaseGoal} ${phaseSection}`.toLowerCase();
|
||||
const stopWords = new Set(['the', 'and', 'for', 'with', 'from', 'that', 'this', 'will', 'are', 'was', 'has', 'have', 'been', 'not', 'but', 'all', 'can', 'into', 'each', 'when', 'any', 'use', 'new']);
|
||||
const phaseKeywords = new Set(
|
||||
phaseText.split(/[\s\-_/.,;:()\[\]{}|]+/)
|
||||
.map(w => w.replace(/[^a-z0-9]/g, ''))
|
||||
.filter(w => w.length > 2 && !stopWords.has(w))
|
||||
);
|
||||
|
||||
// Find phase directory to get expected file paths
|
||||
const phaseInfoDisk = findPhaseInternal(cwd, phase);
|
||||
const phasePlans = [];
|
||||
if (phaseInfoDisk && phaseInfoDisk.found) {
|
||||
try {
|
||||
const phaseDir = path.join(cwd, phaseInfoDisk.directory);
|
||||
const planFiles = fs.readdirSync(phaseDir).filter(f => f.endsWith('-PLAN.md'));
|
||||
for (const pf of planFiles) {
|
||||
try {
|
||||
const planContent = fs.readFileSync(path.join(phaseDir, pf), 'utf-8');
|
||||
const fmFiles = planContent.match(/files_modified:\s*\[([^\]]*)\]/);
|
||||
if (fmFiles) {
|
||||
phasePlans.push(...fmFiles[1].split(',').map(s => s.trim().replace(/['"]/g, '')).filter(Boolean));
|
||||
}
|
||||
} catch {}
|
||||
}
|
||||
} catch {}
|
||||
}
|
||||
|
||||
// Score each todo for relevance
|
||||
const matches = [];
|
||||
for (const todo of todos) {
|
||||
let score = 0;
|
||||
const reasons = [];
|
||||
|
||||
// Keyword match: todo title/body terms in phase text
|
||||
const todoWords = `${todo.title} ${todo.body}`.toLowerCase()
|
||||
.split(/[\s\-_/.,;:()\[\]{}|]+/)
|
||||
.map(w => w.replace(/[^a-z0-9]/g, ''))
|
||||
.filter(w => w.length > 2 && !stopWords.has(w));
|
||||
|
||||
const matchedKeywords = todoWords.filter(w => phaseKeywords.has(w));
|
||||
if (matchedKeywords.length > 0) {
|
||||
score += Math.min(matchedKeywords.length * 0.2, 0.6);
|
||||
reasons.push(`keywords: ${[...new Set(matchedKeywords)].slice(0, 5).join(', ')}`);
|
||||
}
|
||||
|
||||
// Area match: todo area appears in phase text
|
||||
if (todo.area !== 'general' && phaseText.includes(todo.area.toLowerCase())) {
|
||||
score += 0.3;
|
||||
reasons.push(`area: ${todo.area}`);
|
||||
}
|
||||
|
||||
// File match: todo files overlap with phase plan files
|
||||
if (todo.files.length > 0 && phasePlans.length > 0) {
|
||||
const fileOverlap = todo.files.filter(f =>
|
||||
phasePlans.some(pf => pf.includes(f) || f.includes(pf))
|
||||
);
|
||||
if (fileOverlap.length > 0) {
|
||||
score += 0.4;
|
||||
reasons.push(`files: ${fileOverlap.slice(0, 3).join(', ')}`);
|
||||
}
|
||||
}
|
||||
|
||||
if (score > 0) {
|
||||
matches.push({
|
||||
file: todo.file,
|
||||
title: todo.title,
|
||||
area: todo.area,
|
||||
score: Math.round(score * 100) / 100,
|
||||
reasons,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Sort by score descending
|
||||
matches.sort((a, b) => b.score - a.score);
|
||||
|
||||
output({ phase, matches, todo_count: todos.length }, raw);
|
||||
}
|
||||
|
||||
function cmdTodoComplete(cwd, filename, raw) {
|
||||
if (!filename) {
|
||||
error('filename required for todo complete');
|
||||
@@ -547,7 +672,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) {
|
||||
@@ -559,7 +684,7 @@ function cmdStats(cwd, format, raw) {
|
||||
status: 'Not Started',
|
||||
});
|
||||
}
|
||||
} catch {}
|
||||
} catch { /* intentionally empty */ }
|
||||
|
||||
try {
|
||||
const entries = fs.readdirSync(phasesDir, { withFileTypes: true });
|
||||
@@ -595,7 +720,7 @@ function cmdStats(cwd, format, raw) {
|
||||
status,
|
||||
});
|
||||
}
|
||||
} catch {}
|
||||
} catch { /* intentionally empty */ }
|
||||
|
||||
const phases = [...phasesByNumber.values()].sort((a, b) => comparePhaseNum(a.number, b.number));
|
||||
const completedPhases = phases.filter(p => p.status === 'Complete').length;
|
||||
@@ -613,7 +738,7 @@ function cmdStats(cwd, format, raw) {
|
||||
requirementsComplete = checked ? checked.length : 0;
|
||||
requirementsTotal = requirementsComplete + (unchecked ? unchecked.length : 0);
|
||||
}
|
||||
} catch {}
|
||||
} catch { /* intentionally empty */ }
|
||||
|
||||
// Last activity from STATE.md
|
||||
let lastActivity = null;
|
||||
@@ -626,7 +751,7 @@ function cmdStats(cwd, format, raw) {
|
||||
|| stateContent.match(/^Last activity:\s*(.+)$/im);
|
||||
if (activityMatch) lastActivity = activityMatch[1].trim();
|
||||
}
|
||||
} catch {}
|
||||
} catch { /* intentionally empty */ }
|
||||
|
||||
// Git stats
|
||||
let gitCommits = 0;
|
||||
@@ -704,6 +829,7 @@ module.exports = {
|
||||
cmdWebsearch,
|
||||
cmdProgressRender,
|
||||
cmdTodoComplete,
|
||||
cmdTodoMatchPhase,
|
||||
cmdScaffold,
|
||||
cmdStats,
|
||||
};
|
||||
|
||||
@@ -71,7 +71,7 @@ function buildNewProjectConfig(userChoices) {
|
||||
delete userDefaults.depth;
|
||||
try {
|
||||
fs.writeFileSync(globalDefaultsPath, JSON.stringify(userDefaults, null, 2), 'utf-8');
|
||||
} catch {}
|
||||
} catch { /* intentionally empty */ }
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
|
||||
@@ -64,6 +64,8 @@ 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
|
||||
context_window: 200000, // default 200k; set to 1000000 for Opus/Sonnet 4.6 1M models
|
||||
};
|
||||
|
||||
try {
|
||||
@@ -75,7 +77,7 @@ function loadConfig(cwd) {
|
||||
const depthToGranularity = { quick: 'coarse', standard: 'standard', comprehensive: 'fine' };
|
||||
parsed.granularity = depthToGranularity[parsed.depth] || parsed.depth;
|
||||
delete parsed.depth;
|
||||
try { fs.writeFileSync(configPath, JSON.stringify(parsed, null, 2), 'utf-8'); } catch {}
|
||||
try { fs.writeFileSync(configPath, JSON.stringify(parsed, null, 2), 'utf-8'); } catch { /* intentionally empty */ }
|
||||
}
|
||||
|
||||
const get = (key, nested) => {
|
||||
@@ -106,6 +108,8 @@ 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,
|
||||
context_window: get('context_window') ?? defaults.context_window,
|
||||
model_overrides: parsed.model_overrides || null,
|
||||
};
|
||||
} catch {
|
||||
@@ -248,6 +252,27 @@ function execGit(cwd, args) {
|
||||
};
|
||||
}
|
||||
|
||||
// ─── Common path helpers ──────────────────────────────────────────────────────
|
||||
|
||||
/** Get the .planning directory path */
|
||||
function planningDir(cwd) {
|
||||
return path.join(cwd, '.planning');
|
||||
}
|
||||
|
||||
/** Get common .planning file paths */
|
||||
function planningPaths(cwd) {
|
||||
const base = path.join(cwd, '.planning');
|
||||
return {
|
||||
planning: base,
|
||||
state: path.join(base, 'STATE.md'),
|
||||
roadmap: path.join(base, 'ROADMAP.md'),
|
||||
project: path.join(base, 'PROJECT.md'),
|
||||
config: path.join(base, 'config.json'),
|
||||
phases: path.join(base, 'phases'),
|
||||
requirements: path.join(base, 'REQUIREMENTS.md'),
|
||||
};
|
||||
}
|
||||
|
||||
// ─── Phase utilities ──────────────────────────────────────────────────────────
|
||||
|
||||
function escapeRegex(value) {
|
||||
@@ -368,7 +393,7 @@ function findPhaseInternal(cwd, phase) {
|
||||
return result;
|
||||
}
|
||||
}
|
||||
} catch {}
|
||||
} catch { /* intentionally empty */ }
|
||||
|
||||
return null;
|
||||
}
|
||||
@@ -403,7 +428,7 @@ function getArchivedPhaseDirs(cwd) {
|
||||
});
|
||||
}
|
||||
}
|
||||
} catch {}
|
||||
} catch { /* intentionally empty */ }
|
||||
|
||||
return results;
|
||||
}
|
||||
@@ -420,6 +445,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 +554,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 +582,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 +609,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,13 +680,13 @@ 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) {
|
||||
milestonePhaseNums.add(m[1]);
|
||||
}
|
||||
} catch {}
|
||||
} catch { /* intentionally empty */ }
|
||||
|
||||
if (milestonePhaseNums.size === 0) {
|
||||
const passAll = () => true;
|
||||
@@ -597,6 +728,10 @@ module.exports = {
|
||||
getMilestoneInfo,
|
||||
getMilestonePhaseFilter,
|
||||
stripShippedMilestones,
|
||||
extractCurrentMilestone,
|
||||
replaceInCurrentMilestone,
|
||||
toPosixPath,
|
||||
MODEL_ALIAS_MAP,
|
||||
planningDir,
|
||||
planningPaths,
|
||||
};
|
||||
|
||||
@@ -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) {
|
||||
@@ -31,6 +31,7 @@ function cmdInitExecutePhase(cwd, phase, raw) {
|
||||
// Config flags
|
||||
commit_docs: config.commit_docs,
|
||||
parallelization: config.parallelization,
|
||||
context_window: config.context_window,
|
||||
branching_strategy: config.branching_strategy,
|
||||
phase_branch_template: config.phase_branch_template,
|
||||
milestone_branch_template: config.milestone_branch_template,
|
||||
@@ -153,7 +154,7 @@ function cmdInitPlanPhase(cwd, phase, raw) {
|
||||
if (uatFile) {
|
||||
result.uat_path = toPosixPath(path.join(phaseInfo.directory, uatFile));
|
||||
}
|
||||
} catch {}
|
||||
} catch { /* intentionally empty */ }
|
||||
}
|
||||
|
||||
output(result, raw);
|
||||
@@ -167,17 +168,26 @@ function cmdInitNewProject(cwd, raw) {
|
||||
const braveKeyFile = path.join(homedir, '.gsd', 'brave_api_key');
|
||||
const hasBraveSearch = !!(process.env.BRAVE_API_KEY || fs.existsSync(braveKeyFile));
|
||||
|
||||
// Detect existing code
|
||||
// Detect existing code (cross-platform — no Unix `find` dependency)
|
||||
let hasCode = false;
|
||||
let hasPackageFile = false;
|
||||
try {
|
||||
const files = execSync('find . -maxdepth 3 \\( -name "*.ts" -o -name "*.js" -o -name "*.py" -o -name "*.go" -o -name "*.rs" -o -name "*.swift" -o -name "*.java" \\) 2>/dev/null | grep -v node_modules | grep -v .git | head -5', {
|
||||
cwd,
|
||||
encoding: 'utf-8',
|
||||
stdio: ['pipe', 'pipe', 'pipe'],
|
||||
});
|
||||
hasCode = files.trim().length > 0;
|
||||
} catch {}
|
||||
const codeExtensions = new Set(['.ts', '.js', '.py', '.go', '.rs', '.swift', '.java']);
|
||||
const skipDirs = new Set(['node_modules', '.git', '.planning', '.claude', '__pycache__', 'target', 'dist', 'build']);
|
||||
function findCodeFiles(dir, depth) {
|
||||
if (depth > 3) return false;
|
||||
let entries;
|
||||
try { entries = fs.readdirSync(dir, { withFileTypes: true }); } catch { return false; }
|
||||
for (const entry of entries) {
|
||||
if (entry.isFile() && codeExtensions.has(path.extname(entry.name))) return true;
|
||||
if (entry.isDirectory() && !skipDirs.has(entry.name)) {
|
||||
if (findCodeFiles(path.join(dir, entry.name), depth + 1)) return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
hasCode = findCodeFiles(cwd, 0);
|
||||
} catch { /* intentionally empty — best-effort detection */ }
|
||||
|
||||
hasPackageFile = pathExistsInternal(cwd, 'package.json') ||
|
||||
pathExistsInternal(cwd, 'requirements.txt') ||
|
||||
@@ -307,7 +317,7 @@ function cmdInitResume(cwd, raw) {
|
||||
let interruptedAgentId = null;
|
||||
try {
|
||||
interruptedAgentId = fs.readFileSync(path.join(cwd, '.planning', 'current-agent-id.txt'), 'utf-8').trim();
|
||||
} catch {}
|
||||
} catch { /* intentionally empty */ }
|
||||
|
||||
const result = {
|
||||
// File existence
|
||||
@@ -459,7 +469,7 @@ function cmdInitPhaseOp(cwd, phase, raw) {
|
||||
if (uatFile) {
|
||||
result.uat_path = toPosixPath(path.join(phaseInfo.directory, uatFile));
|
||||
}
|
||||
} catch {}
|
||||
} catch { /* intentionally empty */ }
|
||||
}
|
||||
|
||||
output(result, raw);
|
||||
@@ -494,9 +504,9 @@ function cmdInitTodos(cwd, area, raw) {
|
||||
area: todoArea,
|
||||
path: '.planning/todos/pending/' + file,
|
||||
});
|
||||
} catch {}
|
||||
} catch { /* intentionally empty */ }
|
||||
}
|
||||
} catch {}
|
||||
} catch { /* intentionally empty */ }
|
||||
|
||||
const result = {
|
||||
// Config
|
||||
@@ -543,9 +553,9 @@ function cmdInitMilestoneOp(cwd, raw) {
|
||||
const phaseFiles = fs.readdirSync(path.join(phasesDir, dir));
|
||||
const hasSummary = phaseFiles.some(f => f.endsWith('-SUMMARY.md') || f === 'SUMMARY.md');
|
||||
if (hasSummary) completedPhases++;
|
||||
} catch {}
|
||||
} catch { /* intentionally empty */ }
|
||||
}
|
||||
} catch {}
|
||||
} catch { /* intentionally empty */ }
|
||||
|
||||
// Check archive
|
||||
const archiveDir = path.join(cwd, '.planning', 'archive');
|
||||
@@ -554,7 +564,7 @@ function cmdInitMilestoneOp(cwd, raw) {
|
||||
archivedMilestones = fs.readdirSync(archiveDir, { withFileTypes: true })
|
||||
.filter(e => e.isDirectory())
|
||||
.map(e => e.name);
|
||||
} catch {}
|
||||
} catch { /* intentionally empty */ }
|
||||
|
||||
const result = {
|
||||
// Config
|
||||
@@ -593,7 +603,7 @@ function cmdInitMapCodebase(cwd, raw) {
|
||||
let existingMaps = [];
|
||||
try {
|
||||
existingMaps = fs.readdirSync(codebaseDir).filter(f => f.endsWith('.md'));
|
||||
} catch {}
|
||||
} catch { /* intentionally empty */ }
|
||||
|
||||
const result = {
|
||||
// Models
|
||||
@@ -633,8 +643,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;
|
||||
@@ -642,7 +652,7 @@ function cmdInitProgress(cwd, raw) {
|
||||
roadmapPhaseNums.add(hm[1]);
|
||||
roadmapPhaseNames.set(hm[1], hm[2].replace(/\(INSERTED\)/i, '').trim());
|
||||
}
|
||||
} catch {}
|
||||
} catch { /* intentionally empty */ }
|
||||
|
||||
const isDirInMilestone = getMilestonePhaseFilter(cwd);
|
||||
const seenPhaseNums = new Set();
|
||||
@@ -695,7 +705,7 @@ function cmdInitProgress(cwd, raw) {
|
||||
nextPhase = phaseInfo;
|
||||
}
|
||||
}
|
||||
} catch {}
|
||||
} catch { /* intentionally empty */ }
|
||||
|
||||
// Add phases defined in ROADMAP but not yet scaffolded to disk
|
||||
for (const [num, name] of roadmapPhaseNames) {
|
||||
@@ -726,7 +736,7 @@ function cmdInitProgress(cwd, raw) {
|
||||
const state = fs.readFileSync(path.join(cwd, '.planning', 'STATE.md'), 'utf-8');
|
||||
const pauseMatch = state.match(/\*\*Paused At:\*\*\s*(.+)/);
|
||||
if (pauseMatch) pausedAt = pauseMatch[1].trim();
|
||||
} catch {}
|
||||
} catch { /* intentionally empty */ }
|
||||
|
||||
const result = {
|
||||
// Models
|
||||
|
||||
@@ -137,10 +137,10 @@ function cmdMilestoneComplete(cwd, version, options, raw) {
|
||||
// Count tasks
|
||||
const taskMatches = content.match(/##\s*Task\s*\d+/gi) || [];
|
||||
totalTasks += taskMatches.length;
|
||||
} catch {}
|
||||
} catch { /* intentionally empty */ }
|
||||
}
|
||||
}
|
||||
} catch {}
|
||||
} catch { /* intentionally empty */ }
|
||||
|
||||
// Archive ROADMAP.md
|
||||
if (fs.existsSync(roadmapPath)) {
|
||||
@@ -220,7 +220,7 @@ function cmdMilestoneComplete(cwd, version, options, raw) {
|
||||
archivedCount++;
|
||||
}
|
||||
phasesArchived = archivedCount > 0;
|
||||
} catch {}
|
||||
} catch { /* intentionally empty */ }
|
||||
}
|
||||
|
||||
const result = {
|
||||
|
||||
@@ -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
|
||||
@@ -401,7 +401,7 @@ function cmdPhaseInsert(cwd, afterPhase, description, raw) {
|
||||
const dm = dir.match(decimalPattern);
|
||||
if (dm) existingDecimals.push(parseInt(dm[1], 10));
|
||||
}
|
||||
} catch {}
|
||||
} catch { /* intentionally empty */ }
|
||||
|
||||
const nextDecimal = existingDecimals.length === 0 ? 1 : Math.max(...existingDecimals) + 1;
|
||||
const decimalPhase = `${normalizedBase}.${nextDecimal}`;
|
||||
@@ -470,7 +470,7 @@ function cmdPhaseRemove(cwd, targetPhase, options, raw) {
|
||||
const entries = fs.readdirSync(phasesDir, { withFileTypes: true });
|
||||
const dirs = entries.filter(e => e.isDirectory()).map(e => e.name).sort((a, b) => comparePhaseNum(a, b));
|
||||
targetDir = dirs.find(d => d.startsWith(normalized + '-') || d === normalized);
|
||||
} catch {}
|
||||
} catch { /* intentionally empty */ }
|
||||
|
||||
// Check for executed work (SUMMARY.md files)
|
||||
if (targetDir && !force) {
|
||||
@@ -538,7 +538,7 @@ function cmdPhaseRemove(cwd, targetPhase, options, raw) {
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch {}
|
||||
} catch { /* intentionally empty */ }
|
||||
|
||||
} else {
|
||||
// Integer removal: renumber all subsequent integer phases
|
||||
@@ -598,7 +598,7 @@ function cmdPhaseRemove(cwd, targetPhase, options, raw) {
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch {}
|
||||
} catch { /* intentionally empty */ }
|
||||
}
|
||||
|
||||
// Update ROADMAP.md
|
||||
@@ -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')
|
||||
);
|
||||
@@ -818,13 +818,13 @@ function cmdPhaseComplete(cwd, phaseNum, raw) {
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch {}
|
||||
} catch { /* intentionally empty */ }
|
||||
|
||||
// Fallback: if filesystem found no next phase, check ROADMAP.md
|
||||
// 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) {
|
||||
@@ -835,7 +835,7 @@ function cmdPhaseComplete(cwd, phaseNum, raw) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
} catch {}
|
||||
} catch { /* intentionally empty */ }
|
||||
}
|
||||
|
||||
// Update STATE.md
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
@@ -151,7 +151,7 @@ function cmdRoadmapAnalyze(cwd, raw) {
|
||||
else if (hasContext) diskStatus = 'discussed';
|
||||
else diskStatus = 'empty';
|
||||
}
|
||||
} catch {}
|
||||
} catch { /* intentionally empty */ }
|
||||
|
||||
// Check ROADMAP checkbox status
|
||||
const checkboxPattern = new RegExp(`-\\s*\\[(x| )\\]\\s*.*Phase\\s+${escapeRegex(phaseNum)}[:\\s]`, 'i');
|
||||
|
||||
@@ -4,9 +4,14 @@
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const { escapeRegex, loadConfig, getMilestoneInfo, getMilestonePhaseFilter, normalizeMd, output, error } = require('./core.cjs');
|
||||
const { escapeRegex, loadConfig, getMilestoneInfo, getMilestonePhaseFilter, normalizeMd, planningPaths, output, error } = require('./core.cjs');
|
||||
const { extractFrontmatter, reconstructFrontmatter } = require('./frontmatter.cjs');
|
||||
|
||||
/** Shorthand — every state command needs this path */
|
||||
function getStatePath(cwd) {
|
||||
return planningPaths(cwd).state;
|
||||
}
|
||||
|
||||
// Shared helper: extract a field value from STATE.md content.
|
||||
// Supports both **Field:** bold and plain Field: format.
|
||||
function stateExtractField(content, fieldName) {
|
||||
@@ -26,7 +31,7 @@ function cmdStateLoad(cwd, raw) {
|
||||
let stateRaw = '';
|
||||
try {
|
||||
stateRaw = fs.readFileSync(path.join(planningDir, 'STATE.md'), 'utf-8');
|
||||
} catch {}
|
||||
} catch { /* intentionally empty */ }
|
||||
|
||||
const configExists = fs.existsSync(path.join(planningDir, 'config.json'));
|
||||
const roadmapExists = fs.existsSync(path.join(planningDir, 'ROADMAP.md'));
|
||||
@@ -75,7 +80,7 @@ function cmdStateGet(cwd, section, raw) {
|
||||
}
|
||||
|
||||
// Try to find markdown section or field
|
||||
const fieldEscaped = section.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
||||
const fieldEscaped = escapeRegex(section);
|
||||
|
||||
// Check for **field:** value (bold format)
|
||||
const boldPattern = new RegExp(`\\*\\*${fieldEscaped}:\\*\\*\\s*(.*)`, 'i');
|
||||
@@ -125,7 +130,7 @@ function cmdStatePatch(cwd, patches, raw) {
|
||||
const results = { updated: [], failed: [] };
|
||||
|
||||
for (const [field, value] of Object.entries(patches)) {
|
||||
const fieldEscaped = field.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
||||
const fieldEscaped = escapeRegex(field);
|
||||
// Try **Field:** bold format first, then plain Field: format
|
||||
const boldPattern = new RegExp(`(\\*\\*${fieldEscaped}:\\*\\*\\s*)(.*)`, 'i');
|
||||
const plainPattern = new RegExp(`(^${fieldEscaped}:\\s*)(.*)`, 'im');
|
||||
@@ -159,7 +164,7 @@ function cmdStateUpdate(cwd, field, value) {
|
||||
const statePath = path.join(cwd, '.planning', 'STATE.md');
|
||||
try {
|
||||
let content = fs.readFileSync(statePath, 'utf-8');
|
||||
const fieldEscaped = field.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
||||
const fieldEscaped = escapeRegex(field);
|
||||
// Try **Field:** bold format first, then plain Field: format
|
||||
const boldPattern = new RegExp(`(\\*\\*${fieldEscaped}:\\*\\*\\s*)(.*)`, 'i');
|
||||
const plainPattern = new RegExp(`(^${fieldEscaped}:\\s*)(.*)`, 'im');
|
||||
@@ -180,21 +185,10 @@ function cmdStateUpdate(cwd, field, value) {
|
||||
}
|
||||
|
||||
// ─── State Progression Engine ────────────────────────────────────────────────
|
||||
|
||||
function stateExtractField(content, fieldName) {
|
||||
const escaped = fieldName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
||||
// Try **Field:** bold format first
|
||||
const boldPattern = new RegExp(`\\*\\*${escaped}:\\*\\*\\s*(.+)`, 'i');
|
||||
const boldMatch = content.match(boldPattern);
|
||||
if (boldMatch) return boldMatch[1].trim();
|
||||
// Fall back to plain Field: format
|
||||
const plainPattern = new RegExp(`^${escaped}:\\s*(.+)`, 'im');
|
||||
const plainMatch = content.match(plainPattern);
|
||||
return plainMatch ? plainMatch[1].trim() : null;
|
||||
}
|
||||
// stateExtractField is defined above (shared helper) — do not duplicate.
|
||||
|
||||
function stateReplaceField(content, fieldName, newValue) {
|
||||
const escaped = fieldName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
||||
const escaped = escapeRegex(fieldName);
|
||||
// Try **Field:** bold format first, then plain Field: format
|
||||
const boldPattern = new RegExp(`(\\*\\*${escaped}:\\*\\*\\s*)(.*)`, 'i');
|
||||
if (boldPattern.test(content)) {
|
||||
@@ -576,7 +570,7 @@ function buildStateFrontmatter(bodyContent, cwd) {
|
||||
const info = getMilestoneInfo(cwd);
|
||||
milestone = info.version;
|
||||
milestoneName = info.name;
|
||||
} catch {}
|
||||
} catch { /* intentionally empty */ }
|
||||
}
|
||||
|
||||
let totalPhases = totalPhasesRaw ? parseInt(totalPhasesRaw, 10) : null;
|
||||
@@ -611,7 +605,7 @@ function buildStateFrontmatter(bodyContent, cwd) {
|
||||
totalPlans = diskTotalPlans;
|
||||
completedPlans = diskTotalSummaries;
|
||||
}
|
||||
} catch {}
|
||||
} catch { /* intentionally empty */ }
|
||||
}
|
||||
|
||||
let progressPercent = null;
|
||||
@@ -677,10 +671,54 @@ function syncStateFrontmatter(content, cwd) {
|
||||
/**
|
||||
* Write STATE.md with synchronized YAML frontmatter.
|
||||
* All STATE.md writes should use this instead of raw writeFileSync.
|
||||
* Uses a simple lockfile to prevent parallel agents from overwriting
|
||||
* each other's changes (race condition with read-modify-write cycle).
|
||||
*/
|
||||
function writeStateMd(statePath, content, cwd) {
|
||||
const synced = syncStateFrontmatter(content, cwd);
|
||||
fs.writeFileSync(statePath, normalizeMd(synced), 'utf-8');
|
||||
const lockPath = statePath + '.lock';
|
||||
const maxRetries = 10;
|
||||
const retryDelay = 200; // ms
|
||||
|
||||
// Acquire lock (spin with backoff)
|
||||
for (let i = 0; i < maxRetries; i++) {
|
||||
try {
|
||||
// O_EXCL fails if file already exists — atomic lock
|
||||
const fd = fs.openSync(lockPath, fs.constants.O_CREAT | fs.constants.O_EXCL | fs.constants.O_WRONLY);
|
||||
fs.writeSync(fd, String(process.pid));
|
||||
fs.closeSync(fd);
|
||||
break;
|
||||
} catch (err) {
|
||||
if (err.code === 'EEXIST') {
|
||||
// Check for stale lock (> 10s old)
|
||||
try {
|
||||
const stat = fs.statSync(lockPath);
|
||||
if (Date.now() - stat.mtimeMs > 10000) {
|
||||
fs.unlinkSync(lockPath);
|
||||
continue; // retry immediately after clearing stale lock
|
||||
}
|
||||
} catch { /* lock was released between check — retry */ }
|
||||
|
||||
if (i === maxRetries - 1) {
|
||||
// Last resort: write anyway rather than losing data
|
||||
try { fs.unlinkSync(lockPath); } catch {}
|
||||
break;
|
||||
}
|
||||
// Spin-wait with small jitter
|
||||
const jitter = Math.floor(Math.random() * 50);
|
||||
const start = Date.now();
|
||||
while (Date.now() - start < retryDelay + jitter) { /* busy wait */ }
|
||||
continue;
|
||||
}
|
||||
break; // non-EEXIST error — proceed without lock
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
fs.writeFileSync(statePath, normalizeMd(synced), 'utf-8');
|
||||
} finally {
|
||||
try { fs.unlinkSync(lockPath); } catch { /* lock already gone */ }
|
||||
}
|
||||
}
|
||||
|
||||
function cmdStateJson(cwd, raw) {
|
||||
@@ -778,6 +816,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 +881,6 @@ module.exports = {
|
||||
cmdStateSnapshot,
|
||||
cmdStateJson,
|
||||
cmdStateBeginPhase,
|
||||
cmdSignalWaiting,
|
||||
cmdSignalResume,
|
||||
};
|
||||
|
||||
@@ -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();
|
||||
@@ -428,7 +428,7 @@ function cmdValidateConsistency(cwd, raw) {
|
||||
const dm = dir.match(/^(\d+[A-Z]?(?:\.\d+)*)/i);
|
||||
if (dm) diskPhases.add(dm[1]);
|
||||
}
|
||||
} catch {}
|
||||
} catch { /* intentionally empty */ }
|
||||
|
||||
// Check: phases in ROADMAP but not on disk
|
||||
for (const p of roadmapPhases) {
|
||||
@@ -490,7 +490,7 @@ function cmdValidateConsistency(cwd, raw) {
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch {}
|
||||
} catch { /* intentionally empty */ }
|
||||
|
||||
// Check: frontmatter in plans has required fields
|
||||
try {
|
||||
@@ -510,7 +510,7 @@ function cmdValidateConsistency(cwd, raw) {
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch {}
|
||||
} catch { /* intentionally empty */ }
|
||||
|
||||
const passed = errors.length === 0;
|
||||
output({ passed, errors, warnings, warning_count: warnings.length }, raw, passed ? 'passed' : 'failed');
|
||||
@@ -599,7 +599,7 @@ function cmdValidateHealth(cwd, options, raw) {
|
||||
if (m) diskPhases.add(m[1]);
|
||||
}
|
||||
}
|
||||
} catch {}
|
||||
} catch { /* intentionally empty */ }
|
||||
// Check for invalid references
|
||||
for (const ref of phaseRefs) {
|
||||
const normalizedRef = String(parseInt(ref, 10)).padStart(2, '0');
|
||||
@@ -641,7 +641,7 @@ function cmdValidateHealth(cwd, options, raw) {
|
||||
addIssue('warning', 'W008', 'config.json: workflow.nyquist_validation absent (defaults to enabled but agents may skip)', 'Run /gsd:health --repair to add key', true);
|
||||
if (!repairs.includes('addNyquistKey')) repairs.push('addNyquistKey');
|
||||
}
|
||||
} catch {}
|
||||
} catch { /* intentionally empty */ }
|
||||
}
|
||||
|
||||
// ─── Check 6: Phase directory naming (NN-name format) ─────────────────────
|
||||
@@ -652,7 +652,7 @@ function cmdValidateHealth(cwd, options, raw) {
|
||||
addIssue('warning', 'W005', `Phase directory "${e.name}" doesn't follow NN-name format`, 'Rename to match pattern (e.g., 01-setup)');
|
||||
}
|
||||
}
|
||||
} catch {}
|
||||
} catch { /* intentionally empty */ }
|
||||
|
||||
// ─── Check 7: Orphaned plans (PLAN without SUMMARY) ───────────────────────
|
||||
try {
|
||||
@@ -671,7 +671,7 @@ function cmdValidateHealth(cwd, options, raw) {
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch {}
|
||||
} catch { /* intentionally empty */ }
|
||||
|
||||
// ─── Check 7b: Nyquist VALIDATION.md consistency ────────────────────────
|
||||
try {
|
||||
@@ -689,13 +689,13 @@ function cmdValidateHealth(cwd, options, raw) {
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch {}
|
||||
} catch { /* intentionally empty */ }
|
||||
|
||||
// ─── Check 8: Run existing consistency checks ─────────────────────────────
|
||||
// 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;
|
||||
@@ -712,7 +712,7 @@ function cmdValidateHealth(cwd, options, raw) {
|
||||
if (dm) diskPhases.add(dm[1]);
|
||||
}
|
||||
}
|
||||
} catch {}
|
||||
} catch { /* intentionally empty */ }
|
||||
|
||||
// Phases in ROADMAP but not on disk
|
||||
for (const p of roadmapPhases) {
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -61,6 +61,10 @@ node "$HOME/.claude/get-shit-done/bin/gsd-tools.cjs" commit "docs: initialize [p
|
||||
|
||||
Each task gets its own commit immediately after completion.
|
||||
|
||||
> **Parallel agents:** When running as a parallel executor (spawned by execute-phase),
|
||||
> use `--no-verify` on all commits to avoid pre-commit hook lock contention.
|
||||
> The orchestrator validates hooks once after all agents complete.
|
||||
|
||||
```
|
||||
{type}({phase}-{plan}): {task-name}
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -127,6 +127,8 @@ Common types: Tech stack, Timeline, Budget, Dependencies, Compatibility, Perform
|
||||
<evolution>
|
||||
|
||||
PROJECT.md evolves throughout the project lifecycle.
|
||||
These rules are embedded in the generated PROJECT.md (## Evolution section)
|
||||
and implemented by workflows/transition.md and workflows/complete-milestone.md.
|
||||
|
||||
**After each phase transition:**
|
||||
1. Requirements invalidated? → Move to Out of Scope with reason
|
||||
|
||||
@@ -242,6 +242,47 @@ Structure the extracted information:
|
||||
**If no prior context exists:** Continue without — this is expected for early phases.
|
||||
</step>
|
||||
|
||||
<step name="cross_reference_todos">
|
||||
Check if any pending todos are relevant to this phase's scope. Surfaces backlog items that might otherwise be missed.
|
||||
|
||||
**Load and match todos:**
|
||||
```bash
|
||||
TODO_MATCHES=$(node "$HOME/.claude/get-shit-done/bin/gsd-tools.cjs" todo match-phase "${PHASE_NUMBER}")
|
||||
```
|
||||
|
||||
Parse JSON for: `todo_count`, `matches[]` (each with `file`, `title`, `area`, `score`, `reasons`).
|
||||
|
||||
**If `todo_count` is 0 or `matches` is empty:** Skip silently — no workflow slowdown.
|
||||
|
||||
**If matches found:**
|
||||
|
||||
Present matched todos to the user. Show each match with its title, area, and why it matched:
|
||||
|
||||
```
|
||||
📋 Found {N} pending todo(s) that may be relevant to Phase {X}:
|
||||
|
||||
{For each match:}
|
||||
- **{title}** (area: {area}, relevance: {score}) — matched on {reasons}
|
||||
```
|
||||
|
||||
Use AskUserQuestion (multiSelect) asking which todos to fold into this phase's scope:
|
||||
|
||||
```
|
||||
Which of these todos should be folded into Phase {X} scope?
|
||||
(Select any that apply, or none to skip)
|
||||
```
|
||||
|
||||
**For selected (folded) todos:**
|
||||
- Store internally as `<folded_todos>` for inclusion in CONTEXT.md `<decisions>` section
|
||||
- These become additional scope items that downstream agents (researcher, planner) will see
|
||||
|
||||
**For unselected (reviewed but not folded) todos:**
|
||||
- Store internally as `<reviewed_todos>` for inclusion in CONTEXT.md `<deferred>` section
|
||||
- This prevents future phases from re-surfacing the same todos as "missed"
|
||||
|
||||
**Auto mode (`--auto`):** Fold all todos with score >= 0.4 automatically. Log the selection.
|
||||
</step>
|
||||
|
||||
<step name="scout_codebase">
|
||||
Lightweight scan of existing code to inform gray area identification and discussion. Uses ~10% context — acceptable for an interactive session.
|
||||
|
||||
@@ -406,6 +447,30 @@ For each selected area, conduct a focused discussion loop.
|
||||
|
||||
**Batch mode support:** Parse optional `--batch` from `$ARGUMENTS`.
|
||||
- Accept `--batch`, `--batch=N`, or `--batch N`
|
||||
|
||||
**Analyze mode support:** Parse optional `--analyze` from `$ARGUMENTS`.
|
||||
When `--analyze` is active, before presenting each question (or question group in batch mode), provide a brief **trade-off analysis** for the decision:
|
||||
- 2-3 options with pros/cons based on codebase context and common patterns
|
||||
- A recommended approach with reasoning
|
||||
- Known pitfalls or constraints from prior phases
|
||||
|
||||
Example with `--analyze`:
|
||||
```
|
||||
**Trade-off analysis: Authentication strategy**
|
||||
|
||||
| Approach | Pros | Cons |
|
||||
|----------|------|------|
|
||||
| Session cookies | Simple, httpOnly prevents XSS | Requires CSRF protection, sticky sessions |
|
||||
| JWT (stateless) | Scalable, no server state | Token size, revocation complexity |
|
||||
| OAuth 2.0 + PKCE | Industry standard for SPAs | More setup, redirect flow UX |
|
||||
|
||||
💡 Recommended: OAuth 2.0 + PKCE — your app has social login in requirements (REQ-04) and this aligns with the existing NextAuth setup in `src/lib/auth.ts`.
|
||||
|
||||
How should users authenticate?
|
||||
```
|
||||
|
||||
This gives the user context to make informed decisions without extra prompting. When `--analyze` is absent, present questions directly as before.
|
||||
- Accept `--batch`, `--batch=N`, or `--batch N`
|
||||
- Default to 4 questions per batch when no number is provided
|
||||
- Clamp explicit sizes to 2-5 so a batch stays answerable
|
||||
- If `--batch` is absent, keep the existing one-question-at-a-time flow
|
||||
@@ -544,6 +609,11 @@ mkdir -p ".planning/phases/${padded_phase}-${phase_slug}"
|
||||
### Claude's Discretion
|
||||
[Areas where user said "you decide" — note that Claude has flexibility here]
|
||||
|
||||
### Folded Todos
|
||||
[If any todos were folded into scope from the cross_reference_todos step, list them here.
|
||||
Each entry should include the todo title, original problem, and how it fits this phase's scope.
|
||||
If no todos were folded: omit this subsection entirely.]
|
||||
|
||||
</decisions>
|
||||
|
||||
<canonical_refs>
|
||||
@@ -595,6 +665,12 @@ Every entry needs a full relative path — not just a name.]
|
||||
|
||||
[Ideas that came up but belong in other phases. Don't lose them.]
|
||||
|
||||
### Reviewed Todos (not folded)
|
||||
[If any todos were reviewed in cross_reference_todos but not folded into scope,
|
||||
list them here so future phases know they were considered.
|
||||
Each entry: todo title + reason it was deferred (out of scope, belongs in Phase Y, etc.)
|
||||
If no reviewed-but-deferred todos: omit this subsection entirely.]
|
||||
|
||||
[If none: "None — discussion stayed within phase scope"]
|
||||
|
||||
</deferred>
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -111,8 +191,9 @@ Execute each wave in sequence. Within a wave: parallel if `PARALLELIZATION=true`
|
||||
|
||||
2. **Spawn executor agents:**
|
||||
|
||||
Pass paths only — executors read files themselves with their fresh 200k context.
|
||||
This keeps orchestrator context lean (~10-15%).
|
||||
Pass paths only — executors read files themselves with their fresh context window.
|
||||
For 200k models, this keeps orchestrator context lean (~10-15%).
|
||||
For 1M+ models (Opus 4.6, Sonnet 4.6), richer context can be passed directly.
|
||||
|
||||
```
|
||||
Task(
|
||||
@@ -124,6 +205,14 @@ Execute each wave in sequence. Within a wave: parallel if `PARALLELIZATION=true`
|
||||
Commit each task atomically. Create SUMMARY.md. Update STATE.md and ROADMAP.md.
|
||||
</objective>
|
||||
|
||||
<parallel_execution>
|
||||
You are running as a PARALLEL executor agent. Use --no-verify on all git
|
||||
commits to avoid pre-commit hook contention with other agents. The
|
||||
orchestrator validates hooks once after all agents complete.
|
||||
For gsd-tools commits: add --no-verify flag.
|
||||
For direct git commits: use git commit --no-verify -m "..."
|
||||
</parallel_execution>
|
||||
|
||||
<execution_context>
|
||||
@~/.claude/get-shit-done/workflows/execute-plan.md
|
||||
@~/.claude/get-shit-done/templates/summary.md
|
||||
@@ -134,12 +223,20 @@ Execute each wave in sequence. Within a wave: parallel if `PARALLELIZATION=true`
|
||||
<files_to_read>
|
||||
Read these files at execution start using the Read tool:
|
||||
- {phase_dir}/{plan_file} (Plan)
|
||||
- .planning/PROJECT.md (Project context — core value, requirements, evolution rules)
|
||||
- .planning/STATE.md (State)
|
||||
- .planning/config.json (Config, if exists)
|
||||
- ./CLAUDE.md (Project instructions, if exists — follow project-specific guidelines and coding conventions)
|
||||
- .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
|
||||
@@ -153,7 +250,17 @@ Execute each wave in sequence. Within a wave: parallel if `PARALLELIZATION=true`
|
||||
|
||||
3. **Wait for all agents in wave to complete.**
|
||||
|
||||
4. **Report completion — spot-check claims first:**
|
||||
4. **Post-wave hook validation (parallel mode only):**
|
||||
|
||||
When agents committed with `--no-verify`, run pre-commit hooks once after the wave:
|
||||
```bash
|
||||
# Run project's pre-commit hooks on the current state
|
||||
git diff --cached --quiet || git stash # stash any unstaged changes
|
||||
git hook run pre-commit 2>&1 || echo "⚠ Pre-commit hooks failed — review before continuing"
|
||||
```
|
||||
If hooks fail: report the failure and ask "Fix hook issues now?" or "Continue to next wave?"
|
||||
|
||||
5. **Report completion — spot-check claims first:**
|
||||
|
||||
For each SUMMARY.md:
|
||||
- Verify first 2 files from `key-files.created` exist on disk
|
||||
@@ -328,6 +435,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 +580,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 +655,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,12 +665,20 @@ 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>
|
||||
|
||||
<context_efficiency>
|
||||
Orchestrator: ~10-15% context. Subagents: fresh 200k each. No polling (Task blocks). No context bleed.
|
||||
Orchestrator: ~10-15% context for 200k windows, can use more for 1M+ windows.
|
||||
Subagents: fresh context each (200k-1M depending on model). No polling (Task blocks). No context bleed.
|
||||
|
||||
For 1M+ context models, consider:
|
||||
- Passing richer context (code snippets, dependency outputs) directly to executors instead of just file paths
|
||||
- Running small phases (≤3 plans, no dependencies) inline without subagent spawning overhead
|
||||
- Relaxing /clear recommendations — context rot onset is much further out with 5x window
|
||||
</context_efficiency>
|
||||
|
||||
<failure_handling>
|
||||
|
||||
@@ -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.
|
||||
@@ -233,6 +234,10 @@ See `~/.claude/get-shit-done/references/tdd.md` for structure.
|
||||
|
||||
Your commits may trigger pre-commit hooks. Auto-fix hooks handle themselves transparently — files get fixed and re-staged automatically.
|
||||
|
||||
**If running as a parallel executor agent (spawned by execute-phase):**
|
||||
Use `--no-verify` on all commits. Pre-commit hooks cause build lock contention when multiple agents commit simultaneously (e.g., cargo lock fights in Rust projects). The orchestrator validates once after all agents complete.
|
||||
|
||||
**If running as the sole executor (sequential mode):**
|
||||
If a commit is BLOCKED by a hook:
|
||||
|
||||
1. The `git commit` command fails with hook error output
|
||||
@@ -240,9 +245,7 @@ If a commit is BLOCKED by a hook:
|
||||
3. Fix the issue (type error, lint violation, secret leak, etc.)
|
||||
4. `git add` the fixed files
|
||||
5. Retry the commit
|
||||
6. Do NOT use `--no-verify`
|
||||
|
||||
This is normal and expected. Budget 1-2 retry cycles per commit.
|
||||
6. Budget 1-2 retry cycles per commit
|
||||
</precommit_failure_handling>
|
||||
|
||||
<task_commit>
|
||||
@@ -370,7 +373,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">
|
||||
|
||||
105
get-shit-done/workflows/fast.md
Normal file
105
get-shit-done/workflows/fast.md
Normal file
@@ -0,0 +1,105 @@
|
||||
<purpose>
|
||||
Execute a trivial task inline without subagent overhead. No PLAN.md, no Task spawning,
|
||||
no research, no plan checking. Just: understand → do → commit → log.
|
||||
|
||||
For tasks like: fix a typo, update a config value, add a missing import, rename a
|
||||
variable, commit uncommitted work, add a .gitignore entry, bump a version number.
|
||||
|
||||
Use /gsd:quick for anything that needs multi-step planning or research.
|
||||
</purpose>
|
||||
|
||||
<process>
|
||||
|
||||
<step name="parse_task">
|
||||
Parse `$ARGUMENTS` for the task description.
|
||||
|
||||
If empty, ask:
|
||||
```
|
||||
What's the quick fix? (one sentence)
|
||||
```
|
||||
|
||||
Store as `$TASK`.
|
||||
</step>
|
||||
|
||||
<step name="scope_check">
|
||||
**Before doing anything, verify this is actually trivial.**
|
||||
|
||||
A task is trivial if it can be completed in:
|
||||
- ≤ 3 file edits
|
||||
- ≤ 1 minute of work
|
||||
- No new dependencies or architecture changes
|
||||
- No research needed
|
||||
|
||||
If the task seems non-trivial (multi-file refactor, new feature, needs research),
|
||||
say:
|
||||
|
||||
```
|
||||
This looks like it needs planning. Use /gsd:quick instead:
|
||||
/gsd:quick "{task description}"
|
||||
```
|
||||
|
||||
And stop.
|
||||
</step>
|
||||
|
||||
<step name="execute_inline">
|
||||
Do the work directly:
|
||||
|
||||
1. Read the relevant file(s)
|
||||
2. Make the change(s)
|
||||
3. Verify the change works (run existing tests if applicable, or do a quick sanity check)
|
||||
|
||||
**No PLAN.md.** Just do it.
|
||||
</step>
|
||||
|
||||
<step name="commit">
|
||||
Commit the change atomically:
|
||||
|
||||
```bash
|
||||
git add -A
|
||||
git commit -m "fix: {concise description of what changed}"
|
||||
```
|
||||
|
||||
Use conventional commit format: `fix:`, `feat:`, `docs:`, `chore:`, `refactor:` as appropriate.
|
||||
</step>
|
||||
|
||||
<step name="log_to_state">
|
||||
If `.planning/STATE.md` exists, append to the "Quick Tasks Completed" table.
|
||||
If the table doesn't exist, skip this step silently.
|
||||
|
||||
```bash
|
||||
# Check if STATE.md has quick tasks table
|
||||
if grep -q "Quick Tasks Completed" .planning/STATE.md 2>/dev/null; then
|
||||
# Append entry — workflow handles the format
|
||||
echo "| $(date +%Y-%m-%d) | fast | $TASK | ✅ |" >> .planning/STATE.md
|
||||
fi
|
||||
```
|
||||
</step>
|
||||
|
||||
<step name="done">
|
||||
Report completion:
|
||||
|
||||
```
|
||||
✅ Done: {what was changed}
|
||||
Commit: {short hash}
|
||||
Files: {list of changed files}
|
||||
```
|
||||
|
||||
No next-step suggestions. No workflow routing. Just done.
|
||||
</step>
|
||||
|
||||
</process>
|
||||
|
||||
<guardrails>
|
||||
- NEVER spawn a Task/subagent — this runs inline
|
||||
- NEVER create PLAN.md or SUMMARY.md files
|
||||
- NEVER run research or plan-checking
|
||||
- If the task takes more than 3 file edits, STOP and redirect to /gsd:quick
|
||||
- If you're unsure how to implement it, STOP and redirect to /gsd:quick
|
||||
</guardrails>
|
||||
|
||||
<success_criteria>
|
||||
- [ ] Task completed in current context (no subagents)
|
||||
- [ ] Atomic git commit with conventional message
|
||||
- [ ] STATE.md updated if it exists
|
||||
- [ ] Total operation under 2 minutes wall time
|
||||
</success_criteria>
|
||||
@@ -157,3 +157,25 @@ Report final status.
|
||||
- Orphaned plan cleanup
|
||||
|
||||
</repair_actions>
|
||||
|
||||
<stale_task_cleanup>
|
||||
**Windows-specific:** Check for stale Claude Code task directories that accumulate on crash/freeze.
|
||||
These are left behind when subagents are force-killed and consume disk space.
|
||||
|
||||
When `--repair` is active, detect and clean up:
|
||||
|
||||
```bash
|
||||
# Check for stale task directories (older than 24 hours)
|
||||
TASKS_DIR="$HOME/.claude/tasks"
|
||||
if [ -d "$TASKS_DIR" ]; then
|
||||
STALE_COUNT=$(find "$TASKS_DIR" -maxdepth 1 -type d -mtime +1 2>/dev/null | wc -l)
|
||||
if [ "$STALE_COUNT" -gt 0 ]; then
|
||||
echo "⚠️ Found $STALE_COUNT stale task directories in ~/.claude/tasks/"
|
||||
echo " These are leftover from crashed subagent sessions."
|
||||
echo " Run: rm -rf ~/.claude/tasks/* (safe — only affects dead sessions)"
|
||||
fi
|
||||
fi
|
||||
```
|
||||
|
||||
Report as info diagnostic: `I002 | info | Stale subagent task directories found | Yes (--repair removes them)`
|
||||
</stale_task_cleanup>
|
||||
|
||||
@@ -151,6 +151,21 @@ Usage: `/gsd:quick`
|
||||
Usage: `/gsd:quick --research --full`
|
||||
Result: Creates `.planning/quick/NNN-slug/PLAN.md`, `.planning/quick/NNN-slug/SUMMARY.md`
|
||||
|
||||
---
|
||||
|
||||
**`/gsd:fast [description]`**
|
||||
Execute a trivial task inline — no subagents, no planning files, no overhead.
|
||||
|
||||
For tasks too small to justify planning: typo fixes, config changes, forgotten commits, simple additions. Runs in the current context, makes the change, commits, and logs to STATE.md.
|
||||
|
||||
- No PLAN.md or SUMMARY.md created
|
||||
- No subagent spawned (runs inline)
|
||||
- ≤ 3 file edits — redirects to `/gsd:quick` if task is non-trivial
|
||||
- Atomic commit with conventional message
|
||||
|
||||
Usage: `/gsd:fast "fix the typo in README"`
|
||||
Usage: `/gsd:fast "add .env to gitignore"`
|
||||
|
||||
### Roadmap Management
|
||||
|
||||
**`/gsd:add-phase <description>`**
|
||||
@@ -308,6 +323,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]`**
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -54,6 +54,27 @@ Add/update:
|
||||
|
||||
Update Active requirements section and "Last updated" footer.
|
||||
|
||||
Ensure the `## Evolution` section exists in PROJECT.md. If missing (projects created before this feature), add it before the footer:
|
||||
|
||||
```markdown
|
||||
## Evolution
|
||||
|
||||
This document evolves at phase transitions and milestone boundaries.
|
||||
|
||||
**After each phase transition** (via `/gsd:transition`):
|
||||
1. Requirements invalidated? → Move to Out of Scope with reason
|
||||
2. Requirements validated? → Move to Validated with phase reference
|
||||
3. New requirements emerged? → Add to Active
|
||||
4. Decisions to log? → Add to Key Decisions
|
||||
5. "What This Is" still accurate? → Update if drifted
|
||||
|
||||
**After each milestone** (via `/gsd:complete-milestone`):
|
||||
1. Full review of all sections
|
||||
2. Core Value check — still the right priority?
|
||||
3. Audit Out of Scope — reasons still valid?
|
||||
4. Update Context with current state
|
||||
```
|
||||
|
||||
## 5. Update STATE.md
|
||||
|
||||
```markdown
|
||||
|
||||
@@ -323,6 +323,27 @@ Initialize with any decisions made during questioning:
|
||||
*Last updated: [date] after initialization*
|
||||
```
|
||||
|
||||
**Evolution section** (include at the end of PROJECT.md, before the footer):
|
||||
|
||||
```markdown
|
||||
## Evolution
|
||||
|
||||
This document evolves at phase transitions and milestone boundaries.
|
||||
|
||||
**After each phase transition** (via `/gsd:transition`):
|
||||
1. Requirements invalidated? → Move to Out of Scope with reason
|
||||
2. Requirements validated? → Move to Validated with phase reference
|
||||
3. New requirements emerged? → Add to Active
|
||||
4. Decisions to log? → Add to Key Decisions
|
||||
5. "What This Is" still accurate? → Update if drifted
|
||||
|
||||
**After each milestone** (via `/gsd:complete-milestone`):
|
||||
1. Full review of all sections
|
||||
2. Core Value check — still the right priority?
|
||||
3. Audit Out of Scope — reasons still valid?
|
||||
4. Update Context with current state
|
||||
```
|
||||
|
||||
Do not compress. Capture everything gathered.
|
||||
|
||||
**Commit PROJECT.md:**
|
||||
|
||||
97
get-shit-done/workflows/next.md
Normal file
97
get-shit-done/workflows/next.md
Normal 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>
|
||||
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -670,6 +737,30 @@ Verification: {Passed | Passed with override | Skipped}
|
||||
───────────────────────────────────────────────────────────────
|
||||
</offer_next>
|
||||
|
||||
<windows_troubleshooting>
|
||||
**Windows users:** If plan-phase freezes during agent spawning (common on Windows due to
|
||||
stdio deadlocks with MCP servers — see Claude Code issue anthropics/claude-code#28126):
|
||||
|
||||
1. **Force-kill:** Close the terminal (Ctrl+C may not work)
|
||||
2. **Clean up orphaned processes:**
|
||||
```powershell
|
||||
# Kill orphaned node processes from stale MCP servers
|
||||
Get-Process node -ErrorAction SilentlyContinue | Where-Object {$_.StartTime -lt (Get-Date).AddHours(-1)} | Stop-Process -Force
|
||||
```
|
||||
3. **Clean up stale task directories:**
|
||||
```powershell
|
||||
# Remove stale subagent task dirs (Claude Code never cleans these on crash)
|
||||
Remove-Item -Recurse -Force "$env:USERPROFILE\.claude\tasks\*" -ErrorAction SilentlyContinue
|
||||
```
|
||||
4. **Reduce MCP server count:** Temporarily disable non-essential MCP servers in settings.json
|
||||
5. **Retry:** Restart Claude Code and run `/gsd:plan-phase` again
|
||||
|
||||
If freezes persist, try `--skip-research` to reduce the agent chain from 3 to 2 agents:
|
||||
```
|
||||
/gsd:plan-phase N --skip-research
|
||||
```
|
||||
</windows_troubleshooting>
|
||||
|
||||
<success_criteria>
|
||||
- [ ] .planning/ directory validated
|
||||
- [ ] Phase validated against roadmap
|
||||
|
||||
@@ -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
|
||||
|
||||
146
get-shit-done/workflows/session-report.md
Normal file
146
get-shit-done/workflows/session-report.md
Normal 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>
|
||||
@@ -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)" }
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -160,7 +160,8 @@ Merge new settings into existing config.json:
|
||||
"branching_strategy": "none" | "phase" | "milestone"
|
||||
},
|
||||
"hooks": {
|
||||
"context_warnings": true/false
|
||||
"context_warnings": true/false,
|
||||
"workflow_guard": true/false
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
228
get-shit-done/workflows/ship.md
Normal file
228
get-shit-done/workflows/ship.md
Normal 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>
|
||||
@@ -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:**
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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', () => {
|
||||
|
||||
@@ -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) {}
|
||||
}
|
||||
|
||||
|
||||
93
hooks/gsd-workflow-guard.js
Normal file
93
hooks/gsd-workflow-guard.js
Normal file
@@ -0,0 +1,93 @@
|
||||
#!/usr/bin/env node
|
||||
// GSD Workflow Guard — PreToolUse hook
|
||||
// Detects when Claude attempts file edits outside a GSD workflow context
|
||||
// (no active /gsd: command or Task subagent) and injects an advisory warning.
|
||||
//
|
||||
// This is a SOFT guard — it advises, not blocks. The edit still proceeds.
|
||||
// The warning nudges Claude to use /gsd:quick or /gsd:fast instead of
|
||||
// making direct edits that bypass state tracking.
|
||||
//
|
||||
// Enable via config: hooks.workflow_guard: true (default: false)
|
||||
// Only triggers on Write/Edit tool calls to non-.planning/ files.
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
let input = '';
|
||||
const stdinTimeout = setTimeout(() => process.exit(0), 3000);
|
||||
process.stdin.setEncoding('utf8');
|
||||
process.stdin.on('data', chunk => input += chunk);
|
||||
process.stdin.on('end', () => {
|
||||
clearTimeout(stdinTimeout);
|
||||
try {
|
||||
const data = JSON.parse(input);
|
||||
const toolName = data.tool_name;
|
||||
|
||||
// Only guard Write and Edit tool calls
|
||||
if (toolName !== 'Write' && toolName !== 'Edit') {
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
// Check if we're inside a GSD workflow (Task subagent or /gsd: command)
|
||||
// Subagents have a session_id that differs from the parent
|
||||
// and typically have a description field set by the orchestrator
|
||||
if (data.tool_input?.is_subagent || data.session_type === 'task') {
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
// Check the file being edited
|
||||
const filePath = data.tool_input?.file_path || data.tool_input?.path || '';
|
||||
|
||||
// Allow edits to .planning/ files (GSD state management)
|
||||
if (filePath.includes('.planning/') || filePath.includes('.planning\\')) {
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
// Allow edits to common config/docs files that don't need GSD tracking
|
||||
const allowedPatterns = [
|
||||
/\.gitignore$/,
|
||||
/\.env/,
|
||||
/CLAUDE\.md$/,
|
||||
/AGENTS\.md$/,
|
||||
/GEMINI\.md$/,
|
||||
/settings\.json$/,
|
||||
];
|
||||
if (allowedPatterns.some(p => p.test(filePath))) {
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
// Check if workflow guard is enabled
|
||||
const cwd = data.cwd || process.cwd();
|
||||
const configPath = path.join(cwd, '.planning', 'config.json');
|
||||
if (fs.existsSync(configPath)) {
|
||||
try {
|
||||
const config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
|
||||
if (!config.hooks?.workflow_guard) {
|
||||
process.exit(0); // Guard disabled (default)
|
||||
}
|
||||
} catch (e) {
|
||||
process.exit(0);
|
||||
}
|
||||
} else {
|
||||
process.exit(0); // No GSD project — don't guard
|
||||
}
|
||||
|
||||
// If we get here: GSD project, guard enabled, file edit outside .planning/,
|
||||
// not in a subagent context. Inject advisory warning.
|
||||
const output = {
|
||||
hookSpecificOutput: {
|
||||
hookEventName: "PreToolUse",
|
||||
additionalContext: `⚠️ WORKFLOW ADVISORY: You're editing ${path.basename(filePath)} directly without a GSD command. ` +
|
||||
'This edit will not be tracked in STATE.md or produce a SUMMARY.md. ' +
|
||||
'Consider using /gsd:fast for trivial fixes or /gsd:quick for larger changes ' +
|
||||
'to maintain project state tracking. ' +
|
||||
'If this is intentional (e.g., user explicitly asked for a direct edit), proceed normally.'
|
||||
}
|
||||
};
|
||||
|
||||
process.stdout.write(JSON.stringify(output));
|
||||
} catch (e) {
|
||||
// Silent fail — never block tool execution
|
||||
process.exit(0);
|
||||
}
|
||||
});
|
||||
4
package-lock.json
generated
4
package-lock.json
generated
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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');
|
||||
@@ -13,16 +17,38 @@ const DIST_DIR = path.join(HOOKS_DIR, 'dist');
|
||||
const HOOKS_TO_COPY = [
|
||||
'gsd-check-update.js',
|
||||
'gsd-context-monitor.js',
|
||||
'gsd-statusline.js'
|
||||
'gsd-statusline.js',
|
||||
'gsd-workflow-guard.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 +58,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.');
|
||||
|
||||
@@ -567,6 +567,98 @@ describe('todo complete command', () => {
|
||||
});
|
||||
});
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// todo match-phase command
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
describe('todo match-phase command', () => {
|
||||
let tmpDir;
|
||||
|
||||
beforeEach(() => {
|
||||
tmpDir = createTempProject();
|
||||
});
|
||||
afterEach(() => cleanup(tmpDir));
|
||||
|
||||
test('returns empty matches when no todos exist', () => {
|
||||
const result = runGsdTools('todo match-phase 01', tmpDir);
|
||||
assert.ok(result.success, 'should succeed');
|
||||
const output = JSON.parse(result.output);
|
||||
assert.strictEqual(output.todo_count, 0);
|
||||
assert.deepStrictEqual(output.matches, []);
|
||||
});
|
||||
|
||||
test('matches todo by keyword overlap with phase name', () => {
|
||||
const pendingDir = path.join(tmpDir, '.planning', 'todos', 'pending');
|
||||
fs.mkdirSync(pendingDir, { recursive: true });
|
||||
fs.writeFileSync(path.join(pendingDir, 'auth-todo.md'),
|
||||
'title: Add OAuth token refresh\narea: auth\ncreated: 2026-03-01\n\nNeed to handle token expiry for OAuth flows.');
|
||||
fs.writeFileSync(path.join(tmpDir, '.planning', 'ROADMAP.md'),
|
||||
'# Roadmap\n\n### Phase 01: Authentication and Session Management\n\n**Goal:** Implement OAuth login and session handling\n');
|
||||
|
||||
const result = runGsdTools('todo match-phase 01', tmpDir);
|
||||
assert.ok(result.success, 'should succeed');
|
||||
const output = JSON.parse(result.output);
|
||||
assert.strictEqual(output.todo_count, 1, 'should find 1 todo');
|
||||
assert.ok(output.matches.length > 0, 'should have matches');
|
||||
assert.strictEqual(output.matches[0].title, 'Add OAuth token refresh');
|
||||
assert.ok(output.matches[0].score > 0, 'score should be positive');
|
||||
assert.ok(output.matches[0].reasons.length > 0, 'should have reasons');
|
||||
});
|
||||
|
||||
test('does not match unrelated todo', () => {
|
||||
const pendingDir = path.join(tmpDir, '.planning', 'todos', 'pending');
|
||||
fs.mkdirSync(pendingDir, { recursive: true });
|
||||
fs.writeFileSync(path.join(pendingDir, 'auth-todo.md'),
|
||||
'title: Add OAuth token refresh\narea: auth\ncreated: 2026-03-01\n\nOAuth token expiry.');
|
||||
fs.writeFileSync(path.join(pendingDir, 'unrelated-todo.md'),
|
||||
'title: Fix CSS grid layout in dashboard\narea: ui\ncreated: 2026-03-01\n\nGrid columns break on mobile.');
|
||||
fs.writeFileSync(path.join(tmpDir, '.planning', 'ROADMAP.md'),
|
||||
'# Roadmap\n\n### Phase 01: Authentication and Session Management\n\n**Goal:** Implement OAuth login and session handling\n');
|
||||
|
||||
const result = runGsdTools('todo match-phase 01', tmpDir);
|
||||
assert.ok(result.success, 'should succeed');
|
||||
const output = JSON.parse(result.output);
|
||||
const matchTitles = output.matches.map(m => m.title);
|
||||
assert.ok(matchTitles.includes('Add OAuth token refresh'), 'auth todo should match');
|
||||
assert.ok(!matchTitles.includes('Fix CSS grid layout in dashboard'), 'unrelated todo should not match');
|
||||
});
|
||||
|
||||
test('matches todo by area overlap', () => {
|
||||
const pendingDir = path.join(tmpDir, '.planning', 'todos', 'pending');
|
||||
fs.mkdirSync(pendingDir, { recursive: true });
|
||||
fs.writeFileSync(path.join(pendingDir, 'auth-todo.md'),
|
||||
'title: Add OAuth token refresh\narea: auth\ncreated: 2026-03-01\n\nOAuth token handling.');
|
||||
fs.writeFileSync(path.join(tmpDir, '.planning', 'ROADMAP.md'),
|
||||
'# Roadmap\n\n### Phase 01: Auth System\n\n**Goal:** Build auth module\n');
|
||||
|
||||
const result = runGsdTools('todo match-phase 01', tmpDir);
|
||||
const output = JSON.parse(result.output);
|
||||
const authMatch = output.matches.find(m => m.title === 'Add OAuth token refresh');
|
||||
assert.ok(authMatch, 'should find auth todo');
|
||||
const hasAreaReason = authMatch.reasons.some(r => r.startsWith('area:'));
|
||||
assert.ok(hasAreaReason, 'should match on area');
|
||||
});
|
||||
|
||||
test('sorts matches by score descending', () => {
|
||||
const pendingDir = path.join(tmpDir, '.planning', 'todos', 'pending');
|
||||
fs.mkdirSync(pendingDir, { recursive: true });
|
||||
fs.writeFileSync(path.join(pendingDir, 'weak-match.md'),
|
||||
'title: Check token format\narea: general\ncreated: 2026-03-01\n\nToken format validation.');
|
||||
fs.writeFileSync(path.join(pendingDir, 'strong-match.md'),
|
||||
'title: Session management authentication OAuth token handling\narea: auth\ncreated: 2026-03-01\n\nSession auth OAuth tokens.');
|
||||
fs.writeFileSync(path.join(tmpDir, '.planning', 'ROADMAP.md'),
|
||||
'# Roadmap\n\n### Phase 01: Authentication and Session Management\n\n**Goal:** Implement OAuth login, session handling, and token management\n');
|
||||
|
||||
const result = runGsdTools('todo match-phase 01', tmpDir);
|
||||
const output = JSON.parse(result.output);
|
||||
assert.ok(output.matches.length >= 2, 'should have multiple matches');
|
||||
for (let i = 1; i < output.matches.length; i++) {
|
||||
assert.ok(output.matches[i - 1].score >= output.matches[i].score,
|
||||
`match ${i-1} score (${output.matches[i-1].score}) should be >= match ${i} score (${output.matches[i].score})`);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// scaffold command
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
@@ -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, 43, `expected 43 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 = 43;
|
||||
const EXPECTED_AGENTS = 16;
|
||||
|
||||
function runCopilotInstall(cwd) {
|
||||
|
||||
@@ -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', () => {
|
||||
|
||||
81
tests/cursor-conversion.test.cjs
Normal file
81
tests/cursor-conversion.test.cjs
Normal file
@@ -0,0 +1,81 @@
|
||||
/**
|
||||
* Cursor conversion regression tests.
|
||||
*
|
||||
* Ensures Cursor frontmatter names are emitted as plain identifiers
|
||||
* (without surrounding quotes), so Cursor does not treat quotes as
|
||||
* literal parts of skill/subagent names.
|
||||
*/
|
||||
|
||||
process.env.GSD_TEST_MODE = '1';
|
||||
|
||||
const { describe, test } = require('node:test');
|
||||
const assert = require('node:assert');
|
||||
|
||||
const {
|
||||
convertClaudeCommandToCursorSkill,
|
||||
convertClaudeAgentToCursorAgent,
|
||||
} = require('../bin/install.js');
|
||||
|
||||
describe('convertClaudeCommandToCursorSkill', () => {
|
||||
test('writes unquoted Cursor skill name in frontmatter', () => {
|
||||
const input = `---
|
||||
name: quick
|
||||
description: Execute a quick task
|
||||
---
|
||||
|
||||
<objective>
|
||||
Test body
|
||||
</objective>
|
||||
`;
|
||||
|
||||
const result = convertClaudeCommandToCursorSkill(input, 'gsd-quick');
|
||||
const nameMatch = result.match(/^name:\s*(.+)$/m);
|
||||
|
||||
assert.ok(nameMatch, 'frontmatter contains name field');
|
||||
assert.equal(nameMatch[1], 'gsd-quick', 'skill name is plain scalar');
|
||||
assert.ok(!result.includes('name: "gsd-quick"'), 'quoted skill name is not emitted');
|
||||
});
|
||||
|
||||
test('preserves slash for slash commands in markdown body', () => {
|
||||
const input = `---
|
||||
name: gsd:plan-phase
|
||||
description: Plan a phase
|
||||
---
|
||||
|
||||
Next:
|
||||
/gsd:execute-phase 17
|
||||
/gsd-help
|
||||
gsd:progress
|
||||
`;
|
||||
|
||||
const result = convertClaudeCommandToCursorSkill(input, 'gsd-plan-phase');
|
||||
|
||||
assert.ok(result.includes('/gsd-execute-phase 17'), 'slash command remains slash-prefixed');
|
||||
assert.ok(result.includes('/gsd-help'), 'existing slash command is preserved');
|
||||
assert.ok(result.includes('gsd-progress'), 'non-slash gsd: references still normalize');
|
||||
assert.ok(!result.includes('/gsd:execute-phase'), 'legacy colon command form is removed');
|
||||
});
|
||||
});
|
||||
|
||||
describe('convertClaudeAgentToCursorAgent', () => {
|
||||
test('writes unquoted Cursor agent name in frontmatter', () => {
|
||||
const input = `---
|
||||
name: gsd-planner
|
||||
description: Planner agent
|
||||
tools: Read, Write
|
||||
color: green
|
||||
---
|
||||
|
||||
<role>
|
||||
Planner body
|
||||
</role>
|
||||
`;
|
||||
|
||||
const result = convertClaudeAgentToCursorAgent(input);
|
||||
const nameMatch = result.match(/^name:\s*(.+)$/m);
|
||||
|
||||
assert.ok(nameMatch, 'frontmatter contains name field');
|
||||
assert.equal(nameMatch[1], 'gsd-planner', 'agent name is plain scalar');
|
||||
assert.ok(!result.includes('name: "gsd-planner"'), 'quoted agent name is not emitted');
|
||||
});
|
||||
});
|
||||
@@ -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');
|
||||
});
|
||||
});
|
||||
134
tests/model-profiles.test.cjs
Normal file
134
tests/model-profiles.test.cjs
Normal 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');
|
||||
});
|
||||
});
|
||||
197
tests/profile-output.test.cjs
Normal file
197
tests/profile-output.test.cjs
Normal 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');
|
||||
});
|
||||
});
|
||||
160
tests/profile-pipeline.test.cjs
Normal file
160
tests/profile-pipeline.test.cjs
Normal 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');
|
||||
});
|
||||
});
|
||||
@@ -1,19 +1,23 @@
|
||||
/**
|
||||
* 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)
|
||||
* model: inherit is NOT added (OpenCode doesn't support it — see #1156)
|
||||
* but mode: subagent IS added (required by OpenCode agents).
|
||||
* 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 = `---
|
||||
@@ -54,10 +58,10 @@ describe('OpenCode agent conversion (isAgent: true)', () => {
|
||||
assert.ok(frontmatter.includes('name: gsd-executor'), 'name: should be preserved for agents');
|
||||
});
|
||||
|
||||
test('adds model: inherit', () => {
|
||||
test('does not add model: inherit (OpenCode does not support it)', () => {
|
||||
const result = convertClaudeToOpencodeFrontmatter(SAMPLE_AGENT, { isAgent: true });
|
||||
const frontmatter = result.split('---')[1];
|
||||
assert.ok(frontmatter.includes('model: inherit'), 'model: inherit should be added');
|
||||
assert.ok(!frontmatter.includes('model: inherit'), 'model: inherit should NOT be added — OpenCode throws ProviderModelNotFoundError');
|
||||
});
|
||||
|
||||
test('adds mode: subagent', () => {
|
||||
@@ -141,3 +145,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
186
tests/template.test.cjs
Normal 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}`);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user