From da5a030eac36864e25d625261b994d6944e33769 Mon Sep 17 00:00:00 2001 From: Tom Boucher Date: Fri, 3 Apr 2026 11:12:18 -0400 Subject: [PATCH 1/6] fix(nlpm): resolve 5 critical defects from NLPM audit - Add missing name: field to workstreams and reapply-patches commands - Add AskUserQuestion to review-backlog allowed-tools - Fix conflicting path references in gsd-user-profiler (use ~/.claude/get-shit-done/... convention) - Add allowed-tools to cleanup, help, and join-discord commands - Remove empty stub Step 5 from reapply-patches (dead bash comment block) Closes #1578 Co-Authored-By: Claude Sonnet 4.6 --- agents/gsd-user-profiler.md | 4 ++-- commands/gsd/cleanup.md | 5 +++++ commands/gsd/help.md | 2 ++ commands/gsd/join-discord.md | 1 + commands/gsd/reapply-patches.md | 14 +++----------- commands/gsd/review-backlog.md | 1 + commands/gsd/workstreams.md | 5 +++++ 7 files changed, 19 insertions(+), 13 deletions(-) diff --git a/agents/gsd-user-profiler.md b/agents/gsd-user-profiler.md index c6ac1cc58..cacfb3dbf 100644 --- a/agents/gsd-user-profiler.md +++ b/agents/gsd-user-profiler.md @@ -38,7 +38,7 @@ Key characteristics of the input: -@get-shit-done/references/user-profiling.md +@~/.claude/get-shit-done/references/user-profiling.md This is the detection heuristics rubric. Read it in full before analyzing any messages. It defines: - The 8 dimensions and their rating spectrums @@ -52,7 +52,7 @@ This is the detection heuristics rubric. Read it in full before analyzing any me -Read the user-profiling reference document at `get-shit-done/references/user-profiling.md` to load: +Read the user-profiling reference document at `~/.claude/get-shit-done/references/user-profiling.md` to load: - All 8 dimension definitions with rating spectrums - Signal patterns and detection heuristics per dimension - Confidence scoring thresholds (HIGH: 10+ signals across 2+ projects, MEDIUM: 5-9, LOW: <5, UNSCORED: 0) diff --git a/commands/gsd/cleanup.md b/commands/gsd/cleanup.md index c95b2af1b..874bdeab2 100644 --- a/commands/gsd/cleanup.md +++ b/commands/gsd/cleanup.md @@ -1,6 +1,11 @@ --- name: gsd:cleanup description: Archive accumulated phase directories from completed milestones +allowed-tools: + - Read + - Write + - Bash + - AskUserQuestion --- Archive phase directories from completed milestones into `.planning/milestones/v{X.Y}-phases/`. diff --git a/commands/gsd/help.md b/commands/gsd/help.md index d4fedc277..212d24c1a 100644 --- a/commands/gsd/help.md +++ b/commands/gsd/help.md @@ -1,6 +1,8 @@ --- name: gsd:help description: Show available GSD commands and usage guide +allowed-tools: + - Read --- Display the complete GSD command reference. diff --git a/commands/gsd/join-discord.md b/commands/gsd/join-discord.md index 06c218a4e..4290e709f 100644 --- a/commands/gsd/join-discord.md +++ b/commands/gsd/join-discord.md @@ -1,6 +1,7 @@ --- name: gsd:join-discord description: Join the GSD Discord community +allowed-tools: [] --- diff --git a/commands/gsd/reapply-patches.md b/commands/gsd/reapply-patches.md index 39bc93377..e9d0cfbd4 100644 --- a/commands/gsd/reapply-patches.md +++ b/commands/gsd/reapply-patches.md @@ -1,4 +1,5 @@ --- +name: gsd:reapply-patches description: Reapply local modifications after a GSD update allowed-tools: Read, Write, Edit, Bash, Glob, Grep, AskUserQuestion --- @@ -223,22 +224,13 @@ Each matching commit represents an intentional user modification. Use the commit **Never report `Skipped — no custom content`.** If a file is in the backup, it has custom content. -## Step 5: Update manifest - -After reapplying, regenerate the file manifest so future updates correctly detect these as user modifications: - -```bash -# The manifest will be regenerated on next /gsd:update -# For now, just note which files were modified -``` - -## Step 6: Cleanup option +## Step 5: Cleanup option Ask user: - "Keep patch backups for reference?" → preserve `gsd-local-patches/` - "Clean up patch backups?" → remove `gsd-local-patches/` directory -## Step 7: Report +## Step 6: Report ``` ## Patches Reapplied diff --git a/commands/gsd/review-backlog.md b/commands/gsd/review-backlog.md index 91de5dd83..7b34fec89 100644 --- a/commands/gsd/review-backlog.md +++ b/commands/gsd/review-backlog.md @@ -5,6 +5,7 @@ allowed-tools: - Read - Write - Bash + - AskUserQuestion --- diff --git a/commands/gsd/workstreams.md b/commands/gsd/workstreams.md index 7a6677e35..58c680fa2 100644 --- a/commands/gsd/workstreams.md +++ b/commands/gsd/workstreams.md @@ -1,5 +1,10 @@ --- +name: gsd:workstreams description: Manage parallel workstreams — list, create, switch, status, progress, complete, and resume +allowed-tools: + - Read + - Write + - Bash --- # /gsd:workstreams From 9b5458d1ff549790f365bc7309b0114dea2261c4 Mon Sep 17 00:00:00 2001 From: Tom Boucher Date: Fri, 3 Apr 2026 11:12:24 -0400 Subject: [PATCH 2/6] fix(codex): new-project no longer creates both AGENTS.md and CLAUDE.md Detect runtime from execution_context path or env vars at workflow start, then set INSTRUCTION_FILE (AGENTS.md for Codex, CLAUDE.md for all others). Pass --output $INSTRUCTION_FILE to generate-claude-md so the helper writes to the correct file instead of always defaulting to CLAUDE.md. Also add .codex to skipDirs in init.cjs so Codex runtime directories are not mistaken for project content during brownfield codebase analysis. Closes #1521 Co-Authored-By: Claude Sonnet 4.6 --- commands/gsd/new-project.md | 4 +++ get-shit-done/bin/lib/init.cjs | 2 +- get-shit-done/workflows/new-project.md | 37 +++++++++++++++++++++----- 3 files changed, 35 insertions(+), 8 deletions(-) diff --git a/commands/gsd/new-project.md b/commands/gsd/new-project.md index 508b08dd9..339f50f96 100644 --- a/commands/gsd/new-project.md +++ b/commands/gsd/new-project.md @@ -9,6 +9,10 @@ allowed-tools: - Task - AskUserQuestion --- + +**Copilot (VS Code):** Use `vscode_askquestions` wherever this workflow calls `AskUserQuestion`. They are equivalent — `vscode_askquestions` is the VS Code Copilot implementation of the same interactive question API. + + **Flags:** - `--auto` — Automatic mode. After config questions, runs research → requirements → roadmap without further interaction. Expects idea document via @ reference. diff --git a/get-shit-done/bin/lib/init.cjs b/get-shit-done/bin/lib/init.cjs index 544413d25..989f3ef84 100644 --- a/get-shit-done/bin/lib/init.cjs +++ b/get-shit-done/bin/lib/init.cjs @@ -276,7 +276,7 @@ function cmdInitNewProject(cwd, raw) { '.ex', '.exs', // Elixir '.clj', // Clojure ]); - const skipDirs = new Set(['node_modules', '.git', '.planning', '.claude', '__pycache__', 'target', 'dist', 'build']); + const skipDirs = new Set(['node_modules', '.git', '.planning', '.claude', '.codex', '__pycache__', 'target', 'dist', 'build']); function findCodeFiles(dir, depth) { if (depth > 3) return false; let entries; diff --git a/get-shit-done/workflows/new-project.md b/get-shit-done/workflows/new-project.md index a5f50f8b9..d3861db9f 100644 --- a/get-shit-done/workflows/new-project.md +++ b/get-shit-done/workflows/new-project.md @@ -66,6 +66,29 @@ AGENT_SKILLS_ROADMAPPER=$(node "$HOME/.claude/get-shit-done/bin/gsd-tools.cjs" a Parse JSON for: `researcher_model`, `synthesizer_model`, `roadmapper_model`, `commit_docs`, `project_exists`, `has_codebase_map`, `planning_exists`, `has_existing_code`, `has_package_file`, `is_brownfield`, `needs_codebase_map`, `has_git`, `project_path`. +**Detect runtime and set instruction file name:** + +Derive `RUNTIME` from the invoking prompt's `execution_context` path: +- Path contains `/.codex/` → `RUNTIME=codex` +- Path contains `/.gemini/` → `RUNTIME=gemini` +- Path contains `/.config/opencode/` or `/.opencode/` → `RUNTIME=opencode` +- Otherwise → `RUNTIME=claude` + +If `execution_context` path is not available, fall back to env vars: +```bash +if [ -n "$CODEX_HOME" ]; then RUNTIME="codex" +elif [ -n "$GEMINI_CONFIG_DIR" ]; then RUNTIME="gemini" +elif [ -n "$OPENCODE_CONFIG_DIR" ] || [ -n "$OPENCODE_CONFIG" ]; then RUNTIME="opencode" +else RUNTIME="claude"; fi +``` + +Set the instruction file variable: +```bash +if [ "$RUNTIME" = "codex" ]; then INSTRUCTION_FILE="AGENTS.md"; else INSTRUCTION_FILE="CLAUDE.md"; fi +``` + +All subsequent references to the project instruction file use `$INSTRUCTION_FILE`. + **If `project_exists` is true:** Error — project already initialized. Use `/gsd:progress`. **If `has_git` is false:** Initialize git: @@ -1106,18 +1129,18 @@ Use AskUserQuestion: **If "Review full file":** Display raw `cat .planning/ROADMAP.md`, then re-ask. -**Generate or refresh project CLAUDE.md before final commit:** +**Generate or refresh project instruction file before final commit:** ```bash -node "$HOME/.claude/get-shit-done/bin/gsd-tools.cjs" generate-claude-md +node "$HOME/.claude/get-shit-done/bin/gsd-tools.cjs" generate-claude-md --output "$INSTRUCTION_FILE" ``` -This ensures new projects get the default GSD workflow-enforcement guidance and current project context in `CLAUDE.md`. +This ensures new projects get the default GSD workflow-enforcement guidance and current project context in `$INSTRUCTION_FILE`. **Commit roadmap (after approval or auto mode):** ```bash -node "$HOME/.claude/get-shit-done/bin/gsd-tools.cjs" commit "docs: create roadmap ([N] phases)" --files .planning/ROADMAP.md .planning/STATE.md .planning/REQUIREMENTS.md CLAUDE.md +node "$HOME/.claude/get-shit-done/bin/gsd-tools.cjs" commit "docs: create roadmap ([N] phases)" --files .planning/ROADMAP.md .planning/STATE.md .planning/REQUIREMENTS.md "$INSTRUCTION_FILE" ``` ## 9. Done @@ -1138,7 +1161,7 @@ Present completion summary: | Research | `.planning/research/` | | Requirements | `.planning/REQUIREMENTS.md` | | Roadmap | `.planning/ROADMAP.md` | -| Project guide | `CLAUDE.md` | +| Project guide | `$INSTRUCTION_FILE` | **[N] phases** | **[X] requirements** | Ready to build ✓ ``` @@ -1220,7 +1243,7 @@ PHASE1_HAS_UI=$(echo "$PHASE1_SECTION" | grep -qi "UI hint.*yes" && echo "true" - `.planning/REQUIREMENTS.md` - `.planning/ROADMAP.md` - `.planning/STATE.md` -- `CLAUDE.md` +- `$INSTRUCTION_FILE` (`AGENTS.md` for Codex, `CLAUDE.md` for all other runtimes) @@ -1242,7 +1265,7 @@ PHASE1_HAS_UI=$(echo "$PHASE1_SECTION" | grep -qi "UI hint.*yes" && echo "true" - [ ] ROADMAP.md created with phases, requirement mappings, success criteria - [ ] STATE.md initialized - [ ] REQUIREMENTS.md traceability updated -- [ ] CLAUDE.md generated with GSD workflow guidance +- [ ] `$INSTRUCTION_FILE` generated with GSD workflow guidance (AGENTS.md for Codex, CLAUDE.md otherwise) - [ ] User knows next step is `/gsd:discuss-phase 1` **Atomic commits:** Each phase commits its artifacts immediately. If context is lost, artifacts persist. From 9b3e08926e18860e494cbf63485fd8c92280b034 Mon Sep 17 00:00:00 2001 From: Tom Boucher Date: Fri, 3 Apr 2026 11:12:36 -0400 Subject: [PATCH 3/6] fix(worktree): executor agents verify and fix branch base on Windows When EnterWorktree creates a branch from main instead of the current HEAD (a known issue on Windows), executor agents now detect the mismatch and reset their branch base to the correct commit before starting work. - execute-phase: capture EXPECTED_BASE before spawning, inject block into executor prompts - execute-plan: document Pattern A worktree_branch_check requirement - quick.md: inject worktree_branch_check into executor prompt - diagnose-issues: inject worktree_branch_check into debugger prompts - settings: add workflow.use_worktrees option so Windows users can disable worktree isolation via /gsd:settings without editing files Closes #1510 Co-Authored-By: Claude Sonnet 4.6 --- get-shit-done/workflows/diagnose-issues.md | 3 ++- get-shit-done/workflows/execute-phase.md | 28 ++++++++++++++++++++++ get-shit-done/workflows/execute-plan.md | 2 +- get-shit-done/workflows/quick.md | 14 +++++++++++ get-shit-done/workflows/settings.md | 13 +++++++++- 5 files changed, 57 insertions(+), 3 deletions(-) diff --git a/get-shit-done/workflows/diagnose-issues.md b/get-shit-done/workflows/diagnose-issues.md index 47f3e09d6..99cc12b86 100644 --- a/get-shit-done/workflows/diagnose-issues.md +++ b/get-shit-done/workflows/diagnose-issues.md @@ -88,6 +88,7 @@ This runs in parallel - all gaps investigated simultaneously. ```bash AGENT_SKILLS_DEBUGGER=$(node "$HOME/.claude/get-shit-done/bin/gsd-tools.cjs" agent-skills gsd-debugger 2>/dev/null) +EXPECTED_BASE=$(git rev-parse HEAD) ``` **Spawn debug agents in parallel:** @@ -96,7 +97,7 @@ For each gap, fill the debug-subagent-prompt template and spawn: ``` Task( - prompt=filled_debug_subagent_prompt + "\n\n\n- {phase_dir}/{phase_num}-UAT.md\n- .planning/STATE.md\n\n${AGENT_SKILLS_DEBUGGER}", + prompt=filled_debug_subagent_prompt + "\n\n\nFIRST ACTION: run git merge-base HEAD {EXPECTED_BASE} — if result differs from {EXPECTED_BASE}, run git reset --soft {EXPECTED_BASE} to correct the branch base (fixes Windows EnterWorktree creating branches from main).\n\n\n\n- {phase_dir}/{phase_num}-UAT.md\n- .planning/STATE.md\n\n${AGENT_SKILLS_DEBUGGER}", subagent_type="gsd-debugger", ${USE_WORKTREES !== "false" ? 'isolation="worktree",' : ''} description="Debug: {truth_short}" diff --git a/get-shit-done/workflows/execute-phase.md b/get-shit-done/workflows/execute-phase.md index 5077b9b0e..d3a2690ba 100644 --- a/get-shit-done/workflows/execute-phase.md +++ b/get-shit-done/workflows/execute-phase.md @@ -244,6 +244,11 @@ Execute each selected wave in sequence. Within a wave: parallel if `PARALLELIZAT **Worktree mode** (`USE_WORKTREES` is not `false`): + Before spawning, capture the current HEAD: + ```bash + EXPECTED_BASE=$(git rev-parse HEAD) + ``` + ``` Task( subagent_type="gsd-executor", @@ -256,6 +261,29 @@ Execute each selected wave in sequence. Within a wave: parallel if `PARALLELIZAT Commit each task atomically. Create SUMMARY.md. Update STATE.md and ROADMAP.md. + + FIRST ACTION before any other work: verify this worktree's branch is based on the correct commit. + + Run: + ```bash + ACTUAL_BASE=$(git merge-base HEAD {EXPECTED_BASE}) + CURRENT_HEAD=$(git rev-parse HEAD) + ``` + + If `ACTUAL_BASE` != `{EXPECTED_BASE}` (i.e. the worktree branch was created from an older + base such as `main` instead of the feature branch HEAD), rebase onto the correct base: + ```bash + git rebase --onto {EXPECTED_BASE} $(git rev-parse --abbrev-ref HEAD~1 2>/dev/null || git rev-parse HEAD^) HEAD 2>/dev/null || true + # If rebase fails or is a no-op, reset the branch to start from the correct base: + git reset --soft {EXPECTED_BASE} + ``` + + If `ACTUAL_BASE` == `{EXPECTED_BASE}`: the branch base is correct, proceed immediately. + + This check fixes a known issue on Windows where `EnterWorktree` creates branches from + `main` instead of the current feature branch HEAD. + + You are running as a PARALLEL executor agent. Use --no-verify on all git commits to avoid pre-commit hook contention with other agents. The diff --git a/get-shit-done/workflows/execute-plan.md b/get-shit-done/workflows/execute-plan.md index 920e92ab9..345e64073 100644 --- a/get-shit-done/workflows/execute-plan.md +++ b/get-shit-done/workflows/execute-plan.md @@ -72,7 +72,7 @@ grep -n "type=\"checkpoint" .planning/phases/XX-name/{phase}-{plan}-PLAN.md | Verify-only | B (segmented) | Segments between checkpoints. After none/human-verify → SUBAGENT. After decision/human-action → MAIN | | Decision | C (main) | Execute entirely in main context | -**Pattern A:** init_agent_tracking → spawn Task(subagent_type="gsd-executor", model=executor_model) with prompt: execute plan at [path], autonomous, all tasks + SUMMARY + commit, follow deviation/auth rules, report: plan name, tasks, SUMMARY path, commit hash → track agent_id → wait → update tracking → report. **Include `isolation="worktree"` only if `workflow.use_worktrees` is not `false`** (read via `config-get workflow.use_worktrees`). +**Pattern A:** init_agent_tracking → capture `EXPECTED_BASE=$(git rev-parse HEAD)` → spawn Task(subagent_type="gsd-executor", model=executor_model) with prompt: execute plan at [path], autonomous, all tasks + SUMMARY + commit, follow deviation/auth rules, report: plan name, tasks, SUMMARY path, commit hash → track agent_id → wait → update tracking → report. **Include `isolation="worktree"` only if `workflow.use_worktrees` is not `false`** (read via `config-get workflow.use_worktrees`). **When using `isolation="worktree"`, include a `` block in the prompt** instructing the executor to run `git merge-base HEAD {EXPECTED_BASE}` and, if the result differs from `{EXPECTED_BASE}`, reset the branch base with `git reset --soft {EXPECTED_BASE}` before starting work. This corrects a known issue on Windows where `EnterWorktree` creates branches from `main` instead of the feature branch HEAD. **Pattern B:** Execute segment-by-segment. Autonomous segments: spawn subagent for assigned tasks only (no SUMMARY/commit). Checkpoints: main context. After all segments: aggregate, create SUMMARY, commit. See segment_execution. diff --git a/get-shit-done/workflows/quick.md b/get-shit-done/workflows/quick.md index 0f3d8a11f..e78eaae11 100644 --- a/get-shit-done/workflows/quick.md +++ b/get-shit-done/workflows/quick.md @@ -550,6 +550,11 @@ Offer: 1) Force proceed, 2) Abort **Step 6: Spawn executor** +Capture current HEAD before spawning (used for worktree branch check): +```bash +EXPECTED_BASE=$(git rev-parse HEAD) +``` + Spawn gsd-executor with plan reference: ``` @@ -557,6 +562,15 @@ Task( prompt=" Execute quick task ${quick_id}. +${USE_WORKTREES !== "false" ? ` + +FIRST ACTION before any other work: verify this worktree branch is based on the correct commit. +Run: git merge-base HEAD ${EXPECTED_BASE} +If the result differs from ${EXPECTED_BASE}, run: git reset --soft ${EXPECTED_BASE} +This corrects a known issue on Windows where EnterWorktree creates branches from main instead of the feature branch HEAD. + +` : ''} + - ${QUICK_DIR}/${quick_id}-PLAN.md (Plan) - .planning/STATE.md (Project state) diff --git a/get-shit-done/workflows/settings.md b/get-shit-done/workflows/settings.md index 17c1bd593..d463b1130 100644 --- a/get-shit-done/workflows/settings.md +++ b/get-shit-done/workflows/settings.md @@ -34,6 +34,7 @@ Parse current values (default to `true` if not present): - `workflow.ui_safety_gate` — prompt to run /gsd:ui-phase before planning frontend phases (default: true if absent) - `model_profile` — which model each agent uses (default: `balanced`) - `git.branching_strategy` — branching approach (default: `"none"`) +- `workflow.use_worktrees` — whether parallel executor agents run in worktree isolation (default: `true`) @@ -153,6 +154,15 @@ AskUserQuestion([ { label: "No (Recommended)", description: "Run smart discuss before each phase — surfaces gray areas and captures decisions." }, { label: "Yes", description: "Skip discuss in /gsd:autonomous — chain directly to plan. Best for backend/pipeline work where phase descriptions are the spec." } ] + }, + { + question: "Use git worktrees for parallel agent isolation?", + header: "Worktrees", + multiSelect: false, + options: [ + { label: "Yes (Recommended)", description: "Each parallel executor runs in its own worktree branch — no conflicts between agents." }, + { label: "No", description: "Disable worktree isolation. Use on platforms where EnterWorktree is broken (e.g. Windows with feature branches). Agents run sequentially on the main working tree." } + ] } ]) ``` @@ -176,7 +186,8 @@ Merge new settings into existing config.json: "text_mode": true/false, "research_before_questions": true/false, "discuss_mode": "discuss" | "assumptions", - "skip_discuss": true/false + "skip_discuss": true/false, + "use_worktrees": true/false }, "git": { "branching_strategy": "none" | "phase" | "milestone", From 8903202d62baa4d6106960484f345e8e0e6afa40 Mon Sep 17 00:00:00 2001 From: Tom Boucher Date: Fri, 3 Apr 2026 11:12:43 -0400 Subject: [PATCH 4/6] fix(copilot): add vscode_askquestions guidance and AskUserQuestion to plan-phase Copilot agents use vscode_askquestions as the equivalent of AskUserQuestion. Without explicit guidance they sometimes omit questioning steps that depend on AskUserQuestion, causing extra billing and incomplete workflows. - Add to plan-phase, discuss-phase, execute-phase, and new-project commands mapping vscode_askquestions to AskUserQuestion - Add AskUserQuestion to plan-phase allowed-tools (was missing, causing the planner orchestrator to skip user questions in some runtimes) Closes #1476 Co-Authored-By: Claude Sonnet 4.6 --- commands/gsd/discuss-phase.md | 4 ++++ commands/gsd/execute-phase.md | 4 ++++ commands/gsd/plan-phase.md | 5 +++++ 3 files changed, 13 insertions(+) diff --git a/commands/gsd/discuss-phase.md b/commands/gsd/discuss-phase.md index 65dd0be3b..b710bf134 100644 --- a/commands/gsd/discuss-phase.md +++ b/commands/gsd/discuss-phase.md @@ -34,6 +34,10 @@ Extract implementation decisions that downstream agents need — researcher and @~/.claude/get-shit-done/templates/context.md + +**Copilot (VS Code):** Use `vscode_askquestions` wherever this workflow calls `AskUserQuestion`. They are equivalent — `vscode_askquestions` is the VS Code Copilot implementation of the same interactive question API. + + Phase number: $ARGUMENTS (required) diff --git a/commands/gsd/execute-phase.md b/commands/gsd/execute-phase.md index 478a097ec..ef2a54935 100644 --- a/commands/gsd/execute-phase.md +++ b/commands/gsd/execute-phase.md @@ -35,6 +35,10 @@ Context budget: ~15% orchestrator, 100% fresh per subagent. @~/.claude/get-shit-done/references/ui-brand.md + +**Copilot (VS Code):** Use `vscode_askquestions` wherever this workflow calls `AskUserQuestion`. They are equivalent — `vscode_askquestions` is the VS Code Copilot implementation of the same interactive question API. + + Phase: $ARGUMENTS diff --git a/commands/gsd/plan-phase.md b/commands/gsd/plan-phase.md index 9af3ce0e7..f37b305a4 100644 --- a/commands/gsd/plan-phase.md +++ b/commands/gsd/plan-phase.md @@ -10,6 +10,7 @@ allowed-tools: - Glob - Grep - Task + - AskUserQuestion - WebFetch - mcp__context7__* --- @@ -26,6 +27,10 @@ Create executable phase prompts (PLAN.md files) for a roadmap phase with integra @~/.claude/get-shit-done/references/ui-brand.md + +**Copilot (VS Code):** Use `vscode_askquestions` wherever this workflow calls `AskUserQuestion`. They are equivalent — `vscode_askquestions` is the VS Code Copilot implementation of the same interactive question API. Do not skip questioning steps because `AskUserQuestion` appears unavailable; use `vscode_askquestions` instead. + + Phase number: $ARGUMENTS (optional — auto-detects next unplanned phase if omitted) From 00e0446b99b9d18faac9f40ae5731891b978ccf6 Mon Sep 17 00:00:00 2001 From: Tom Boucher Date: Fri, 3 Apr 2026 11:19:42 -0400 Subject: [PATCH 5/6] =?UTF-8?q?fix:=20four=20bug=20fixes=20=E2=80=94=20bol?= =?UTF-8?q?d=20plan=20checkboxes,=20BACKLOG=20recommendations,=20executor?= =?UTF-8?q?=5Fmodel=20regression=20tests,=20session=5Fid=20path=20traversa?= =?UTF-8?q?l?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - fix(#1572): phase complete now marks bold-wrapped plan checkboxes in ROADMAP.md (`- [ ] **01-01**` format) by allowing optional `**` around plan IDs in the planCheckboxPattern regex in both phase.cjs and roadmap.cjs - fix(#1569): manager init no longer recommends 999.x (BACKLOG) phases as next actions; add guard in cmdManagerInit that skips phases matching /^999(?:\.|$)/ - fix(#1568): add regression tests confirming init execute-phase respects model_overrides for executor_model, including when resolve_model_ids is 'omit' - fix(#1533): reject session_id values containing path traversal sequences (../, /, \) in gsd-context-monitor and gsd-statusline before constructing /tmp file paths; add security tests covering both hooks Co-Authored-By: Claude Sonnet 4.6 --- get-shit-done/bin/lib/init.cjs | 2 + get-shit-done/bin/lib/phase.cjs | 3 +- get-shit-done/bin/lib/roadmap.cjs | 5 +- hooks/gsd-context-monitor.js | 7 +++ hooks/gsd-statusline.js | 5 +- tests/init-manager.test.cjs | 27 +++++++++ tests/init.test.cjs | 34 ++++++++++++ tests/phase.test.cjs | 38 +++++++++++++ tests/security.test.cjs | 91 +++++++++++++++++++++++++++++++ 9 files changed, 207 insertions(+), 5 deletions(-) diff --git a/get-shit-done/bin/lib/init.cjs b/get-shit-done/bin/lib/init.cjs index 989f3ef84..48e340c1e 100644 --- a/get-shit-done/bin/lib/init.cjs +++ b/get-shit-done/bin/lib/init.cjs @@ -956,9 +956,11 @@ function cmdInitManager(cwd, raw) { } catch { /* intentionally empty */ } // Compute recommended actions (execute > plan > discuss) + // Skip BACKLOG phases (999.x numbering) — they are parked ideas, not active work const recommendedActions = []; for (const phase of phases) { if (phase.disk_status === 'complete') continue; + if (/^999(?:\.|$)/.test(phase.number)) continue; if (phase.disk_status === 'planned' && phase.deps_satisfied) { recommendedActions.push({ diff --git a/get-shit-done/bin/lib/phase.cjs b/get-shit-done/bin/lib/phase.cjs index d6080375a..11f49b0ad 100644 --- a/get-shit-done/bin/lib/phase.cjs +++ b/get-shit-done/bin/lib/phase.cjs @@ -743,12 +743,13 @@ function cmdPhaseComplete(cwd, phaseNum, raw) { ); // Mark completed plan checkboxes (safety net for missed per-plan updates) + // Handles both plain IDs ("- [ ] 01-01-PLAN.md") and bold-wrapped IDs ("- [ ] **01-01**") for (const summaryFile of phaseInfo.summaries) { const planId = summaryFile.replace('-SUMMARY.md', '').replace('SUMMARY.md', ''); if (!planId) continue; const planEscaped = escapeRegex(planId); const planCheckboxPattern = new RegExp( - `(-\\s*\\[) (\\]\\s*${planEscaped})`, + `(-\\s*\\[) (\\]\\s*(?:\\*\\*)?${planEscaped}(?:\\*\\*)?)`, 'i' ); roadmapContent = roadmapContent.replace(planCheckboxPattern, '$1x$2'); diff --git a/get-shit-done/bin/lib/roadmap.cjs b/get-shit-done/bin/lib/roadmap.cjs index e817c53e3..b0cd2f357 100644 --- a/get-shit-done/bin/lib/roadmap.cjs +++ b/get-shit-done/bin/lib/roadmap.cjs @@ -300,13 +300,13 @@ function cmdRoadmapUpdatePlanProgress(cwd, phaseNum, raw) { roadmapContent = replaceInCurrentMilestone(roadmapContent, checkboxPattern, `$1x$2 (completed ${today})`); } - // Mark completed plan checkboxes (e.g. "- [ ] 50-01-PLAN.md" or "- [ ] 50-01:") + // Mark completed plan checkboxes (e.g. "- [ ] 50-01-PLAN.md", "- [ ] 50-01:", or "- [ ] **50-01**") for (const summaryFile of phaseInfo.summaries) { const planId = summaryFile.replace('-SUMMARY.md', '').replace('SUMMARY.md', ''); if (!planId) continue; const planEscaped = escapeRegex(planId); const planCheckboxPattern = new RegExp( - `(-\\s*\\[) (\\]\\s*${planEscaped})`, + `(-\\s*\\[) (\\]\\s*(?:\\*\\*)?${planEscaped}(?:\\*\\*)?)`, 'i' ); roadmapContent = roadmapContent.replace(planCheckboxPattern, '$1x$2'); @@ -314,7 +314,6 @@ function cmdRoadmapUpdatePlanProgress(cwd, phaseNum, raw) { fs.writeFileSync(roadmapPath, roadmapContent, 'utf-8'); }); - output({ updated: true, phase: phaseNum, diff --git a/hooks/gsd-context-monitor.js b/hooks/gsd-context-monitor.js index ae1bbf9a3..505141072 100644 --- a/hooks/gsd-context-monitor.js +++ b/hooks/gsd-context-monitor.js @@ -45,6 +45,13 @@ process.stdin.on('end', () => { process.exit(0); } + // Reject session IDs that contain path traversal sequences or path separators. + // session_id is used to construct file paths in /tmp — an unsanitized value + // could escape the temp directory and read or write arbitrary files. + if (/[/\\]|\.\./.test(sessionId)) { + process.exit(0); + } + // Check if context warnings are disabled via config const cwd = data.cwd || process.cwd(); const configPath = path.join(cwd, '.planning', 'config.json'); diff --git a/hooks/gsd-statusline.js b/hooks/gsd-statusline.js index 32742693e..e745350c8 100755 --- a/hooks/gsd-statusline.js +++ b/hooks/gsd-statusline.js @@ -35,7 +35,10 @@ process.stdin.on('end', () => { // Write context metrics to bridge file for the context-monitor PostToolUse hook. // The monitor reads this file to inject agent-facing warnings when context is low. - if (session) { + // Reject session IDs with path separators or traversal sequences to prevent + // a malicious session_id from writing files outside the temp directory. + const sessionSafe = session && !/[/\\]|\.\./.test(session); + if (sessionSafe) { try { const bridgePath = path.join(os.tmpdir(), `claude-ctx-${session}.json`); const bridgeData = JSON.stringify({ diff --git a/tests/init-manager.test.cjs b/tests/init-manager.test.cjs index ebdb508e7..02a86eceb 100644 --- a/tests/init-manager.test.cjs +++ b/tests/init-manager.test.cjs @@ -479,4 +479,31 @@ describe('init manager', () => { assert.strictEqual(output.manager_flags.plan, '--valid-flag', 'valid flag should pass through'); assert.strictEqual(output.manager_flags.execute, '', 'command substitution should be sanitized'); }); + + test('does not recommend BACKLOG phases (999.x) as next actions', () => { + writeState(tmpDir); + // Regular phase (planned, deps met) plus a backlog phase (999.1) also planned + writeRoadmap(tmpDir, [ + { number: '1', name: 'Foundation' }, + { number: '999.1', name: 'Nice to have feature (BACKLOG)' }, + ]); + // Phase 1: planned (has plan, no summary) + scaffoldPhase(tmpDir, 1, { plans: 1 }); + // Phase 999.1: planned (has plan, no summary) + const backlogDir = path.join(tmpDir, '.planning', 'phases', '999.1-backlog'); + fs.mkdirSync(backlogDir, { recursive: true }); + fs.writeFileSync(path.join(backlogDir, '999.1-01-PLAN.md'), '# Backlog Plan'); + + const result = runGsdTools('init manager', tmpDir); + assert.ok(result.success, `Command failed: ${result.error}`); + + const output = JSON.parse(result.output); + const recommended = output.recommended_actions || []; + const backlogRecs = recommended.filter(r => /^999/.test(r.phase)); + assert.strictEqual(backlogRecs.length, 0, 'no 999.x phases should appear in recommended_actions'); + + // Phase 1 (non-backlog) should still be recommended + const activeRecs = recommended.filter(r => r.phase === '1'); + assert.strictEqual(activeRecs.length, 1, 'phase 1 should still be recommended'); + }); }); diff --git a/tests/init.test.cjs b/tests/init.test.cjs index 8550728be..c625091f2 100644 --- a/tests/init.test.cjs +++ b/tests/init.test.cjs @@ -33,6 +33,40 @@ describe('init commands', () => { assert.strictEqual(output.config_path, '.planning/config.json'); }); + test('init execute-phase respects model_overrides for executor_model', () => { + const phaseDir = path.join(tmpDir, '.planning', 'phases', '01-foundation'); + fs.mkdirSync(phaseDir, { recursive: true }); + fs.writeFileSync(path.join(phaseDir, '01-01-PLAN.md'), '# Plan'); + fs.writeFileSync(path.join(tmpDir, '.planning', 'config.json'), JSON.stringify({ + model_profile: 'balanced', + model_overrides: { 'gsd-executor': 'openai/o4-mini' }, + })); + + const result = runGsdTools('init execute-phase 1 --raw', tmpDir, { HOME: tmpDir }); + assert.ok(result.success, `Command failed: ${result.error}`); + + const output = JSON.parse(result.output); + assert.strictEqual(output.executor_model, 'openai/o4-mini', + 'model_overrides["gsd-executor"] must take precedence over profile'); + }); + + test('init execute-phase respects model_overrides when resolve_model_ids is omit', () => { + const phaseDir = path.join(tmpDir, '.planning', 'phases', '01-foundation'); + fs.mkdirSync(phaseDir, { recursive: true }); + fs.writeFileSync(path.join(phaseDir, '01-01-PLAN.md'), '# Plan'); + fs.writeFileSync(path.join(tmpDir, '.planning', 'config.json'), JSON.stringify({ + resolve_model_ids: 'omit', + model_overrides: { 'gsd-executor': 'openai/o4-mini' }, + })); + + const result = runGsdTools('init execute-phase 1 --raw', tmpDir, { HOME: tmpDir }); + assert.ok(result.success, `Command failed: ${result.error}`); + + const output = JSON.parse(result.output); + assert.strictEqual(output.executor_model, 'openai/o4-mini', + 'model_overrides must take precedence even when resolve_model_ids is omit'); + }); + test('init plan-phase returns file paths', () => { const phaseDir = path.join(tmpDir, '.planning', 'phases', '03-api'); fs.mkdirSync(phaseDir, { recursive: true }); diff --git a/tests/phase.test.cjs b/tests/phase.test.cjs index c45ddb54d..6ba019389 100644 --- a/tests/phase.test.cjs +++ b/tests/phase.test.cjs @@ -1743,6 +1743,44 @@ Plans: assert.ok(!roadmap.includes('[ ] 01-01-PLAN.md'), 'plan 01-01 should not remain unchecked'); assert.ok(!roadmap.includes('[ ] 01-02-PLAN.md'), 'plan 01-02 should not remain unchecked'); }); + + test('marks bold-wrapped plan-level checkboxes on phase complete', () => { + fs.writeFileSync( + path.join(tmpDir, '.planning', 'ROADMAP.md'), + `# Roadmap + +- [ ] Phase 1: Foundation + +### Phase 1: Foundation +**Goal:** Setup +**Plans:** 2 plans + +Plans: +- [ ] **01-01**: Schema migration +- [ ] **01-02**: Auth setup +` + ); + fs.writeFileSync( + path.join(tmpDir, '.planning', 'STATE.md'), + `# State\n\n**Current Phase:** 01\n**Status:** In progress\n**Current Plan:** 01-02\n**Last Activity:** 2025-01-01\n**Last Activity Description:** Working\n` + ); + + const p1 = path.join(tmpDir, '.planning', 'phases', '01-foundation'); + fs.mkdirSync(p1, { recursive: true }); + fs.writeFileSync(path.join(p1, '01-01-PLAN.md'), '# Plan'); + fs.writeFileSync(path.join(p1, '01-01-SUMMARY.md'), '# Summary'); + fs.writeFileSync(path.join(p1, '01-02-PLAN.md'), '# Plan'); + fs.writeFileSync(path.join(p1, '01-02-SUMMARY.md'), '# Summary'); + + const result = runGsdTools('phase complete 1', tmpDir); + assert.ok(result.success, `Command failed: ${result.error}`); + + const roadmap = fs.readFileSync(path.join(tmpDir, '.planning', 'ROADMAP.md'), 'utf-8'); + assert.ok(roadmap.includes('[x] **01-01**'), 'bold plan 01-01 checkbox should be checked'); + assert.ok(roadmap.includes('[x] **01-02**'), 'bold plan 01-02 checkbox should be checked'); + assert.ok(!roadmap.includes('[ ] **01-01**'), 'bold plan 01-01 should not remain unchecked'); + assert.ok(!roadmap.includes('[ ] **01-02**'), 'bold plan 01-02 should not remain unchecked'); + }); }); // ───────────────────────────────────────────────────────────────────────────── diff --git a/tests/security.test.cjs b/tests/security.test.cjs index f6a12d38b..a4294f2b8 100644 --- a/tests/security.test.cjs +++ b/tests/security.test.cjs @@ -8,6 +8,7 @@ const { describe, test } = require('node:test'); const assert = require('node:assert/strict'); const path = require('path'); const os = require('os'); +const fs = require('fs'); const { validatePath, @@ -414,3 +415,93 @@ describe('validateFieldName', () => { assert.ok(!validateFieldName('-field').valid); }); }); + +// ─── Hook session_id path traversal (#1533) ──────────────────────────────── +// Verify that gsd-context-monitor and gsd-statusline reject session_id values +// containing path traversal sequences before constructing temp file paths. + +const { execFileSync } = require('child_process'); + +function runHook(hookPath, inputJson) { + try { + const result = execFileSync(process.execPath, [hookPath], { + input: JSON.stringify(inputJson), + encoding: 'utf-8', + stdio: ['pipe', 'pipe', 'pipe'], + timeout: 3000, + }); + return { exitCode: 0, stdout: result }; + } catch (err) { + return { exitCode: err.status || 1, stdout: err.stdout || '', stderr: err.stderr || '' }; + } +} + +describe('gsd-context-monitor session_id path traversal', () => { + const monitorPath = path.join(__dirname, '..', 'hooks', 'gsd-context-monitor.js'); + const tmpDir = os.tmpdir(); + + test('exits silently for session_id with ../ traversal', () => { + const maliciousId = '../../../etc/passwd'; + const result = runHook(monitorPath, { session_id: maliciousId }); + assert.strictEqual(result.exitCode, 0, 'hook should exit 0 for malicious session_id'); + assert.strictEqual(result.stdout.trim(), '', 'hook should produce no output for malicious session_id'); + const escapedPath = path.join(tmpDir, 'claude-ctx-' + maliciousId + '.json'); + assert.ok(!fs.existsSync(escapedPath), 'traversal file must not be created'); + }); + + test('exits silently for session_id with / separator', () => { + const maliciousId = 'foo/bar'; + const result = runHook(monitorPath, { session_id: maliciousId }); + assert.strictEqual(result.exitCode, 0); + assert.strictEqual(result.stdout.trim(), ''); + }); + + test('exits silently for session_id with backslash', () => { + const maliciousId = 'foo\\bar'; + const result = runHook(monitorPath, { session_id: maliciousId }); + assert.strictEqual(result.exitCode, 0); + assert.strictEqual(result.stdout.trim(), ''); + }); +}); + +describe('gsd-statusline session_id path traversal', () => { + const statuslinePath = path.join(__dirname, '..', 'hooks', 'gsd-statusline.js'); + const tmpDir = os.tmpdir(); + + const baseInput = { + model: { display_name: 'Claude' }, + context_window: { remaining_percentage: 80 }, + workspace: { current_dir: os.tmpdir() }, + }; + + test('does not write bridge file for session_id with ../ traversal', () => { + const maliciousId = '../../../etc/gsd-test'; + const bridgePath = path.join(tmpDir, 'claude-ctx-' + maliciousId + '.json'); + try { fs.unlinkSync(bridgePath); } catch { /* intentionally empty */ } + + runHook(statuslinePath, { ...baseInput, session_id: maliciousId }); + + assert.ok(!fs.existsSync(bridgePath), 'bridge file must not be written for traversal session_id'); + }); + + test('does not write bridge file for session_id with forward slash', () => { + const maliciousId = 'sub/path'; + const bridgePath = path.join(tmpDir, 'claude-ctx-' + maliciousId + '.json'); + try { fs.unlinkSync(bridgePath); } catch { /* intentionally empty */ } + + runHook(statuslinePath, { ...baseInput, session_id: maliciousId }); + + assert.ok(!fs.existsSync(bridgePath), 'bridge file must not be written for session_id with /'); + }); + + test('writes bridge file for safe session_id', () => { + const safeId = 'abc123-safe-session'; + const bridgePath = path.join(tmpDir, 'claude-ctx-' + safeId + '.json'); + try { fs.unlinkSync(bridgePath); } catch { /* intentionally empty */ } + + runHook(statuslinePath, { ...baseInput, session_id: safeId }); + + assert.ok(fs.existsSync(bridgePath), 'bridge file must be written for safe session_id'); + try { fs.unlinkSync(bridgePath); } catch { /* intentionally empty */ } + }); +}); From 9af67156da281902182e3b20ea9374b4e44aed6b Mon Sep 17 00:00:00 2001 From: Tom Boucher Date: Fri, 3 Apr 2026 11:26:24 -0400 Subject: [PATCH 6/6] test: update claude-md tests for \$INSTRUCTION_FILE codex fix Commit 512a80b changed new-project.md to use \$INSTRUCTION_FILE (AGENTS.md for Codex, CLAUDE.md for all other runtimes) instead of hardcoding CLAUDE.md. Two test assertions still checked for the hardcoded string and failed on CI. Co-Authored-By: Claude Sonnet 4.6 --- tests/claude-md.test.cjs | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/tests/claude-md.test.cjs b/tests/claude-md.test.cjs index 8b204f434..7e4bb6d1e 100644 --- a/tests/claude-md.test.cjs +++ b/tests/claude-md.test.cjs @@ -65,18 +65,20 @@ describe('new-project workflow includes CLAUDE.md generation', () => { const workflowPath = path.join(__dirname, '..', 'get-shit-done', 'workflows', 'new-project.md'); const commandsPath = path.join(__dirname, '..', 'docs', 'COMMANDS.md'); - test('new-project workflow generates CLAUDE.md before final commit', () => { + test('new-project workflow generates instruction file before final commit', () => { const content = fs.readFileSync(workflowPath, 'utf-8'); assert.ok(content.includes('generate-claude-md')); - assert.ok(content.includes('--files .planning/ROADMAP.md .planning/STATE.md .planning/REQUIREMENTS.md CLAUDE.md')); + // Codex fix: workflow now uses $INSTRUCTION_FILE (AGENTS.md for Codex, CLAUDE.md otherwise) + assert.ok(content.includes('--files .planning/ROADMAP.md .planning/STATE.md .planning/REQUIREMENTS.md "$INSTRUCTION_FILE"')); }); - test('new-project artifacts mention CLAUDE.md', () => { + test('new-project artifacts reference instruction file variable', () => { const workflowContent = fs.readFileSync(workflowPath, 'utf-8'); const commandsContent = fs.readFileSync(commandsPath, 'utf-8'); - assert.ok(workflowContent.includes('| Project guide | `CLAUDE.md`')); - assert.ok(workflowContent.includes('- `CLAUDE.md`')); + // Codex fix: hardcoded CLAUDE.md replaced with $INSTRUCTION_FILE variable + assert.ok(workflowContent.includes('| Project guide | `$INSTRUCTION_FILE`')); + assert.ok(workflowContent.includes('- `$INSTRUCTION_FILE`')); assert.ok(commandsContent.includes('`CLAUDE.md`')); }); });