Compare commits

...

7 Commits

Author SHA1 Message Date
Maxim Brashenko
cc04baa524 feat(statusline): surface GSD milestone/phase/status when no active todo (#1990)
When no in_progress todo is active, fill the middle slot of
gsd-statusline.js with GSD state read from .planning/STATE.md.

Format: <milestone> · <status> · <phase name> (N/total)

- Add readGsdState() — walks up from workspace dir looking for
  .planning/STATE.md (bounded at 10 levels / home dir)
- Add parseStateMd() — reads YAML frontmatter (status, milestone,
  milestone_name) and Phase line from body; falls back to body Status:
  parsing for older STATE.md files without frontmatter
- Add formatGsdState() — joins available parts with ' · ', degrades
  gracefully when fields are missing
- Wrap stdin handler in runStatusline() and export helpers so unit
  tests can require the file without triggering the script behavior

Strictly additive: active todo wins the slot (unchanged); missing
STATE.md leaves the slot empty (unchanged). Only the "no active todo
AND STATE.md present" path is new.

Uses the YAML frontmatter added for #628, completing the statusline
display that issue originally proposed.

Closes #1989
2026-04-10 15:56:19 -04:00
Tibsfox
46cc28251a feat(review): add Qwen Code and Cursor CLI as peer reviewers (#1966)
* feat(review): add Qwen Code and Cursor CLI as peer reviewers (#1938, #1960)

Add qwen and cursor to the /gsd-review pipeline following the
established pattern from CodeRabbit and OpenCode integrations:
- CLI detection via command -v
- --qwen and --cursor flags
- Invocation blocks with empty-output fallback
- Install help URLs

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix(review): correct qwen/cursor invocations and add doc surfaces (#1966)

Address review feedback from trek-e, kturk, and lawsontaylor:

- Use positional form for qwen (qwen "prompt") — -p flag is deprecated
  upstream and will be removed in a future version
- Fix cursor invocation to use cursor agent -p --mode ask --trust
  instead of cursor --prompt which launches the editor GUI
- Add --qwen and --cursor flags to COMMANDS.md, FEATURES.md, help.md,
  commands/gsd/review.md, and localized docs (ja-JP, ko-KR)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 15:19:56 -04:00
Tibsfox
7857d35dc1 refactor(workflow): deduplicate deviation rules and commit protocol (#1968) (#2057)
The deviation rules and task commit protocol were duplicated between
gsd-executor.md (agent definition) and execute-plan.md (workflow).
The copies had diverged: the agent had scope boundary and fix attempt
limits the workflow lacked; the workflow had 3 extra commit types
(perf, docs, style) the agent lacked.

Consolidate gsd-executor.md as the single source of truth:
- Add missing commit types (perf, docs, style) to gsd-executor.md
- Replace execute-plan.md's ~90 lines of duplicated content with
  concise references to the agent definition

Saves ~1,600 tokens per workflow spawn and eliminates maintenance
drift between the two copies.

Closes #1968

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-10 15:17:03 -04:00
Andreas Brauchli
2a08f11f46 fix(config): allow intel.enabled in config-set whitelist (#2021)
`intel.enabled` is the documented opt-in for the intel subsystem
(see commands/gsd/intel.md and docs/CONFIGURATION.md), but it was
missing from VALID_CONFIG_KEYS in config.cjs, so the canonical
command failed:

  $ gsd-tools config-set intel.enabled true
  Error: Unknown config key: "intel.enabled"

Add the key to the whitelist, document it under a new "Intel Fields"
section in planning-config.md alongside the other namespaced fields,
and cover it with a config-set test.

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 15:00:38 -04:00
Berkay Karaman
d85a42c7ad fix(install): guard writeSettings against null settingsPath for cline runtime (#2035)
* fix(install): guard writeSettings against null settingsPath for cline runtime

Cline returns settingsPath: null from install() because it uses .clinerules
instead of settings.json. The finishInstall() guard was missing !isCline,
causing a crash with ERR_INVALID_ARG_TYPE when installing with the cline runtime.

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>

* test(cline): add regression tests for ERR_INVALID_ARG_TYPE null settingsPath guard

Adds two regression tests to tests/cline-install.test.cjs for gsd-build/get-shit-done#2044:
- Assert install(false, 'cline') does not throw ERR_INVALID_ARG_TYPE
- Assert settings.json is not written for cline runtime

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>

* test(cline): fix regression tests to directly call finishInstall with null settingsPath

The previous regression tests called install() which returns early for cline
before reaching finishInstall(), so the crash was never exercised. Fix by:
- Exporting finishInstall from bin/install.js
- Calling finishInstall(null, null, ..., 'cline') directly so the null
  settingsPath guard is actually tested

Tests now fail (ERR_INVALID_ARG_TYPE) without the fix and pass with it.

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 13:58:16 -04:00
Tom Boucher
50537e5f67 fix(install): extend buildHookCommand to .sh hooks — absolute quoted paths (#2049)
* fix(autonomous): add Agent to allowed-tools in gsd-autonomous skill

Closes #2043

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(install): extend buildHookCommand to .sh hooks — absolute quoted paths

- Extend buildHookCommand() to branch on .sh suffix, using 'bash' runner
  instead of 'node', so all hook paths go through the same quoted-path
  construction: bash "/absolute/path/hooks/gsd-*.sh"
- Replace three manual 'bash ' + targetDir + '...' concatenations for
  gsd-validate-commit.sh, gsd-session-state.sh, gsd-phase-boundary.sh
  with buildHookCommand(targetDir, hookName) for the global-install branch
- Global .sh hook paths are now double-quoted, fixing invocation failure
  when the config dir path contains spaces (Windows usernames, #2045)
- Adds regression tests in tests/sh-hook-paths.test.cjs

Closes #2045
Closes #2046

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-10 13:55:27 -04:00
Tom Boucher
6960fd28fe fix(autonomous): add Agent to allowed-tools in gsd-autonomous skill (#2048)
Closes #2043

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-10 13:55:13 -04:00
20 changed files with 697 additions and 101 deletions

View File

@@ -8,6 +8,7 @@ Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
### Added
- **Review model configuration** — Per-CLI model selection for /gsd-review via `review.models.<cli>` config keys. Falls back to CLI defaults when not set. (#1849)
- **Statusline now surfaces GSD milestone/phase/status** — when no `in_progress` todo is active, `gsd-statusline.js` reads `.planning/STATE.md` (walking up from the workspace dir) and fills the middle slot with `<milestone> · <status> · <phase> (N/total)`. Gracefully degrades when fields are missing; identical to previous behavior when there is no STATE.md or an active todo wins the slot. Uses the YAML frontmatter added for #628.
## [1.34.2] - 2026-04-06

View File

@@ -342,6 +342,9 @@ git add src/types/user.ts
| `fix` | Bug fix, error correction |
| `test` | Test-only changes (TDD RED) |
| `refactor` | Code cleanup, no behavior change |
| `perf` | Performance improvement, no behavior change |
| `docs` | Documentation only |
| `style` | Formatting, whitespace, no logic change |
| `chore` | Config, tooling, dependencies |
**4. Commit:**

View File

@@ -443,7 +443,13 @@ function expandTilde(filePath) {
function buildHookCommand(configDir, hookName) {
// Use forward slashes for Node.js compatibility on all platforms
const hooksPath = configDir.replace(/\\/g, '/') + '/hooks/' + hookName;
return `node "${hooksPath}"`;
// .sh hooks use bash; .js hooks use node. Both wrap the path in double quotes
// so that paths with spaces (e.g. Windows "C:/Users/First Last/") work correctly
// (fixes #2045). Routing .sh hooks through this function also ensures they always
// receive an absolute path rather than the bare relative string that the old manual
// concatenation produced (fixes #2046).
const runner = hookName.endsWith('.sh') ? 'bash' : 'node';
return `${runner} "${hooksPath}"`;
}
/**
@@ -6038,7 +6044,7 @@ function install(isGlobal, runtime = 'claude') {
// Configure commit validation hook (Conventional Commits enforcement, opt-in)
const validateCommitCommand = isGlobal
? 'bash ' + targetDir.replace(/\\/g, '/') + '/hooks/gsd-validate-commit.sh'
? buildHookCommand(targetDir, 'gsd-validate-commit.sh')
: 'bash ' + localPrefix + '/hooks/gsd-validate-commit.sh';
const hasValidateCommitHook = settings.hooks[preToolEvent].some(entry =>
entry.hooks && entry.hooks.some(h => h.command && h.command.includes('gsd-validate-commit'))
@@ -6065,7 +6071,7 @@ function install(isGlobal, runtime = 'claude') {
// Configure session state orientation hook (opt-in)
const sessionStateCommand = isGlobal
? 'bash ' + targetDir.replace(/\\/g, '/') + '/hooks/gsd-session-state.sh'
? buildHookCommand(targetDir, 'gsd-session-state.sh')
: 'bash ' + localPrefix + '/hooks/gsd-session-state.sh';
const hasSessionStateHook = settings.hooks.SessionStart.some(entry =>
entry.hooks && entry.hooks.some(h => h.command && h.command.includes('gsd-session-state'))
@@ -6087,7 +6093,7 @@ function install(isGlobal, runtime = 'claude') {
// Configure phase boundary detection hook (opt-in)
const phaseBoundaryCommand = isGlobal
? 'bash ' + targetDir.replace(/\\/g, '/') + '/hooks/gsd-phase-boundary.sh'
? buildHookCommand(targetDir, 'gsd-phase-boundary.sh')
: 'bash ' + localPrefix + '/hooks/gsd-phase-boundary.sh';
const hasPhaseBoundaryHook = settings.hooks[postToolEvent].some(entry =>
entry.hooks && entry.hooks.some(h => h.command && h.command.includes('gsd-phase-boundary'))
@@ -6124,6 +6130,7 @@ function finishInstall(settingsPath, settings, statuslineCommand, shouldInstallS
const isCursor = runtime === 'cursor';
const isWindsurf = runtime === 'windsurf';
const isTrae = runtime === 'trae';
const isCline = runtime === 'cline';
if (shouldInstallStatusline && !isOpencode && !isKilo && !isCodex && !isCopilot && !isCursor && !isWindsurf && !isTrae) {
settings.statusLine = {
@@ -6134,7 +6141,7 @@ function finishInstall(settingsPath, settings, statuslineCommand, shouldInstallS
}
// Write settings when runtime supports settings.json
if (!isCodex && !isCopilot && !isKilo && !isCursor && !isWindsurf && !isTrae) {
if (!isCodex && !isCopilot && !isKilo && !isCursor && !isWindsurf && !isTrae && !isCline) {
writeSettings(settingsPath, settings);
}
@@ -6483,6 +6490,7 @@ if (process.env.GSD_TEST_MODE) {
validateHookFields,
preserveUserArtifacts,
restoreUserArtifacts,
finishInstall,
};
} else {

View File

@@ -10,6 +10,7 @@ allowed-tools:
- Grep
- AskUserQuestion
- Task
- Agent
---
<objective>
Execute all remaining milestone phases autonomously. For each phase: discuss → plan → execute. Pauses only for user decisions (grey area acceptance, blockers, validation requests).

View File

@@ -1,7 +1,7 @@
---
name: gsd:review
description: Request cross-AI peer review of phase plans from external AI CLIs
argument-hint: "--phase N [--gemini] [--claude] [--codex] [--opencode] [--all]"
argument-hint: "--phase N [--gemini] [--claude] [--codex] [--opencode] [--qwen] [--cursor] [--all]"
allowed-tools:
- Read
- Write
@@ -11,7 +11,7 @@ allowed-tools:
---
<objective>
Invoke external AI CLIs (Gemini, Claude, Codex, OpenCode) to independently review phase plans.
Invoke external AI CLIs (Gemini, Claude, Codex, OpenCode, Qwen Code, Cursor) to independently review phase plans.
Produces a structured REVIEWS.md with per-reviewer feedback that can be fed back into
planning via /gsd-plan-phase --reviews.
@@ -30,6 +30,8 @@ Phase number: extracted from $ARGUMENTS (required)
- `--claude` — Include Claude CLI review (uses separate session)
- `--codex` — Include Codex CLI review
- `--opencode` — Include OpenCode review (uses model from user's OpenCode config)
- `--qwen` — Include Qwen Code review (Alibaba Qwen models)
- `--cursor` — Include Cursor agent review
- `--all` — Include all available CLIs
</context>

View File

@@ -1023,6 +1023,8 @@ Cross-AI peer review of phase plans from external AI CLIs.
| `--codex` | Include Codex CLI review |
| `--coderabbit` | Include CodeRabbit review |
| `--opencode` | Include OpenCode review (via GitHub Copilot) |
| `--qwen` | Include Qwen Code review (Alibaba Qwen models) |
| `--cursor` | Include Cursor agent review |
| `--all` | Include all available CLIs |
**Produces:** `{phase}-REVIEWS.md` — consumable by `/gsd-plan-phase --reviews`

View File

@@ -1068,9 +1068,9 @@ When verification returns `human_needed`, items are persisted as a trackable HUM
### 42. Cross-AI Peer Review
**Command:** `/gsd-review --phase N [--gemini] [--claude] [--codex] [--coderabbit] [--all]`
**Command:** `/gsd-review --phase N [--gemini] [--claude] [--codex] [--coderabbit] [--opencode] [--qwen] [--cursor] [--all]`
**Purpose:** Invoke external AI CLIs (Gemini, Claude, Codex, CodeRabbit) to independently review phase plans. Produces structured REVIEWS.md with per-reviewer feedback.
**Purpose:** Invoke external AI CLIs (Gemini, Claude, Codex, CodeRabbit, OpenCode, Qwen Code, Cursor) to independently review phase plans. Produces structured REVIEWS.md with per-reviewer feedback.
**Requirements:**
- REQ-REVIEW-01: System MUST detect available AI CLIs on the system

View File

@@ -839,6 +839,9 @@ GSDアップデート後にローカルの変更を復元します。
| `--claude` | Claude CLIレビューを含める別セッション |
| `--codex` | Codex CLIレビューを含める |
| `--coderabbit` | CodeRabbitレビューを含める |
| `--opencode` | OpenCodeレビューを含めるGitHub Copilot経由 |
| `--qwen` | Qwen Codeレビューを含めるAlibaba Qwenモデル |
| `--cursor` | Cursorエージェントレビューを含める |
| `--all` | 利用可能なすべてのCLIを含める |
**生成物:** `{phase}-REVIEWS.md``/gsd-plan-phase --reviews` で利用可能

View File

@@ -839,6 +839,9 @@ GSD 업데이트 후 로컬 수정사항을 복원합니다.
| `--claude` | Claude CLI 리뷰 포함 (별도 세션) |
| `--codex` | Codex CLI 리뷰 포함 |
| `--coderabbit` | CodeRabbit 리뷰 포함 |
| `--opencode` | OpenCode 리뷰 포함 (GitHub Copilot 경유) |
| `--qwen` | Qwen Code 리뷰 포함 (Alibaba Qwen 모델) |
| `--cursor` | Cursor 에이전트 리뷰 포함 |
| `--all` | 사용 가능한 모든 CLI 포함 |
**생성 파일:** `{phase}-REVIEWS.md``/gsd-plan-phase --reviews`에서 사용 가능

View File

@@ -36,6 +36,7 @@ const VALID_CONFIG_KEYS = new Set([
'project_code', 'phase_naming',
'manager.flags.discuss', 'manager.flags.plan', 'manager.flags.execute',
'response_language',
'intel.enabled',
]);
/**

View File

@@ -310,6 +310,14 @@ Set via `learnings.*` namespace (e.g., `"learnings": { "max_inject": 5 }`). Used
|-----|------|---------|----------------|-------------|
| `learnings.max_inject` | number | `10` | Any positive integer | Maximum number of global learning entries to inject into agent prompts per session |
### Intel Fields
Set via `intel.*` namespace (e.g., `"intel": { "enabled": true }`). Controls the queryable codebase intelligence system consumed by `/gsd-intel`.
| Key | Type | Default | Allowed Values | Description |
|-----|------|---------|----------------|-------------|
| `intel.enabled` | boolean | `false` | `true`, `false` | Enable queryable codebase intelligence system. When `true`, `/gsd-intel` commands build and query a JSON index in `.planning/intel/`. |
### Manager Fields
Set via `manager.*` namespace (e.g., `"manager": { "flags": { "discuss": "--auto" } }`).

View File

@@ -188,32 +188,12 @@ Auth errors during execution are NOT failures — they're expected interaction p
## Deviation Rules
You WILL discover unplanned work. Apply automatically, track all for Summary.
| Rule | Trigger | Action | Permission |
|------|---------|--------|------------|
| **1: Bug** | Broken behavior, errors, wrong queries, type errors, security vulns, race conditions, leaks | Fix → test → verify → track `[Rule 1 - Bug]` | Auto |
| **2: Missing Critical** | Missing essentials: error handling, validation, auth, CSRF/CORS, rate limiting, indexes, logging | Add → test → verify → track `[Rule 2 - Missing Critical]` | Auto |
| **3: Blocking** | Prevents completion: missing deps, wrong types, broken imports, missing env/config/files, circular deps | Fix blocker → verify proceeds → track `[Rule 3 - Blocking]` | Auto |
| **4: Architectural** | Structural change: new DB table, schema change, new service, switching libs, breaking API, new infra | STOP → present decision (below) → track `[Rule 4 - Architectural]` | Ask user |
**Rule 4 format:**
```
⚠️ Architectural Decision Needed
Current task: [task name]
Discovery: [what prompted this]
Proposed change: [modification]
Why needed: [rationale]
Impact: [what this affects]
Alternatives: [other approaches]
Proceed with proposed change? (yes / different approach / defer)
```
**Priority:** Rule 4 (STOP) > Rules 1-3 (auto) > unsure → Rule 4
**Edge cases:** missing validation → R2 | null crash → R1 | new table → R4 | new column → R1/2
**Heuristic:** Affects correctness/security/completion? → R1-3. Maybe? → R4.
Apply deviation rules from the gsd-executor agent definition (single source of truth):
- **Rules 1-3** (bugs, missing critical, blockers): auto-fix, test, verify, track as deviations
- **Rule 4** (architectural changes): STOP, present decision to user, await approval
- **Scope boundary**: do not auto-fix pre-existing issues unrelated to current task
- **Fix attempt limit**: max 3 retries per deviation before escalating
- **Priority**: Rule 4 (STOP) > Rules 1-3 (auto) > unsure → Rule 4
</deviation_rules>
@@ -266,59 +246,13 @@ If a commit is BLOCKED by a hook:
<task_commit>
## Task Commit Protocol
After each task (verification passed, done criteria met), commit immediately.
**1. Check:** `git status --short`
**2. Stage individually** (NEVER `git add .` or `git add -A`):
```bash
git add src/api/auth.ts
git add src/types/user.ts
```
**3. Commit type:**
| Type | When | Example |
|------|------|---------|
| `feat` | New functionality | feat(08-02): create user registration endpoint |
| `fix` | Bug fix | fix(08-02): correct email validation regex |
| `test` | Test-only (TDD RED) | test(08-02): add failing test for password hashing |
| `refactor` | No behavior change (TDD REFACTOR) | refactor(08-02): extract validation to helper |
| `perf` | Performance | perf(08-02): add database index |
| `docs` | Documentation | docs(08-02): add API docs |
| `style` | Formatting | style(08-02): format auth module |
| `chore` | Config/deps | chore(08-02): add bcrypt dependency |
**4. Format:** `{type}({phase}-{plan}): {description}` with bullet points for key changes.
<sub_repos_commit_flow>
**Sub-repos mode:** If `sub_repos` is configured (non-empty array from init context), use `commit-to-subrepo` instead of standard git commit. This routes files to their correct sub-repo based on path prefix.
```bash
node ~/.claude/get-shit-done/bin/gsd-tools.cjs commit-to-subrepo "{type}({phase}-{plan}): {description}" --files file1 file2 ...
```
The command groups files by sub-repo prefix and commits atomically to each. Returns JSON: `{ committed: true, repos: { "backend": { hash: "abc", files: [...] }, ... } }`.
Record hashes from each repo in the response for SUMMARY tracking.
**If `sub_repos` is empty or not set:** Use standard git commit flow below.
</sub_repos_commit_flow>
**5. Record hash:**
```bash
TASK_COMMIT=$(git rev-parse --short HEAD)
TASK_COMMITS+=("Task ${TASK_NUM}: ${TASK_COMMIT}")
```
**6. Check for untracked generated files:**
```bash
git status --short | grep '^??'
```
If new untracked files appeared after running scripts or tools, decide for each:
- **Commit it** — if it's a source file, config, or intentional artifact
- **Add to .gitignore** — if it's a generated/runtime output (build artifacts, `.env` files, cache files, compiled output)
- Do NOT leave generated files untracked
Follow the task commit protocol from the gsd-executor agent definition (single source of truth):
- Stage files individually (NEVER `git add .` or `git add -A`)
- Format: `{type}({phase}-{plan}): {concise description}` with bullet points for key changes
- Types: feat, fix, test, refactor, perf, docs, style, chore
- Sub-repos: use `commit-to-subrepo` when `sub_repos` is configured
- Record commit hash for SUMMARY tracking
- Check for untracked generated files after each commit
</task_commit>

View File

@@ -345,7 +345,7 @@ Usage: `/gsd-ship 4` or `/gsd-ship 4 --draft`
---
**`/gsd-review --phase N [--gemini] [--claude] [--codex] [--coderabbit] [--all]`**
**`/gsd-review --phase N [--gemini] [--claude] [--codex] [--coderabbit] [--opencode] [--qwen] [--cursor] [--all]`**
Cross-AI peer review — invoke external AI CLIs to independently review phase plans.
- Detects available CLIs (gemini, claude, codex, coderabbit)

View File

@@ -20,6 +20,8 @@ command -v claude >/dev/null 2>&1 && echo "claude:available" || echo "claude:mis
command -v codex >/dev/null 2>&1 && echo "codex:available" || echo "codex:missing"
command -v coderabbit >/dev/null 2>&1 && echo "coderabbit:available" || echo "coderabbit:missing"
command -v opencode >/dev/null 2>&1 && echo "opencode:available" || echo "opencode:missing"
command -v qwen >/dev/null 2>&1 && echo "qwen:available" || echo "qwen:missing"
command -v cursor >/dev/null 2>&1 && echo "cursor:available" || echo "cursor:missing"
```
Parse flags from `$ARGUMENTS`:
@@ -28,6 +30,8 @@ Parse flags from `$ARGUMENTS`:
- `--codex` → include Codex
- `--coderabbit` → include CodeRabbit
- `--opencode` → include OpenCode
- `--qwen` → include Qwen Code
- `--cursor` → include Cursor
- `--all` → include all available
- No flags → include all available
@@ -38,6 +42,8 @@ No external AI CLIs found. Install at least one:
- codex: https://github.com/openai/codex
- claude: https://github.com/anthropics/claude-code
- opencode: https://opencode.ai (leverages GitHub Copilot subscription models)
- qwen: https://github.com/nicepkg/qwen-code (Alibaba Qwen models)
- cursor: https://cursor.com (Cursor IDE agent mode)
Then run /gsd-review again.
```
@@ -197,6 +203,22 @@ if [ ! -s /tmp/gsd-review-opencode-{phase}.md ]; then
fi
```
**Qwen Code:**
```bash
qwen "$(cat /tmp/gsd-review-prompt-{phase}.md)" 2>/dev/null > /tmp/gsd-review-qwen-{phase}.md
if [ ! -s /tmp/gsd-review-qwen-{phase}.md ]; then
echo "Qwen review failed or returned empty output." > /tmp/gsd-review-qwen-{phase}.md
fi
```
**Cursor:**
```bash
cat /tmp/gsd-review-prompt-{phase}.md | cursor agent -p --mode ask --trust 2>/dev/null > /tmp/gsd-review-cursor-{phase}.md
if [ ! -s /tmp/gsd-review-cursor-{phase}.md ]; then
echo "Cursor review failed or returned empty output." > /tmp/gsd-review-cursor-{phase}.md
fi
```
If a CLI fails, log the error and continue with remaining CLIs.
Display progress:
@@ -216,7 +238,7 @@ Combine all review responses into `{phase_dir}/{padded_phase}-REVIEWS.md`:
```markdown
---
phase: {N}
reviewers: [gemini, claude, codex, coderabbit, opencode]
reviewers: [gemini, claude, codex, coderabbit, opencode, qwen, cursor]
reviewed_at: {ISO timestamp}
plans_reviewed: [{list of PLAN.md files}]
---

View File

@@ -1,20 +1,120 @@
#!/usr/bin/env node
// gsd-hook-version: {{GSD_VERSION}}
// Claude Code Statusline - GSD Edition
// Shows: model | current task | directory | context usage
// Shows: model | current task (or GSD state) | directory | context usage
const fs = require('fs');
const path = require('path');
const os = require('os');
// Read JSON from stdin
let input = '';
// Timeout guard: if stdin doesn't close within 3s (e.g. pipe issues on
// Windows/Git Bash), exit silently instead of hanging. See #775.
const stdinTimeout = setTimeout(() => process.exit(0), 3000);
process.stdin.setEncoding('utf8');
process.stdin.on('data', chunk => input += chunk);
process.stdin.on('end', () => {
// --- GSD state reader -------------------------------------------------------
/**
* Walk up from dir looking for .planning/STATE.md.
* Returns parsed state object or null.
*/
function readGsdState(dir) {
const home = os.homedir();
let current = dir;
for (let i = 0; i < 10; i++) {
const candidate = path.join(current, '.planning', 'STATE.md');
if (fs.existsSync(candidate)) {
try {
return parseStateMd(fs.readFileSync(candidate, 'utf8'));
} catch (e) {
return null;
}
}
const parent = path.dirname(current);
if (parent === current || current === home) break;
current = parent;
}
return null;
}
/**
* Parse STATE.md frontmatter + Phase line from body.
* Returns { status, milestone, milestoneName, phaseNum, phaseTotal, phaseName }
*/
function parseStateMd(content) {
const state = {};
// YAML frontmatter between --- markers
const fmMatch = content.match(/^---\n([\s\S]*?)\n---/);
if (fmMatch) {
for (const line of fmMatch[1].split('\n')) {
const m = line.match(/^(\w+):\s*(.+)/);
if (!m) continue;
const [, key, val] = m;
const v = val.trim().replace(/^["']|["']$/g, '');
if (key === 'status') state.status = v === 'null' ? null : v;
if (key === 'milestone') state.milestone = v === 'null' ? null : v;
if (key === 'milestone_name') state.milestoneName = v === 'null' ? null : v;
}
}
// Phase: N of M (name) or Phase: none active (...)
const phaseMatch = content.match(/^Phase:\s*(\d+)\s+of\s+(\d+)(?:\s+\(([^)]+)\))?/m);
if (phaseMatch) {
state.phaseNum = phaseMatch[1];
state.phaseTotal = phaseMatch[2];
state.phaseName = phaseMatch[3] || null;
}
// Fallback: parse Status: from body when frontmatter is absent
if (!state.status) {
const bodyStatus = content.match(/^Status:\s*(.+)/m);
if (bodyStatus) {
const raw = bodyStatus[1].trim().toLowerCase();
if (raw.includes('ready to plan') || raw.includes('planning')) state.status = 'planning';
else if (raw.includes('execut')) state.status = 'executing';
else if (raw.includes('complet') || raw.includes('archived')) state.status = 'complete';
}
}
return state;
}
/**
* Format GSD state into display string.
* Format: "v1.9 Code Quality · executing · fix-graphiti-deployment (1/5)"
* Gracefully degrades when parts are missing.
*/
function formatGsdState(s) {
const parts = [];
// Milestone: version + name (skip placeholder "milestone")
if (s.milestone || s.milestoneName) {
const ver = s.milestone || '';
const name = (s.milestoneName && s.milestoneName !== 'milestone') ? s.milestoneName : '';
const ms = [ver, name].filter(Boolean).join(' ');
if (ms) parts.push(ms);
}
// Status
if (s.status) parts.push(s.status);
// Phase
if (s.phaseNum && s.phaseTotal) {
const phase = s.phaseName
? `${s.phaseName} (${s.phaseNum}/${s.phaseTotal})`
: `ph ${s.phaseNum}/${s.phaseTotal}`;
parts.push(phase);
}
return parts.join(' · ');
}
// --- stdin ------------------------------------------------------------------
function runStatusline() {
let input = '';
// Timeout guard: if stdin doesn't close within 3s (e.g. pipe issues on
// Windows/Git Bash), exit silently instead of hanging. See #775.
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);
@@ -94,6 +194,9 @@ process.stdin.on('end', () => {
}
}
// GSD state (milestone · status · phase) — shown when no todo task
const gsdStateStr = task ? '' : formatGsdState(readGsdState(dir) || {});
// GSD update available?
// Check shared cache first (#1421), fall back to runtime-specific cache for
// backward compatibility with older gsd-check-update.js versions.
@@ -115,8 +218,14 @@ process.stdin.on('end', () => {
// Output
const dirname = path.basename(dir);
if (task) {
process.stdout.write(`${gsdUpdate}\x1b[2m${model}\x1b[0m │ \x1b[1m${task}\x1b[0m │ \x1b[2m${dirname}\x1b[0m${ctx}`);
const middle = task
? `\x1b[1m${task}\x1b[0m`
: gsdStateStr
? `\x1b[2m${gsdStateStr}\x1b[0m`
: null;
if (middle) {
process.stdout.write(`${gsdUpdate}\x1b[2m${model}\x1b[0m │ ${middle}\x1b[2m${dirname}\x1b[0m${ctx}`);
} else {
process.stdout.write(`${gsdUpdate}\x1b[2m${model}\x1b[0m │ \x1b[2m${dirname}\x1b[0m${ctx}`);
}
@@ -124,3 +233,9 @@ process.stdin.on('end', () => {
// Silent fail - don't break statusline on parse errors
}
});
}
// Export helpers for unit tests. Harmless when run as a script.
module.exports = { readGsdState, parseStateMd, formatGsdState };
if (require.main === module) runStatusline();

View File

@@ -0,0 +1,37 @@
/**
* Regression test for #2043 — autonomous.md must include Agent in allowed-tools.
*
* The gsd-autonomous skill spawns background agents via Agent(..., run_in_background=true).
* Without Agent in allowed-tools the runtime rejects those calls silently.
*/
'use strict';
const { describe, test } = require('node:test');
const assert = require('node:assert/strict');
const fs = require('fs');
const path = require('path');
describe('commands/gsd/autonomous.md allowed-tools', () => {
test('includes Agent in allowed-tools list', () => {
const filePath = path.join(__dirname, '..', 'commands', 'gsd', 'autonomous.md');
const content = fs.readFileSync(filePath, 'utf-8');
// Extract the YAML frontmatter block between the first pair of --- delimiters
const frontmatterMatch = content.match(/^---\n([\s\S]*?)\n---/);
assert.ok(frontmatterMatch, 'autonomous.md must have YAML frontmatter');
const frontmatter = frontmatterMatch[1];
// Parse the allowed-tools list items (lines starting with " - ")
const toolLines = frontmatter
.split('\n')
.filter((line) => /^\s+-\s+/.test(line))
.map((line) => line.replace(/^\s+-\s+/, '').trim());
assert.ok(
toolLines.includes('Agent'),
`allowed-tools must include "Agent" but found: [${toolLines.join(', ')}]`
);
});
});

View File

@@ -29,6 +29,7 @@ const {
getConfigDirFromHome,
convertClaudeToCliineMarkdown,
install,
finishInstall,
} = require('../bin/install.js');
describe('Cline runtime directory mapping', () => {
@@ -154,6 +155,23 @@ describe('Cline install (local)', () => {
assert.ok(fs.existsSync(engineDir), 'get-shit-done directory must exist after install');
});
test('finishInstall does not throw ERR_INVALID_ARG_TYPE for cline runtime (regression: null settingsPath guard)', () => {
// install() returns settingsPath: null for cline — finishInstall() must not call
// writeSettings(null, ...) or it crashes with ERR_INVALID_ARG_TYPE.
// Before fix: isCline was missing from the writeSettings guard in finishInstall().
// After fix: !isCline is in the guard, matching codex/copilot/cursor/windsurf/trae.
assert.doesNotThrow(
() => finishInstall(null, null, null, false, 'cline', false, tmpDir),
'finishInstall must not throw when called with null settingsPath for cline runtime'
);
});
test('settings.json is not written for cline runtime', () => {
finishInstall(null, null, null, false, 'cline', false, tmpDir);
const settingsJson = path.join(tmpDir, 'settings.json');
assert.ok(!fs.existsSync(settingsJson), 'settings.json must not be written for cline runtime');
});
test('installed engine files have no leaked .claude paths', () => {
install(false, 'cline');
const engineDir = path.join(tmpDir, 'get-shit-done');

View File

@@ -252,6 +252,16 @@ describe('config-set command', () => {
assert.strictEqual(config.git.base_branch, 'master');
});
test('sets intel.enabled to opt into the intel subsystem', () => {
writeConfig(tmpDir, {});
const result = runGsdTools('config-set intel.enabled true', tmpDir);
assert.ok(result.success, `Command failed: ${result.error}`);
const config = readConfig(tmpDir);
assert.strictEqual(config.intel.enabled, true);
});
test('errors when no key path provided', () => {
const result = runGsdTools('config-set', tmpDir);
assert.strictEqual(result.success, false);

View File

@@ -0,0 +1,249 @@
/**
* Tests for gsd-statusline.js GSD state display helpers.
*
* Covers:
* - parseStateMd across YAML-frontmatter, body-fallback, and partial formats
* - formatGsdState graceful degradation when fields are missing
* - readGsdState walk-up search with proper bounds
*/
'use strict';
const { test, describe } = require('node:test');
const assert = require('node:assert/strict');
const fs = require('node:fs');
const os = require('node:os');
const path = require('node:path');
const { parseStateMd, formatGsdState, readGsdState } = require('../hooks/gsd-statusline.js');
// ─── parseStateMd ───────────────────────────────────────────────────────────
describe('parseStateMd', () => {
test('parses full YAML frontmatter', () => {
const content = [
'---',
'status: executing',
'milestone: v1.9',
'milestone_name: Code Quality',
'---',
'',
'# State',
'Phase: 1 of 5 (fix-graphiti-deployment)',
].join('\n');
const s = parseStateMd(content);
assert.equal(s.status, 'executing');
assert.equal(s.milestone, 'v1.9');
assert.equal(s.milestoneName, 'Code Quality');
assert.equal(s.phaseNum, '1');
assert.equal(s.phaseTotal, '5');
assert.equal(s.phaseName, 'fix-graphiti-deployment');
});
test('treats literal "null" values as null', () => {
const content = [
'---',
'status: null',
'milestone: null',
'milestone_name: null',
'---',
].join('\n');
const s = parseStateMd(content);
assert.equal(s.status, null);
assert.equal(s.milestone, null);
assert.equal(s.milestoneName, null);
});
test('strips surrounding quotes from frontmatter values', () => {
const content = [
'---',
'milestone_name: "Code Quality"',
"milestone: 'v1.9'",
'---',
].join('\n');
const s = parseStateMd(content);
assert.equal(s.milestone, 'v1.9');
assert.equal(s.milestoneName, 'Code Quality');
});
test('parses phase without name', () => {
const content = [
'---',
'status: planning',
'---',
'Phase: 3 of 10',
].join('\n');
const s = parseStateMd(content);
assert.equal(s.phaseNum, '3');
assert.equal(s.phaseTotal, '10');
assert.equal(s.phaseName, null);
});
test('falls back to body Status when frontmatter is missing', () => {
const content = [
'# State',
'Status: Ready to plan',
].join('\n');
const s = parseStateMd(content);
assert.equal(s.status, 'planning');
});
test('body fallback recognizes executing state', () => {
const content = 'Status: Executing phase 2';
assert.equal(parseStateMd(content).status, 'executing');
});
test('body fallback recognizes complete state', () => {
const content = 'Status: Complete';
assert.equal(parseStateMd(content).status, 'complete');
});
test('body fallback recognizes archived as complete', () => {
const content = 'Status: Archived';
assert.equal(parseStateMd(content).status, 'complete');
});
test('returns empty object for empty content', () => {
const s = parseStateMd('');
assert.deepEqual(s, {});
});
test('returns partial state when only some fields present', () => {
const content = [
'---',
'milestone: v2.0',
'---',
].join('\n');
const s = parseStateMd(content);
assert.equal(s.milestone, 'v2.0');
assert.equal(s.status, undefined);
assert.equal(s.phaseNum, undefined);
});
});
// ─── formatGsdState ─────────────────────────────────────────────────────────
describe('formatGsdState', () => {
test('formats full state with milestone name, status, and phase name', () => {
const out = formatGsdState({
milestone: 'v1.9',
milestoneName: 'Code Quality',
status: 'executing',
phaseNum: '1',
phaseTotal: '5',
phaseName: 'fix-graphiti-deployment',
});
assert.equal(out, 'v1.9 Code Quality · executing · fix-graphiti-deployment (1/5)');
});
test('skips placeholder "milestone" value in milestoneName', () => {
const out = formatGsdState({
milestone: 'v1.0',
milestoneName: 'milestone',
status: 'planning',
});
assert.equal(out, 'v1.0 · planning');
});
test('uses short phase form when phase name is missing', () => {
const out = formatGsdState({
milestone: 'v2.0',
status: 'executing',
phaseNum: '3',
phaseTotal: '7',
});
assert.equal(out, 'v2.0 · executing · ph 3/7');
});
test('omits phase entirely when phaseNum/phaseTotal missing', () => {
const out = formatGsdState({
milestone: 'v1.0',
status: 'planning',
});
assert.equal(out, 'v1.0 · planning');
});
test('handles milestone version only (no name)', () => {
const out = formatGsdState({
milestone: 'v1.9',
status: 'executing',
});
assert.equal(out, 'v1.9 · executing');
});
test('handles milestone name only (no version)', () => {
const out = formatGsdState({
milestoneName: 'Foundations',
status: 'planning',
});
assert.equal(out, 'Foundations · planning');
});
test('returns empty string for empty state', () => {
assert.equal(formatGsdState({}), '');
});
test('returns only available parts when everything else is missing', () => {
assert.equal(formatGsdState({ status: 'planning' }), 'planning');
});
});
// ─── readGsdState ───────────────────────────────────────────────────────────
describe('readGsdState', () => {
const tmpRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-statusline-test-'));
test('finds STATE.md in the starting directory', () => {
const proj = fs.mkdtempSync(path.join(tmpRoot, 'proj-'));
fs.mkdirSync(path.join(proj, '.planning'), { recursive: true });
fs.writeFileSync(
path.join(proj, '.planning', 'STATE.md'),
'---\nstatus: executing\nmilestone: v1.0\n---\n'
);
const s = readGsdState(proj);
assert.equal(s.status, 'executing');
assert.equal(s.milestone, 'v1.0');
});
test('walks up to find STATE.md in a parent directory', () => {
const proj = fs.mkdtempSync(path.join(tmpRoot, 'proj-'));
fs.mkdirSync(path.join(proj, '.planning'), { recursive: true });
fs.writeFileSync(
path.join(proj, '.planning', 'STATE.md'),
'---\nstatus: planning\n---\n'
);
const nested = path.join(proj, 'src', 'components', 'deep');
fs.mkdirSync(nested, { recursive: true });
const s = readGsdState(nested);
assert.equal(s.status, 'planning');
});
test('returns null when no STATE.md exists in the walk-up chain', () => {
const proj = fs.mkdtempSync(path.join(tmpRoot, 'proj-'));
const nested = path.join(proj, 'src');
fs.mkdirSync(nested, { recursive: true });
assert.equal(readGsdState(nested), null);
});
test('returns null on malformed STATE.md without crashing', () => {
const proj = fs.mkdtempSync(path.join(tmpRoot, 'proj-'));
fs.mkdirSync(path.join(proj, '.planning'), { recursive: true });
// Valid file (no content to crash on) — parseStateMd returns {}
fs.writeFileSync(path.join(proj, '.planning', 'STATE.md'), '');
const s = readGsdState(proj);
// Empty file yields an empty state object, not null — the function
// only returns null when no file is found.
assert.deepEqual(s, {});
});
});

View File

@@ -0,0 +1,179 @@
/**
* Regression tests for bugs #2045 and #2046
*
* #2046 (macOS/Linux): The three .sh hooks (gsd-validate-commit.sh,
* gsd-session-state.sh, gsd-phase-boundary.sh) were registered in
* settings.json with RELATIVE paths (bash .claude/hooks/...) for local
* installs, causing "No such file or directory" when Claude Code's cwd
* is not the project root.
*
* #2045 (Windows): The same three .sh hooks were registered WITHOUT quotes
* around the path, so usernames with spaces (e.g. C:/Users/First Last/)
* break bash invocation with a syntax error.
*
* Root cause: buildHookCommand() only handled .js files. The .sh hooks were
* built via manual string concatenation without quoting, and local installs
* used localPrefix (.claude/...) instead of the $CLAUDE_PROJECT_DIR-anchored
* form that .js local hooks use.
*
* Fix: extend buildHookCommand() to handle .sh files (uses 'bash' instead of
* 'node') so that all paths go through the same quoted-path construction.
*/
'use strict';
const { describe, test } = require('node:test');
const assert = require('node:assert/strict');
const fs = require('fs');
const path = require('path');
const INSTALL_SRC = path.join(__dirname, '..', 'bin', 'install.js');
const SH_HOOKS = [
{ name: 'gsd-validate-commit.sh', commandVar: 'validateCommitCommand' },
{ name: 'gsd-session-state.sh', commandVar: 'sessionStateCommand' },
{ name: 'gsd-phase-boundary.sh', commandVar: 'phaseBoundaryCommand' },
];
describe('bugs #2045 #2046: .sh hook paths must be absolute and quoted', () => {
let src;
try {
src = fs.readFileSync(INSTALL_SRC, 'utf-8');
} catch {
src = '';
}
// ── Test 1: buildHookCommand supports .sh files ──────────────────────────
describe('buildHookCommand', () => {
test('returns a bash command for .sh hookName', () => {
// Extract buildHookCommand from source and verify it branches on .sh
const fnStart = src.indexOf('function buildHookCommand(');
assert.ok(fnStart !== -1, 'buildHookCommand function not found in install.js');
// Find the closing brace of the function (scan for the balanced brace)
let depth = 0;
let fnEnd = fnStart;
for (let i = fnStart; i < src.length; i++) {
if (src[i] === '{') depth++;
else if (src[i] === '}') {
depth--;
if (depth === 0) { fnEnd = i + 1; break; }
}
}
const fnBody = src.slice(fnStart, fnEnd);
assert.ok(
fnBody.includes('.sh') || fnBody.includes('bash'),
'buildHookCommand must handle .sh files by using "bash" as the runner. ' +
'The function body must contain ".sh" or "bash" for branching logic.'
);
});
test('buildHookCommand produces bash runner for .sh and node runner for .js', () => {
const fnStart = src.indexOf('function buildHookCommand(');
assert.ok(fnStart !== -1, 'buildHookCommand function not found in install.js');
let depth = 0;
let fnEnd = fnStart;
for (let i = fnStart; i < src.length; i++) {
if (src[i] === '{') depth++;
else if (src[i] === '}') {
depth--;
if (depth === 0) { fnEnd = i + 1; break; }
}
}
const fnBody = src.slice(fnStart, fnEnd);
// Must still produce "node" for .js (existing behavior)
assert.ok(
fnBody.includes('node'),
'buildHookCommand must still produce a "node" command for .js hooks'
);
// Must produce "bash" for .sh
assert.ok(
fnBody.includes('bash'),
'buildHookCommand must produce a "bash" command for .sh hooks'
);
});
});
// ── Test 2: each .sh command variable uses a quoted path ─────────────────
for (const { name, commandVar } of SH_HOOKS) {
describe(`${name} command`, () => {
test(`${commandVar} uses double-quoted path (fixes #2045 Windows spaces)`, () => {
const varIdx = src.indexOf(commandVar);
assert.ok(varIdx !== -1, `${commandVar} not found in install.js`);
// Extract the assignment block (~300 chars should cover a single declaration)
const blockEnd = Math.min(src.length, varIdx + 400);
const block = src.slice(varIdx, blockEnd);
// The command string for the global branch must contain a quoted path:
// bash "..." — the path must be wrapped in double quotes.
assert.ok(
block.includes('bash "') || block.includes("bash '") || block.includes('buildHookCommand'),
`${commandVar} must use buildHookCommand() (which quotes the path) or manually ` +
`quote the path. Found: ${block.slice(0, 200)}`
);
});
test(`${commandVar} does not use bare localPrefix without quoting (fixes #2046 relative path)`, () => {
const varIdx = src.indexOf(commandVar);
assert.ok(varIdx !== -1, `${commandVar} not found in install.js`);
const blockEnd = Math.min(src.length, varIdx + 400);
const block = src.slice(varIdx, blockEnd);
// The old bad pattern was: 'bash ' + localPrefix + '/hooks/...'
// where localPrefix === '.claude' (relative, no quotes).
// The fix routes through buildHookCommand which emits bash "absolutePath".
// So the raw string '.claude/hooks' must NOT appear unquoted in this block.
const hasBareRelativePath = /bash ['"]?\.claude\/hooks/.test(block);
assert.ok(
!hasBareRelativePath,
`${commandVar} must not use a bare relative path ".claude/hooks". ` +
`Use buildHookCommand() so the path is absolute and quoted.`
);
});
});
}
// ── Test 3: global .sh hooks must not use unquoted manual concatenation ───
test('global .sh hook commands use buildHookCommand, not unquoted string concat', () => {
// Old bad pattern for global installs:
// 'bash ' + targetDir.replace(/\\/g, '/') + '/hooks/gsd-*.sh'
// This left the absolute path unquoted, breaking paths with spaces (#2045).
// The fix routes all global .sh hooks through buildHookCommand() which
// wraps the path in double quotes: bash "/absolute/path/hooks/gsd-*.sh"
const oldGlobalPattern = /'bash ' \+ targetDir/g;
const globalMatches = src.match(oldGlobalPattern) || [];
assert.strictEqual(
globalMatches.length, 0,
`Found ${globalMatches.length} occurrence(s) of unquoted global .sh path construction ` +
`('bash ' + targetDir). Use buildHookCommand(targetDir, 'gsd-*.sh') instead.`
);
});
// ── Test 4: global .sh hook commands contain double-quoted absolute paths ─
test('global .sh hook commands in source use bash with double-quoted path', () => {
// After the fix, buildHookCommand produces: bash "/abs/path/hooks/gsd-*.sh"
// Verify each hook's command variable is assigned via buildHookCommand for the global branch.
for (const { commandVar } of SH_HOOKS) {
const varIdx = src.indexOf(commandVar);
assert.ok(varIdx !== -1, `${commandVar} not found in install.js`);
// The ternary assignment: const xCommand = isGlobal ? buildHookCommand(...) : ...
const blockEnd = Math.min(src.length, varIdx + 300);
const block = src.slice(varIdx, blockEnd);
assert.ok(
block.includes('buildHookCommand'),
`${commandVar} global branch must use buildHookCommand() to produce a quoted absolute path. ` +
`Found: ${block.slice(0, 150)}`
);
}
});
});