mirror of
https://github.com/glittercowboy/get-shit-done
synced 2026-04-25 17:25:23 +02:00
Compare commits
4 Commits
fix/2047-b
...
docs/2071-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cc04baa524 | ||
|
|
46cc28251a | ||
|
|
7857d35dc1 | ||
|
|
2a08f11f46 |
@@ -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
|
||||
|
||||
|
||||
@@ -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:**
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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`
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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` で利用可能
|
||||
|
||||
@@ -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`에서 사용 가능
|
||||
|
||||
@@ -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',
|
||||
]);
|
||||
|
||||
/**
|
||||
|
||||
@@ -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" } }`).
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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}]
|
||||
---
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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);
|
||||
|
||||
249
tests/gsd-statusline.test.cjs
Normal file
249
tests/gsd-statusline.test.cjs
Normal 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, {});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user