Compare commits

...

4 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
15 changed files with 449 additions and 96 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

@@ -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

@@ -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, {});
});
});