Merge branch 'main' into docs/undocumented-features-v1.36

This commit is contained in:
Tom Boucher
2026-04-11 10:39:23 -04:00
31 changed files with 2593 additions and 159 deletions

View File

@@ -213,6 +213,10 @@ Track auto-fix attempts per task. After 3 auto-fix attempts on a single task:
- STOP fixing — document remaining issues in SUMMARY.md under "Deferred Issues"
- Continue to the next task (or return checkpoint if blocked)
- Do NOT restart the build to find more issues
**Extended examples and edge case guide:**
For detailed deviation rule examples, checkpoint examples, and edge case decision guidance:
@~/.claude/get-shit-done/references/executor-examples.md
</deviation_rules>
<analysis_paralysis_guard>

View File

@@ -338,6 +338,8 @@ issue:
- `"future enhancement"`, `"placeholder"`, `"basic version"`, `"minimal"`
- `"will be wired later"`, `"dynamic in future"`, `"skip for now"`
- `"not wired to"`, `"not connected to"`, `"stub"`
- `"too complex"`, `"too difficult"`, `"challenging"`, `"non-trivial"` (when used to justify omission)
- Time estimates used as scope justification: `"would take"`, `"hours"`, `"days"`, `"minutes"` (in sizing context)
2. For each match, cross-reference with the CONTEXT.md decision it claims to implement
3. Compare: does the task deliver what D-XX actually says, or a reduced version?
4. If reduced: BLOCKER — the planner must either deliver fully or propose phase split

View File

@@ -98,38 +98,47 @@ The orchestrator provides user decisions in `<user_decisions>` tags from `/gsd-d
- "v1", "v2", "simplified version", "static for now", "hardcoded for now"
- "future enhancement", "placeholder", "basic version", "minimal implementation"
- "will be wired later", "dynamic in future phase", "skip for now"
- Any language that reduces a CONTEXT.md decision to less than what the user decided
- Any language that reduces a source artifact decision to less than what was specified
**The rule:** If D-XX says "display cost calculated from billing table in impulses", the plan MUST deliver cost calculated from billing table in impulses. NOT "static label /min" as a "v1".
**When the phase is too complex to implement ALL decisions:**
**When the plan set cannot cover all source items within context budget:**
Do NOT silently simplify decisions. Instead:
Do NOT silently omit features. Instead:
1. **Create a decision coverage matrix** mapping every D-XX to a plan/task
2. **If any D-XX cannot fit** within the plan budget (too many tasks, too complex):
1. **Create a multi-source coverage audit** (see below) covering ALL four artifact types
2. **If any item cannot fit** within the plan budget (context cost exceeds capacity):
- Return `## PHASE SPLIT RECOMMENDED` to the orchestrator
- Propose how to split: which D-XX groups form natural sub-phases
- Example: "D-01 to D-19 = Phase 17a (processing core), D-20 to D-27 = Phase 17b (billing + config UX)"
3. The orchestrator will present the split to the user for approval
- Propose how to split: which item groups form natural sub-phases
3. The orchestrator presents the split to the user for approval
4. After approval, plan each sub-phase within budget
**Why this matters:** The user spent time making decisions. Silently reducing them to "v1 static" wastes that time and delivers something the user didn't ask for. Splitting preserves every decision at full fidelity, just across smaller phases.
## Multi-Source Coverage Audit (MANDATORY in every plan set)
**Decision coverage matrix (MANDATORY in every plan set):**
@planner-source-audit.md for full format, examples, and gap-handling rules.
Before finalizing plans, produce internally:
Audit ALL four source types before finalizing: **GOAL** (ROADMAP phase goal), **REQ** (phase_req_ids from REQUIREMENTS.md), **RESEARCH** (RESEARCH.md features/constraints), **CONTEXT** (D-XX decisions from CONTEXT.md).
```
D-XX | Plan | Task | Full/Partial | Notes
D-01 | 01 | 1 | Full |
D-02 | 01 | 2 | Full |
D-23 | 03 | 1 | PARTIAL | ← BLOCKER: must be Full or split phase
```
Every item must be COVERED by a plan. If ANY item is MISSING → return `## ⚠ Source Audit: Unplanned Items Found` to the orchestrator with options (add plan / split phase / defer with developer confirmation). Never finalize silently with gaps.
If ANY decision is "Partial" → either fix the task to deliver fully, or return PHASE SPLIT RECOMMENDED.
Exclusions (not gaps): Deferred Ideas in CONTEXT.md, items scoped to other phases, RESEARCH.md "out of scope" items.
</scope_reduction_prohibition>
<planner_authority_limits>
## The Planner Does Not Decide What Is Too Hard
@planner-source-audit.md for constraint examples.
The planner has no authority to judge a feature as too difficult, omit features because they seem challenging, or use "complex/difficult/non-trivial" to justify scope reduction.
**Only three legitimate reasons to split or flag:**
1. **Context cost:** implementation would consume >50% of a single agent's context window
2. **Missing information:** required data not present in any source artifact
3. **Dependency conflict:** feature cannot be built until another phase ships
If a feature has none of these three constraints, it gets planned. Period.
</planner_authority_limits>
<philosophy>
## Solo Developer + Claude Workflow
@@ -137,7 +146,7 @@ If ANY decision is "Partial" → either fix the task to deliver fully, or return
Planning for ONE person (the user) and ONE implementer (Claude).
- No teams, stakeholders, ceremonies, coordination overhead
- User = visionary/product owner, Claude = builder
- Estimate effort in Claude execution time, not human dev time
- Estimate effort in context window cost, not time
## Plans Are Prompts
@@ -165,7 +174,8 @@ Plan -> Execute -> Ship -> Learn -> Repeat
**Anti-enterprise patterns (delete if seen):**
- Team structures, RACI matrices, stakeholder management
- Sprint ceremonies, change management processes
- Human dev time estimates (hours, days, weeks)
- Time estimates in human units (see `<planner_authority_limits>`)
- Complexity/difficulty as scope justification (see `<planner_authority_limits>`)
- Documentation for documentation's sake
</philosophy>
@@ -246,13 +256,19 @@ Every task has four required fields:
## Task Sizing
Each task: **15-60 minutes** Claude execution time.
Each task targets **10–30% context consumption**.
| Duration | Action |
|----------|--------|
| < 15 min | Too small — combine with related task |
| 15-60 min | Right size |
| > 60 min | Too large — split |
| Context Cost | Action |
|--------------|--------|
| < 10% context | Too small — combine with a related task |
| 10-30% context | Right size — proceed |
| > 30% context | Too large — split into two tasks |
**Context cost signals (use these, not time estimates):**
- Files modified: 0-3 = ~10-15%, 4-6 = ~20-30%, 7+ = ~40%+ (split)
- New subsystem: ~25-35%
- Migration + data transform: ~30-40%
- Pure config/wiring: ~5-10%
**Too large signals:** Touches >3-5 files, multiple distinct chunks, action section >1 paragraph.
@@ -268,17 +284,9 @@ When a plan creates new interfaces consumed by subsequent tasks:
This prevents the "scavenger hunt" anti-pattern where executors explore the codebase to understand contracts. They receive the contracts in the plan itself.
## Specificity Examples
## Specificity
| TOO VAGUE | JUST RIGHT |
|-----------|------------|
| "Add authentication" | "Add JWT auth with refresh rotation using jose library, store in httpOnly cookie, 15min access / 7day refresh" |
| "Create the API" | "Create POST /api/projects endpoint accepting {name, description}, validates name length 3-50 chars, returns 201 with project object" |
| "Style the dashboard" | "Add Tailwind classes to Dashboard.tsx: grid layout (3 cols on lg, 1 on mobile), card shadows, hover states on action buttons" |
| "Handle errors" | "Wrap API calls in try/catch, return {error: string} on 4xx/5xx, show toast via sonner on client" |
| "Set up the database" | "Add User and Project models to schema.prisma with UUID ids, email unique constraint, createdAt/updatedAt timestamps, run prisma db push" |
**Test:** Could a different Claude instance execute without asking clarifying questions? If not, add specificity.
**Test:** Could a different Claude instance execute without asking clarifying questions? If not, add specificity. See @~/.claude/get-shit-done/references/planner-antipatterns.md for vague-vs-specific comparison table.
## TDD Detection
@@ -336,49 +344,9 @@ Record in `user_setup` frontmatter. Only include what Claude literally cannot do
- `creates`: What this produces
- `has_checkpoint`: Requires user interaction?
**Example with 6 tasks:**
**Example:** A→C, B→D, C+D→E, E→F(checkpoint). Waves: {A,B} → {C,D} → {E} → {F}.
```
Task A (User model): needs nothing, creates src/models/user.ts
Task B (Product model): needs nothing, creates src/models/product.ts
Task C (User API): needs Task A, creates src/api/users.ts
Task D (Product API): needs Task B, creates src/api/products.ts
Task E (Dashboard): needs Task C + D, creates src/components/Dashboard.tsx
Task F (Verify UI): checkpoint:human-verify, needs Task E
Graph:
A --> C --\
--> E --> F
B --> D --/
Wave analysis:
Wave 1: A, B (independent roots)
Wave 2: C, D (depend only on Wave 1)
Wave 3: E (depends on Wave 2)
Wave 4: F (checkpoint, depends on Wave 3)
```
## Vertical Slices vs Horizontal Layers
**Vertical slices (PREFER):**
```
Plan 01: User feature (model + API + UI)
Plan 02: Product feature (model + API + UI)
Plan 03: Order feature (model + API + UI)
```
Result: All three run parallel (Wave 1)
**Horizontal layers (AVOID):**
```
Plan 01: Create User model, Product model, Order model
Plan 02: Create User API, Product API, Order API
Plan 03: Create User UI, Product UI, Order UI
```
Result: Fully sequential (02 needs 01, 03 needs 02)
**When vertical slices work:** Features are independent, self-contained, no cross-feature dependencies.
**When horizontal layers necessary:** Shared foundation required (auth before protected features), genuine type dependencies, infrastructure setup.
**Prefer vertical slices** (User feature: model+API+UI) over horizontal layers (all models → all APIs → all UIs). Vertical = parallel. Horizontal = sequential. Use horizontal only when shared foundation is required.
## File Ownership for Parallel Execution
@@ -404,11 +372,11 @@ Plans should complete within ~50% context (not 80%). No context anxiety, quality
**Each plan: 2-3 tasks maximum.**
| Task Complexity | Tasks/Plan | Context/Task | Total |
|-----------------|------------|--------------|-------|
| Simple (CRUD, config) | 3 | ~10-15% | ~30-45% |
| Complex (auth, payments) | 2 | ~20-30% | ~40-50% |
| Very complex (migrations) | 1-2 | ~30-40% | ~30-50% |
| Context Weight | Tasks/Plan | Context/Task | Total |
|----------------|------------|--------------|-------|
| Light (CRUD, config) | 3 | ~10-15% | ~30-45% |
| Medium (auth, payments) | 2 | ~20-30% | ~40-50% |
| Heavy (migrations, multi-subsystem) | 1-2 | ~30-40% | ~30-50% |
## Split Signals
@@ -419,7 +387,7 @@ Plans should complete within ~50% context (not 80%). No context anxiety, quality
- Checkpoint + implementation in same plan
- Discovery + implementation in same plan
**CONSIDER splitting:** >5 files total, complex domains, uncertainty about approach, natural semantic boundaries.
**CONSIDER splitting:** >5 files total, natural semantic boundaries, context cost estimate exceeds 40% for a single plan. See `<planner_authority_limits>` for prohibited split reasons.
## Granularity Calibration
@@ -429,22 +397,7 @@ Plans should complete within ~50% context (not 80%). No context anxiety, quality
| Standard | 3-5 | 2-3 |
| Fine | 5-10 | 2-3 |
Derive plans from actual work. Granularity determines compression tolerance, not a target. Don't pad small work to hit a number. Don't compress complex work to look efficient.
## Context Per Task Estimates
| Files Modified | Context Impact |
|----------------|----------------|
| 0-3 files | ~10-15% (small) |
| 4-6 files | ~20-30% (medium) |
| 7+ files | ~40%+ (split) |
| Complexity | Context/Task |
|------------|--------------|
| Simple CRUD | ~15% |
| Business logic | ~25% |
| Complex algorithms | ~40% |
| Domain modeling | ~35% |
Derive plans from actual work. Granularity determines compression tolerance, not a target.
</scope_estimation>
@@ -797,36 +750,10 @@ When Claude tries CLI/API and gets auth error → creates checkpoint → user au
**DON'T:** Ask human to do work Claude can automate, mix multiple verifications, place checkpoints before automation completes.
## Anti-Patterns
## Anti-Patterns and Extended Examples
**Bad - Asking human to automate:**
```xml
<task type="checkpoint:human-action">
<action>Deploy to Vercel</action>
<instructions>Visit vercel.com, import repo, click deploy...</instructions>
</task>
```
Why bad: Vercel has a CLI. Claude should run `vercel --yes`.
**Bad - Too many checkpoints:**
```xml
<task type="auto">Create schema</task>
<task type="checkpoint:human-verify">Check schema</task>
<task type="auto">Create API</task>
<task type="checkpoint:human-verify">Check API</task>
```
Why bad: Verification fatigue. Combine into one checkpoint at end.
**Good - Single verification checkpoint:**
```xml
<task type="auto">Create schema</task>
<task type="auto">Create API</task>
<task type="auto">Create UI</task>
<task type="checkpoint:human-verify">
<what-built>Complete auth flow (schema + API + UI)</what-built>
<how-to-verify>Test full flow: register, login, access protected page</how-to-verify>
</task>
```
For checkpoint anti-patterns, specificity comparison tables, context section anti-patterns, and scope reduction patterns:
@~/.claude/get-shit-done/references/planner-antipatterns.md
</checkpoints>

View File

@@ -0,0 +1,22 @@
---
name: gsd:extract-learnings
description: Extract decisions, lessons, patterns, and surprises from completed phase artifacts
argument-hint: <phase-number>
allowed-tools:
- Read
- Write
- Bash
- Grep
- Glob
- Agent
type: prompt
---
<objective>
Extract structured learnings from completed phase artifacts (PLAN.md, SUMMARY.md, VERIFICATION.md, UAT.md, STATE.md) into a LEARNINGS.md file that captures decisions, lessons learned, patterns discovered, and surprises encountered.
</objective>
<execution_context>
@~/.claude/get-shit-done/workflows/extract_learnings.md
</execution_context>
Execute the extract-learnings workflow from @~/.claude/get-shit-done/workflows/extract_learnings.md end-to-end.

View File

@@ -1049,9 +1049,9 @@ fix(03-01): correct auth token expiry
### 42. クロス AI ピアレビュー
**コマンド:** `/gsd-review --phase N [--gemini] [--claude] [--codex] [--coderabbit] [--all]`
**コマンド:** `/gsd-review --phase N [--gemini] [--claude] [--codex] [--coderabbit] [--opencode] [--qwen] [--cursor] [--all]`
**目的:** 外部の AI CLI(Gemini、Claude、Codex、CodeRabbit)を呼び出して、フェーズプランを独立してレビューします。レビュアーごとのフィードバックを含む構造化された REVIEWS.md を生成します。
**目的:** 外部の AI CLI(Gemini、Claude、Codex、CodeRabbit、OpenCode、Qwen Code、Cursor)を呼び出して、フェーズプランを独立してレビューします。レビュアーごとのフィードバックを含む構造化された REVIEWS.md を生成します。
**要件:**
- REQ-REVIEW-01: システムはシステム上で利用可能な AI CLI を検出しなければならない

View File

@@ -1049,9 +1049,9 @@ fix(03-01): correct auth token expiry
### 42. Cross-AI Peer Review
**명령어:** `/gsd-review --phase N [--gemini] [--claude] [--codex] [--coderabbit] [--all]`
**명령어:** `/gsd-review --phase N [--gemini] [--claude] [--codex] [--coderabbit] [--opencode] [--qwen] [--cursor] [--all]`
**목적:** 외부 AI CLI(Gemini, Claude, Codex, CodeRabbit)를 호출하여 페이즈 계획을 독립적으로 검토합니다. 검토자별 피드백이 담긴 구조화된 REVIEWS.md를 생성합니다.
**목적:** 외부 AI CLI(Gemini, Claude, Codex, CodeRabbit, OpenCode, Qwen Code, Cursor)를 호출하여 페이즈 계획을 독립적으로 검토합니다. 검토자별 피드백이 담긴 구조화된 REVIEWS.md를 생성합니다.
**요구사항.**
- REQ-REVIEW-01: 시스템에서 사용 가능한 AI CLI를 감지해야 합니다.

View File

@@ -25,8 +25,13 @@ const VALID_CONFIG_KEYS = new Set([
'workflow.use_worktrees',
'workflow.code_review',
'workflow.code_review_depth',
'workflow.code_review_command',
'workflow.plan_bounce',
'workflow.plan_bounce_script',
'workflow.plan_bounce_passes',
'git.branching_strategy', 'git.base_branch', 'git.phase_branch_template', 'git.milestone_branch_template', 'git.quick_branch_template',
'planning.commit_docs', 'planning.search_gitignored',
'workflow.cross_ai_execution', 'workflow.cross_ai_command', 'workflow.cross_ai_timeout',
'workflow.subagent_timeout',
'hooks.context_warnings',
'features.thinking_partner',
@@ -65,6 +70,7 @@ const CONFIG_KEY_SUGGESTIONS = {
'hooks.research_questions': 'workflow.research_before_questions',
'workflow.research_questions': 'workflow.research_before_questions',
'workflow.codereview': 'workflow.code_review',
'workflow.review_command': 'workflow.code_review_command',
'workflow.review': 'workflow.code_review',
'workflow.code_review_level': 'workflow.code_review_depth',
'workflow.review_depth': 'workflow.code_review_depth',
@@ -155,6 +161,10 @@ function buildNewProjectConfig(userChoices) {
skip_discuss: false,
code_review: true,
code_review_depth: 'standard',
code_review_command: null,
plan_bounce: false,
plan_bounce_script: null,
plan_bounce_passes: 2,
},
hooks: {
context_warnings: true,

View File

@@ -0,0 +1,110 @@
# Executor Extended Examples
> Reference file for gsd-executor agent. Loaded on-demand via `@` reference.
> For sub-200K context windows, this content is stripped from the agent prompt and available here for on-demand loading.
## Deviation Rule Examples
### Rule 1 — Auto-fix bugs
**Examples of Rule 1 triggers:**
- Wrong queries returning incorrect data
- Logic errors in conditionals
- Type errors and type mismatches
- Null pointer exceptions / undefined access
- Broken validation (accepts invalid input)
- Security vulnerabilities (XSS, SQL injection)
- Race conditions in async code
- Memory leaks from uncleaned resources
### Rule 2 — Auto-add missing critical functionality
**Examples of Rule 2 triggers:**
- Missing error handling (unhandled promise rejections, no try/catch on I/O)
- No input validation on user-facing endpoints
- Missing null checks before property access
- No auth on protected routes
- Missing authorization checks (user can access other users' data)
- No CSRF/CORS configuration
- No rate limiting on public endpoints
- Missing DB indexes on frequently queried columns
- No error logging (failures silently swallowed)
### Rule 3 — Auto-fix blocking issues
**Examples of Rule 3 triggers:**
- Missing dependency not in package.json
- Wrong types preventing compilation
- Broken imports (wrong path, wrong export name)
- Missing env var required at runtime
- DB connection error (wrong URL, missing credentials)
- Build config error (wrong entry point, missing loader)
- Missing referenced file (import points to non-existent module)
- Circular dependency preventing module load
### Rule 4 — Ask about architectural changes
**Examples of Rule 4 triggers:**
- New DB table (not just adding a column)
- Major schema changes (renaming tables, changing relationships)
- New service layer (adding a queue, cache, or message bus)
- Switching libraries/frameworks (e.g., replacing Express with Fastify)
- Changing auth approach (switching from session to JWT)
- New infrastructure (adding Redis, S3, etc.)
- Breaking API changes (removing or renaming endpoints)
## Edge Case Decision Guide
| Scenario | Rule | Rationale |
|----------|------|-----------|
| Missing validation on input | Rule 2 | Security requirement |
| Crashes on null input | Rule 1 | Bug — incorrect behavior |
| Need new database table | Rule 4 | Architectural decision |
| Need new column on existing table | Rule 1 or 2 | Depends on context |
| Pre-existing linting warnings | Out of scope | Not caused by current task |
| Unrelated test failures | Out of scope | Not caused by current task |
**Decision heuristic:** "Does this affect correctness, security, or ability to complete the current task?"
- YES → Rules 1-3 (fix automatically)
- MAYBE → Rule 4 (ask the user)
- NO → Out of scope (log to deferred-items.md)
## Checkpoint Examples
### Good checkpoint placement
```xml
<!-- Automate everything, then verify at the end -->
<task type="auto">Create database schema</task>
<task type="auto">Create API endpoints</task>
<task type="auto">Create UI components</task>
<task type="checkpoint:human-verify">
<what-built>Complete auth flow (schema + API + UI)</what-built>
<how-to-verify>
1. Visit http://localhost:3000/register
2. Create account with test@example.com
3. Log in with those credentials
4. Verify dashboard loads with user name
</how-to-verify>
</task>
```
### Bad checkpoint placement
```xml
<!-- Too many checkpoints — causes verification fatigue -->
<task type="auto">Create schema</task>
<task type="checkpoint:human-verify">Check schema</task>
<task type="auto">Create API</task>
<task type="checkpoint:human-verify">Check API</task>
<task type="auto">Create UI</task>
<task type="checkpoint:human-verify">Check UI</task>
```
### Auth gate handling
When an auth error occurs during `type="auto"` execution:
1. Recognize it as an auth gate (not a bug) — indicators: "Not authenticated", "401", "403", "Please run X login"
2. STOP the current task
3. Return a `checkpoint:human-action` with exact auth steps
4. In SUMMARY.md, document auth gates as normal flow, not deviations

View File

@@ -0,0 +1,89 @@
# Planner Anti-Patterns and Specificity Examples
> Reference file for gsd-planner agent. Loaded on-demand via `@` reference.
> For sub-200K context windows, this content is stripped from the agent prompt and available here for on-demand loading.
## Checkpoint Anti-Patterns
### Bad — Asking human to automate
```xml
<task type="checkpoint:human-action">
<action>Deploy to Vercel</action>
<instructions>Visit vercel.com, import repo, click deploy...</instructions>
</task>
```
**Why bad:** Vercel has a CLI. Claude should run `vercel --yes`. Never ask the user to do what Claude can automate via CLI/API.
### Bad — Too many checkpoints
```xml
<task type="auto">Create schema</task>
<task type="checkpoint:human-verify">Check schema</task>
<task type="auto">Create API</task>
<task type="checkpoint:human-verify">Check API</task>
```
**Why bad:** Verification fatigue. Users should not be asked to verify every small step. Combine into one checkpoint at the end of meaningful work.
### Good — Single verification checkpoint
```xml
<task type="auto">Create schema</task>
<task type="auto">Create API</task>
<task type="auto">Create UI</task>
<task type="checkpoint:human-verify">
<what-built>Complete auth flow (schema + API + UI)</what-built>
<how-to-verify>Test full flow: register, login, access protected page</how-to-verify>
</task>
```
### Bad — Mixing checkpoints with implementation
A plan should not interleave multiple checkpoint types with implementation tasks. Checkpoints belong at natural verification boundaries, not scattered throughout.
## Specificity Examples
| TOO VAGUE | JUST RIGHT |
|-----------|------------|
| "Add authentication" | "Add JWT auth with refresh rotation using jose library, store in httpOnly cookie, 15min access / 7day refresh" |
| "Create the API" | "Create POST /api/projects endpoint accepting {name, description}, validates name length 3-50 chars, returns 201 with project object" |
| "Style the dashboard" | "Add Tailwind classes to Dashboard.tsx: grid layout (3 cols on lg, 1 on mobile), card shadows, hover states on action buttons" |
| "Handle errors" | "Wrap API calls in try/catch, return {error: string} on 4xx/5xx, show toast via sonner on client" |
| "Set up the database" | "Add User and Project models to schema.prisma with UUID ids, email unique constraint, createdAt/updatedAt timestamps, run prisma db push" |
**Specificity test:** Could a different Claude instance execute the task without asking clarifying questions? If not, add more detail.
## Context Section Anti-Patterns
### Bad — Reflexive SUMMARY chaining
```markdown
<context>
@.planning/phases/01-foundation/01-01-SUMMARY.md
@.planning/phases/01-foundation/01-02-SUMMARY.md <!-- Does Plan 02 actually need Plan 01's output? -->
@.planning/phases/01-foundation/01-03-SUMMARY.md <!-- Chain grows, context bloats -->
</context>
```
**Why bad:** Plans are often independent. Reflexive chaining (02 refs 01, 03 refs 02...) wastes context. Only reference prior SUMMARY files when the plan genuinely uses types/exports from that prior plan or a decision from it affects the current plan.
### Good — Selective context
```markdown
<context>
@.planning/PROJECT.md
@.planning/STATE.md
@.planning/phases/01-foundation/01-01-SUMMARY.md <!-- Uses User type defined in Plan 01 -->
</context>
```
## Scope Reduction Anti-Patterns
**Prohibited language in task actions:**
- "v1", "v2", "simplified version", "static for now", "hardcoded for now"
- "future enhancement", "placeholder", "basic version", "minimal implementation"
- "will be wired later", "dynamic in future phase", "skip for now"
If a decision from CONTEXT.md says "display cost calculated from billing table in impulses", the plan must deliver exactly that. Not "static label /min" as a "v1". If the phase is too complex, recommend a phase split instead of silently reducing scope.

View File

@@ -0,0 +1,73 @@
# Planner Source Audit & Authority Limits
Reference for `agents/gsd-planner.md` — extended rules for multi-source coverage audits and planner authority constraints.
## Multi-Source Coverage Audit Format
Before finalizing plans, produce a **source audit** covering ALL four artifact types:
```
SOURCE | ID | Feature/Requirement | Plan | Status | Notes
--------- | ------- | ---------------------------- | ----- | --------- | ------
GOAL | — | {phase goal from ROADMAP.md} | 01-03 | COVERED |
REQ | REQ-14 | OAuth login with Google + GH | 02 | COVERED |
REQ | REQ-22 | Email verification flow | 03 | COVERED |
RESEARCH | — | Rate limiting on auth routes | 01 | COVERED |
RESEARCH | — | Refresh token rotation | NONE | ⚠ MISSING | No plan covers this
CONTEXT | D-01 | Use jose library for JWT | 02 | COVERED |
CONTEXT | D-04 | 15min access / 7day refresh | 02 | COVERED |
```
### Four Source Types
1. **GOAL** — The `goal:` field from ROADMAP.md for this phase. The primary success condition.
2. **REQ** — Every REQ-ID in `phase_req_ids`. Cross-reference REQUIREMENTS.md for descriptions.
3. **RESEARCH** — Technical approaches, discovered constraints, and features identified in RESEARCH.md. Exclude items explicitly marked "out of scope" or "future work" by the researcher.
4. **CONTEXT** — Every D-XX decision from CONTEXT.md `<decisions>` section.
### What is NOT a Gap
Do not flag these as MISSING:
- Items in `## Deferred Ideas` in CONTEXT.md — developer chose to defer these
- Items scoped to a different phase via `phase_req_ids` — not assigned to this phase
- Items in RESEARCH.md explicitly marked "out of scope" or "future work" by the researcher
### Handling MISSING Items
If ANY row is `⚠ MISSING`, do NOT finalize the plan set silently. Return to the orchestrator:
```
## ⚠ Source Audit: Unplanned Items Found
The following items from source artifacts have no corresponding plan:
1. **{SOURCE}: {item description}** (from {artifact file}, section "{section}")
- {why this was identified as required}
Options:
A) Add a plan to cover this item
B) Split phase: move to a sub-phase
C) Defer explicitly: add to backlog with developer confirmation
→ Awaiting developer decision before finalizing plan set.
```
If ALL rows are COVERED → return `## PLANNING COMPLETE` as normal.
---
## Authority Limits — Constraint Examples
The planner's only legitimate reasons to split or flag a feature are **constraints**, not judgments about difficulty:
**Valid (constraints):**
- ✓ "This task touches 9 files and would consume ~45% context — split into two tasks"
- ✓ "No API key or endpoint is defined in any source artifact — need developer input"
- ✓ "This feature depends on the auth system built in Phase 03, which is not yet complete"
**Invalid (difficulty judgments):**
- ✗ "This is complex and would be difficult to implement correctly"
- ✗ "Integrating with an external service could take a long time"
- ✗ "This is a challenging feature that might be better left to a future phase"
If a feature has none of the three legitimate constraints (context cost, missing information, dependency conflict), it gets planned. Period.

View File

@@ -11,7 +11,14 @@
"security_asvs_level": 1,
"security_block_on": "high",
"discuss_mode": "discuss",
"research_before_questions": false
"research_before_questions": false,
"code_review_command": null,
"plan_bounce": false,
"plan_bounce_script": null,
"plan_bounce_passes": 2,
"cross_ai_execution": false,
"cross_ai_command": "",
"cross_ai_timeout": 300
},
"planning": {
"commit_docs": true,

View File

@@ -57,6 +57,8 @@ Parse `$ARGUMENTS` before loading any context:
- First positional token → `PHASE_ARG`
- Optional `--wave N` → `WAVE_FILTER`
- Optional `--gaps-only` keeps its current meaning
- Optional `--cross-ai` → `CROSS_AI_FORCE=true` (force all plans through cross-AI execution)
- Optional `--no-cross-ai` → `CROSS_AI_DISABLED=true` (disable cross-AI for this run, overrides config and frontmatter)
If `--wave` is absent, preserve the current behavior of executing all incomplete waves in the phase.
</step>
@@ -93,6 +95,12 @@ When `CONTEXT_WINDOW >= 500000` (1M-class models), subagent prompts include rich
- Verifier agents receive all PLAN.md, SUMMARY.md, CONTEXT.md files plus REQUIREMENTS.md
- This enables cross-phase awareness and history-aware verification
When `CONTEXT_WINDOW < 200000` (sub-200K models), subagent prompts are thinned to reduce static overhead:
- Executor agents omit extended deviation rule examples and checkpoint examples from inline prompt — load on-demand via @~/.claude/get-shit-done/references/executor-examples.md
- Planner agents omit extended anti-pattern lists and specificity examples from inline prompt — load on-demand via @~/.claude/get-shit-done/references/planner-antipatterns.md
- Core rules and decision logic remain inline; only verbose examples and edge-case lists are extracted
- This reduces executor static overhead by ~40% while preserving behavioral correctness
**If `phase_found` is false:** Error — phase directory not found.
**If `plan_count` is 0:** Error — no plans found in phase.
**If `state_exists` is false but `.planning/` exists:** Offer reconstruct or continue.
@@ -243,6 +251,77 @@ Report:
```
</step>
<step name="cross_ai_delegation">
**Optional step 2.5 — Delegate plans to an external AI runtime.**
This step runs after plan discovery and before normal wave execution. It identifies plans
that should be delegated to an external AI command and executes them via stdin-based prompt
delivery. Plans handled here are removed from the execute_waves plan list so the normal
executor skips them.
**Activation logic:**
1. If `CROSS_AI_DISABLED` is true (`--no-cross-ai` flag): skip this step entirely.
2. If `CROSS_AI_FORCE` is true (`--cross-ai` flag): mark ALL incomplete plans for cross-AI execution.
3. Otherwise: check each plan's frontmatter for `cross_ai: true` AND verify config
`workflow.cross_ai_execution` is `true`. Plans matching both conditions are marked for cross-AI.
```bash
CROSS_AI_ENABLED=$(node "$HOME/.claude/get-shit-done/bin/gsd-tools.cjs" config-get workflow.cross_ai_execution --default false 2>/dev/null)
CROSS_AI_CMD=$(node "$HOME/.claude/get-shit-done/bin/gsd-tools.cjs" config-get workflow.cross_ai_command --default "" 2>/dev/null)
CROSS_AI_TIMEOUT=$(node "$HOME/.claude/get-shit-done/bin/gsd-tools.cjs" config-get workflow.cross_ai_timeout --default 300 2>/dev/null)
```
**If no plans are marked for cross-AI:** Skip to execute_waves.
**If plans are marked but `cross_ai_command` is empty:** Error — tell user to set
`workflow.cross_ai_command` via `gsd-tools.cjs config-set workflow.cross_ai_command "<command>"`.
**For each cross-AI plan (sequentially):**
1. **Construct the task prompt** from the plan file:
- Extract `<objective>` and `<tasks>` sections from the PLAN.md
- Append PROJECT.md context (project name, description, tech stack)
- Format as a self-contained execution prompt
2. **Check for dirty working tree before execution:**
```bash
if ! git diff --quiet HEAD 2>/dev/null; then
echo "WARNING: dirty working tree detected — the external AI command may produce uncommitted changes that conflict with existing modifications"
fi
```
3. **Run the external command** from the project root, writing the prompt to stdin.
Never shell-interpolate the prompt — always pipe via stdin to prevent injection:
```bash
echo "$TASK_PROMPT" | timeout "${CROSS_AI_TIMEOUT}s" ${CROSS_AI_CMD} > "$CANDIDATE_SUMMARY" 2>"$ERROR_LOG"
EXIT_CODE=$?
```
4. **Evaluate the result:**
**Success (exit 0 + valid summary):**
- Read `$CANDIDATE_SUMMARY` and validate it contains meaningful content
(not empty, has at least a heading and description — a valid SUMMARY.md structure)
- Write it as the plan's SUMMARY.md file
- Update STATE.md plan status to complete
- Update ROADMAP.md progress
- Mark plan as handled — skip it in execute_waves
**Failure (non-zero exit or invalid summary):**
- Display the error output and exit code
- Warn: "The external command may have left uncommitted changes or partial edits
in the working tree. Review `git status` and `git diff` before proceeding."
- Offer three choices:
- **retry** — run the same plan through cross-AI again
- **skip** — fall back to normal executor for this plan (re-add to execute_waves list)
- **abort** — stop execution entirely, preserve state for resume
5. **After all cross-AI plans processed:** Remove successfully handled plans from the
incomplete plan list so execute_waves skips them. Any skipped-to-fallback plans remain
in the list for normal executor processing.
</step>
<step name="execute_waves">
Execute each selected wave in sequence. Within a wave: parallel if `PARALLELIZATION=true`, sequential if `false`.
@@ -395,6 +474,7 @@ Execute each selected wave in sequence. Within a wave: parallel if `PARALLELIZAT
@~/.claude/get-shit-done/templates/summary.md
@~/.claude/get-shit-done/references/checkpoints.md
@~/.claude/get-shit-done/references/tdd.md
${CONTEXT_WINDOW < 200000 ? '' : '@~/.claude/get-shit-done/references/executor-examples.md'}
</execution_context>
<files_to_read>

View File

@@ -0,0 +1,232 @@
<purpose>
Extract decisions, lessons learned, patterns discovered, and surprises encountered from completed phase artifacts into a structured LEARNINGS.md file. Captures institutional knowledge that would otherwise be lost between phases.
</purpose>
<required_reading>
Read all files referenced by the invoking prompt's execution_context before starting.
</required_reading>
<objective>
Analyze completed phase artifacts (PLAN.md, SUMMARY.md, VERIFICATION.md, UAT.md, STATE.md) and extract structured learnings into 4 categories: decisions, lessons, patterns, and surprises. Each extracted item includes source attribution. The output is a LEARNINGS.md file with YAML frontmatter containing metadata about the extraction.
</objective>
<process>
<step name="initialize">
Parse arguments and load project state:
```bash
INIT=$(node "$HOME/.claude/get-shit-done/bin/gsd-tools.cjs" init phase-op "${PHASE_ARG}")
if [[ "$INIT" == @file:* ]]; then INIT=$(cat "${INIT#@file:}"); fi
```
Parse from init JSON: `phase_found`, `phase_dir`, `phase_number`, `phase_name`, `padded_phase`.
If phase not found, exit with error: "Phase {PHASE_ARG} not found."
</step>
<step name="collect_artifacts">
Read the phase artifacts. PLAN.md and SUMMARY.md are required; VERIFICATION.md, UAT.md, and STATE.md are optional.
**Required artifacts:**
- `${PHASE_DIR}/*-PLAN.md` — all plan files for the phase
- `${PHASE_DIR}/*-SUMMARY.md` — all summary files for the phase
If PLAN.md or SUMMARY.md files are not found or missing, exit with error: "Required artifacts missing. PLAN.md and SUMMARY.md are required for learning extraction."
**Optional artifacts (read if available, skip if not found):**
- `${PHASE_DIR}/*-VERIFICATION.md` — verification results
- `${PHASE_DIR}/*-UAT.md` — user acceptance test results
- `.planning/STATE.md` — project state with decisions and blockers
Track which optional artifacts are missing for the `missing_artifacts` frontmatter field.
</step>
<step name="extract_learnings">
Analyze all collected artifacts and extract learnings into 4 categories:
### 1. Decisions
Technical and architectural decisions made during the phase. Look for:
- Explicit decisions documented in PLAN.md or SUMMARY.md
- Technology choices and their rationale
- Trade-offs that were evaluated
- Design decisions recorded in STATE.md
Each decision entry must include:
- **What** was decided
- **Why** it was decided (rationale)
- **Source:** attribution to the artifact where the decision was found (e.g., "Source: 03-01-PLAN.md")
### 2. Lessons
Things learned during execution that were not known beforehand. Look for:
- Unexpected complexity in SUMMARY.md
- Issues discovered during verification in VERIFICATION.md
- Failed approaches documented in SUMMARY.md
- UAT feedback that revealed gaps
Each lesson entry must include:
- **What** was learned
- **Context** for the lesson
- **Source:** attribution to the originating artifact
### 3. Patterns
Reusable patterns, approaches, or techniques discovered. Look for:
- Successful implementation patterns in SUMMARY.md
- Testing patterns from VERIFICATION.md or UAT.md
- Workflow patterns that worked well
- Code organization patterns from PLAN.md
Each pattern entry must include:
- **Pattern** name/description
- **When to use** it
- **Source:** attribution to the originating artifact
### 4. Surprises
Unexpected findings, behaviors, or outcomes. Look for:
- Things that took longer or shorter than estimated
- Unexpected dependencies or interactions
- Edge cases not anticipated in planning
- Performance or behavior that differed from expectations
Each surprise entry must include:
- **What** was surprising
- **Impact** of the surprise
- **Source:** attribution to the originating artifact
</step>
<step name="capture_thought_integration">
If the `capture_thought` tool is available in the current session, capture each extracted learning as a thought with metadata:
```
capture_thought({
category: "decision" | "lesson" | "pattern" | "surprise",
phase: PHASE_NUMBER,
content: LEARNING_TEXT,
source: ARTIFACT_NAME
})
```
If `capture_thought` is not available (e.g., runtime does not support it), gracefully skip this step and continue. The LEARNINGS.md file is the primary output — capture_thought is a supplementary integration that provides a fallback for runtimes with thought capture support. The workflow must not fail or warn if capture_thought is unavailable.
</step>
<step name="write_learnings">
Write the LEARNINGS.md file to the phase directory. If a previous LEARNINGS.md exists, overwrite it (replace the file entirely).
Output path: `${PHASE_DIR}/${PADDED_PHASE}-LEARNINGS.md`
The file must have YAML frontmatter with these fields:
```yaml
---
phase: {PHASE_NUMBER}
phase_name: "{PHASE_NAME}"
project: "{PROJECT_NAME}"
generated: "{ISO_DATE}"
counts:
decisions: {N}
lessons: {N}
patterns: {N}
surprises: {N}
missing_artifacts:
- "{ARTIFACT_NAME}"
---
```
The body follows this structure:
```markdown
# Phase {PHASE_NUMBER} Learnings: {PHASE_NAME}
## Decisions
### {Decision Title}
{What was decided}
**Rationale:** {Why}
**Source:** {artifact file}
---
## Lessons
### {Lesson Title}
{What was learned}
**Context:** {context}
**Source:** {artifact file}
---
## Patterns
### {Pattern Name}
{Description}
**When to use:** {applicability}
**Source:** {artifact file}
---
## Surprises
### {Surprise Title}
{What was surprising}
**Impact:** {impact description}
**Source:** {artifact file}
```
</step>
<step name="update_state">
Update STATE.md to reflect the learning extraction:
```bash
node "$HOME/.claude/get-shit-done/bin/gsd-tools.cjs" state update "Last Activity" "$(date +%Y-%m-%d)"
```
</step>
<step name="report">
```
---------------------------------------------------------------
## Learnings Extracted: Phase {X} — {Name}
Decisions: {N}
Lessons: {N}
Patterns: {N}
Surprises: {N}
Total: {N}
Output: {PHASE_DIR}/{PADDED_PHASE}-LEARNINGS.md
Missing artifacts: {list or "none"}
Next steps:
- Review extracted learnings for accuracy
- /gsd-progress — see overall project state
- /gsd-execute-phase {next} — continue to next phase
---------------------------------------------------------------
```
</step>
</process>
<success_criteria>
- [ ] Phase artifacts located and read successfully
- [ ] All 4 categories extracted: decisions, lessons, patterns, surprises
- [ ] Each extracted item has source attribution
- [ ] LEARNINGS.md written with correct YAML frontmatter
- [ ] Missing optional artifacts tracked in frontmatter
- [ ] capture_thought integration attempted if tool available
- [ ] STATE.md updated with extraction activity
- [ ] User receives summary report
</success_criteria>
<critical_rules>
- PLAN.md and SUMMARY.md are required — exit with clear error if missing
- VERIFICATION.md, UAT.md, and STATE.md are optional — extract from them if present, skip gracefully if not found
- Every extracted learning must have source attribution back to the originating artifact
- Running extract-learnings twice on the same phase must overwrite (replace) the previous LEARNINGS.md, not append
- Do not fabricate learnings — only extract what is explicitly documented in artifacts
- If capture_thought is unavailable, the workflow must not fail — graceful degradation to file-only output
- LEARNINGS.md frontmatter must include counts for all 4 categories and list any missing_artifacts
</critical_rules>

View File

@@ -46,7 +46,7 @@ Parse JSON for: `researcher_model`, `planner_model`, `checker_model`, `research_
## 2. Parse and Normalize Arguments
Extract from $ARGUMENTS: phase number (integer or decimal like `2.1`), flags (`--research`, `--skip-research`, `--gaps`, `--skip-verify`, `--skip-ui`, `--prd <filepath>`, `--reviews`, `--text`).
Extract from $ARGUMENTS: phase number (integer or decimal like `2.1`), flags (`--research`, `--skip-research`, `--gaps`, `--skip-verify`, `--skip-ui`, `--prd <filepath>`, `--reviews`, `--text`, `--bounce`, `--skip-bounce`).
Set `TEXT_MODE=true` if `--text` is present in $ARGUMENTS OR `text_mode` from init JSON is `true`. When `TEXT_MODE` is active, replace every `AskUserQuestion` call with a plain-text numbered list and ask the user to type their choice number. This is required for Claude Code remote sessions (`/rc` mode) where TUI menus don't work through the Claude App.
@@ -719,41 +719,70 @@ Task(
## 9. Handle Planner Return
- **`## PLANNING COMPLETE`:** Display plan count. If `--skip-verify` or `plan_checker_enabled` is false (from init): skip to step 13. Otherwise: step 10.
- **`## PHASE SPLIT RECOMMENDED`:** The planner determined the phase is too complex to implement all user decisions without simplifying them. Handle in step 9b.
- **`## PHASE SPLIT RECOMMENDED`:** The planner determined the phase exceeds the context budget for full-fidelity implementation of all source items. Handle in step 9b.
- **`## ⚠ Source Audit: Unplanned Items Found`:** The planner's multi-source coverage audit found items from REQUIREMENTS.md, RESEARCH.md, ROADMAP goal, or CONTEXT.md decisions that are not covered by any plan. Handle in step 9c.
- **`## CHECKPOINT REACHED`:** Present to user, get response, spawn continuation (step 12)
- **`## PLANNING INCONCLUSIVE`:** Show attempts, offer: Add context / Retry / Manual
## 9b. Handle Phase Split Recommendation
When the planner returns `## PHASE SPLIT RECOMMENDED`, it means the phase has too many decisions to implement at full fidelity within the plan budget. The planner proposes groupings.
When the planner returns `## PHASE SPLIT RECOMMENDED`, it means the phase's source items exceed the context budget for full-fidelity implementation. The planner proposes groupings.
**Extract from planner return:**
- Proposed sub-phases (e.g., "17a: processing core (D-01 to D-19)", "17b: billing + config UX (D-20 to D-27)")
- Which D-XX decisions go in each sub-phase
- Why the split is necessary (decision count, complexity estimate)
- Which source items (REQ-IDs, D-XX decisions, RESEARCH items) go in each sub-phase
- Why the split is necessary (context cost estimate, file count)
**Present to user:**
```
## Phase {X} is too complex for full-fidelity implementation
## Phase {X} exceeds context budget for full-fidelity implementation
The planner found {N} decisions that cannot all be implemented without
simplifying some. Instead of reducing your decisions, we recommend splitting:
The planner found {N} source items that exceed the context budget when
planned at full fidelity. Instead of reducing scope, we recommend splitting:
**Option 1: Split into sub-phases**
- Phase {X}a: {name} — {D-XX to D-YY} ({N} decisions)
- Phase {X}b: {name} — {D-XX to D-YY} ({M} decisions)
- Phase {X}a: {name} — {items} ({N} source items, ~{P}% context)
- Phase {X}b: {name} — {items} ({M} source items, ~{Q}% context)
**Option 2: Proceed anyway** (planner will attempt all, quality may degrade)
**Option 2: Proceed anyway** (planner will attempt all, quality may degrade past 50% context)
**Option 3: Prioritize** — you choose which decisions to implement now,
**Option 3: Prioritize** — you choose which items to implement now,
rest become a follow-up phase
```
Use AskUserQuestion with these 3 options.
**If "Split":** Use `/gsd-insert-phase` to create the sub-phases, then replan each.
**If "Proceed":** Return to planner with instruction to attempt all decisions at full fidelity, accepting more plans/tasks.
**If "Prioritize":** Use AskUserQuestion (multiSelect) to let user pick which D-XX are "now" vs "later". Create CONTEXT.md for each sub-phase with the selected decisions.
**If "Proceed":** Return to planner with instruction to attempt all items at full fidelity, accepting more plans/tasks.
**If "Prioritize":** Use AskUserQuestion (multiSelect) to let user pick which items are "now" vs "later". Create CONTEXT.md for each sub-phase with the selected items.
## 9c. Handle Source Audit Gaps
When the planner returns `## ⚠ Source Audit: Unplanned Items Found`, it means items from REQUIREMENTS.md, RESEARCH.md, ROADMAP goal, or CONTEXT.md decisions have no corresponding plan.
**Extract from planner return:**
- Each unplanned item with its source artifact and section
- The planner's suggested options (A: add plan, B: split phase, C: defer with confirmation)
**Present each gap to user.** For each unplanned item:
```
## ⚠ Unplanned: {item description}
Source: {RESEARCH.md / REQUIREMENTS.md / ROADMAP goal / CONTEXT.md}
Details: {why the planner flagged this}
Options:
1. Add a plan to cover this item (recommended)
2. Split phase — move to a sub-phase with related items
3. Defer — add to backlog (developer confirms this is intentional)
```
Use AskUserQuestion for each gap (or batch if multiple gaps).
**If "Add plan":** Return to planner (step 8) with instruction to add plans covering the missing items, preserving existing plans.
**If "Split":** Use `/gsd-insert-phase` for overflow items, then replan.
**If "Defer":** Record in CONTEXT.md `## Deferred Ideas` with developer's confirmation. Proceed to step 10.
## 10. Spawn gsd-plan-checker Agent
@@ -901,6 +930,77 @@ Display: `Max iterations reached. {N} issues remain:` + issue list
Offer: 1) Force proceed, 2) Provide guidance and retry, 3) Abandon
## 12.5. Plan Bounce (Optional External Refinement)
**Skip if:** `--skip-bounce` flag, `--gaps` flag, or bounce is not activated.
**Activation:** Bounce runs when `--bounce` flag is present OR `workflow.plan_bounce` config is `true`. The `--skip-bounce` flag always wins (disables bounce even if config enables it). The `--gaps` flag also disables bounce (gap-closure mode should not modify plans externally).
**Prerequisites:** `workflow.plan_bounce_script` must be set to a valid script path. If bounce is activated but no script is configured, display warning and skip:
```
⚠ Plan bounce activated but no script configured.
Set workflow.plan_bounce_script to the path of your refinement script.
Skipping bounce step.
```
**Read pass count:**
```bash
BOUNCE_PASSES=$(node "$HOME/.claude/get-shit-done/bin/gsd-tools.cjs" config-get workflow.plan_bounce_passes --default 2)
BOUNCE_SCRIPT=$(node "$HOME/.claude/get-shit-done/bin/gsd-tools.cjs" config-get workflow.plan_bounce_script)
```
Display banner:
```
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
GSD ► BOUNCING PLANS (External Refinement)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Script: ${BOUNCE_SCRIPT}
Max passes: ${BOUNCE_PASSES}
```
**For each PLAN.md file in the phase directory:**
1. **Backup:** Copy `*-PLAN.md` to `*-PLAN.pre-bounce.md`
```bash
cp "${PLAN_FILE}" "${PLAN_FILE%.md}.pre-bounce.md"
```
2. **Invoke bounce script:**
```bash
"${BOUNCE_SCRIPT}" "${PLAN_FILE}" "${BOUNCE_PASSES}"
```
3. **Validate bounced plan — YAML frontmatter integrity:**
After the script returns, check that the bounced file still has valid YAML frontmatter (opening and closing `---` delimiters with parseable content between them). If the bounced plan breaks YAML frontmatter validation, restore the original from the pre-bounce.md backup and continue to the next plan:
```
⚠ Bounced plan ${PLAN_FILE} has broken YAML frontmatter — restoring original from pre-bounce backup.
```
4. **Handle script failure:** If the bounce script exits non-zero, restore the original plan from the pre-bounce.md backup and continue to the next plan:
```
⚠ Bounce script failed for ${PLAN_FILE} (exit code ${EXIT_CODE}) — restoring original from pre-bounce backup.
```
**After all plans are bounced:**
5. **Re-run plan checker on bounced plans:** Spawn gsd-plan-checker (same as step 10) on all modified plans. If a bounced plan fails the checker, restore original from its pre-bounce.md backup:
```
⚠ Bounced plan ${PLAN_FILE} failed checker validation — restoring original from pre-bounce backup.
```
6. **Commit surviving bounced plans:** If at least one plan survived both the frontmatter validation and the checker re-run, commit the changes:
```bash
node "$HOME/.claude/get-shit-done/bin/gsd-tools.cjs" commit "refactor(${padded_phase}): bounce plans through external refinement" --files "${PHASE_DIR}/*-PLAN.md"
```
Display summary:
```
Plan bounce complete: {survived}/{total} plans refined
```
**Clean up:** Remove all `*-PLAN.pre-bounce.md` backup files after the bounce step completes (whether plans survived or were restored).
## 13. Requirements Coverage Gate
After plans pass the checker (or checker is skipped), verify that all phase requirements are covered by at least one plan.

View File

@@ -56,6 +56,9 @@ Determine which CLI to skip based on the current runtime environment:
if [ "$ANTIGRAVITY_AGENT" = "1" ]; then
# Antigravity is a separate client — all CLIs are external, skip none
SELF_CLI="none"
elif [ -n "$CURSOR_SESSION_ID" ]; then
# Running inside Cursor agent — skip cursor for independence
SELF_CLI="cursor"
elif [ -n "$CLAUDE_CODE_ENTRYPOINT" ]; then
# Running inside Claude Code CLI — skip claude for independence
SELF_CLI="claude"
@@ -275,6 +278,18 @@ plans_reviewed: [{list of PLAN.md files}]
---
## Qwen Review
{qwen review content}
---
## Cursor Review
{cursor review content}
---
## Consensus Summary
{synthesize common concerns across all reviewers}

View File

@@ -159,6 +159,68 @@ Report: "PR #{number} created: {url}"
</step>
<step name="optional_review">
**External code review command (automated sub-step):**
Before prompting the user, check if an external review command is configured:
```bash
REVIEW_CMD=$(node "$HOME/.claude/get-shit-done/bin/gsd-tools.cjs" config-get workflow.code_review_command --default "" 2>/dev/null)
```
If `REVIEW_CMD` is non-empty and not `"null"`, run the external review:
1. **Generate diff and stats:**
```bash
DIFF=$(git diff ${BASE_BRANCH}...HEAD)
DIFF_STATS=$(git diff --stat ${BASE_BRANCH}...HEAD)
```
2. **Load phase context from STATE.md:**
```bash
STATE_STATUS=$(node "$HOME/.claude/get-shit-done/bin/gsd-tools.cjs" state load 2>/dev/null | head -20)
```
3. **Build review prompt and pipe to command via stdin:**
Construct a review prompt containing the diff, diff stats, and phase context, then pipe it to the configured command:
```bash
REVIEW_PROMPT="You are reviewing a pull request.\n\nDiff stats:\n${DIFF_STATS}\n\nPhase context:\n${STATE_STATUS}\n\nFull diff:\n${DIFF}\n\nRespond with JSON: { \"verdict\": \"APPROVED\" or \"REVISE\", \"confidence\": 0-100, \"summary\": \"...\", \"issues\": [{\"severity\": \"...\", \"file\": \"...\", \"line_range\": \"...\", \"description\": \"...\", \"suggestion\": \"...\"}] }"
REVIEW_OUTPUT=$(echo "${REVIEW_PROMPT}" | timeout 120 ${REVIEW_CMD} 2>/tmp/gsd-review-stderr.log)
REVIEW_EXIT=$?
```
4. **Handle timeout (120s) and failure:**
If `REVIEW_EXIT` is non-zero or the command times out:
```bash
if [ $REVIEW_EXIT -ne 0 ]; then
REVIEW_STDERR=$(cat /tmp/gsd-review-stderr.log 2>/dev/null)
echo "WARNING: External review command failed (exit ${REVIEW_EXIT}). stderr: ${REVIEW_STDERR}"
echo "Continuing with manual review flow..."
fi
```
On failure, warn with stderr output and fall through to the manual review flow below.
5. **Parse JSON result:**
If the command succeeded, parse the JSON output and report the verdict:
```bash
# Parse verdict and summary from REVIEW_OUTPUT JSON
VERDICT=$(echo "${REVIEW_OUTPUT}" | node -e "
let d=''; process.stdin.on('data',c=>d+=c); process.stdin.on('end',()=>{
try { const r=JSON.parse(d); console.log(r.verdict); }
catch(e) { console.log('INVALID_JSON'); }
});
")
```
- If `verdict` is `"APPROVED"`: report approval with confidence and summary.
- If `verdict` is `"REVISE"`: report issues found, list each issue with severity, file, line_range, description, and suggestion.
- If JSON is invalid (`INVALID_JSON`): warn "External review returned invalid JSON" with stderr and continue.
Regardless of the external review result, fall through to the manual review options below.
---
**Manual review options:**
Ask if user wants to trigger a code review:

View File

@@ -15,6 +15,7 @@ import { GSD } from './index.js';
import { CLITransport } from './cli-transport.js';
import { WSTransport } from './ws-transport.js';
import { InitRunner } from './init-runner.js';
import { validateWorkstreamName } from './workstream-utils.js';
// ─── Parsed CLI args ─────────────────────────────────────────────────────────
@@ -29,6 +30,8 @@ export interface ParsedCliArgs {
wsPort: number | undefined;
model: string | undefined;
maxBudget: number | undefined;
/** Workstream name for multi-workstream projects. Routes .planning/ to .planning/workstreams/<name>/. */
ws: string | undefined;
help: boolean;
version: boolean;
}
@@ -43,6 +46,7 @@ export function parseCliArgs(argv: string[]): ParsedCliArgs {
options: {
'project-dir': { type: 'string', default: process.cwd() },
'ws-port': { type: 'string' },
ws: { type: 'string' },
model: { type: 'string' },
'max-budget': { type: 'string' },
init: { type: 'string' },
@@ -69,6 +73,7 @@ export function parseCliArgs(argv: string[]): ParsedCliArgs {
wsPort: values['ws-port'] ? Number(values['ws-port']) : undefined,
model: values.model as string | undefined,
maxBudget: values['max-budget'] ? Number(values['max-budget']) : undefined,
ws: values.ws as string | undefined,
help: values.help as boolean,
version: values.version as boolean,
};
@@ -92,6 +97,7 @@ Options:
--init <input> Bootstrap from a PRD before running (auto only)
Accepts @path/to/prd.md or "description text"
--project-dir <dir> Project directory (default: cwd)
--ws <name> Route .planning/ to .planning/workstreams/<name>/
--ws-port <port> Enable WebSocket transport on <port>
--model <model> Override LLM model
--max-budget <n> Max budget per step in USD
@@ -194,6 +200,13 @@ export async function main(argv: string[] = process.argv.slice(2)): Promise<void
return;
}
// Validate --ws flag if provided
if (args.ws !== undefined && !validateWorkstreamName(args.ws)) {
console.error(`Error: Invalid workstream name "${args.ws}". Use alphanumeric, hyphens, underscores, or dots only.`);
process.exitCode = 1;
return;
}
if (args.command !== 'run' && args.command !== 'init' && args.command !== 'auto') {
console.error('Error: Expected "gsd-sdk run <prompt>", "gsd-sdk auto", or "gsd-sdk init [input]"');
console.error(USAGE);
@@ -226,6 +239,7 @@ export async function main(argv: string[] = process.argv.slice(2)): Promise<void
projectDir: args.projectDir,
model: args.model,
maxBudgetUsd: args.maxBudget,
workstream: args.ws,
});
// Wire CLI transport
@@ -296,6 +310,7 @@ export async function main(argv: string[] = process.argv.slice(2)): Promise<void
model: args.model,
maxBudgetUsd: args.maxBudget,
autoMode: true,
workstream: args.ws,
});
// Wire CLI transport (always active)
@@ -384,6 +399,7 @@ export async function main(argv: string[] = process.argv.slice(2)): Promise<void
projectDir: args.projectDir,
model: args.model,
maxBudgetUsd: args.maxBudget,
workstream: args.ws,
});
// Wire CLI transport (always active)

View File

@@ -7,6 +7,7 @@
import { readFile } from 'node:fs/promises';
import { join } from 'node:path';
import { relPlanningPath } from './workstream-utils.js';
// ─── Types ───────────────────────────────────────────────────────────────────
@@ -99,15 +100,25 @@ export const CONFIG_DEFAULTS: GSDConfig = {
* Returns full defaults when file is missing or empty.
* Throws on malformed JSON with a helpful error message.
*/
export async function loadConfig(projectDir: string): Promise<GSDConfig> {
const configPath = join(projectDir, '.planning', 'config.json');
export async function loadConfig(projectDir: string, workstream?: string): Promise<GSDConfig> {
const configPath = join(projectDir, relPlanningPath(workstream), 'config.json');
const rootConfigPath = join(projectDir, '.planning', 'config.json');
let raw: string;
try {
raw = await readFile(configPath, 'utf-8');
} catch {
// File missing — normal for new projects
return structuredClone(CONFIG_DEFAULTS);
// If workstream config missing, fall back to root config
if (workstream) {
try {
raw = await readFile(rootConfigPath, 'utf-8');
} catch {
return structuredClone(CONFIG_DEFAULTS);
}
} else {
// File missing — normal for new projects
return structuredClone(CONFIG_DEFAULTS);
}
}
const trimmed = raw.trim();

View File

@@ -25,6 +25,7 @@ import {
DEFAULT_TRUNCATION_OPTIONS,
type TruncationOptions,
} from './context-truncation.js';
import { relPlanningPath } from './workstream-utils.js';
// ─── File manifest per phase ─────────────────────────────────────────────────
@@ -77,8 +78,8 @@ export class ContextEngine {
private readonly logger?: GSDLogger;
private readonly truncation: TruncationOptions;
constructor(projectDir: string, logger?: GSDLogger, truncation?: Partial<TruncationOptions>) {
this.planningDir = join(projectDir, '.planning');
constructor(projectDir: string, logger?: GSDLogger, truncation?: Partial<TruncationOptions>, workstream?: string) {
this.planningDir = join(projectDir, relPlanningPath(workstream));
this.logger = logger;
this.truncation = { ...DEFAULT_TRUNCATION_OPTIONS, ...truncation };
}

View File

@@ -39,16 +39,19 @@ export class GSDTools {
private readonly projectDir: string;
private readonly gsdToolsPath: string;
private readonly timeoutMs: number;
private readonly workstream?: string;
constructor(opts: {
projectDir: string;
gsdToolsPath?: string;
timeoutMs?: number;
workstream?: string;
}) {
this.projectDir = opts.projectDir;
this.gsdToolsPath =
opts.gsdToolsPath ?? resolveGsdToolsPath(opts.projectDir);
this.timeoutMs = opts.timeoutMs ?? DEFAULT_TIMEOUT_MS;
this.workstream = opts.workstream;
}
// ─── Core exec ───────────────────────────────────────────────────────────
@@ -58,7 +61,8 @@ export class GSDTools {
* Handles the `@file:` prefix pattern for large results.
*/
async exec(command: string, args: string[] = []): Promise<unknown> {
const fullArgs = [this.gsdToolsPath, command, ...args];
const wsArgs = this.workstream ? ['--ws', this.workstream] : [];
const fullArgs = [this.gsdToolsPath, command, ...args, ...wsArgs];
return new Promise<unknown>((resolve, reject) => {
const child = execFile(
@@ -160,7 +164,8 @@ export class GSDTools {
* Use for commands like `config-set` that return plain text, not JSON.
*/
async execRaw(command: string, args: string[] = []): Promise<string> {
const fullArgs = [this.gsdToolsPath, command, ...args, '--raw'];
const wsArgs = this.workstream ? ['--ws', this.workstream] : [];
const fullArgs = [this.gsdToolsPath, command, ...args, ...wsArgs, '--raw'];
return new Promise<string>((resolve, reject) => {
const child = execFile(

View File

@@ -44,6 +44,7 @@ export class GSD {
private readonly defaultMaxBudgetUsd: number;
private readonly defaultMaxTurns: number;
private readonly autoMode: boolean;
private readonly workstream?: string;
readonly eventStream: GSDEventStream;
constructor(options: GSDOptions) {
@@ -54,6 +55,7 @@ export class GSD {
this.defaultMaxBudgetUsd = options.maxBudgetUsd ?? 5.0;
this.defaultMaxTurns = options.maxTurns ?? 50;
this.autoMode = options.autoMode ?? false;
this.workstream = options.workstream;
this.eventStream = new GSDEventStream();
}
@@ -75,7 +77,7 @@ export class GSD {
const plan = await parsePlanFile(absolutePlanPath);
// Load project config
const config = await loadConfig(this.projectDir);
const config = await loadConfig(this.projectDir, this.workstream);
// Try to load agent definition for tool restrictions
const agentDef = await this.loadAgentDefinition();
@@ -117,6 +119,7 @@ export class GSD {
return new GSDTools({
projectDir: this.projectDir,
gsdToolsPath: this.gsdToolsPath,
workstream: this.workstream,
});
}
@@ -133,8 +136,8 @@ export class GSD {
async runPhase(phaseNumber: string, options?: PhaseRunnerOptions): Promise<PhaseRunnerResult> {
const tools = this.createTools();
const promptFactory = new PromptFactory();
const contextEngine = new ContextEngine(this.projectDir);
const config = await loadConfig(this.projectDir);
const contextEngine = new ContextEngine(this.projectDir, undefined, undefined, this.workstream);
const config = await loadConfig(this.projectDir, this.workstream);
// Auto mode: force auto_advance on and skip_discuss off so self-discuss kicks in
if (this.autoMode) {
@@ -314,6 +317,9 @@ export { CLITransport } from './cli-transport.js';
export { WSTransport } from './ws-transport.js';
export type { WSTransportOptions } from './ws-transport.js';
// Workstream utilities
export { validateWorkstreamName, relPlanningPath } from './workstream-utils.js';
// Init workflow
export { InitRunner } from './init-runner.js';
export type { InitRunnerDeps } from './init-runner.js';

View File

@@ -207,6 +207,8 @@ export interface GSDOptions {
maxTurns?: number;
/** Enable auto mode: sets auto_advance=true, skip_discuss=false in workflow config. */
autoMode?: boolean;
/** Workstream name. Routes all .planning/ paths to .planning/workstreams/<name>/. */
workstream?: string;
}
// ─── S02: Event stream types ─────────────────────────────────────────────────

View File

@@ -0,0 +1,32 @@
/**
* Workstream utility functions for multi-workstream project support.
*
* When --ws <name> is provided, all .planning/ paths are routed to
* .planning/workstreams/<name>/ instead.
*/
import { join } from 'node:path';
/**
* Validate a workstream name.
* Allowed: alphanumeric, hyphens, underscores, dots.
* Disallowed: empty, spaces, slashes, special chars, path traversal.
*/
export function validateWorkstreamName(name: string): boolean {
if (!name || name.length === 0) return false;
// Only allow alphanumeric, hyphens, underscores, dots
// Must not be ".." or start with ".." (path traversal)
if (name === '..' || name.startsWith('../')) return false;
return /^[a-zA-Z0-9][a-zA-Z0-9._-]*$/.test(name);
}
/**
* Return the relative planning directory path.
*
* - Without workstream: `.planning`
* - With workstream: `.planning/workstreams/<name>`
*/
export function relPlanningPath(workstream?: string): string {
if (!workstream) return '.planning';
return join('.planning', 'workstreams', workstream);
}

285
sdk/src/ws-flag.test.ts Normal file
View File

@@ -0,0 +1,285 @@
/**
* Tests for --ws (workstream) flag support.
*
* Validates:
* - CLI parsing of --ws flag
* - Workstream name validation
* - GSDOptions.workstream propagation
* - GSDTools workstream-aware invocation
* - Config path resolution with workstream
* - ContextEngine workstream-aware planning dir
*/
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import { mkdir, writeFile, rm } from 'node:fs/promises';
import { join } from 'node:path';
import { tmpdir } from 'node:os';
// ─── Workstream name validation ─────────────────────────────────────────────
import { validateWorkstreamName } from './workstream-utils.js';
describe('validateWorkstreamName', () => {
it('accepts alphanumeric names', () => {
expect(validateWorkstreamName('frontend')).toBe(true);
expect(validateWorkstreamName('backend2')).toBe(true);
});
it('accepts names with hyphens', () => {
expect(validateWorkstreamName('my-feature')).toBe(true);
});
it('accepts names with underscores', () => {
expect(validateWorkstreamName('my_feature')).toBe(true);
});
it('accepts names with dots', () => {
expect(validateWorkstreamName('v1.0')).toBe(true);
});
it('rejects empty strings', () => {
expect(validateWorkstreamName('')).toBe(false);
});
it('rejects names with spaces', () => {
expect(validateWorkstreamName('my feature')).toBe(false);
});
it('rejects names with slashes', () => {
expect(validateWorkstreamName('my/feature')).toBe(false);
});
it('rejects names with special characters', () => {
expect(validateWorkstreamName('feat@ure')).toBe(false);
expect(validateWorkstreamName('feat!ure')).toBe(false);
expect(validateWorkstreamName('feat#ure')).toBe(false);
});
it('rejects path traversal attempts', () => {
expect(validateWorkstreamName('..')).toBe(false);
expect(validateWorkstreamName('../etc')).toBe(false);
});
});
// ─── relPlanningPath helper ─────────────────────────────────────────────────
import { relPlanningPath } from './workstream-utils.js';
describe('relPlanningPath', () => {
it('returns .planning/ in flat mode (no workstream)', () => {
expect(relPlanningPath()).toBe('.planning');
expect(relPlanningPath(undefined)).toBe('.planning');
});
it('returns .planning/workstreams/<name>/ with workstream', () => {
expect(relPlanningPath('frontend')).toBe('.planning/workstreams/frontend');
expect(relPlanningPath('api-v2')).toBe('.planning/workstreams/api-v2');
});
});
// ─── CLI --ws flag parsing ──────────────────────────────────────────────────
import { parseCliArgs } from './cli.js';
describe('parseCliArgs --ws flag', () => {
it('parses --ws flag', () => {
const result = parseCliArgs(['run', 'build auth', '--ws', 'frontend']);
expect(result.ws).toBe('frontend');
});
it('ws is undefined when not provided', () => {
const result = parseCliArgs(['run', 'build auth']);
expect(result.ws).toBeUndefined();
});
it('works with other flags', () => {
const result = parseCliArgs([
'run', 'build auth',
'--ws', 'backend',
'--model', 'claude-sonnet-4-6',
'--project-dir', '/tmp/test',
]);
expect(result.ws).toBe('backend');
expect(result.model).toBe('claude-sonnet-4-6');
expect(result.projectDir).toBe('/tmp/test');
});
});
// ─── GSDOptions.workstream ──────────────────────────────────────────────────
describe('GSDOptions.workstream', () => {
it('GSD class accepts workstream option', async () => {
// This is a compile-time check -- if the type is wrong, TS will fail
const { GSD } = await import('./index.js');
const gsd = new GSD({
projectDir: '/tmp/test-ws',
workstream: 'frontend',
});
// If we get here without a type error, the option is accepted
expect(gsd).toBeDefined();
});
});
// ─── GSDTools workstream injection ──────────────────────────────────────────
describe('GSDTools workstream injection', () => {
let tmpDir: string;
let fixtureDir: string;
beforeEach(async () => {
tmpDir = join(tmpdir(), `gsd-ws-test-${Date.now()}-${Math.random().toString(36).slice(2)}`);
fixtureDir = join(tmpDir, 'fixtures');
await mkdir(fixtureDir, { recursive: true });
await mkdir(join(tmpDir, '.planning'), { recursive: true });
});
afterEach(async () => {
await rm(tmpDir, { recursive: true, force: true });
});
async function createScript(name: string, code: string): Promise<string> {
const scriptPath = join(fixtureDir, name);
await writeFile(scriptPath, code, { mode: 0o755 });
return scriptPath;
}
it('passes --ws flag to gsd-tools.cjs when workstream is set', async () => {
const { GSDTools } = await import('./gsd-tools.js');
// Script echoes its arguments as JSON
const scriptPath = await createScript(
'echo-args.cjs',
'process.stdout.write(JSON.stringify(process.argv.slice(2)));',
);
const tools = new GSDTools({
projectDir: tmpDir,
gsdToolsPath: scriptPath,
workstream: 'frontend',
});
const result = await tools.exec('state', ['load']) as string[];
// Should contain --ws frontend in the arguments
expect(result).toContain('--ws');
expect(result).toContain('frontend');
});
it('does not pass --ws when workstream is undefined', async () => {
const { GSDTools } = await import('./gsd-tools.js');
const scriptPath = await createScript(
'echo-args-no-ws.cjs',
'process.stdout.write(JSON.stringify(process.argv.slice(2)));',
);
const tools = new GSDTools({
projectDir: tmpDir,
gsdToolsPath: scriptPath,
});
const result = await tools.exec('state', ['load']) as string[];
expect(result).not.toContain('--ws');
});
});
// ─── Config workstream-aware path ───────────────────────────────────────────
import { loadConfig } from './config.js';
describe('loadConfig with workstream', () => {
let tmpDir: string;
beforeEach(async () => {
tmpDir = join(tmpdir(), `gsd-config-ws-${Date.now()}-${Math.random().toString(36).slice(2)}`);
await mkdir(tmpDir, { recursive: true });
});
afterEach(async () => {
await rm(tmpDir, { recursive: true, force: true });
});
it('loads config from workstream path when workstream is provided', async () => {
const wsDir = join(tmpDir, '.planning', 'workstreams', 'frontend');
await mkdir(wsDir, { recursive: true });
await writeFile(
join(wsDir, 'config.json'),
JSON.stringify({ model_profile: 'performance' }),
);
const config = await loadConfig(tmpDir, 'frontend');
expect(config.model_profile).toBe('performance');
});
it('falls back to root config when workstream config is missing', async () => {
// Create root config but no workstream config
await mkdir(join(tmpDir, '.planning'), { recursive: true });
await writeFile(
join(tmpDir, '.planning', 'config.json'),
JSON.stringify({ model_profile: 'balanced' }),
);
const config = await loadConfig(tmpDir, 'frontend');
expect(config.model_profile).toBe('balanced');
});
it('loads from root .planning/ when workstream is undefined', async () => {
await mkdir(join(tmpDir, '.planning'), { recursive: true });
await writeFile(
join(tmpDir, '.planning', 'config.json'),
JSON.stringify({ model_profile: 'economy' }),
);
const config = await loadConfig(tmpDir);
expect(config.model_profile).toBe('economy');
});
});
// ─── ContextEngine workstream-aware planning dir ────────────────────────────
describe('ContextEngine with workstream', () => {
let tmpDir: string;
beforeEach(async () => {
tmpDir = join(tmpdir(), `gsd-ctx-ws-${Date.now()}-${Math.random().toString(36).slice(2)}`);
await mkdir(tmpDir, { recursive: true });
});
afterEach(async () => {
await rm(tmpDir, { recursive: true, force: true });
});
it('resolves files from workstream planning dir', async () => {
const { ContextEngine } = await import('./context-engine.js');
const { PhaseType } = await import('./types.js');
const wsDir = join(tmpDir, '.planning', 'workstreams', 'backend');
await mkdir(wsDir, { recursive: true });
await writeFile(join(wsDir, 'STATE.md'), '# State\nPhase: 01');
const engine = new ContextEngine(tmpDir, undefined, undefined, 'backend');
const files = await engine.resolveContextFiles(PhaseType.Execute);
expect(files.state).toContain('Phase: 01');
});
it('resolves files from root .planning/ without workstream', async () => {
const { ContextEngine } = await import('./context-engine.js');
const { PhaseType } = await import('./types.js');
await mkdir(join(tmpDir, '.planning'), { recursive: true });
await writeFile(join(tmpDir, '.planning', 'STATE.md'), '# State\nPhase: 02');
const engine = new ContextEngine(tmpDir);
const files = await engine.resolveContextFiles(PhaseType.Execute);
expect(files.state).toContain('Phase: 02');
});
});

View File

@@ -0,0 +1,140 @@
/**
* Tests for code_review_command hook in ship workflow (#1876)
*
* Validates that the external code review command integration is properly
* wired into config, templates, and the ship workflow.
*/
const { describe, test, beforeEach, afterEach } = require('node:test');
const assert = require('node:assert/strict');
const fs = require('fs');
const path = require('path');
const { createTempProject, cleanup, runGsdTools } = require('./helpers.cjs');
const CONFIG_CJS_PATH = path.join(__dirname, '..', 'get-shit-done', 'bin', 'lib', 'config.cjs');
const SHIP_MD_PATH = path.join(__dirname, '..', 'get-shit-done', 'workflows', 'ship.md');
const CONFIG_TEMPLATE_PATH = path.join(__dirname, '..', 'get-shit-done', 'templates', 'config.json');
describe('code_review_command config key', () => {
test('workflow.code_review_command is in VALID_CONFIG_KEYS', () => {
const { VALID_CONFIG_KEYS } = require(CONFIG_CJS_PATH);
assert.ok(
VALID_CONFIG_KEYS.has('workflow.code_review_command'),
'workflow.code_review_command must be in VALID_CONFIG_KEYS'
);
});
test('config-set accepts workflow.code_review_command', () => {
const tmpDir = createTempProject();
try {
// Create config.json first
fs.mkdirSync(path.join(tmpDir, '.planning'), { recursive: true });
fs.writeFileSync(
path.join(tmpDir, '.planning', 'config.json'),
JSON.stringify({ workflow: {} }, null, 2)
);
const result = runGsdTools(
['config-set', 'workflow.code_review_command', 'my-review-tool --review'],
tmpDir,
{ HOME: tmpDir }
);
assert.ok(result.success, 'config-set should succeed');
const parsed = JSON.parse(result.output);
assert.strictEqual(parsed.updated, true);
assert.strictEqual(parsed.key, 'workflow.code_review_command');
assert.strictEqual(parsed.value, 'my-review-tool --review');
} finally {
cleanup(tmpDir);
}
});
});
describe('config template', () => {
test('config.json template has code_review_command under workflow section', () => {
const template = JSON.parse(fs.readFileSync(CONFIG_TEMPLATE_PATH, 'utf-8'));
assert.ok(template.workflow, 'template must have workflow section');
assert.ok(
'code_review_command' in template.workflow,
'workflow section must contain code_review_command key'
);
assert.strictEqual(
template.workflow.code_review_command,
null,
'code_review_command default should be null'
);
});
});
describe('ship workflow code_review_command integration', () => {
const shipContent = fs.readFileSync(SHIP_MD_PATH, 'utf-8');
test('ship.md contains code_review_command config check', () => {
assert.ok(
shipContent.includes('code_review_command'),
'ship.md must reference code_review_command'
);
});
test('ship.md has external review sub-step that reads config', () => {
assert.ok(
shipContent.includes('config-get') && shipContent.includes('workflow.code_review_command'),
'ship.md must read workflow.code_review_command from config'
);
});
test('ship.md generates diff against base branch for review', () => {
assert.ok(
shipContent.includes('git diff') && shipContent.includes('BASE_BRANCH'),
'ship.md must generate a diff using BASE_BRANCH for the external review'
);
});
test('ship.md has JSON parsing for external review output', () => {
assert.ok(
shipContent.includes('verdict') && shipContent.includes('APPROVED'),
'ship.md must parse JSON output with verdict field'
);
assert.ok(
shipContent.includes('REVISE'),
'ship.md must handle REVISE verdict'
);
});
test('ship.md has timeout handling for external review command (120s)', () => {
assert.ok(
shipContent.includes('120') || shipContent.includes('timeout'),
'ship.md must have timeout handling (120s) for external review command'
);
});
test('ship.md has stderr capture on failure', () => {
assert.ok(
shipContent.includes('stderr'),
'ship.md must capture stderr on external review command failure'
);
});
test('ship.md pipes review prompt to command via stdin', () => {
assert.ok(
shipContent.includes('stdin'),
'ship.md must pipe the review prompt to the command via stdin'
);
});
test('ship.md includes diff stats in review prompt', () => {
assert.ok(
shipContent.includes('diff --stat') || shipContent.includes('diffstat') || shipContent.includes('--stat'),
'ship.md must include diff stats in the review prompt'
);
});
test('ship.md falls through to existing review flow on failure', () => {
// The external review should not block the existing manual review options
assert.ok(
shipContent.includes('AskUserQuestion') || shipContent.includes('Skip review'),
'ship.md must still offer the existing manual review flow after external review'
);
});
});

View File

@@ -0,0 +1,173 @@
const { test, describe } = require('node:test');
const assert = require('node:assert/strict');
const fs = require('fs');
const path = require('path');
const CONFIG_PATH = path.join(__dirname, '..', 'get-shit-done', 'bin', 'lib', 'config.cjs');
const EXECUTE_PHASE_PATH = path.join(__dirname, '..', 'get-shit-done', 'workflows', 'execute-phase.md');
const CONFIG_TEMPLATE_PATH = path.join(__dirname, '..', 'get-shit-done', 'templates', 'config.json');
describe('cross-AI execution', () => {
describe('config keys', () => {
test('workflow.cross_ai_execution is in VALID_CONFIG_KEYS', () => {
const { VALID_CONFIG_KEYS } = require(CONFIG_PATH);
assert.ok(VALID_CONFIG_KEYS.has('workflow.cross_ai_execution'),
'VALID_CONFIG_KEYS must include workflow.cross_ai_execution');
});
test('workflow.cross_ai_command is in VALID_CONFIG_KEYS', () => {
const { VALID_CONFIG_KEYS } = require(CONFIG_PATH);
assert.ok(VALID_CONFIG_KEYS.has('workflow.cross_ai_command'),
'VALID_CONFIG_KEYS must include workflow.cross_ai_command');
});
test('workflow.cross_ai_timeout is in VALID_CONFIG_KEYS', () => {
const { VALID_CONFIG_KEYS } = require(CONFIG_PATH);
assert.ok(VALID_CONFIG_KEYS.has('workflow.cross_ai_timeout'),
'VALID_CONFIG_KEYS must include workflow.cross_ai_timeout');
});
});
describe('config template defaults', () => {
test('config template has cross_ai_execution default', () => {
const template = JSON.parse(fs.readFileSync(CONFIG_TEMPLATE_PATH, 'utf-8'));
assert.strictEqual(template.workflow.cross_ai_execution, false,
'cross_ai_execution should default to false');
});
test('config template has cross_ai_command default', () => {
const template = JSON.parse(fs.readFileSync(CONFIG_TEMPLATE_PATH, 'utf-8'));
assert.strictEqual(template.workflow.cross_ai_command, '',
'cross_ai_command should default to empty string');
});
test('config template has cross_ai_timeout default', () => {
const template = JSON.parse(fs.readFileSync(CONFIG_TEMPLATE_PATH, 'utf-8'));
assert.strictEqual(template.workflow.cross_ai_timeout, 300,
'cross_ai_timeout should default to 300 seconds');
});
});
describe('execute-phase.md cross-AI step', () => {
let content;
test('execute-phase.md has a cross-AI execution step', () => {
content = fs.readFileSync(EXECUTE_PHASE_PATH, 'utf-8');
assert.ok(content.includes('<step name="cross_ai_delegation">'),
'execute-phase.md must have a step named cross_ai_delegation');
});
test('cross-AI step appears between discover_and_group_plans and execute_waves', () => {
content = content || fs.readFileSync(EXECUTE_PHASE_PATH, 'utf-8');
const discoverIdx = content.indexOf('<step name="discover_and_group_plans">');
const crossAiIdx = content.indexOf('<step name="cross_ai_delegation">');
const executeIdx = content.indexOf('<step name="execute_waves">');
assert.ok(discoverIdx < crossAiIdx, 'cross_ai_delegation must come after discover_and_group_plans');
assert.ok(crossAiIdx < executeIdx, 'cross_ai_delegation must come before execute_waves');
});
test('cross-AI step handles --cross-ai flag', () => {
content = content || fs.readFileSync(EXECUTE_PHASE_PATH, 'utf-8');
assert.ok(content.includes('--cross-ai'),
'execute-phase.md must reference --cross-ai flag');
});
test('cross-AI step handles --no-cross-ai flag', () => {
content = content || fs.readFileSync(EXECUTE_PHASE_PATH, 'utf-8');
assert.ok(content.includes('--no-cross-ai'),
'execute-phase.md must reference --no-cross-ai flag');
});
test('cross-AI step uses stdin-based prompt delivery', () => {
content = content || fs.readFileSync(EXECUTE_PHASE_PATH, 'utf-8');
// The step must describe piping prompt via stdin, not shell interpolation
assert.ok(content.includes('stdin'),
'cross-AI step must describe stdin-based prompt delivery');
});
test('cross-AI step validates summary output', () => {
content = content || fs.readFileSync(EXECUTE_PHASE_PATH, 'utf-8');
// The step must describe validating the captured summary
const crossAiSection = content.substring(
content.indexOf('<step name="cross_ai_delegation">'),
content.indexOf('</step>', content.indexOf('<step name="cross_ai_delegation">')) + '</step>'.length
);
assert.ok(
crossAiSection.includes('SUMMARY') && crossAiSection.includes('valid'),
'cross-AI step must validate the summary output'
);
});
test('cross-AI step warns about dirty working tree', () => {
content = content || fs.readFileSync(EXECUTE_PHASE_PATH, 'utf-8');
const crossAiSection = content.substring(
content.indexOf('<step name="cross_ai_delegation">'),
content.indexOf('</step>', content.indexOf('<step name="cross_ai_delegation">')) + '</step>'.length
);
assert.ok(
crossAiSection.includes('dirty') || crossAiSection.includes('uncommitted') || crossAiSection.includes('working tree'),
'cross-AI step must warn about dirty/uncommitted changes from external command'
);
});
test('cross-AI step reads cross_ai_command from config', () => {
content = content || fs.readFileSync(EXECUTE_PHASE_PATH, 'utf-8');
const crossAiSection = content.substring(
content.indexOf('<step name="cross_ai_delegation">'),
content.indexOf('</step>', content.indexOf('<step name="cross_ai_delegation">')) + '</step>'.length
);
assert.ok(
crossAiSection.includes('cross_ai_command'),
'cross-AI step must read cross_ai_command from config'
);
});
test('cross-AI step reads cross_ai_timeout from config', () => {
content = content || fs.readFileSync(EXECUTE_PHASE_PATH, 'utf-8');
const crossAiSection = content.substring(
content.indexOf('<step name="cross_ai_delegation">'),
content.indexOf('</step>', content.indexOf('<step name="cross_ai_delegation">')) + '</step>'.length
);
assert.ok(
crossAiSection.includes('cross_ai_timeout'),
'cross-AI step must read cross_ai_timeout from config'
);
});
test('cross-AI step handles failure with retry/skip/abort', () => {
content = content || fs.readFileSync(EXECUTE_PHASE_PATH, 'utf-8');
const crossAiSection = content.substring(
content.indexOf('<step name="cross_ai_delegation">'),
content.indexOf('</step>', content.indexOf('<step name="cross_ai_delegation">')) + '</step>'.length
);
assert.ok(crossAiSection.includes('retry'), 'cross-AI step must offer retry on failure');
assert.ok(crossAiSection.includes('skip'), 'cross-AI step must offer skip on failure');
assert.ok(crossAiSection.includes('abort'), 'cross-AI step must offer abort on failure');
});
test('cross-AI step skips normal executor for handled plans', () => {
content = content || fs.readFileSync(EXECUTE_PHASE_PATH, 'utf-8');
const crossAiSection = content.substring(
content.indexOf('<step name="cross_ai_delegation">'),
content.indexOf('</step>', content.indexOf('<step name="cross_ai_delegation">')) + '</step>'.length
);
assert.ok(
crossAiSection.includes('skip') && (crossAiSection.includes('executor') || crossAiSection.includes('execute_waves')),
'cross-AI step must describe skipping normal executor for cross-AI handled plans'
);
});
test('parse_args step includes --cross-ai and --no-cross-ai', () => {
content = content || fs.readFileSync(EXECUTE_PHASE_PATH, 'utf-8');
const parseArgsSection = content.substring(
content.indexOf('<step name="parse_args"'),
content.indexOf('</step>', content.indexOf('<step name="parse_args"')) + '</step>'.length
);
assert.ok(parseArgsSection.includes('--cross-ai'),
'parse_args step must parse --cross-ai flag');
assert.ok(parseArgsSection.includes('--no-cross-ai'),
'parse_args step must parse --no-cross-ai flag');
});
});
});

View File

@@ -0,0 +1,222 @@
/**
* Cursor CLI Reviewer Tests (#1960)
*
* Verifies that /gsd-review includes Cursor CLI as a peer reviewer:
* - review.md workflow contains cursor detection, flag parsing, self-detection, invocation
* - commands/gsd/review.md command file mentions --cursor flag
* - help.md lists --cursor in the /gsd-review signature
* - docs/COMMANDS.md has --cursor flag row
* - docs/FEATURES.md has Cursor in the review section
* - i18n docs mirror the same content
* - REVIEWS.md template includes Cursor Review section
*/
const { test, describe } = require('node:test');
const assert = require('node:assert/strict');
const fs = require('fs');
const path = require('path');
const ROOT = path.join(__dirname, '..');
describe('Cursor CLI reviewer in /gsd-review (#1960)', () => {
// --- review.md workflow ---
describe('review.md workflow', () => {
const reviewPath = path.join(ROOT, 'get-shit-done', 'workflows', 'review.md');
let content;
test('review.md exists', () => {
assert.ok(fs.existsSync(reviewPath), 'review.md should exist');
content = fs.readFileSync(reviewPath, 'utf-8');
});
test('contains cursor CLI detection via command -v', () => {
const c = fs.readFileSync(reviewPath, 'utf-8');
assert.ok(
c.includes('command -v cursor'),
'review.md should detect cursor CLI via "command -v cursor"'
);
});
test('contains --cursor flag parsing', () => {
const c = fs.readFileSync(reviewPath, 'utf-8');
assert.ok(
c.includes('--cursor'),
'review.md should parse --cursor flag'
);
});
test('contains CURSOR_SESSION_ID self-detection', () => {
const c = fs.readFileSync(reviewPath, 'utf-8');
assert.ok(
c.includes('CURSOR_SESSION_ID'),
'review.md should detect self-CLI via CURSOR_SESSION_ID env var'
);
});
test('contains cursor agent invocation command', () => {
const c = fs.readFileSync(reviewPath, 'utf-8');
assert.ok(
c.includes('cursor agent -p --mode ask --trust'),
'review.md should invoke cursor via "cursor agent -p --mode ask --trust"'
);
});
test('contains Cursor Review section in REVIEWS.md template', () => {
const c = fs.readFileSync(reviewPath, 'utf-8');
assert.ok(
c.includes('Cursor Review'),
'review.md should include a "Cursor Review" section in the REVIEWS.md template'
);
});
test('lists cursor in the reviewers frontmatter array', () => {
const c = fs.readFileSync(reviewPath, 'utf-8');
assert.ok(
/reviewers:.*cursor/.test(c),
'review.md should list cursor in the reviewers array'
);
});
});
// --- commands/gsd/review.md ---
describe('commands/gsd/review.md', () => {
const cmdPath = path.join(ROOT, 'commands', 'gsd', 'review.md');
test('mentions --cursor flag', () => {
const c = fs.readFileSync(cmdPath, 'utf-8');
assert.ok(
c.includes('--cursor'),
'commands/gsd/review.md should mention --cursor flag'
);
});
test('mentions Cursor in objective or context', () => {
const c = fs.readFileSync(cmdPath, 'utf-8');
assert.ok(
c.includes('Cursor'),
'commands/gsd/review.md should mention Cursor'
);
});
});
// --- help.md ---
describe('help.md', () => {
const helpPath = path.join(ROOT, 'get-shit-done', 'workflows', 'help.md');
test('lists --cursor in /gsd-review signature', () => {
const c = fs.readFileSync(helpPath, 'utf-8');
assert.ok(
c.includes('--cursor'),
'help.md should list --cursor in the /gsd-review command signature'
);
});
});
// --- docs/COMMANDS.md ---
describe('docs/COMMANDS.md', () => {
const docsPath = path.join(ROOT, 'docs', 'COMMANDS.md');
test('has --cursor flag row', () => {
const c = fs.readFileSync(docsPath, 'utf-8');
assert.ok(
c.includes('--cursor'),
'docs/COMMANDS.md should have a --cursor flag row'
);
});
});
// --- docs/FEATURES.md ---
describe('docs/FEATURES.md', () => {
const featPath = path.join(ROOT, 'docs', 'FEATURES.md');
test('has --cursor in review command signature', () => {
const c = fs.readFileSync(featPath, 'utf-8');
assert.ok(
c.includes('--cursor'),
'docs/FEATURES.md should include --cursor in the review command signature'
);
});
test('mentions Cursor in the review purpose', () => {
const c = fs.readFileSync(featPath, 'utf-8');
assert.ok(
c.includes('Cursor'),
'docs/FEATURES.md should mention Cursor in the review section'
);
});
});
// --- i18n: ja-JP ---
describe('docs/ja-JP/COMMANDS.md', () => {
const jaPath = path.join(ROOT, 'docs', 'ja-JP', 'COMMANDS.md');
test('has --cursor flag row', () => {
const c = fs.readFileSync(jaPath, 'utf-8');
assert.ok(
c.includes('--cursor'),
'docs/ja-JP/COMMANDS.md should have a --cursor flag row'
);
});
});
describe('docs/ja-JP/FEATURES.md', () => {
const jaPath = path.join(ROOT, 'docs', 'ja-JP', 'FEATURES.md');
test('has --cursor in review command signature', () => {
const c = fs.readFileSync(jaPath, 'utf-8');
assert.ok(
c.includes('--cursor'),
'docs/ja-JP/FEATURES.md should include --cursor in the review command signature'
);
});
test('mentions Cursor in the review section', () => {
const c = fs.readFileSync(jaPath, 'utf-8');
assert.ok(
/Cursor/i.test(fs.readFileSync(jaPath, 'utf-8')),
'docs/ja-JP/FEATURES.md should mention Cursor in the review section'
);
});
});
// --- i18n: ko-KR ---
describe('docs/ko-KR/COMMANDS.md', () => {
const koPath = path.join(ROOT, 'docs', 'ko-KR', 'COMMANDS.md');
test('has --cursor flag row', () => {
const c = fs.readFileSync(koPath, 'utf-8');
assert.ok(
c.includes('--cursor'),
'docs/ko-KR/COMMANDS.md should have a --cursor flag row'
);
});
});
describe('docs/ko-KR/FEATURES.md', () => {
const koPath = path.join(ROOT, 'docs', 'ko-KR', 'FEATURES.md');
test('has --cursor in review command signature', () => {
const c = fs.readFileSync(koPath, 'utf-8');
assert.ok(
c.includes('--cursor'),
'docs/ko-KR/FEATURES.md should include --cursor in the review command signature'
);
});
test('mentions Cursor in the review section', () => {
const c = fs.readFileSync(koPath, 'utf-8');
assert.ok(
/Cursor/i.test(fs.readFileSync(koPath, 'utf-8')),
'docs/ko-KR/FEATURES.md should mention Cursor in the review section'
);
});
});
});

View File

@@ -0,0 +1,168 @@
/**
* Extract-Learnings Command & Workflow Tests
*
* Validates command file existence, frontmatter correctness, workflow content,
* 4 learning categories, capture_thought handling, graceful degradation,
* LEARNINGS.md output, and missing artifact handling.
*/
const { describe, test } = require('node:test');
const assert = require('node:assert/strict');
const fs = require('fs');
const path = require('path');
const COMMAND_PATH = path.join(__dirname, '..', 'commands', 'gsd', 'extract_learnings.md');
const WORKFLOW_PATH = path.join(__dirname, '..', 'get-shit-done', 'workflows', 'extract_learnings.md');
describe('extract-learnings command', () => {
test('command file exists', () => {
assert.ok(fs.existsSync(COMMAND_PATH), 'commands/gsd/extract_learnings.md should exist');
});
test('command file has correct name frontmatter', () => {
const content = fs.readFileSync(COMMAND_PATH, 'utf-8');
assert.ok(content.includes('name: gsd:extract-learnings'), 'Command must have name: gsd:extract-learnings');
});
test('command file has description frontmatter', () => {
const content = fs.readFileSync(COMMAND_PATH, 'utf-8');
assert.ok(content.includes('description:'), 'Command must have description frontmatter');
});
test('command file has argument-hint for phase-number', () => {
const content = fs.readFileSync(COMMAND_PATH, 'utf-8');
assert.ok(content.includes('argument-hint:'), 'Command must have argument-hint');
assert.ok(content.includes('<phase-number>'), 'argument-hint must reference <phase-number>');
});
test('command file has allowed-tools list', () => {
const content = fs.readFileSync(COMMAND_PATH, 'utf-8');
assert.ok(content.includes('allowed-tools:'), 'Command must have allowed-tools');
assert.ok(content.includes('Read'), 'allowed-tools must include Read');
assert.ok(content.includes('Write'), 'allowed-tools must include Write');
assert.ok(content.includes('Bash'), 'allowed-tools must include Bash');
assert.ok(content.includes('Grep'), 'allowed-tools must include Grep');
assert.ok(content.includes('Glob'), 'allowed-tools must include Glob');
assert.ok(content.includes('Agent'), 'allowed-tools must include Agent');
});
test('command file has type: prompt', () => {
const content = fs.readFileSync(COMMAND_PATH, 'utf-8');
assert.ok(content.includes('type: prompt'), 'Command must have type: prompt');
});
test('command references the workflow via execution_context', () => {
const content = fs.readFileSync(COMMAND_PATH, 'utf-8');
assert.ok(
content.includes('workflows/extract_learnings.md'),
'Command must reference workflows/extract_learnings.md in execution_context'
);
});
});
describe('extract-learnings workflow', () => {
test('workflow file exists', () => {
assert.ok(fs.existsSync(WORKFLOW_PATH), 'workflows/extract_learnings.md should exist');
});
test('workflow has objective tag', () => {
const content = fs.readFileSync(WORKFLOW_PATH, 'utf-8');
assert.ok(content.includes('<objective>'), 'Workflow must have <objective> tag');
assert.ok(content.includes('</objective>'), 'Workflow must close <objective> tag');
});
test('workflow has process tag', () => {
const content = fs.readFileSync(WORKFLOW_PATH, 'utf-8');
assert.ok(content.includes('<process>'), 'Workflow must have <process> tag');
assert.ok(content.includes('</process>'), 'Workflow must close <process> tag');
});
test('workflow has step tags', () => {
const content = fs.readFileSync(WORKFLOW_PATH, 'utf-8');
assert.ok(content.includes('<step name='), 'Workflow must have named step tags');
assert.ok(content.includes('</step>'), 'Workflow must close step tags');
});
test('workflow has success_criteria tag', () => {
const content = fs.readFileSync(WORKFLOW_PATH, 'utf-8');
assert.ok(content.includes('<success_criteria>'), 'Workflow must have <success_criteria> tag');
assert.ok(content.includes('</success_criteria>'), 'Workflow must close <success_criteria> tag');
});
test('workflow has critical_rules tag', () => {
const content = fs.readFileSync(WORKFLOW_PATH, 'utf-8');
assert.ok(content.includes('<critical_rules>'), 'Workflow must have <critical_rules> tag');
assert.ok(content.includes('</critical_rules>'), 'Workflow must close <critical_rules> tag');
});
test('workflow reads required artifacts (PLAN.md and SUMMARY.md)', () => {
const content = fs.readFileSync(WORKFLOW_PATH, 'utf-8');
assert.ok(content.includes('PLAN.md'), 'Workflow must reference PLAN.md');
assert.ok(content.includes('SUMMARY.md'), 'Workflow must reference SUMMARY.md');
});
test('workflow reads optional artifacts (VERIFICATION.md, UAT.md, STATE.md)', () => {
const content = fs.readFileSync(WORKFLOW_PATH, 'utf-8');
assert.ok(content.includes('VERIFICATION.md'), 'Workflow must reference VERIFICATION.md');
assert.ok(content.includes('UAT.md'), 'Workflow must reference UAT.md');
assert.ok(content.includes('STATE.md'), 'Workflow must reference STATE.md');
});
test('workflow extracts all 4 learning categories', () => {
const content = fs.readFileSync(WORKFLOW_PATH, 'utf-8');
assert.ok(content.toLowerCase().includes('decision'), 'Workflow must extract decisions');
assert.ok(content.toLowerCase().includes('lesson'), 'Workflow must extract lessons');
assert.ok(content.toLowerCase().includes('pattern'), 'Workflow must extract patterns');
assert.ok(content.toLowerCase().includes('surprise'), 'Workflow must extract surprises');
});
test('workflow handles capture_thought tool availability', () => {
const content = fs.readFileSync(WORKFLOW_PATH, 'utf-8');
assert.ok(content.includes('capture_thought'), 'Workflow must reference capture_thought tool');
});
test('workflow degrades gracefully when capture_thought is unavailable', () => {
const content = fs.readFileSync(WORKFLOW_PATH, 'utf-8');
assert.ok(
content.includes('graceful') || content.includes('not available') || content.includes('unavailable') || content.includes('fallback'),
'Workflow must handle graceful degradation when capture_thought is unavailable'
);
});
test('workflow outputs LEARNINGS.md', () => {
const content = fs.readFileSync(WORKFLOW_PATH, 'utf-8');
assert.ok(content.includes('LEARNINGS.md'), 'Workflow must output LEARNINGS.md');
});
test('workflow handles missing artifacts gracefully', () => {
const content = fs.readFileSync(WORKFLOW_PATH, 'utf-8');
assert.ok(
content.includes('missing') || content.includes('not found') || content.includes('optional'),
'Workflow must handle missing artifacts'
);
});
test('workflow includes source attribution for extracted items', () => {
const content = fs.readFileSync(WORKFLOW_PATH, 'utf-8');
assert.ok(
content.includes('source') || content.includes('attribution') || content.includes('Source:'),
'Workflow must include source attribution for extracted items'
);
});
test('workflow specifies LEARNINGS.md YAML frontmatter fields', () => {
const content = fs.readFileSync(WORKFLOW_PATH, 'utf-8');
assert.ok(content.includes('phase'), 'LEARNINGS.md frontmatter must include phase');
assert.ok(content.includes('phase_name'), 'LEARNINGS.md frontmatter must include phase_name');
assert.ok(content.includes('generated'), 'LEARNINGS.md frontmatter must include generated');
assert.ok(content.includes('missing_artifacts'), 'LEARNINGS.md frontmatter must include missing_artifacts');
});
test('workflow supports overwriting previous LEARNINGS.md on re-run', () => {
const content = fs.readFileSync(WORKFLOW_PATH, 'utf-8');
assert.ok(
content.includes('overwrite') || content.includes('overwrit') || content.includes('replace'),
'Workflow must support overwriting previous LEARNINGS.md'
);
});
});

170
tests/plan-bounce.test.cjs Normal file
View File

@@ -0,0 +1,170 @@
/**
* Plan Bounce Tests
*
* Validates plan bounce hook feature (step 12.5 in plan-phase):
* - Config key registration (workflow.plan_bounce, workflow.plan_bounce_script, workflow.plan_bounce_passes)
* - Config template defaults
* - Workflow step 12.5 content in plan-phase.md
* - Flag handling (--bounce, --skip-bounce)
* - Backup/restore pattern (pre-bounce.md)
* - Frontmatter integrity validation
* - Re-runs checker on bounced plans
*/
const { test, describe } = require('node:test');
const assert = require('node:assert/strict');
const fs = require('fs');
const path = require('path');
const GSD_ROOT = path.join(__dirname, '..', 'get-shit-done');
const CONFIG_CJS_PATH = path.join(GSD_ROOT, 'bin', 'lib', 'config.cjs');
const CONFIG_TEMPLATE_PATH = path.join(GSD_ROOT, 'templates', 'config.json');
const PLAN_PHASE_PATH = path.join(GSD_ROOT, 'workflows', 'plan-phase.md');
describe('Plan Bounce: config keys', () => {
test('workflow.plan_bounce is in VALID_CONFIG_KEYS', () => {
const content = fs.readFileSync(CONFIG_CJS_PATH, 'utf-8');
assert.ok(
content.includes("'workflow.plan_bounce'"),
'VALID_CONFIG_KEYS should contain workflow.plan_bounce'
);
});
test('workflow.plan_bounce_script is in VALID_CONFIG_KEYS', () => {
const content = fs.readFileSync(CONFIG_CJS_PATH, 'utf-8');
assert.ok(
content.includes("'workflow.plan_bounce_script'"),
'VALID_CONFIG_KEYS should contain workflow.plan_bounce_script'
);
});
test('workflow.plan_bounce_passes is in VALID_CONFIG_KEYS', () => {
const content = fs.readFileSync(CONFIG_CJS_PATH, 'utf-8');
assert.ok(
content.includes("'workflow.plan_bounce_passes'"),
'VALID_CONFIG_KEYS should contain workflow.plan_bounce_passes'
);
});
});
describe('Plan Bounce: config template defaults', () => {
test('config template has plan_bounce default (false)', () => {
const template = JSON.parse(fs.readFileSync(CONFIG_TEMPLATE_PATH, 'utf-8'));
assert.strictEqual(
template.workflow.plan_bounce,
false,
'config template workflow.plan_bounce should default to false'
);
});
test('config template has plan_bounce_script default (null)', () => {
const template = JSON.parse(fs.readFileSync(CONFIG_TEMPLATE_PATH, 'utf-8'));
assert.strictEqual(
template.workflow.plan_bounce_script,
null,
'config template workflow.plan_bounce_script should default to null'
);
});
test('config template has plan_bounce_passes default (2)', () => {
const template = JSON.parse(fs.readFileSync(CONFIG_TEMPLATE_PATH, 'utf-8'));
assert.strictEqual(
template.workflow.plan_bounce_passes,
2,
'config template workflow.plan_bounce_passes should default to 2'
);
});
});
describe('Plan Bounce: plan-phase.md step 12.5', () => {
let content;
test('plan-phase.md contains step 12.5', () => {
content = fs.readFileSync(PLAN_PHASE_PATH, 'utf-8');
assert.ok(
content.includes('## 12.5'),
'plan-phase.md should contain step 12.5'
);
});
test('step 12.5 references plan bounce', () => {
content = content || fs.readFileSync(PLAN_PHASE_PATH, 'utf-8');
// The step title should mention bounce
assert.ok(
/## 12\.5.*[Bb]ounce/i.test(content),
'step 12.5 should reference plan bounce in its title'
);
});
test('plan-phase.md has --bounce flag handling', () => {
content = content || fs.readFileSync(PLAN_PHASE_PATH, 'utf-8');
assert.ok(
content.includes('--bounce'),
'plan-phase.md should handle --bounce flag'
);
});
test('plan-phase.md has --skip-bounce flag handling', () => {
content = content || fs.readFileSync(PLAN_PHASE_PATH, 'utf-8');
assert.ok(
content.includes('--skip-bounce'),
'plan-phase.md should handle --skip-bounce flag'
);
});
test('plan-phase.md has backup pattern (pre-bounce.md)', () => {
content = content || fs.readFileSync(PLAN_PHASE_PATH, 'utf-8');
assert.ok(
content.includes('pre-bounce.md'),
'plan-phase.md should reference pre-bounce.md backup files'
);
});
test('plan-phase.md has frontmatter integrity validation for bounced plans', () => {
content = content || fs.readFileSync(PLAN_PHASE_PATH, 'utf-8');
// Should mention YAML frontmatter validation after bounce
assert.ok(
/frontmatter.*bounced|bounced.*frontmatter|YAML.*bounce|bounce.*YAML/i.test(content),
'plan-phase.md should validate frontmatter integrity on bounced plans'
);
});
test('plan-phase.md re-runs checker on bounced plans', () => {
content = content || fs.readFileSync(PLAN_PHASE_PATH, 'utf-8');
// Should mention re-running plan checker after bounce
assert.ok(
/[Rr]e-run.*checker.*bounce|bounce.*checker.*re-run|checker.*bounced/i.test(content),
'plan-phase.md should re-run plan checker on bounced plans'
);
});
test('plan-phase.md references plan_bounce config keys', () => {
content = content || fs.readFileSync(PLAN_PHASE_PATH, 'utf-8');
assert.ok(
content.includes('plan_bounce_script'),
'plan-phase.md should reference plan_bounce_script config'
);
assert.ok(
content.includes('plan_bounce_passes'),
'plan-phase.md should reference plan_bounce_passes config'
);
});
test('plan-phase.md disables bounce when --gaps flag is present', () => {
content = content || fs.readFileSync(PLAN_PHASE_PATH, 'utf-8');
// Should mention that --gaps disables bounce
assert.ok(
/--gaps.*bounce|bounce.*--gaps/i.test(content),
'plan-phase.md should disable bounce when --gaps flag is present'
);
});
test('plan-phase.md restores original on script failure', () => {
content = content || fs.readFileSync(PLAN_PHASE_PATH, 'utf-8');
// Should mention restoring from backup on failure
assert.ok(
/restore.*original|restore.*pre-bounce|original.*restore/i.test(content),
'plan-phase.md should restore original plan on script failure'
);
});
});

View File

@@ -0,0 +1,333 @@
'use strict';
/**
* Planner Language Regression Tests (#2091, #2092)
*
* Prevents time-based reasoning and complexity-as-scope-justification
* from leaking back into planning artifacts via future PRs.
*
* These tests scan agent definitions, workflow files, and references
* for prohibited patterns that import human-world constraints into
* an AI execution context where those constraints do not exist.
*/
const { test, describe } = require('node:test');
const assert = require('node:assert/strict');
const fs = require('fs');
const path = require('path');
const ROOT = path.join(__dirname, '..');
const AGENTS_DIR = path.join(ROOT, 'agents');
const WORKFLOWS_DIR = path.join(ROOT, 'get-shit-done', 'workflows');
const REFERENCES_DIR = path.join(ROOT, 'get-shit-done', 'references');
const TEMPLATES_DIR = path.join(ROOT, 'get-shit-done', 'templates');
/**
* Collect all .md files from a directory (non-recursive).
*/
function mdFiles(dir) {
if (!fs.existsSync(dir)) return [];
return fs.readdirSync(dir)
.filter(f => f.endsWith('.md'))
.map(f => ({ name: f, path: path.join(dir, f) }));
}
/**
* Collect all .md files recursively.
*/
function mdFilesRecursive(dir) {
if (!fs.existsSync(dir)) return [];
const results = [];
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
const full = path.join(dir, entry.name);
if (entry.isDirectory()) {
results.push(...mdFilesRecursive(full));
} else if (entry.name.endsWith('.md')) {
results.push({ name: entry.name, path: full });
}
}
return results;
}
/**
* Files that define planning behavior — agents, workflows, references.
* These are the files where time-based and complexity-based scope
* reasoning must never appear.
*/
const PLANNING_FILES = [
...mdFiles(AGENTS_DIR),
...mdFiles(WORKFLOWS_DIR),
...mdFiles(REFERENCES_DIR),
...mdFilesRecursive(TEMPLATES_DIR),
];
// -- Prohibited patterns --
/**
* Time-based task sizing patterns.
* Matches "15-60 minutes", "X minutes Claude execution time", etc.
* Does NOT match operational timeouts ("timeout: 5 minutes"),
* API docs examples ("100 requests per 15 minutes"),
* or human-readable timeout descriptions in workflow execution steps.
*/
const TIME_SIZING_PATTERNS = [
// "N-M minutes" in task sizing context (not timeout context)
/each task[:\s]*\*?\*?\d+[-–]\d+\s*min/i,
// "minutes Claude execution time" or "minutes execution time"
/minutes?\s+(claude\s+)?execution\s+time/i,
// Duration-based sizing table rows: "< 15 min", "15-60 min", "> 60 min"
/[<>]\s*\d+\s*min\s*\|/i,
];
/**
* Complexity-as-scope-justification patterns.
* Matches "too complex to implement", "challenging feature", etc.
* Does NOT match legitimate uses like:
* - "complex domains" in research/discovery context (describing what to research)
* - "non-trivial" in verification context (confirming substantive code exists)
* - "challenging" in user-profiling context (quoting user reactions)
*/
const COMPLEXITY_SCOPE_PATTERNS = [
// "too complex to" — always a scope-reduction justification
/too\s+complex\s+to/i,
// "too difficult" — always a scope-reduction justification
/too\s+difficult/i,
// "is too complex for" — scope justification (e.g. "Phase X is too complex for")
/is\s+too\s+complex\s+for/i,
];
/**
* Files allowed to contain certain patterns because they document
* the prohibition itself, or use the terms in non-scope-reduction context.
*/
const ALLOWLIST = {
// Plan-checker scans FOR these patterns — it's a detection list, not usage
'gsd-plan-checker.md': ['complexity_scope', 'time_sizing'],
// Planner defines the prohibition and the authority limits — uses terms to explain what NOT to do
'gsd-planner.md': ['complexity_scope'],
// Debugger uses "30+ minutes" as anti-pattern detection, not task sizing
'gsd-debugger.md': ['time_sizing'],
// Doc-writer uses "15 minutes" in API rate limit example, "2 minutes" for doc quality
'gsd-doc-writer.md': ['time_sizing'],
// Discovery-phase uses time for level descriptions (operational, not scope)
'discovery-phase.md': ['time_sizing'],
// Explore uses "~30 seconds" as operational estimate
'explore.md': ['time_sizing'],
// Review uses "up to 5 minutes" for CodeRabbit timeout
'review.md': ['time_sizing'],
// Fast uses "under 2 minutes wall time" as operational constraint
'fast.md': ['time_sizing'],
// Execute-phase uses "timeout: 5 minutes" for test runner
'execute-phase.md': ['time_sizing'],
// Verify-phase uses "timeout: 5 minutes" for test runner
'verify-phase.md': ['time_sizing'],
// Map-codebase documents subagent_timeout
'map-codebase.md': ['time_sizing'],
// Help documents CodeRabbit timing
'help.md': ['time_sizing'],
};
function isAllowlisted(fileName, category) {
const entry = ALLOWLIST[fileName];
return entry && entry.includes(category);
}
// -- Tests --
describe('Planner language regression — time-based task sizing (#2092)', () => {
for (const file of PLANNING_FILES) {
test(`${file.name} must not use time-based task sizing`, () => {
if (isAllowlisted(file.name, 'time_sizing')) return;
const content = fs.readFileSync(file.path, 'utf-8');
for (const pattern of TIME_SIZING_PATTERNS) {
const match = content.match(pattern);
assert.ok(
!match,
[
`${file.name} contains time-based task sizing: "${match?.[0]}"`,
'Task sizing must use context-window percentage, not time units.',
'See issue #2092 for rationale.',
].join('\n')
);
}
});
}
});
describe('Planner language regression — complexity-as-scope-justification (#2092)', () => {
for (const file of PLANNING_FILES) {
test(`${file.name} must not use complexity to justify scope reduction`, () => {
if (isAllowlisted(file.name, 'complexity_scope')) return;
const content = fs.readFileSync(file.path, 'utf-8');
for (const pattern of COMPLEXITY_SCOPE_PATTERNS) {
const match = content.match(pattern);
assert.ok(
!match,
[
`${file.name} contains complexity-as-scope-justification: "${match?.[0]}"`,
'Scope decisions must be based on context cost, missing information,',
'or dependency conflicts — not perceived difficulty.',
'See issue #2092 for rationale.',
].join('\n')
);
}
});
}
});
describe('gsd-planner.md — required structural sections (#2091, #2092)', () => {
let plannerContent;
test('planner file exists and is readable', () => {
const plannerPath = path.join(AGENTS_DIR, 'gsd-planner.md');
assert.ok(fs.existsSync(plannerPath), 'agents/gsd-planner.md must exist');
plannerContent = fs.readFileSync(plannerPath, 'utf-8');
});
test('contains <planner_authority_limits> section', () => {
assert.ok(
plannerContent.includes('<planner_authority_limits>'),
'gsd-planner.md must contain a <planner_authority_limits> section defining what the planner cannot decide'
);
});
test('authority limits prohibit difficulty-based scope decisions', () => {
assert.ok(
plannerContent.includes('The planner has no authority to'),
'planner_authority_limits must explicitly state what the planner cannot decide'
);
});
test('authority limits list three legitimate split reasons: context cost, missing info, dependency', () => {
assert.ok(
plannerContent.includes('Context cost') || plannerContent.includes('context cost'),
'authority limits must list context cost as a legitimate split reason'
);
assert.ok(
plannerContent.includes('Missing information') || plannerContent.includes('missing information'),
'authority limits must list missing information as a legitimate split reason'
);
assert.ok(
plannerContent.includes('Dependency conflict') || plannerContent.includes('dependency conflict'),
'authority limits must list dependency conflict as a legitimate split reason'
);
});
test('task sizing uses context percentage, not time units', () => {
assert.ok(
plannerContent.includes('context consumption') || plannerContent.includes('context cost'),
'task sizing must reference context consumption, not time'
);
assert.ok(
!(/each task[:\s]*\*?\*?\d+[-–]\d+\s*min/i.test(plannerContent)),
'task sizing must not use minutes as sizing unit'
);
});
test('contains multi-source coverage audit (not just D-XX decisions)', () => {
assert.ok(
plannerContent.includes('Multi-Source Coverage Audit') ||
plannerContent.includes('multi-source coverage audit'),
'gsd-planner.md must contain a multi-source coverage audit, not just D-XX decision matrix'
);
});
test('coverage audit includes all four source types: GOAL, REQ, RESEARCH, CONTEXT', () => {
// The planner file or its referenced planner-source-audit.md must define all four types.
// The inline compact version uses **GOAL**, **REQ**, **RESEARCH**, **CONTEXT**.
const refPath = path.join(ROOT, 'get-shit-done', 'references', 'planner-source-audit.md');
const combined = plannerContent + (fs.existsSync(refPath) ? fs.readFileSync(refPath, 'utf-8') : '');
const hasGoal = combined.includes('**GOAL**');
const hasReq = combined.includes('**REQ**');
const hasResearch = combined.includes('**RESEARCH**');
const hasContext = combined.includes('**CONTEXT**');
assert.ok(hasGoal, 'coverage audit must include GOAL source type (ROADMAP.md phase goal)');
assert.ok(hasReq, 'coverage audit must include REQ source type (REQUIREMENTS.md)');
assert.ok(hasResearch, 'coverage audit must include RESEARCH source type (RESEARCH.md)');
assert.ok(hasContext, 'coverage audit must include CONTEXT source type (CONTEXT.md decisions)');
});
test('coverage audit defines MISSING item handling with developer escalation', () => {
assert.ok(
plannerContent.includes('Source Audit: Unplanned Items Found') ||
plannerContent.includes('MISSING'),
'coverage audit must define handling for MISSING items'
);
assert.ok(
plannerContent.includes('Awaiting developer decision') ||
plannerContent.includes('developer confirmation'),
'MISSING items must escalate to developer, not be silently dropped'
);
});
});
describe('plan-phase.md — source audit orchestration (#2091)', () => {
let workflowContent;
test('plan-phase workflow exists and is readable', () => {
const workflowPath = path.join(WORKFLOWS_DIR, 'plan-phase.md');
assert.ok(fs.existsSync(workflowPath), 'workflows/plan-phase.md must exist');
workflowContent = fs.readFileSync(workflowPath, 'utf-8');
});
test('step 9 handles Source Audit return from planner', () => {
assert.ok(
workflowContent.includes('Source Audit: Unplanned Items Found'),
'plan-phase.md step 9 must handle the Source Audit return from the planner'
);
});
test('step 9c exists for source audit gap handling', () => {
assert.ok(
workflowContent.includes('9c') && workflowContent.includes('Source Audit'),
'plan-phase.md must have a step 9c for handling source audit gaps'
);
});
test('step 9b does not use "too complex" language', () => {
// Extract just step 9b content (between "## 9b" and "## 9c" or "## 10")
const step9bMatch = workflowContent.match(/## 9b\.([\s\S]*?)(?=## 9c|## 10)/);
if (step9bMatch) {
const step9b = step9bMatch[1];
assert.ok(
!step9b.includes('too complex'),
'step 9b must not use "too complex" — use context budget language instead'
);
}
});
test('phase split recommendation uses context budget framing', () => {
assert.ok(
workflowContent.includes('context budget') || workflowContent.includes('context cost'),
'phase split recommendation must be framed in terms of context budget, not complexity'
);
});
});
describe('gsd-plan-checker.md — scope reduction detection includes time/complexity (#2092)', () => {
let checkerContent;
test('plan-checker exists and is readable', () => {
const checkerPath = path.join(AGENTS_DIR, 'gsd-plan-checker.md');
assert.ok(fs.existsSync(checkerPath), 'agents/gsd-plan-checker.md must exist');
checkerContent = fs.readFileSync(checkerPath, 'utf-8');
});
test('scope reduction scan includes complexity-based justification patterns', () => {
assert.ok(
checkerContent.includes('too complex') || checkerContent.includes('too difficult'),
'plan-checker scope reduction scan must detect complexity-based justification language'
);
});
test('scope reduction scan includes time-based justification patterns', () => {
assert.ok(
checkerContent.includes('would take') || checkerContent.includes('hours') || checkerContent.includes('minutes'),
'plan-checker scope reduction scan must detect time-based justification language'
);
});
});

View File

@@ -0,0 +1,137 @@
'use strict';
/**
* Prompt Thinning Tests (#1978)
*
* Validates context-window-aware prompt thinning for sub-200K models.
* When CONTEXT_WINDOW < 200000, agent prompts strip extended examples
* and anti-pattern lists, referencing them as @-required_reading files instead.
*/
const { test, describe } = require('node:test');
const assert = require('node:assert/strict');
const fs = require('fs');
const path = require('path');
const EXECUTE_PHASE = path.join(__dirname, '..', 'get-shit-done', 'workflows', 'execute-phase.md');
const EXECUTOR_AGENT = path.join(__dirname, '..', 'agents', 'gsd-executor.md');
const PLANNER_AGENT = path.join(__dirname, '..', 'agents', 'gsd-planner.md');
const EXECUTOR_EXAMPLES_REF = path.join(__dirname, '..', 'get-shit-done', 'references', 'executor-examples.md');
const PLANNER_ANTIPATTERNS_REF = path.join(__dirname, '..', 'get-shit-done', 'references', 'planner-antipatterns.md');
describe('prompt thinning — sub-200K context window support (#1978)', () => {
describe('execute-phase.md — thinning conditional', () => {
test('has a CONTEXT_WINDOW < 200000 thinning conditional', () => {
const content = fs.readFileSync(EXECUTE_PHASE, 'utf-8');
assert.ok(
content.includes('CONTEXT_WINDOW < 200000') || content.includes('CONTEXT_WINDOW< 200000'),
'execute-phase.md must contain a CONTEXT_WINDOW < 200000 conditional for prompt thinning'
);
});
test('preserves the existing CONTEXT_WINDOW >= 500000 enrichment conditional', () => {
const content = fs.readFileSync(EXECUTE_PHASE, 'utf-8');
assert.ok(
content.includes('CONTEXT_WINDOW >= 500000'),
'execute-phase.md must preserve the existing 500K enrichment conditional'
);
});
test('thinning block references executor-examples.md for on-demand loading', () => {
const content = fs.readFileSync(EXECUTE_PHASE, 'utf-8');
assert.ok(
content.includes('executor-examples.md'),
'execute-phase.md thinning block must reference executor-examples.md'
);
});
});
describe('gsd-executor.md — reference to extracted examples', () => {
test('references executor-examples.md for extended examples', () => {
const content = fs.readFileSync(EXECUTOR_AGENT, 'utf-8');
assert.ok(
content.includes('executor-examples.md'),
'gsd-executor.md must reference executor-examples.md for extended deviation/checkpoint examples'
);
});
});
describe('gsd-planner.md — reference to extracted anti-patterns', () => {
test('references planner-antipatterns.md for extended anti-patterns', () => {
const content = fs.readFileSync(PLANNER_AGENT, 'utf-8');
assert.ok(
content.includes('planner-antipatterns.md'),
'gsd-planner.md must reference planner-antipatterns.md for extended checkpoint anti-patterns and specificity examples'
);
});
});
describe('executor-examples.md — extracted reference file', () => {
test('file exists', () => {
assert.ok(
fs.existsSync(EXECUTOR_EXAMPLES_REF),
'get-shit-done/references/executor-examples.md must exist'
);
});
test('contains deviation rule examples', () => {
const content = fs.readFileSync(EXECUTOR_EXAMPLES_REF, 'utf-8');
assert.ok(
content.includes('Rule 1') || content.includes('RULE 1'),
'executor-examples.md must contain deviation rule examples'
);
});
test('contains checkpoint examples', () => {
const content = fs.readFileSync(EXECUTOR_EXAMPLES_REF, 'utf-8');
assert.ok(
content.includes('checkpoint') || content.includes('Checkpoint'),
'executor-examples.md must contain checkpoint examples'
);
});
test('contains edge case examples', () => {
const content = fs.readFileSync(EXECUTOR_EXAMPLES_REF, 'utf-8');
assert.ok(
content.includes('Edge case') || content.includes('edge case') || content.includes('Edge Case'),
'executor-examples.md must contain edge case guidance'
);
});
});
describe('planner-antipatterns.md — extracted reference file', () => {
test('file exists', () => {
assert.ok(
fs.existsSync(PLANNER_ANTIPATTERNS_REF),
'get-shit-done/references/planner-antipatterns.md must exist'
);
});
test('contains checkpoint anti-patterns', () => {
const content = fs.readFileSync(PLANNER_ANTIPATTERNS_REF, 'utf-8');
assert.ok(
content.includes('anti-pattern') || content.includes('Anti-Pattern') || content.includes('Bad'),
'planner-antipatterns.md must contain checkpoint anti-pattern examples'
);
});
test('contains specificity examples', () => {
const content = fs.readFileSync(PLANNER_ANTIPATTERNS_REF, 'utf-8');
assert.ok(
content.includes('TOO VAGUE') || content.includes('Specificity') || content.includes('specificity'),
'planner-antipatterns.md must contain specificity examples'
);
});
});
describe('three-tier consistency', () => {
test('thinning tier (< 200K), standard tier (200K-500K), and enrichment tier (>= 500K) all coexist', () => {
const content = fs.readFileSync(EXECUTE_PHASE, 'utf-8');
const hasThinning = content.includes('CONTEXT_WINDOW < 200000');
const hasEnrichment = content.includes('CONTEXT_WINDOW >= 500000');
assert.ok(hasThinning, 'must have thinning conditional (< 200K)');
assert.ok(hasEnrichment, 'must have enrichment conditional (>= 500K)');
});
});
});