Merge pull request #1591 from gsd-build/fix/multi-bug-1533-1568-1569-1572
fix: bold plan checkboxes, BACKLOG phase filtering, executor_model tests, session_id path traversal
This commit is contained in:
@@ -38,7 +38,7 @@ Key characteristics of the input:
|
||||
</input>
|
||||
|
||||
<reference>
|
||||
@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
|
||||
<process>
|
||||
|
||||
<step name="load_rubric">
|
||||
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)
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
---
|
||||
name: gsd:cleanup
|
||||
description: Archive accumulated phase directories from completed milestones
|
||||
allowed-tools:
|
||||
- Read
|
||||
- Write
|
||||
- Bash
|
||||
- AskUserQuestion
|
||||
---
|
||||
<objective>
|
||||
Archive phase directories from completed milestones into `.planning/milestones/v{X.Y}-phases/`.
|
||||
|
||||
@@ -34,6 +34,10 @@ Extract implementation decisions that downstream agents need — researcher and
|
||||
@~/.claude/get-shit-done/templates/context.md
|
||||
</execution_context>
|
||||
|
||||
<runtime_note>
|
||||
**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.
|
||||
</runtime_note>
|
||||
|
||||
<context>
|
||||
Phase number: $ARGUMENTS (required)
|
||||
|
||||
|
||||
@@ -35,6 +35,10 @@ Context budget: ~15% orchestrator, 100% fresh per subagent.
|
||||
@~/.claude/get-shit-done/references/ui-brand.md
|
||||
</execution_context>
|
||||
|
||||
<runtime_note>
|
||||
**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.
|
||||
</runtime_note>
|
||||
|
||||
<context>
|
||||
Phase: $ARGUMENTS
|
||||
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
---
|
||||
name: gsd:help
|
||||
description: Show available GSD commands and usage guide
|
||||
allowed-tools:
|
||||
- Read
|
||||
---
|
||||
<objective>
|
||||
Display the complete GSD command reference.
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
---
|
||||
name: gsd:join-discord
|
||||
description: Join the GSD Discord community
|
||||
allowed-tools: []
|
||||
---
|
||||
|
||||
<objective>
|
||||
|
||||
@@ -9,6 +9,10 @@ allowed-tools:
|
||||
- Task
|
||||
- AskUserQuestion
|
||||
---
|
||||
<runtime_note>
|
||||
**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.
|
||||
</runtime_note>
|
||||
|
||||
<context>
|
||||
**Flags:**
|
||||
- `--auto` — Automatic mode. After config questions, runs research → requirements → roadmap without further interaction. Expects idea document via @ reference.
|
||||
|
||||
@@ -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
|
||||
</execution_context>
|
||||
|
||||
<runtime_note>
|
||||
**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.
|
||||
</runtime_note>
|
||||
|
||||
<context>
|
||||
Phase number: $ARGUMENTS (optional — auto-detects next unplanned phase if omitted)
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -5,6 +5,7 @@ allowed-tools:
|
||||
- Read
|
||||
- Write
|
||||
- Bash
|
||||
- AskUserQuestion
|
||||
---
|
||||
|
||||
<objective>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
@@ -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({
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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<files_to_read>\n- {phase_dir}/{phase_num}-UAT.md\n- .planning/STATE.md\n</files_to_read>\n${AGENT_SKILLS_DEBUGGER}",
|
||||
prompt=filled_debug_subagent_prompt + "\n\n<worktree_branch_check>\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</worktree_branch_check>\n\n<files_to_read>\n- {phase_dir}/{phase_num}-UAT.md\n- .planning/STATE.md\n</files_to_read>\n${AGENT_SKILLS_DEBUGGER}",
|
||||
subagent_type="gsd-debugger",
|
||||
${USE_WORKTREES !== "false" ? 'isolation="worktree",' : ''}
|
||||
description="Debug: {truth_short}"
|
||||
|
||||
@@ -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.
|
||||
</objective>
|
||||
|
||||
<worktree_branch_check>
|
||||
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.
|
||||
</worktree_branch_check>
|
||||
|
||||
<parallel_execution>
|
||||
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
|
||||
|
||||
@@ -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 `<worktree_branch_check>` 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.
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
</output>
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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" ? `
|
||||
<worktree_branch_check>
|
||||
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.
|
||||
</worktree_branch_check>
|
||||
` : ''}
|
||||
|
||||
<files_to_read>
|
||||
- ${QUICK_DIR}/${quick_id}-PLAN.md (Plan)
|
||||
- .planning/STATE.md (Project state)
|
||||
|
||||
@@ -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`)
|
||||
</step>
|
||||
|
||||
<step name="present_settings">
|
||||
@@ -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",
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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`'));
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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 });
|
||||
|
||||
@@ -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');
|
||||
});
|
||||
});
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
@@ -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 */ }
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user